Files
suixinkan_ios_new/suixinkan/Features/WithdrawalAudit/Views/WithdrawalAuditView.swift
汉秋 1a0d1c25f4 Add order tail flows and migrate queue, message, settlement, and audit modules.
Wire deposit orders, historical shooting, and multi-travel uploads into Orders, and replace home placeholders for queue management, message center, scenic settlement, and withdrawal audit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:39:02 +08:00

312 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 ?? "--"
}
}