// // WithdrawalAuditView.swift // suixinkan // // Created by Codex on 2026/6/25. // import SwiftUI /// 提现审核列表页,以审核视角展示钱包提现记录和处理进度。 struct WithdrawalAuditView: View { @Environment(WalletAPI.self) private var walletAPI @Environment(ToastCenter.self) private var toastCenter @State private var viewModel = WithdrawalAuditViewModel() @State private var selectedRecord: WalletWithdrawRecord? var body: some View { ScrollView { VStack(spacing: AppMetrics.Spacing.small) { summarySection filterSection contentSection } .padding(AppMetrics.Spacing.medium) } .background(Color(hex: 0xF5F7FA).ignoresSafeArea()) .navigationTitle("提现审核") .navigationBarTitleDisplayMode(.inline) .task { await viewModel.reload(api: walletAPI) } .refreshable { await viewModel.reload(api: walletAPI) } .sheet(item: $selectedRecord) { record in NavigationStack { WithdrawalAuditDetailView(record: record) } } .onChange(of: viewModel.errorMessage) { _, message in guard let message else { return } toastCenter.show(message) viewModel.errorMessage = nil } } private var summarySection: some View { HStack(spacing: AppMetrics.Spacing.xSmall) { summaryCard("记录总数", "\(viewModel.totalCount)") summaryCard("处理中", "\(viewModel.processingCount)") summaryCard("已完成", "\(viewModel.completedCount)") } } private var filterSection: some View { VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) { Picker("审核筛选", selection: Binding( get: { viewModel.selectedFilter }, set: { viewModel.selectFilter($0) } )) { ForEach(WithdrawalAuditFilter.allCases) { filter in Text(filter.title).tag(filter) } } .pickerStyle(.segmented) HStack(spacing: AppMetrics.Spacing.xSmall) { badge("当前 \(viewModel.selectedFilter.title)", tint: AppDesign.primary) badge("共 \(viewModel.filteredRecords.count) 条", tint: AppDesign.textSecondary) Spacer(minLength: 0) } } .padding(AppMetrics.Spacing.small) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } @ViewBuilder private var contentSection: some View { if viewModel.isLoading && viewModel.records.isEmpty { ProgressView() .frame(maxWidth: .infinity, minHeight: 220) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } else if viewModel.loadFailed && viewModel.records.isEmpty { VStack(spacing: AppMetrics.Spacing.small) { ContentUnavailableView( "提现审核记录加载失败", systemImage: "exclamationmark.triangle", description: Text(viewModel.loadFailureReason ?? "请检查网络后重试") ) Button("重新加载") { Task { await viewModel.reload(api: walletAPI) } } .font(.system(size: AppMetrics.FontSize.footnote, weight: .semibold)) .foregroundStyle(AppDesign.primary) .padding(.horizontal, AppMetrics.Spacing.small) .frame(height: 34) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 8)) .buttonStyle(.plain) } .frame(maxWidth: .infinity, minHeight: 220) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } else if viewModel.filteredRecords.isEmpty { ContentUnavailableView("暂无提现审核记录", systemImage: "tray", description: Text("可切换筛选条件或下拉刷新")) .frame(maxWidth: .infinity, minHeight: 220) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } else { VStack(spacing: AppMetrics.Spacing.small) { ForEach(viewModel.filteredRecords) { record in recordCard(record) } if viewModel.hasMore { Button(viewModel.isLoadingMore ? "加载中..." : "加载更多") { Task { await viewModel.loadMore(api: walletAPI) } } .font(.system(size: AppMetrics.FontSize.footnote, weight: .medium)) .foregroundStyle(AppDesign.primary) .frame(maxWidth: .infinity) .frame(height: 40) .background(.white, in: RoundedRectangle(cornerRadius: 10)) .buttonStyle(.plain) .disabled(viewModel.isLoadingMore) } } } } private func recordCard(_ record: WalletWithdrawRecord) -> some View { HStack(alignment: .top, spacing: AppMetrics.Spacing.small) { VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) { Text("¥ \(moneyText(record.amount))") .font(.system(size: AppMetrics.FontSize.title, weight: .semibold)) .foregroundStyle(AppDesign.primary) Text("申请时间:\(record.createdAt.withdrawalAuditDash)") .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(AppDesign.textSecondary) } Spacer(minLength: AppMetrics.Spacing.small) VStack(alignment: .trailing, spacing: AppMetrics.Spacing.xSmall) { Text(statusText(record.statusLabel)) .font(.system(size: AppMetrics.FontSize.caption, weight: .medium)) .foregroundStyle(statusColor(record.statusLabel)) .padding(.horizontal, AppMetrics.Spacing.small) .frame(height: 24) .background(statusColor(record.statusLabel).opacity(0.12), in: Capsule()) Button("查看详情") { selectedRecord = record } .font(.system(size: AppMetrics.FontSize.caption, weight: .medium)) .foregroundStyle(AppDesign.primary) .padding(.horizontal, AppMetrics.Spacing.small) .frame(height: 28) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 8)) .buttonStyle(.plain) } } .padding(AppMetrics.Spacing.small) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } private func summaryCard(_ title: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: 3) { Text(title) .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(AppDesign.textSecondary) Text(value) .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(AppMetrics.Spacing.small) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } private func badge(_ text: String, tint: Color) -> some View { Text(text) .font(.system(size: AppMetrics.FontSize.caption, weight: .medium)) .foregroundStyle(tint) .padding(.horizontal, AppMetrics.Spacing.small) .frame(height: 22) .background(tint.opacity(0.12), in: Capsule()) } } /// 提现审核详情页,展示单笔提现记录的审核时间线。 private struct WithdrawalAuditDetailView: View { let record: WalletWithdrawRecord @Environment(\.dismiss) private var dismiss var body: some View { ScrollView { VStack(spacing: AppMetrics.Spacing.small) { card { row("提现金额", "¥ \(moneyText(record.amount))") row("申请时间", record.createdAt.withdrawalAuditDash) HStack { Text("当前状态") .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textSecondary) Spacer() Text(statusText(record.statusLabel)) .font(.system(size: AppMetrics.FontSize.caption, weight: .medium)) .foregroundStyle(statusColor(record.statusLabel)) .padding(.horizontal, AppMetrics.Spacing.small) .frame(height: 24) .background(statusColor(record.statusLabel).opacity(0.12), in: Capsule()) } if let expectedAt = record.expectedAt?.withdrawalAuditNonEmpty { row("预计到账", expectedAt) } if let auditTime = record.auditTime?.withdrawalAuditNonEmpty { row("审核时间", auditTime) } if let completedAt = record.completedAt?.withdrawalAuditNonEmpty { row("到账时间", completedAt) } } card { Text("处理进度") .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) timelineRow("1. 提交申请", record.createdAt.withdrawalAuditDash, done: true) timelineRow("2. 审核中", record.auditTime?.withdrawalAuditDash ?? "--", done: true) timelineRow("3. 打款中", record.expectedAt?.withdrawalAuditDash ?? "--", done: record.completedAt?.withdrawalAuditNonEmpty == nil) timelineRow("4. 已完成", record.completedAt?.withdrawalAuditDash ?? "--", done: record.completedAt?.withdrawalAuditNonEmpty != nil) } } .padding(AppMetrics.Spacing.medium) } .background(Color(hex: 0xF5F7FA).ignoresSafeArea()) .navigationTitle("审核详情") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("关闭") { dismiss() } } } } private func card(@ViewBuilder _ content: () -> Content) -> some View { VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) { content() } .padding(AppMetrics.Spacing.small) .background(.white, in: RoundedRectangle(cornerRadius: 12)) } private func row(_ title: String, _ value: String) -> some View { HStack { Text(title) .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textSecondary) Spacer() Text(value) .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textPrimary) .multilineTextAlignment(.trailing) } } private func timelineRow(_ title: String, _ time: String, done: Bool) -> some View { HStack(spacing: AppMetrics.Spacing.xSmall) { Circle() .fill(done ? AppDesign.success : Color(hex: 0xD1D5DB)) .frame(width: 8, height: 8) Text(title) .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textPrimary) Spacer() Text(time) .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(AppDesign.textSecondary) } .padding(.horizontal, AppMetrics.Spacing.small) .frame(height: 34) .background(Color(hex: 0xF8FAFC), in: RoundedRectangle(cornerRadius: 8)) } } private func statusText(_ status: String) -> String { status.withdrawalAuditNonEmpty ?? "处理中" } private func statusColor(_ status: String) -> Color { if status.contains("通过") || status.contains("完成") || status.contains("到账") { return AppDesign.success } if status.contains("拒") || status.contains("失败") { return Color(hex: 0xDC2626) } return AppDesign.warning } private func moneyText(_ raw: String) -> String { let value = raw.filter { "0123456789.-".contains($0) } guard let decimal = Decimal(string: value), decimal > 0 else { return "0.00" } return NSDecimalNumber(decimal: decimal).stringValue } private extension String { var withdrawalAuditNonEmpty: String? { let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? nil : trimmed } var withdrawalAuditDash: String { withdrawalAuditNonEmpty ?? "--" } }