新增订单长尾流程,并迁移排队、消息、结算与审核模块

将定金订单、历史拍摄与多行程上传接入 Orders,并以真实页面替换首页排队管理、消息中心、景区结算与提现审核占位入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 13:39:02 +08:00
parent 311a70d610
commit c39c3d3c75
63 changed files with 9823 additions and 58 deletions

View File

@ -0,0 +1,311 @@
//
// 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 ?? "--"
}
}