Files
suixinkan_uikit/suixinkan/UI/Wallet/WalletCells.swift
汉秋 5138c1c11a 完善实名认证、钱包与打卡点模块 UI 与业务逻辑。
对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 16:28:10 +08:00

399 lines
16 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.

//
// WalletCells.swift
// suixinkan
//
import SnapKit
import UIKit
/// Cell
final class WalletWithdrawRecordCell: UITableViewCell {
static let reuseIdentifier = "WalletWithdrawRecordCell"
private let cardView = UIView()
private let amountLabel = UILabel()
private let statusLabel = UILabel()
private let timeLabel = UILabel()
private let progressView = UIProgressView(progressViewStyle: .bar)
private let stepsStack = UIStackView()
private let infoLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(_ record: WalletWithdrawItem) {
amountLabel.text = WalletViewModel.formatAmount(record.amount)
statusLabel.text = record.statusLabel.isEmpty ? "--" : record.statusLabel
timeLabel.text = record.createdAt.isEmpty ? "--" : record.createdAt
let style = statusStyle(for: record.settlementStatus)
statusLabel.textColor = style.text
statusLabel.backgroundColor = style.background
progressView.progressTintColor = style.progress
progressView.progress = progress(for: record.settlementStatus)
configureSteps(progress: progress(for: record.settlementStatus), tint: style.progress)
progressView.isHidden = record.settlementStatus == 50
stepsStack.isHidden = record.settlementStatus == 50
infoLabel.attributedText = infoText(for: record)
infoLabel.isHidden = infoLabel.attributedText?.length == 0
infoLabel.snp.remakeConstraints { make in
if record.settlementStatus == 50 {
make.top.equalTo(timeLabel.snp.bottom).offset(WalletTokens.compactGap)
} else {
make.top.equalTo(stepsStack.snp.bottom).offset(10)
}
make.leading.trailing.equalToSuperview().inset(WalletTokens.cardPadding)
make.bottom.equalToSuperview().inset(14)
}
}
private func setupUI() {
selectionStyle = .none
backgroundColor = .white
contentView.backgroundColor = .white
cardView.backgroundColor = .white
cardView.layer.cornerRadius = WalletTokens.contentRadius
cardView.layer.borderWidth = 1
cardView.layer.borderColor = WalletTokens.cardBorder.cgColor
cardView.clipsToBounds = true
amountLabel.font = WalletTokens.metricBoldFont
amountLabel.textColor = WalletTokens.textPrimary
statusLabel.font = WalletTokens.captionMediumFont
statusLabel.textAlignment = .center
statusLabel.layer.cornerRadius = WalletTokens.chipRadius
statusLabel.clipsToBounds = true
timeLabel.font = WalletTokens.bodyFont
timeLabel.textColor = WalletTokens.textMuted
progressView.trackTintColor = WalletTokens.progressTrack
progressView.layer.cornerRadius = 3
progressView.clipsToBounds = true
stepsStack.axis = .horizontal
stepsStack.distribution = .equalSpacing
stepsStack.alignment = .center
infoLabel.font = WalletTokens.bodyFont
infoLabel.textColor = WalletTokens.textSecondary
infoLabel.numberOfLines = 0
contentView.addSubview(cardView)
[amountLabel, statusLabel, timeLabel, progressView, stepsStack, infoLabel].forEach(cardView.addSubview)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
}
amountLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(16)
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-12)
}
statusLabel.snp.makeConstraints { make in
make.centerY.equalTo(amountLabel)
make.trailing.equalToSuperview().inset(16)
make.height.equalTo(24)
make.width.greaterThanOrEqualTo(62)
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(amountLabel.snp.bottom).offset(6)
make.leading.trailing.equalToSuperview().inset(16)
}
progressView.snp.makeConstraints { make in
make.top.equalTo(timeLabel.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(6)
}
stepsStack.snp.makeConstraints { make in
make.top.equalTo(progressView.snp.bottom).offset(10)
make.leading.trailing.equalToSuperview().inset(16)
}
infoLabel.snp.makeConstraints { make in
make.top.equalTo(stepsStack.snp.bottom).offset(10)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalToSuperview().inset(14)
}
}
private func configureSteps(progress: Float, tint: UIColor) {
stepsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
["提交申请", "审核中", "打款中", "已完成"].enumerated().forEach { index, title in
let label = UILabel()
label.text = title
label.font = WalletTokens.stepFont
let threshold = Float(index) / 3.0
label.textColor = progress >= threshold ? tint : WalletTokens.textDisabled
stepsStack.addArrangedSubview(label)
}
let percent = UILabel()
percent.text = "\(Int(progress * 100))%"
percent.font = WalletTokens.captionMediumFont
percent.textColor = tint
stepsStack.addArrangedSubview(percent)
}
private func statusStyle(for status: Int) -> (background: UIColor, text: UIColor, progress: UIColor) {
switch status {
case 50, 20:
(WalletTokens.settlementSuccessBackground, WalletTokens.settlementSuccess, WalletTokens.summaryBackground)
case 60, 30:
(WalletTokens.settlementDangerBackground, WalletTokens.progressDanger, WalletTokens.progressDanger)
case 40:
(WalletTokens.warningBackground, WalletTokens.warning, WalletTokens.summaryBackground)
default:
(WalletTokens.infoBackground, WalletTokens.summaryBackground, WalletTokens.summaryBackground)
}
}
private func progress(for status: Int) -> Float {
switch status {
case 10: 0.1
case 40: 0.4
case 20: 0.8
case 30: 1.0
case 60: 0.0
default: 1.0
}
}
private func infoText(for record: WalletWithdrawItem) -> NSAttributedString {
let result = NSMutableAttributedString()
let normal: [NSAttributedString.Key: Any] = [
.font: WalletTokens.bodyFont,
.foregroundColor: WalletTokens.textSecondary,
]
let success: [NSAttributedString.Key: Any] = [
.font: WalletTokens.bodyFont,
.foregroundColor: WalletTokens.settlementSuccess,
]
let danger: [NSAttributedString.Key: Any] = [
.font: WalletTokens.bodyFont,
.foregroundColor: WalletTokens.progressDanger,
]
if (record.settlementStatus == 10 || record.settlementStatus == 40), !record.expectedAt.isEmpty {
result.append(NSAttributedString(string: "预计到账时间:\(record.expectedAt)", attributes: normal))
}
if (record.settlementStatus == 60 || record.settlementStatus == 30), !record.auditTime.isEmpty {
result.append(NSAttributedString(string: "审核时间:\(record.auditTime)", attributes: normal))
}
if (record.settlementStatus == 60 || record.settlementStatus == 30), !record.auditRemark.isEmpty {
if result.length > 0 { result.append(NSAttributedString(string: "\n")) }
result.append(NSAttributedString(string: "备注信息:\(record.auditRemark)", attributes: danger))
}
if record.settlementStatus == 50, !record.expectedAt.isEmpty {
result.append(NSAttributedString(string: "到账时间:\(record.expectedAt)", attributes: success))
}
return result
}
}
/// Cell
final class WalletTransactionHeaderCell: UITableViewCell {
static let reuseIdentifier = "WalletTransactionHeaderCell"
private let dateLabel = UILabel()
private let incomeLabel = UILabel()
private let pointsLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(_ group: EarningDetailGroup) {
dateLabel.text = group.date
incomeLabel.text = "当日收益:\(WalletViewModel.formatAmount(group.dayAmount))"
pointsLabel.text = group.dayPoints > 0 ? "积分+\(group.dayPoints)" : ""
}
private func setupUI() {
selectionStyle = .none
backgroundColor = .white
dateLabel.font = WalletTokens.bodyFont
dateLabel.textColor = WalletTokens.textTertiary
incomeLabel.font = WalletTokens.bodyFont
incomeLabel.textColor = WalletTokens.textSecondary
incomeLabel.textAlignment = .right
pointsLabel.font = WalletTokens.bodyFont
pointsLabel.textColor = WalletTokens.points
pointsLabel.textAlignment = .right
contentView.addSubview(dateLabel)
contentView.addSubview(incomeLabel)
contentView.addSubview(pointsLabel)
dateLabel.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.leading.equalToSuperview().offset(16)
}
pointsLabel.snp.makeConstraints { make in
make.centerY.equalTo(dateLabel)
make.trailing.equalToSuperview().inset(16)
}
incomeLabel.snp.makeConstraints { make in
make.centerY.equalTo(dateLabel)
make.leading.greaterThanOrEqualTo(dateLabel.snp.trailing).offset(8)
make.trailing.equalTo(pointsLabel.snp.leading).offset(-12)
}
}
}
/// Cell
final class WalletTransactionRecordCell: UITableViewCell {
static let reuseIdentifier = "WalletTransactionRecordCell"
private let cardView = UIView()
private let amountLabel = UILabel()
private let pointsLabel = UILabel()
private let typeLabel = UILabel()
private let orderLabel = UILabel()
private let timeLabel = UILabel()
private let withdrawLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(_ item: EarningDetailItem) {
amountLabel.text = item.amount.isEmpty ? "" : WalletViewModel.formatAmount(item.amount)
pointsLabel.text = item.points > 0 ? "积分+\(item.points)" : ""
amountLabel.isHidden = item.amount.isEmpty
pointsLabel.isHidden = item.points <= 0
if item.amount.isEmpty, item.points <= 0 {
amountLabel.text = "--"
amountLabel.isHidden = false
}
typeLabel.text = item.typeLabel
orderLabel.text = item.orderNumberSuffix
orderLabel.isHidden = item.orderNumberSuffix.isEmpty
timeLabel.text = item.createdAt
withdrawLabel.text = item.withdrawLabel
withdrawLabel.isHidden = item.withdrawLabel.isEmpty
let style = labelStyle(for: item.withdrawLabel)
withdrawLabel.textColor = style.text
withdrawLabel.backgroundColor = style.background
orderLabel.textColor = style.orderText
orderLabel.backgroundColor = style.orderBackground
}
private func setupUI() {
selectionStyle = .none
backgroundColor = .white
cardView.backgroundColor = .white
cardView.layer.cornerRadius = WalletTokens.cardRadius
cardView.layer.borderWidth = 1
cardView.layer.borderColor = WalletTokens.cardBorder.cgColor
cardView.clipsToBounds = true
amountLabel.font = WalletTokens.metricMediumFont
amountLabel.textColor = WalletTokens.textPrimary
pointsLabel.font = WalletTokens.metricMediumFont
pointsLabel.textColor = WalletTokens.points
pointsLabel.textAlignment = .right
typeLabel.font = WalletTokens.bodyFont
typeLabel.textColor = WalletTokens.textTertiary
orderLabel.font = WalletTokens.captionFont
orderLabel.textAlignment = .center
orderLabel.layer.cornerRadius = WalletTokens.chipRadius
orderLabel.clipsToBounds = true
timeLabel.font = WalletTokens.bodyFont
timeLabel.textColor = WalletTokens.textSecondary
withdrawLabel.font = WalletTokens.captionFont
withdrawLabel.textAlignment = .center
withdrawLabel.layer.cornerRadius = WalletTokens.chipRadius
withdrawLabel.clipsToBounds = true
contentView.addSubview(cardView)
[amountLabel, pointsLabel, typeLabel, orderLabel, timeLabel, withdrawLabel].forEach(cardView.addSubview)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
}
amountLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(16)
}
pointsLabel.snp.makeConstraints { make in
make.centerY.equalTo(amountLabel)
make.leading.greaterThanOrEqualTo(amountLabel.snp.trailing).offset(12)
make.trailing.equalToSuperview().inset(16)
}
typeLabel.snp.makeConstraints { make in
make.top.equalTo(amountLabel.snp.bottom).offset(10)
make.leading.equalToSuperview().offset(16)
}
orderLabel.snp.makeConstraints { make in
make.centerY.equalTo(typeLabel)
make.leading.equalTo(typeLabel.snp.trailing).offset(8)
make.height.equalTo(20)
make.width.greaterThanOrEqualTo(36)
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(typeLabel.snp.bottom).offset(12)
make.leading.equalToSuperview().offset(16)
make.bottom.equalToSuperview().inset(16)
}
withdrawLabel.snp.makeConstraints { make in
make.centerY.equalTo(timeLabel)
make.trailing.equalToSuperview().inset(16)
make.height.equalTo(20)
make.width.greaterThanOrEqualTo(48)
}
}
private func labelStyle(for label: String) -> (background: UIColor, text: UIColor, orderBackground: UIColor, orderText: UIColor) {
switch label {
case "可提现":
return (WalletTokens.successBackground, WalletTokens.success, WalletTokens.orderBackground, WalletTokens.primary)
default:
return (WalletTokens.neutralChipBackground, WalletTokens.textMuted, WalletTokens.neutralChipBackground, WalletTokens.textMuted)
}
}
}
/// Cell Android
final class WalletLoadingCell: UITableViewCell {
static let reuseIdentifier = "WalletLoadingCell"
private let indicator = UIActivityIndicatorView(style: .medium)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .white
indicator.color = WalletTokens.summaryBackground
indicator.startAnimating()
contentView.addSubview(indicator)
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
make.top.bottom.equalToSuperview().inset(WalletTokens.cardPadding)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}