对齐 Android 立即收款页面 UI,并补齐推送状态与语音开关。

统一收款详情布局、设置金额弹窗和收款记录样式,接入 type 6/1 推送驱动与轮询 fallback,使 iOS 收款反馈与 Android 一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 15:17:09 +08:00
parent 5bdf4a7dbf
commit 3dcfb99254
11 changed files with 924 additions and 228 deletions

View File

@ -0,0 +1,399 @@
//
// 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
}
}