将定金订单、历史拍摄与多行程上传接入 Orders,并以真实页面替换首页排队管理、消息中心、景区结算与提现审核占位入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
312 lines
13 KiB
Swift
312 lines
13 KiB
Swift
//
|
||
// 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<Content: View>(@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 ?? "--"
|
||
}
|
||
}
|