对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
399 lines
16 KiB
Swift
399 lines
16 KiB
Swift
//
|
||
// 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")
|
||
}
|
||
}
|