完善钱包提现状态展示与举报摄影师列表交互。
对齐 Android 提现进度与时间线映射,优化钱包与积分兑换页刷新逻辑,并补充相关单元测试与真机测试说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -173,6 +173,16 @@ final class WalletViewModel {
|
||||
await loadWithdraw(api: api, refresh: true)
|
||||
}
|
||||
|
||||
/// 仅刷新当前选中的钱包 Tab。
|
||||
func refreshSelectedTab(api: any WalletPageServing) async {
|
||||
switch selectedTab {
|
||||
case .withdraw:
|
||||
await refreshWithdraw(api: api)
|
||||
case .transaction:
|
||||
await refreshTransaction(api: api)
|
||||
}
|
||||
}
|
||||
|
||||
/// 滚动到底部时加载更多提现记录。
|
||||
func loadMoreWithdrawIfNeeded(currentIndex: Int, api: any WalletPageServing) async {
|
||||
guard currentIndex >= withdrawRecords.count - 2, withdrawCanLoadMore, !withdrawLoading else { return }
|
||||
@ -336,6 +346,85 @@ enum WalletTransactionEntry: Hashable {
|
||||
case item(EarningDetailItem)
|
||||
}
|
||||
|
||||
/// 钱包记录状态的语义色调,由页面映射为模块内固定色值。
|
||||
enum WalletStatusTone: Equatable {
|
||||
case info
|
||||
case success
|
||||
case warning
|
||||
case danger
|
||||
}
|
||||
|
||||
/// 银行卡提现记录的状态展示映射,对齐 Android 结算进度和时间线可见性。
|
||||
struct WalletWithdrawStatusDisplay: Equatable {
|
||||
let tone: WalletStatusTone
|
||||
let progress: Int
|
||||
let showsTimeline: Bool
|
||||
|
||||
/// 根据后端结算状态创建展示数据。
|
||||
init(status: Int) {
|
||||
switch status {
|
||||
case 20:
|
||||
tone = .success
|
||||
progress = 80
|
||||
showsTimeline = true
|
||||
case 30:
|
||||
tone = .danger
|
||||
progress = 100
|
||||
showsTimeline = true
|
||||
case 40:
|
||||
tone = .warning
|
||||
progress = 40
|
||||
showsTimeline = true
|
||||
case 50:
|
||||
tone = .success
|
||||
progress = 100
|
||||
showsTimeline = false
|
||||
case 60:
|
||||
tone = .danger
|
||||
progress = 0
|
||||
showsTimeline = true
|
||||
default:
|
||||
tone = .info
|
||||
progress = status == 10 ? 10 : 100
|
||||
showsTimeline = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 积分兑现记录的状态文案、进度与时间字段映射。
|
||||
struct PointWithdrawStatusDisplay: Equatable {
|
||||
let title: String
|
||||
let tone: WalletStatusTone
|
||||
let progress: Int
|
||||
let timeTitle: String
|
||||
|
||||
/// 根据后端积分提现状态创建展示数据。
|
||||
init(status: Int) {
|
||||
switch status {
|
||||
case 1:
|
||||
title = "提现中"
|
||||
tone = .warning
|
||||
progress = 40
|
||||
timeTitle = "预计到账时间:"
|
||||
case 2:
|
||||
title = "提现成功"
|
||||
tone = .success
|
||||
progress = 0
|
||||
timeTitle = "到账时间:"
|
||||
case 3:
|
||||
title = "提现失败"
|
||||
tone = .danger
|
||||
progress = 100
|
||||
timeTitle = "审核时间:"
|
||||
default:
|
||||
title = "未知"
|
||||
tone = .info
|
||||
progress = 0
|
||||
timeTitle = "预计到账时间:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 提现申请 ViewModel,管理金额、短信验证码、倒计时和提交状态。
|
||||
final class WithdrawViewModel {
|
||||
private(set) var withdrawInfo: WithdrawInfoResponse?
|
||||
|
||||
@ -888,8 +888,7 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
||||
id: content.id > 0 ? String(content.id) : content.time,
|
||||
submitTime: content.time,
|
||||
text: content.desc,
|
||||
imageCount: content.imageCount,
|
||||
videoCount: content.videoCount
|
||||
evidences: content.evidences
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -956,8 +955,11 @@ struct WildReportSupplementaryEvidence: Hashable {
|
||||
let id: String
|
||||
let submitTime: String
|
||||
let text: String
|
||||
let imageCount: Int
|
||||
let videoCount: Int
|
||||
let evidences: [WildReportDetailEvidence]
|
||||
|
||||
var imageCount: Int { evidences.filter(\.isImage).count }
|
||||
var videoCount: Int { evidences.filter(\.isVideo).count }
|
||||
var hasMedia: Bool { imageCount > 0 || videoCount > 0 }
|
||||
|
||||
var summaryText: String {
|
||||
var parts: [String] = []
|
||||
|
||||
@ -372,11 +372,12 @@ private final class PointWithdrawRecordCell: UITableViewCell {
|
||||
amountLabel.text = record.amount != nil && record.status == 2 ? "+\(formatAmount(record.amount))" : ""
|
||||
amountLabel.isHidden = amountLabel.text?.isEmpty ?? true
|
||||
timeLabel.text = record.createdAt.isEmpty ? "----" : record.createdAt
|
||||
let style = statusStyle(for: record.status)
|
||||
statusLabel.text = statusText(for: record.status)
|
||||
let display = PointWithdrawStatusDisplay(status: record.status)
|
||||
let style = statusStyle(for: display.tone)
|
||||
statusLabel.text = display.title
|
||||
statusLabel.textColor = style.text
|
||||
statusLabel.backgroundColor = style.background
|
||||
let progress = progress(for: record.status)
|
||||
let progress = display.progress
|
||||
progressView.progress = Float(progress) / 100.0
|
||||
progressView.progressTintColor = style.progress
|
||||
progressView.isHidden = progress == 0
|
||||
@ -498,45 +499,28 @@ private final class PointWithdrawRecordCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
private func statusText(for status: Int) -> String {
|
||||
switch status {
|
||||
case 2: "提现成功"
|
||||
case 1: "提现中"
|
||||
case 3: "提现失败"
|
||||
default: "未知"
|
||||
}
|
||||
}
|
||||
|
||||
private func statusStyle(for status: Int) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
||||
switch status {
|
||||
case 2:
|
||||
private func statusStyle(for tone: WalletStatusTone) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
||||
switch tone {
|
||||
case .success:
|
||||
(WalletTokens.successBackground, WalletTokens.success, WalletTokens.summaryBackground)
|
||||
case 1:
|
||||
case .warning:
|
||||
(WalletTokens.pointsPendingBackground, WalletTokens.points, WalletTokens.summaryBackground)
|
||||
case 3:
|
||||
case .danger:
|
||||
(WalletTokens.dangerBackground, WalletTokens.danger, WalletTokens.progressDanger)
|
||||
default:
|
||||
case .info:
|
||||
(WalletTokens.infoBackground, WalletTokens.summaryBackground, WalletTokens.summaryBackground)
|
||||
}
|
||||
}
|
||||
|
||||
private func progress(for status: Int) -> Int {
|
||||
switch status {
|
||||
case 1: 40
|
||||
case 3: 100
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
|
||||
private func infoText(for record: PointWithdrawItem) -> String {
|
||||
let display = PointWithdrawStatusDisplay(status: record.status)
|
||||
let value: String
|
||||
switch record.status {
|
||||
case 3:
|
||||
return "审核时间:\(record.reviewedAt.isEmpty ? "暂无" : record.reviewedAt)"
|
||||
case 2:
|
||||
return "到账时间:\(record.paymentTime.isEmpty ? "暂无" : record.paymentTime)"
|
||||
default:
|
||||
return "预计到账时间:\(record.estimatedReceiptTime.isEmpty ? "暂无" : record.estimatedReceiptTime)"
|
||||
case 3: value = record.reviewedAt
|
||||
case 2: value = record.paymentTime
|
||||
default: value = record.estimatedReceiptTime
|
||||
}
|
||||
return "\(display.timeTitle)\(value.isEmpty ? "暂无" : value)"
|
||||
}
|
||||
|
||||
private func formatAmount(_ value: Double?) -> String {
|
||||
|
||||
@ -33,14 +33,16 @@ final class WalletWithdrawRecordCell: UITableViewCell {
|
||||
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)
|
||||
let display = WalletWithdrawStatusDisplay(status: record.settlementStatus)
|
||||
let style = statusStyle(for: display.tone)
|
||||
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
|
||||
let progress = Float(display.progress) / 100
|
||||
progressView.progress = progress
|
||||
configureSteps(progress: progress, tint: style.progress)
|
||||
progressView.isHidden = !display.showsTimeline
|
||||
stepsStack.isHidden = !display.showsTimeline
|
||||
infoLabel.attributedText = infoText(for: record)
|
||||
infoLabel.isHidden = infoLabel.attributedText?.length == 0
|
||||
infoLabel.snp.remakeConstraints { make in
|
||||
@ -141,30 +143,19 @@ final class WalletWithdrawRecordCell: UITableViewCell {
|
||||
stepsStack.addArrangedSubview(percent)
|
||||
}
|
||||
|
||||
private func statusStyle(for status: Int) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
||||
switch status {
|
||||
case 50, 20:
|
||||
private func statusStyle(for tone: WalletStatusTone) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
||||
switch tone {
|
||||
case .success:
|
||||
(WalletTokens.settlementSuccessBackground, WalletTokens.settlementSuccess, WalletTokens.summaryBackground)
|
||||
case 60, 30:
|
||||
case .danger:
|
||||
(WalletTokens.settlementDangerBackground, WalletTokens.progressDanger, WalletTokens.progressDanger)
|
||||
case 40:
|
||||
case .warning:
|
||||
(WalletTokens.warningBackground, WalletTokens.warning, WalletTokens.summaryBackground)
|
||||
default:
|
||||
case .info:
|
||||
(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] = [
|
||||
|
||||
@ -320,14 +320,7 @@ final class WalletViewController: BaseViewController, UITableViewDelegate {
|
||||
}
|
||||
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
switch viewModel.selectedTab {
|
||||
case .withdraw:
|
||||
await viewModel.refreshWithdraw(api: walletAPI)
|
||||
case .transaction:
|
||||
await viewModel.refreshTransaction(api: walletAPI)
|
||||
}
|
||||
}
|
||||
Task { await viewModel.refreshSelectedTab(api: walletAPI) }
|
||||
}
|
||||
|
||||
@objc private func withdrawTapped() {
|
||||
|
||||
@ -816,23 +816,11 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
|
||||
card.stack.addArrangedSubview(WildReportUI.label("补充证据记录", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary))
|
||||
let items = viewModel.supplementaryEvidences
|
||||
items.enumerated().forEach { index, item in
|
||||
let itemStack = UIStackView()
|
||||
itemStack.axis = .vertical
|
||||
itemStack.spacing = 8
|
||||
|
||||
let header = UIStackView()
|
||||
header.axis = .horizontal
|
||||
header.alignment = .center
|
||||
header.addArrangedSubview(WildReportUI.label(item.submitTime, font: .app(.captionMedium), color: AppColor.textSecondary))
|
||||
header.addArrangedSubview(UIView())
|
||||
header.addArrangedSubview(WildReportUI.label(
|
||||
WildReportSupplementDisplayFormatter.sequenceTitle(forDisplayIndex: index),
|
||||
font: .app(.captionMedium),
|
||||
color: AppColor.primary
|
||||
))
|
||||
itemStack.addArrangedSubview(header)
|
||||
itemStack.addArrangedSubview(WildReportUI.label(item.summaryText, font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0))
|
||||
card.stack.addArrangedSubview(itemStack)
|
||||
let row = WildReportSupplementEvidenceRowView(item: item, displayIndex: index)
|
||||
row.onPreview = { [weak self] in
|
||||
self?.presentSupplementMediaPreview(item)
|
||||
}
|
||||
card.stack.addArrangedSubview(row)
|
||||
if index < items.count - 1 {
|
||||
card.stack.addArrangedSubview(makeDivider())
|
||||
}
|
||||
@ -1288,6 +1276,23 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
|
||||
presentMediaPreview(entries: entries, selectedKey: selectedKey, emptyMessage: "当前处理凭证暂无可预览文件")
|
||||
}
|
||||
|
||||
private func presentSupplementMediaPreview(_ record: WildReportSupplementaryEvidence) {
|
||||
let items = record.evidences.compactMap { evidence -> MediaPreviewItem? in
|
||||
if evidence.isVideo {
|
||||
return .remoteVideo(evidence.fileURL)
|
||||
}
|
||||
if evidence.isImage {
|
||||
return .remoteImage(evidence.fileURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
guard !items.isEmpty else {
|
||||
showToast("当前补充记录暂无可预览文件")
|
||||
return
|
||||
}
|
||||
presentMediaPreview(items: items, startIndex: 0)
|
||||
}
|
||||
|
||||
private func previewEntries(for mediaItems: [WildReportReporterMediaItem]) -> [(key: String, item: MediaPreviewItem)] {
|
||||
mediaItems.compactMap { mediaItem in
|
||||
guard let key = mediaItem.previewKey,
|
||||
@ -1372,6 +1377,79 @@ private final class WildReportDetailIconTextButton: UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// 举报详情页补充证据记录行,包含媒体时提供整行预览交互。
|
||||
private final class WildReportSupplementEvidenceRowView: UIControl {
|
||||
var onPreview: (() -> Void)?
|
||||
|
||||
private let previewEnabled: Bool
|
||||
|
||||
init(item: WildReportSupplementaryEvidence, displayIndex: Int) {
|
||||
previewEnabled = item.hasMedia
|
||||
super.init(frame: .zero)
|
||||
|
||||
let sequenceTitle = WildReportSupplementDisplayFormatter.sequenceTitle(forDisplayIndex: displayIndex)
|
||||
let header = UIStackView()
|
||||
header.axis = .horizontal
|
||||
header.alignment = .center
|
||||
header.spacing = 6
|
||||
header.isUserInteractionEnabled = false
|
||||
header.addArrangedSubview(WildReportUI.label(item.submitTime, font: .app(.captionMedium), color: AppColor.textSecondary))
|
||||
header.addArrangedSubview(UIView())
|
||||
header.addArrangedSubview(WildReportUI.label(sequenceTitle, font: .app(.captionMedium), color: AppColor.primary))
|
||||
|
||||
if previewEnabled {
|
||||
let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||||
chevron.tintColor = AppColor.primary
|
||||
chevron.contentMode = .scaleAspectFit
|
||||
chevron.snp.makeConstraints { make in
|
||||
make.size.equalTo(12)
|
||||
}
|
||||
header.addArrangedSubview(chevron)
|
||||
}
|
||||
|
||||
let summaryLabel = WildReportUI.label(item.summaryText, font: .app(.bodyMedium), color: AppColor.textPrimary, lines: 0)
|
||||
summaryLabel.isUserInteractionEnabled = false
|
||||
let contentStack = UIStackView(arrangedSubviews: [header, summaryLabel])
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 8
|
||||
contentStack.isUserInteractionEnabled = false
|
||||
|
||||
addSubview(contentStack)
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
isEnabled = previewEnabled
|
||||
guard previewEnabled else { return }
|
||||
|
||||
addTarget(self, action: #selector(previewTapped), for: .touchUpInside)
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
accessibilityLabel = [sequenceTitle, item.submitTime, item.summaryText]
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: ",")
|
||||
accessibilityHint = "双击全屏预览图片或视频"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
guard previewEnabled else { return }
|
||||
UIView.animate(withDuration: 0.15) {
|
||||
self.alpha = self.isHighlighted ? 0.55 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func previewTapped() {
|
||||
onPreview?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 举报详情页信息行,适配短文本与多行说明。
|
||||
private final class WildReportDetailInfoRowView: UIView {
|
||||
init(title: String, value: String, multiline: Bool = false, trailingIconName: String? = nil) {
|
||||
|
||||
Reference in New Issue
Block a user