Files
suixinkan_ios_new/suixinkan/Features/Payment/Views/PaymentCollectionComponents.swift
汉秋 3dcfb99254 对齐 Android 立即收款页面 UI,并补齐推送状态与语音开关。
统一收款详情布局、设置金额弹窗和收款记录样式,接入 type 6/1 推送驱动与轮询 fallback,使 iOS 收款反馈与 Android 一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 15:17:09 +08:00

400 lines
15 KiB
Swift
Raw Permalink 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.

//
// 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<Content: View>: 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
}
}