import SwiftUI /// 省钱战绩:顶部累计/战绩卡 + 省钱订单明细(实际下单省了多少)。 struct SavingsView: View { @State private var vm = SavingsViewModel() var body: some View { Group { if vm.isLoading && vm.summary == nil && vm.records.isEmpty { ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity) } else { List { Section { SavingsHeaderCard(summary: vm.summary, battle: vm.battle) } if vm.loaded && vm.records.isEmpty { Section { Text("还没有省钱订单。比价后下单,省了多少会记在这里。") .font(.subheadline) .foregroundStyle(.secondary) } } else { Section("省钱明细") { ForEach(vm.records) { SavingsRecordRow(record: $0) } if vm.hasMore { HStack { Spacer(); ProgressView(); Spacer() } .onAppear { Task { await vm.loadMore() } } } } } } .listStyle(.insetGrouped) .refreshable { await vm.loadFirst() } } } .navigationTitle("省钱战绩") .navigationBarTitleDisplayMode(.inline) .task { if !vm.loaded { await vm.loadFirst() } } } } private struct SavingsHeaderCard: View { let summary: SavingsSummaryDTO? let battle: SavingsBattleDTO? var body: some View { VStack(spacing: 12) { Text("累计帮你省了") .font(.subheadline) .foregroundStyle(.secondary) Text((summary?.totalSavedCents ?? 0).centsToYuanText) .font(.system(size: 36, weight: .bold)) .foregroundStyle(AppColors.success) Text("共 \(summary?.orderCount ?? 0) 单 · 平均每单省 \((summary?.avgSavedCents ?? 0).centsToYuanText)") .font(.caption) .foregroundStyle(.secondary) Divider() HStack { stat("本周省", (battle?.weekSavedCents ?? 0).centsToYuanText) Divider() stat("击败用户", "\(battle?.beatPercent ?? 0)%") Divider() stat("连续打卡", "\(battle?.streakDays ?? 0)天") } .frame(height: 44) } .frame(maxWidth: .infinity) .padding(.vertical, 8) } private func stat(_ title: String, _ value: String) -> some View { VStack(spacing: 4) { Text(value).font(.headline) Text(title).font(.caption2).foregroundStyle(.secondary) } .frame(maxWidth: .infinity) } } private struct SavingsRecordRow: View { let record: SavingsRecordDTO var body: some View { VStack(alignment: .leading, spacing: 6) { HStack { Text(record.platform ?? record.shopName ?? "订单") .font(.subheadline.bold()) .lineLimit(1) Spacer() Text(AppDate.friendly(record.createdAt)) .font(.caption2) .foregroundStyle(.secondary) } if !record.dishes.isEmpty { Text(record.dishes.joined(separator: "、")) .font(.caption).foregroundStyle(.secondary).lineLimit(1) } else if let title = record.title { Text(title).font(.caption).foregroundStyle(.secondary).lineLimit(1) } HStack(spacing: 8) { Text("实付 \(record.orderAmountCents.centsToYuanText)") .font(.subheadline) if let orig = record.originalPriceCents, orig > record.orderAmountCents { Text(orig.centsToYuanText) .font(.caption).strikethrough().foregroundStyle(.secondary) } Spacer() if record.savedAmountCents > 0 { Text("省\(record.savedAmountCents.centsToYuanText)") .font(.caption.bold()) .padding(.horizontal, 8).padding(.vertical, 3) .background(AppColors.successBg) .foregroundStyle(AppColors.success) .clipShape(Capsule()) } } } .padding(.vertical, 4) } }