// // PaymentCollectionComponents.swift // suixinkan // // Created by Codex on 2026/6/29. // import SwiftUI private enum PaymentCollectionDesign { static let pageBackground = Color(hex: 0xF4F4F4) static let cardRadius: CGFloat = 12 static let qrSize: CGFloat = 182 static let qrPlaceholderRadius: CGFloat = 24 static let scanSuccess = Color(hex: 0x22C55E) static let errorText = Color(hex: 0xEF4444) static let mutedText = Color(hex: 0x9CA3AF) static let recordFooter = Color(hex: 0xB6BECA) static let recordBorder = Color(hex: 0xF3F4F6) static let orderBadgeBackground = Color(hex: 0xF4F4F4) static let orderBadgeText = Color(hex: 0x7B8EAA) static let navigationRowHeight: CGFloat = 52 } /// 收款模块通用白底圆角卡片容器。 struct PaymentWhiteCard: View { let verticalPadding: CGFloat let horizontalPadding: CGFloat @ViewBuilder var content: () -> Content /// 创建收款白卡容器。 init( verticalPadding: CGFloat = AppMetrics.Spacing.medium, horizontalPadding: CGFloat = AppMetrics.Spacing.medium, @ViewBuilder content: @escaping () -> Content ) { self.verticalPadding = verticalPadding self.horizontalPadding = horizontalPadding self.content = content } var body: some View { content() .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, horizontalPadding) .padding(.vertical, verticalPadding) .background(.white, in: RoundedRectangle(cornerRadius: PaymentCollectionDesign.cardRadius)) } } /// 收款信息键值行,对齐 Android 信息卡 label/value 布局。 struct PaymentInfoRow: View { let label: String let value: String var body: some View { HStack(alignment: .top) { Text(label) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) Spacer(minLength: AppMetrics.Spacing.small) Text(value) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(.black) .multilineTextAlignment(.trailing) } } } /// 收款页导航行,用于收款记录等入口。 struct PaymentNavigationRow: View { let title: String var body: some View { HStack { Text(title) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) Spacer() Image(systemName: "chevron.right") .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(PaymentCollectionDesign.mutedText) } .frame(height: PaymentCollectionDesign.navigationRowHeight) .padding(.horizontal, AppMetrics.Spacing.medium) .background(.white, in: RoundedRectangle(cornerRadius: PaymentCollectionDesign.cardRadius)) } } /// 收款二维码区域,包含正常、失败和付款成功三态。 struct PaymentQRSection: View { let scenicName: String let qrImage: UIImage? let paymentSuccessData: PaymentSuccessDisplayData? let onSetAmount: () -> Void let onSaveQRCode: () -> Void let onRefresh: () -> Void var body: some View { PaymentWhiteCard(verticalPadding: 32, horizontalPadding: 0) { VStack(spacing: AppMetrics.Spacing.xLarge) { if let paymentSuccessData { paymentSuccessContent(paymentSuccessData) } else { Text(scenicName) .font(.system(size: AppMetrics.FontSize.body, weight: .medium)) .foregroundStyle(.black) .lineLimit(1) .padding(.horizontal, AppMetrics.Spacing.medium) if let qrImage { Image(uiImage: qrImage) .interpolation(.none) .resizable() .scaledToFit() .frame(width: PaymentCollectionDesign.qrSize, height: PaymentCollectionDesign.qrSize) } else { qrPlaceholder } HStack(spacing: 32) { actionLink(title: "设置金额", action: onSetAmount) actionLink(title: "保存二维码", action: onSaveQRCode) } } } .frame(maxWidth: .infinity) } } /// 二维码缺失时的失败占位。 private var qrPlaceholder: some View { VStack(spacing: AppMetrics.Spacing.xSmall) { Text("未选景区或网络问题") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(PaymentCollectionDesign.errorText) Button(action: onRefresh) { HStack(spacing: AppMetrics.Spacing.xxSmall) { Image(systemName: "arrow.clockwise") .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) Text("点击刷新") .font(.system(size: AppMetrics.FontSize.subheadline)) } .foregroundStyle(AppDesign.primary) } } .frame(width: PaymentCollectionDesign.qrSize, height: PaymentCollectionDesign.qrSize) .background(PaymentCollectionDesign.pageBackground, in: RoundedRectangle(cornerRadius: PaymentCollectionDesign.qrPlaceholderRadius)) } /// 付款成功后 QR 区展示的大成功态。 private func paymentSuccessContent(_ data: PaymentSuccessDisplayData) -> some View { VStack(spacing: AppMetrics.Spacing.medium) { Text("付款成功") .font(.system(size: AppMetrics.FontSize.body, weight: .medium)) .foregroundStyle(.black) Image(systemName: "checkmark.circle.fill") .font(.system(size: 90)) .foregroundStyle(PaymentCollectionDesign.scanSuccess) Text("¥ \(data.orderAmount)") .font(.system(size: 28, weight: .bold)) .foregroundStyle(.black) } .frame(maxWidth: .infinity) .padding(.horizontal, AppMetrics.Spacing.medium) } /// 蓝色文字操作链。 private func actionLink(title: String, action: @escaping () -> Void) -> some View { Button(action: action) { Text(title) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.primary) } } } /// 扫码成功或付款详情状态卡。 struct PaymentStatusSection: View { let scanSuccessTime: String? let paymentSuccessData: PaymentSuccessDisplayData? var body: some View { if let scanSuccessTime { PaymentWhiteCard { HStack { Text(scanSuccessTime) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) Spacer() Text("扫码成功支付中...") .font(.system(size: AppMetrics.FontSize.title3, weight: .bold)) .foregroundStyle(PaymentCollectionDesign.scanSuccess) } } } else if let paymentSuccessData { PaymentWhiteCard { VStack(spacing: 6) { PaymentDetailRow(label: "付款金额", value: "¥ \(paymentSuccessData.orderAmount)", valueColor: AppDesign.primary) PaymentDetailRow(label: "付款时间", value: paymentSuccessData.payTime) PaymentDetailRow(label: "付款单号", value: paymentSuccessData.orderNumber) if !paymentSuccessData.remark.isEmpty { PaymentDetailRow(label: "备注信息", value: paymentSuccessData.remark) } } } } } } /// 付款详情键值行。 private struct PaymentDetailRow: View { let label: String let value: String var valueColor: Color = .black var body: some View { HStack(alignment: .top) { Text(label) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) Spacer(minLength: AppMetrics.Spacing.small) Text(value) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(valueColor) .multilineTextAlignment(.trailing) } } } /// 设置收款金额弹窗,对齐 Android SetAmountDialog。 struct PaymentSetAmountDialog: View { @Binding var amountText: String @Binding var remarkText: String let canConfirm: Bool let onConfirm: () -> Void let onDismiss: () -> Void var body: some View { ZStack { Color.black.opacity(0.35) .ignoresSafeArea() .onTapGesture { } VStack(alignment: .leading, spacing: 0) { Text("收款金额") .font(.system(size: AppMetrics.FontSize.title3, weight: .bold)) .foregroundStyle(.black) .padding(.bottom, AppMetrics.Spacing.large) HStack(spacing: AppMetrics.Spacing.xSmall) { Text("¥") .font(.system(size: AppMetrics.FontSize.title2, weight: .bold)) .foregroundStyle(.black) TextField("请输入收款金额", text: $amountText) .keyboardType(.decimalPad) .font(.system(size: AppMetrics.FontSize.title3, weight: .medium)) .foregroundStyle(.black) } .padding(.horizontal, AppMetrics.Spacing.medium) .frame(minHeight: AppMetrics.ControlSize.inputHeight) .background(PaymentCollectionDesign.pageBackground, in: RoundedRectangle(cornerRadius: 8)) .padding(.bottom, AppMetrics.Spacing.medium) Text("备注 (选填)") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) .padding(.bottom, AppMetrics.Spacing.xSmall) TextField("请输入备注信息...", text: $remarkText, axis: .vertical) .lineLimit(4, reservesSpace: true) .font(.system(size: AppMetrics.FontSize.subheadline)) .padding(AppMetrics.Spacing.small) .background(.white, in: RoundedRectangle(cornerRadius: 8)) .overlay { RoundedRectangle(cornerRadius: 8) .stroke(Color(hex: 0xE5E7EB), lineWidth: 1) } .onChange(of: remarkText) { newValue in if newValue.count > 200 { remarkText = String(newValue.prefix(200)) } } HStack { Spacer() Text("\(remarkText.count)/200") .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(PaymentCollectionDesign.mutedText) } .padding(.top, AppMetrics.Spacing.xxSmall) .padding(.bottom, AppMetrics.Spacing.medium) HStack { Spacer() Button(action: onDismiss) { Text("取消") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.primary) .frame(width: 84, height: 38) .background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 8)) } Button(action: onConfirm) { Text("确定") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(.white) .frame(width: 84, height: 38) .background(AppDesign.primary.opacity(canConfirm ? 1 : 0.5), in: RoundedRectangle(cornerRadius: 8)) } .disabled(!canConfirm) } } .padding(AppMetrics.Spacing.medium) .background(.white, in: RoundedRectangle(cornerRadius: 16)) .shadow(color: .black.opacity(0.12), radius: 16, y: 8) .padding(.horizontal, AppMetrics.Spacing.pageHorizontal) } } } /// 收款记录日期分组头部。 struct PaymentRecordDayHeader: View { let group: PaymentCollectionRecordGroup var body: some View { HStack { Text(group.analyse.date) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(PaymentCollectionDesign.mutedText) Spacer() Text("共收款\(group.analyse.orderCount)笔, 当日收益:¥\(group.analyse.orderAmountSum)") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) } .padding(.vertical, AppMetrics.Spacing.xSmall) } } /// 收款记录明细卡片。 struct PaymentRecordItemCard: View { let item: PaymentCollectionRecordItem var body: some View { VStack(spacing: 6) { HStack(alignment: .center) { Text("¥\(item.orderAmount)") .font(.system(size: AppMetrics.FontSize.title3, weight: .bold)) .foregroundStyle(AppDesign.primary) Spacer() Text("订单号:\(item.orderNumber)") .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(PaymentCollectionDesign.orderBadgeText) .padding(.horizontal, AppMetrics.Spacing.xxSmall) .padding(.vertical, 2) .background(PaymentCollectionDesign.orderBadgeBackground, in: RoundedRectangle(cornerRadius: 4)) } HStack { Text(item.userPhone) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(PaymentCollectionDesign.mutedText) Spacer() Text(item.createTime) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.orderLabelColor) } } .padding(AppMetrics.Spacing.medium) .overlay { RoundedRectangle(cornerRadius: PaymentCollectionDesign.cardRadius) .stroke(PaymentCollectionDesign.recordBorder, lineWidth: 1) } } } /// 收款记录空态。 struct PaymentRecordEmptyView: View { var body: some View { VStack { Spacer(minLength: 80) Text("暂无收款记录") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(PaymentCollectionDesign.mutedText) Spacer() } .frame(maxWidth: .infinity) } } /// 收款页背景色。 extension PaymentCollectionView { static var pageBackground: Color { PaymentCollectionDesign.pageBackground } }