完善钱包提现状态展示与举报摄影师列表交互。
对齐 Android 提现进度与时间线映射,优化钱包与积分兑换页刷新逻辑,并补充相关单元测试与真机测试说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -71,7 +71,7 @@ Swift · UIKit · SnapKit(代码布局,约束写在 `setupUI()` / `setupCons
|
|||||||
|
|
||||||
## 单元测试
|
## 单元测试
|
||||||
|
|
||||||
每增一功能必须附带单元测试且全部通过(`Cmd + U` / `xcodebuild test`),与功能同次提交。
|
每增一功能必须附带单元测试且全部通过,与功能同次提交。单元测试必须在已连接的 iPhone 真机上运行,不使用模拟器;支持使用与开发机处于同一 Wi-Fi 下并已启用无线连接的真机。可在 Xcode 中选择真机后执行 `Cmd + U`,或使用 `xcodebuild test -destination 'platform=iOS,id=<设备 UDID>'`。
|
||||||
|
|
||||||
| 层级 | 优先级 |
|
| 层级 | 优先级 |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
|
|||||||
@ -173,6 +173,16 @@ final class WalletViewModel {
|
|||||||
await loadWithdraw(api: api, refresh: true)
|
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 {
|
func loadMoreWithdrawIfNeeded(currentIndex: Int, api: any WalletPageServing) async {
|
||||||
guard currentIndex >= withdrawRecords.count - 2, withdrawCanLoadMore, !withdrawLoading else { return }
|
guard currentIndex >= withdrawRecords.count - 2, withdrawCanLoadMore, !withdrawLoading else { return }
|
||||||
@ -336,6 +346,85 @@ enum WalletTransactionEntry: Hashable {
|
|||||||
case item(EarningDetailItem)
|
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,管理金额、短信验证码、倒计时和提交状态。
|
/// 提现申请 ViewModel,管理金额、短信验证码、倒计时和提交状态。
|
||||||
final class WithdrawViewModel {
|
final class WithdrawViewModel {
|
||||||
private(set) var withdrawInfo: WithdrawInfoResponse?
|
private(set) var withdrawInfo: WithdrawInfoResponse?
|
||||||
|
|||||||
@ -888,8 +888,7 @@ struct WildReportDetailData: Decodable, Equatable, Hashable {
|
|||||||
id: content.id > 0 ? String(content.id) : content.time,
|
id: content.id > 0 ? String(content.id) : content.time,
|
||||||
submitTime: content.time,
|
submitTime: content.time,
|
||||||
text: content.desc,
|
text: content.desc,
|
||||||
imageCount: content.imageCount,
|
evidences: content.evidences
|
||||||
videoCount: content.videoCount
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -956,8 +955,11 @@ struct WildReportSupplementaryEvidence: Hashable {
|
|||||||
let id: String
|
let id: String
|
||||||
let submitTime: String
|
let submitTime: String
|
||||||
let text: String
|
let text: String
|
||||||
let imageCount: Int
|
let evidences: [WildReportDetailEvidence]
|
||||||
let videoCount: Int
|
|
||||||
|
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 summaryText: String {
|
||||||
var parts: [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.text = record.amount != nil && record.status == 2 ? "+\(formatAmount(record.amount))" : ""
|
||||||
amountLabel.isHidden = amountLabel.text?.isEmpty ?? true
|
amountLabel.isHidden = amountLabel.text?.isEmpty ?? true
|
||||||
timeLabel.text = record.createdAt.isEmpty ? "----" : record.createdAt
|
timeLabel.text = record.createdAt.isEmpty ? "----" : record.createdAt
|
||||||
let style = statusStyle(for: record.status)
|
let display = PointWithdrawStatusDisplay(status: record.status)
|
||||||
statusLabel.text = statusText(for: record.status)
|
let style = statusStyle(for: display.tone)
|
||||||
|
statusLabel.text = display.title
|
||||||
statusLabel.textColor = style.text
|
statusLabel.textColor = style.text
|
||||||
statusLabel.backgroundColor = style.background
|
statusLabel.backgroundColor = style.background
|
||||||
let progress = progress(for: record.status)
|
let progress = display.progress
|
||||||
progressView.progress = Float(progress) / 100.0
|
progressView.progress = Float(progress) / 100.0
|
||||||
progressView.progressTintColor = style.progress
|
progressView.progressTintColor = style.progress
|
||||||
progressView.isHidden = progress == 0
|
progressView.isHidden = progress == 0
|
||||||
@ -498,45 +499,28 @@ private final class PointWithdrawRecordCell: UITableViewCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func statusText(for status: Int) -> String {
|
private func statusStyle(for tone: WalletStatusTone) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
||||||
switch status {
|
switch tone {
|
||||||
case 2: "提现成功"
|
case .success:
|
||||||
case 1: "提现中"
|
|
||||||
case 3: "提现失败"
|
|
||||||
default: "未知"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func statusStyle(for status: Int) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
|
||||||
switch status {
|
|
||||||
case 2:
|
|
||||||
(WalletTokens.successBackground, WalletTokens.success, WalletTokens.summaryBackground)
|
(WalletTokens.successBackground, WalletTokens.success, WalletTokens.summaryBackground)
|
||||||
case 1:
|
case .warning:
|
||||||
(WalletTokens.pointsPendingBackground, WalletTokens.points, WalletTokens.summaryBackground)
|
(WalletTokens.pointsPendingBackground, WalletTokens.points, WalletTokens.summaryBackground)
|
||||||
case 3:
|
case .danger:
|
||||||
(WalletTokens.dangerBackground, WalletTokens.danger, WalletTokens.progressDanger)
|
(WalletTokens.dangerBackground, WalletTokens.danger, WalletTokens.progressDanger)
|
||||||
default:
|
case .info:
|
||||||
(WalletTokens.infoBackground, WalletTokens.summaryBackground, WalletTokens.summaryBackground)
|
(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 {
|
private func infoText(for record: PointWithdrawItem) -> String {
|
||||||
|
let display = PointWithdrawStatusDisplay(status: record.status)
|
||||||
|
let value: String
|
||||||
switch record.status {
|
switch record.status {
|
||||||
case 3:
|
case 3: value = record.reviewedAt
|
||||||
return "审核时间:\(record.reviewedAt.isEmpty ? "暂无" : record.reviewedAt)"
|
case 2: value = record.paymentTime
|
||||||
case 2:
|
default: value = record.estimatedReceiptTime
|
||||||
return "到账时间:\(record.paymentTime.isEmpty ? "暂无" : record.paymentTime)"
|
|
||||||
default:
|
|
||||||
return "预计到账时间:\(record.estimatedReceiptTime.isEmpty ? "暂无" : record.estimatedReceiptTime)"
|
|
||||||
}
|
}
|
||||||
|
return "\(display.timeTitle)\(value.isEmpty ? "暂无" : value)"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatAmount(_ value: Double?) -> String {
|
private func formatAmount(_ value: Double?) -> String {
|
||||||
|
|||||||
@ -33,14 +33,16 @@ final class WalletWithdrawRecordCell: UITableViewCell {
|
|||||||
amountLabel.text = WalletViewModel.formatAmount(record.amount)
|
amountLabel.text = WalletViewModel.formatAmount(record.amount)
|
||||||
statusLabel.text = record.statusLabel.isEmpty ? "--" : record.statusLabel
|
statusLabel.text = record.statusLabel.isEmpty ? "--" : record.statusLabel
|
||||||
timeLabel.text = record.createdAt.isEmpty ? "--" : record.createdAt
|
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.textColor = style.text
|
||||||
statusLabel.backgroundColor = style.background
|
statusLabel.backgroundColor = style.background
|
||||||
progressView.progressTintColor = style.progress
|
progressView.progressTintColor = style.progress
|
||||||
progressView.progress = progress(for: record.settlementStatus)
|
let progress = Float(display.progress) / 100
|
||||||
configureSteps(progress: progress(for: record.settlementStatus), tint: style.progress)
|
progressView.progress = progress
|
||||||
progressView.isHidden = record.settlementStatus == 50
|
configureSteps(progress: progress, tint: style.progress)
|
||||||
stepsStack.isHidden = record.settlementStatus == 50
|
progressView.isHidden = !display.showsTimeline
|
||||||
|
stepsStack.isHidden = !display.showsTimeline
|
||||||
infoLabel.attributedText = infoText(for: record)
|
infoLabel.attributedText = infoText(for: record)
|
||||||
infoLabel.isHidden = infoLabel.attributedText?.length == 0
|
infoLabel.isHidden = infoLabel.attributedText?.length == 0
|
||||||
infoLabel.snp.remakeConstraints { make in
|
infoLabel.snp.remakeConstraints { make in
|
||||||
@ -141,30 +143,19 @@ final class WalletWithdrawRecordCell: UITableViewCell {
|
|||||||
stepsStack.addArrangedSubview(percent)
|
stepsStack.addArrangedSubview(percent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func statusStyle(for status: Int) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
private func statusStyle(for tone: WalletStatusTone) -> (background: UIColor, text: UIColor, progress: UIColor) {
|
||||||
switch status {
|
switch tone {
|
||||||
case 50, 20:
|
case .success:
|
||||||
(WalletTokens.settlementSuccessBackground, WalletTokens.settlementSuccess, WalletTokens.summaryBackground)
|
(WalletTokens.settlementSuccessBackground, WalletTokens.settlementSuccess, WalletTokens.summaryBackground)
|
||||||
case 60, 30:
|
case .danger:
|
||||||
(WalletTokens.settlementDangerBackground, WalletTokens.progressDanger, WalletTokens.progressDanger)
|
(WalletTokens.settlementDangerBackground, WalletTokens.progressDanger, WalletTokens.progressDanger)
|
||||||
case 40:
|
case .warning:
|
||||||
(WalletTokens.warningBackground, WalletTokens.warning, WalletTokens.summaryBackground)
|
(WalletTokens.warningBackground, WalletTokens.warning, WalletTokens.summaryBackground)
|
||||||
default:
|
case .info:
|
||||||
(WalletTokens.infoBackground, WalletTokens.summaryBackground, WalletTokens.summaryBackground)
|
(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 {
|
private func infoText(for record: WalletWithdrawItem) -> NSAttributedString {
|
||||||
let result = NSMutableAttributedString()
|
let result = NSMutableAttributedString()
|
||||||
let normal: [NSAttributedString.Key: Any] = [
|
let normal: [NSAttributedString.Key: Any] = [
|
||||||
|
|||||||
@ -320,14 +320,7 @@ final class WalletViewController: BaseViewController, UITableViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func refreshPulled() {
|
@objc private func refreshPulled() {
|
||||||
Task {
|
Task { await viewModel.refreshSelectedTab(api: walletAPI) }
|
||||||
switch viewModel.selectedTab {
|
|
||||||
case .withdraw:
|
|
||||||
await viewModel.refreshWithdraw(api: walletAPI)
|
|
||||||
case .transaction:
|
|
||||||
await viewModel.refreshTransaction(api: walletAPI)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func withdrawTapped() {
|
@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))
|
card.stack.addArrangedSubview(WildReportUI.label("补充证据记录", font: .systemFont(ofSize: 18, weight: .bold), color: AppColor.textPrimary))
|
||||||
let items = viewModel.supplementaryEvidences
|
let items = viewModel.supplementaryEvidences
|
||||||
items.enumerated().forEach { index, item in
|
items.enumerated().forEach { index, item in
|
||||||
let itemStack = UIStackView()
|
let row = WildReportSupplementEvidenceRowView(item: item, displayIndex: index)
|
||||||
itemStack.axis = .vertical
|
row.onPreview = { [weak self] in
|
||||||
itemStack.spacing = 8
|
self?.presentSupplementMediaPreview(item)
|
||||||
|
}
|
||||||
let header = UIStackView()
|
card.stack.addArrangedSubview(row)
|
||||||
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)
|
|
||||||
if index < items.count - 1 {
|
if index < items.count - 1 {
|
||||||
card.stack.addArrangedSubview(makeDivider())
|
card.stack.addArrangedSubview(makeDivider())
|
||||||
}
|
}
|
||||||
@ -1288,6 +1276,23 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
|
|||||||
presentMediaPreview(entries: entries, selectedKey: selectedKey, emptyMessage: "当前处理凭证暂无可预览文件")
|
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)] {
|
private func previewEntries(for mediaItems: [WildReportReporterMediaItem]) -> [(key: String, item: MediaPreviewItem)] {
|
||||||
mediaItems.compactMap { mediaItem in
|
mediaItems.compactMap { mediaItem in
|
||||||
guard let key = mediaItem.previewKey,
|
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 {
|
private final class WildReportDetailInfoRowView: UIView {
|
||||||
init(title: String, value: String, multiline: Bool = false, trailingIconName: String? = nil) {
|
init(title: String, value: String, multiline: Bool = false, trailingIconName: String? = nil) {
|
||||||
|
|||||||
@ -37,6 +37,7 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
func testWalletAPIUsesExpectedRequests() async throws {
|
func testWalletAPIUsesExpectedRequests() async throws {
|
||||||
let session = MockURLSession(responses: [
|
let session = MockURLSession(responses: [
|
||||||
Self.envelope(#"{"amount_total":"100.00","amount_current_balance":"80.00","amount_withdrawable":"50.00"}"#),
|
Self.envelope(#"{"amount_total":"100.00","amount_current_balance":"80.00","amount_withdrawable":"50.00"}"#),
|
||||||
|
Self.envelope(#"{"total":0,"list":[]}"#),
|
||||||
Self.envelope(#"{"total":0,"item":[]}"#),
|
Self.envelope(#"{"total":0,"item":[]}"#),
|
||||||
Self.envelope(#"{"total_amount":"0.00","total_points":0,"total":0,"list":[]}"#),
|
Self.envelope(#"{"total_amount":"0.00","total_points":0,"total":0,"list":[]}"#),
|
||||||
Self.envelope(#"{"amount_withdrawable":"50.00","min_withdraw_amount":"1","max_single_withdraw_amount":"100","max_daily_withdraw_amount":"200","user_phone":"13800138000","bank_card":{"real_name":"张三","bank_name":"招商银行","card_number":"622200001234"},"settlement":"T+1","withdraw_info":["说明"]}"#),
|
Self.envelope(#"{"amount_withdrawable":"50.00","min_withdraw_amount":"1","max_single_withdraw_amount":"100","max_daily_withdraw_amount":"200","user_phone":"13800138000","bank_card":{"real_name":"张三","bank_name":"招商银行","card_number":"622200001234"},"settlement":"T+1","withdraw_info":["说明"]}"#),
|
||||||
@ -49,6 +50,7 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
let api = WalletAPI(client: APIClient(environment: .testing, session: session))
|
let api = WalletAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
|
||||||
_ = try await api.walletSummary(type: 0)
|
_ = try await api.walletSummary(type: 0)
|
||||||
|
_ = try await api.walletTransactionList(startDate: "2026-07-01", endDate: "2026-07-09", page: 0, pageSize: 0, type: 2)
|
||||||
_ = try await api.walletWithdrawList(page: 0, pageSize: 0)
|
_ = try await api.walletWithdrawList(page: 0, pageSize: 0)
|
||||||
_ = try await api.earningDetail(startDate: "2026-07-01", endDate: "2026-07-09", page: 2, pageSize: 10)
|
_ = try await api.earningDetail(startDate: "2026-07-01", endDate: "2026-07-09", page: 2, pageSize: 10)
|
||||||
_ = try await api.withdrawInfo()
|
_ = try await api.withdrawInfo()
|
||||||
@ -60,6 +62,7 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||||
"/api/yf-handset-app/photog/wallet/summary",
|
"/api/yf-handset-app/photog/wallet/summary",
|
||||||
|
"/api/yf-handset-app/photog/wallet/transaction-list",
|
||||||
"/api/yf-handset-app/photog/wallet/withdraw-list",
|
"/api/yf-handset-app/photog/wallet/withdraw-list",
|
||||||
"/api/yf-handset-app/photog/wallet/earning-detail",
|
"/api/yf-handset-app/photog/wallet/earning-detail",
|
||||||
"/api/yf-handset-app/photog/wallet/withdraw-info",
|
"/api/yf-handset-app/photog/wallet/withdraw-info",
|
||||||
@ -69,14 +72,21 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
"/api/yf-handset-app/photog/point/withdraw/apply",
|
"/api/yf-handset-app/photog/point/withdraw/apply",
|
||||||
"/api/yf-handset-app/photog/point/withdraw/list",
|
"/api/yf-handset-app/photog/point/withdraw/list",
|
||||||
])
|
])
|
||||||
XCTAssertEqual(Self.queryItems(from: session.requests[1])["page"], "1")
|
XCTAssertEqual(session.requests.map(\.httpMethod), ["GET", "GET", "GET", "GET", "GET", "POST", "POST", "POST", "POST", "POST"])
|
||||||
XCTAssertEqual(Self.queryItems(from: session.requests[2])["start_date"], "2026-07-01")
|
XCTAssertEqual(Self.queryItems(from: session.requests[0])["type"], "0")
|
||||||
XCTAssertEqual(Self.queryItems(from: session.requests[6])["staff_id"], "9")
|
XCTAssertEqual(Self.queryItems(from: session.requests[1]), [
|
||||||
XCTAssertNil(session.requests[4].httpBody)
|
"start_date": "2026-07-01", "end_date": "2026-07-09", "page": "1", "page_size": "1", "type": "2",
|
||||||
XCTAssertNil(session.requests[6].httpBody)
|
])
|
||||||
XCTAssertEqual(Self.bodyString(from: session.requests[5]).contains(#""sms_code":"123456""#), true)
|
XCTAssertEqual(Self.queryItems(from: session.requests[2]), ["page": "1", "page_size": "1"])
|
||||||
XCTAssertEqual(Self.bodyString(from: session.requests[7]).contains(#""points":24"#), true)
|
XCTAssertEqual(Self.queryItems(from: session.requests[3]), [
|
||||||
XCTAssertEqual(Self.bodyString(from: session.requests[8]).contains(#""page_size":10"#), true)
|
"start_date": "2026-07-01", "end_date": "2026-07-09", "page": "2", "page_size": "10",
|
||||||
|
])
|
||||||
|
XCTAssertEqual(Self.queryItems(from: session.requests[7])["staff_id"], "9")
|
||||||
|
XCTAssertNil(session.requests[5].httpBody)
|
||||||
|
XCTAssertNil(session.requests[7].httpBody)
|
||||||
|
XCTAssertEqual(Self.bodyString(from: session.requests[6]).contains(#""sms_code":"123456""#), true)
|
||||||
|
XCTAssertEqual(Self.bodyString(from: session.requests[8]).contains(#""points":24"#), true)
|
||||||
|
XCTAssertEqual(Self.bodyString(from: session.requests[9]).contains(#""page_size":10"#), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWalletViewModelLoadsAndPaginates() async {
|
func testWalletViewModelLoadsAndPaginates() async {
|
||||||
@ -139,6 +149,38 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
XCTAssertEqual(pointsViewModel.withdrawRecords.count, 1)
|
XCTAssertEqual(pointsViewModel.withdrawRecords.count, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCurrentTabRefreshAndPaginationThresholds() async {
|
||||||
|
let api = FakeWalletPageService()
|
||||||
|
api.pagedRecordTotal = 25
|
||||||
|
let walletViewModel = WalletViewModel(staffIdProvider: { 9 })
|
||||||
|
let pointsViewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||||
|
|
||||||
|
await walletViewModel.refreshSelectedTab(api: api)
|
||||||
|
XCTAssertEqual(api.withdrawListPages, [1])
|
||||||
|
XCTAssertTrue(api.earningQueries.isEmpty)
|
||||||
|
|
||||||
|
await walletViewModel.loadMoreWithdrawIfNeeded(currentIndex: 7, api: api)
|
||||||
|
XCTAssertEqual(api.withdrawListPages, [1])
|
||||||
|
await walletViewModel.loadMoreWithdrawIfNeeded(currentIndex: 8, api: api)
|
||||||
|
XCTAssertEqual(api.withdrawListPages, [1, 2])
|
||||||
|
|
||||||
|
await walletViewModel.selectTab(.transaction, api: api)
|
||||||
|
XCTAssertEqual(api.earningQueries.map(\.page), [1])
|
||||||
|
await walletViewModel.refreshSelectedTab(api: api)
|
||||||
|
XCTAssertEqual(api.earningQueries.map(\.page), [1, 1])
|
||||||
|
await walletViewModel.loadMoreTransactionIfNeeded(currentIndex: 8, api: api)
|
||||||
|
XCTAssertEqual(api.earningQueries.map(\.page), [1, 1])
|
||||||
|
await walletViewModel.loadMoreTransactionIfNeeded(currentIndex: 9, api: api)
|
||||||
|
XCTAssertEqual(api.earningQueries.map(\.page), [1, 1, 2])
|
||||||
|
|
||||||
|
await pointsViewModel.refreshWithdrawList(api: api)
|
||||||
|
XCTAssertEqual(api.pointWithdrawListPages, [1])
|
||||||
|
await pointsViewModel.loadMoreWithdrawListIfNeeded(currentIndex: 7, api: api)
|
||||||
|
XCTAssertEqual(api.pointWithdrawListPages, [1])
|
||||||
|
await pointsViewModel.loadMoreWithdrawListIfNeeded(currentIndex: 8, api: api)
|
||||||
|
XCTAssertEqual(api.pointWithdrawListPages, [1, 2])
|
||||||
|
}
|
||||||
|
|
||||||
func testWalletWithdrawDestinationFlow() async throws {
|
func testWalletWithdrawDestinationFlow() async throws {
|
||||||
let viewModel = WalletViewModel()
|
let viewModel = WalletViewModel()
|
||||||
|
|
||||||
@ -201,6 +243,22 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.countdown, 59)
|
XCTAssertEqual(viewModel.countdown, 59)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testWithdrawAllAndSubmitFailureKeepsForm() async {
|
||||||
|
let api = FakeWalletPageService()
|
||||||
|
let viewModel = WithdrawViewModel()
|
||||||
|
await viewModel.load(api: api)
|
||||||
|
viewModel.withdrawAll()
|
||||||
|
viewModel.updateVerificationCode("123456")
|
||||||
|
api.shouldFailWithdrawApply = true
|
||||||
|
|
||||||
|
let success = await viewModel.submit(api: api)
|
||||||
|
|
||||||
|
XCTAssertFalse(success)
|
||||||
|
XCTAssertEqual(viewModel.amount, "50.00")
|
||||||
|
XCTAssertEqual(viewModel.verificationCode, "123456")
|
||||||
|
XCTAssertEqual(viewModel.errorMessage, "请求失败")
|
||||||
|
}
|
||||||
|
|
||||||
func testPointsRedemptionViewModelClampsAndRefreshes() async {
|
func testPointsRedemptionViewModelClampsAndRefreshes() async {
|
||||||
let api = FakeWalletPageService()
|
let api = FakeWalletPageService()
|
||||||
let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||||
@ -218,6 +276,20 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.withdrawRecords.count, 1)
|
XCTAssertEqual(viewModel.withdrawRecords.count, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPointApplyFailureKeepsInputAndExistingRecords() async {
|
||||||
|
let api = FakeWalletPageService()
|
||||||
|
let viewModel = PointsRedemptionViewModel(staffIdProvider: { 9 })
|
||||||
|
await viewModel.loadInitial(api: api)
|
||||||
|
viewModel.updatePointsInput("24")
|
||||||
|
api.shouldFailPointApply = true
|
||||||
|
|
||||||
|
await viewModel.applyWithdraw(api: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.pointsInput, "24")
|
||||||
|
XCTAssertEqual(viewModel.withdrawRecords.count, 1)
|
||||||
|
XCTAssertEqual(viewModel.errorMessage, "请求失败")
|
||||||
|
}
|
||||||
|
|
||||||
func testWalletTokensAndHomeIconMatchAndroid() {
|
func testWalletTokensAndHomeIconMatchAndroid() {
|
||||||
XCTAssertEqual(WalletTokens.pageBackground.walletHexRGB, 0xF5F6F8)
|
XCTAssertEqual(WalletTokens.pageBackground.walletHexRGB, 0xF5F6F8)
|
||||||
XCTAssertEqual(WalletTokens.withdrawPageBackground.walletHexRGB, 0xF9FAFB)
|
XCTAssertEqual(WalletTokens.withdrawPageBackground.walletHexRGB, 0xF9FAFB)
|
||||||
@ -230,6 +302,32 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
XCTAssertNotNil(UIImage(named: "home_menu_wallet"))
|
XCTAssertNotNil(UIImage(named: "home_menu_wallet"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testWalletAndPointStatusPresentationMatchesAndroid() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
[10, 20, 30, 40, 50, 60].map { WalletWithdrawStatusDisplay(status: $0).progress },
|
||||||
|
[10, 80, 100, 40, 100, 0]
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
[10, 20, 30, 40, 50, 60].map { WalletWithdrawStatusDisplay(status: $0).showsTimeline },
|
||||||
|
[true, true, true, true, false, true]
|
||||||
|
)
|
||||||
|
|
||||||
|
let pending = PointWithdrawStatusDisplay(status: 1)
|
||||||
|
XCTAssertEqual(pending.title, "提现中")
|
||||||
|
XCTAssertEqual(pending.progress, 40)
|
||||||
|
XCTAssertEqual(pending.timeTitle, "预计到账时间:")
|
||||||
|
|
||||||
|
let success = PointWithdrawStatusDisplay(status: 2)
|
||||||
|
XCTAssertEqual(success.title, "提现成功")
|
||||||
|
XCTAssertEqual(success.progress, 0)
|
||||||
|
XCTAssertEqual(success.timeTitle, "到账时间:")
|
||||||
|
|
||||||
|
let failed = PointWithdrawStatusDisplay(status: 3)
|
||||||
|
XCTAssertEqual(failed.title, "提现失败")
|
||||||
|
XCTAssertEqual(failed.progress, 100)
|
||||||
|
XCTAssertEqual(failed.timeTitle, "审核时间:")
|
||||||
|
}
|
||||||
|
|
||||||
private static func envelope(_ dataJSON: String) -> Data {
|
private static func envelope(_ dataJSON: String) -> Data {
|
||||||
Data(#"{"code":100000,"msg":"success","data":\#(dataJSON)}"#.utf8)
|
Data(#"{"code":100000,"msg":"success","data":\#(dataJSON)}"#.utf8)
|
||||||
}
|
}
|
||||||
@ -252,11 +350,16 @@ final class WalletFeatureTests: XCTestCase {
|
|||||||
/// 钱包页面服务测试替身。
|
/// 钱包页面服务测试替身。
|
||||||
private final class FakeWalletPageService: WalletPageServing {
|
private final class FakeWalletPageService: WalletPageServing {
|
||||||
private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
|
private(set) var earningQueries: [(start: String, end: String, page: Int)] = []
|
||||||
|
private(set) var withdrawListPages: [Int] = []
|
||||||
|
private(set) var pointWithdrawListPages: [Int] = []
|
||||||
private(set) var withdrawApplyRequests: [(amount: String, smsCode: String)] = []
|
private(set) var withdrawApplyRequests: [(amount: String, smsCode: String)] = []
|
||||||
private(set) var pointApplyRequests: [Int] = []
|
private(set) var pointApplyRequests: [Int] = []
|
||||||
|
var pagedRecordTotal = 1
|
||||||
var shouldFailWithdrawList = false
|
var shouldFailWithdrawList = false
|
||||||
var shouldFailEarningDetail = false
|
var shouldFailEarningDetail = false
|
||||||
var shouldFailPointWithdrawList = false
|
var shouldFailPointWithdrawList = false
|
||||||
|
var shouldFailWithdrawApply = false
|
||||||
|
var shouldFailPointApply = false
|
||||||
|
|
||||||
func walletSummary(type: Int) async throws -> WalletSummaryResponse {
|
func walletSummary(type: Int) async throws -> WalletSummaryResponse {
|
||||||
WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "80.00", amountWithdrawable: "50.00")
|
WalletSummaryResponse(amountTotal: "100.00", amountCurrentBalance: "80.00", amountWithdrawable: "50.00")
|
||||||
@ -274,25 +377,32 @@ private final class FakeWalletPageService: WalletPageServing {
|
|||||||
|
|
||||||
func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse {
|
func walletWithdrawList(page: Int, pageSize: Int) async throws -> WalletWithdrawListResponse {
|
||||||
if shouldFailWithdrawList { throw FakeWalletError.requestFailed }
|
if shouldFailWithdrawList { throw FakeWalletError.requestFailed }
|
||||||
WalletWithdrawListResponse(
|
withdrawListPages.append(page)
|
||||||
total: 1,
|
let count = pagedRecordTotal > 1 ? pageSize : 1
|
||||||
item: [WalletWithdrawItem(id: page, amount: "10.00", createdAt: "2026-07-01", settlementStatus: 40, statusLabel: "打款中")]
|
return WalletWithdrawListResponse(
|
||||||
|
total: pagedRecordTotal,
|
||||||
|
item: (0 ..< count).map {
|
||||||
|
WalletWithdrawItem(id: page * 100 + $0, amount: "10.00", createdAt: "2026-07-01", settlementStatus: 40, statusLabel: "打款中")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func earningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> EarningDetailResponse {
|
func earningDetail(startDate: String, endDate: String, page: Int, pageSize: Int) async throws -> EarningDetailResponse {
|
||||||
if shouldFailEarningDetail { throw FakeWalletError.requestFailed }
|
if shouldFailEarningDetail { throw FakeWalletError.requestFailed }
|
||||||
earningQueries.append((startDate, endDate, page))
|
earningQueries.append((startDate, endDate, page))
|
||||||
|
let count = pagedRecordTotal > 1 ? pageSize : 1
|
||||||
return EarningDetailResponse(
|
return EarningDetailResponse(
|
||||||
totalAmount: "12.30",
|
totalAmount: "12.30",
|
||||||
totalPoints: 7,
|
totalPoints: 7,
|
||||||
total: 1,
|
total: pagedRecordTotal,
|
||||||
list: [
|
list: [
|
||||||
EarningDetailGroup(
|
EarningDetailGroup(
|
||||||
date: "2026-07-01",
|
date: "2026-07-\(String(format: "%02d", page))",
|
||||||
dayAmount: "12.30",
|
dayAmount: "12.30",
|
||||||
dayPoints: 7,
|
dayPoints: 7,
|
||||||
items: [EarningDetailItem(id: page, amount: "12.30", points: 7, typeLabel: "订单收益", createdAt: "10:00", withdrawLabel: "可提现")]
|
items: (0 ..< count).map {
|
||||||
|
EarningDetailItem(id: page * 100 + $0, amount: "12.30", points: 7, typeLabel: "订单收益", createdAt: "10:00", withdrawLabel: "可提现")
|
||||||
|
}
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -314,6 +424,7 @@ private final class FakeWalletPageService: WalletPageServing {
|
|||||||
func withdrawSendSMS() async throws {}
|
func withdrawSendSMS() async throws {}
|
||||||
|
|
||||||
func withdrawApply(amount: String, smsCode: String) async throws {
|
func withdrawApply(amount: String, smsCode: String) async throws {
|
||||||
|
if shouldFailWithdrawApply { throw FakeWalletError.requestFailed }
|
||||||
withdrawApplyRequests.append((amount, smsCode))
|
withdrawApplyRequests.append((amount, smsCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,12 +433,20 @@ private final class FakeWalletPageService: WalletPageServing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pointWithdrawApply(points: Int, remark: String) async throws {
|
func pointWithdrawApply(points: Int, remark: String) async throws {
|
||||||
|
if shouldFailPointApply { throw FakeWalletError.requestFailed }
|
||||||
pointApplyRequests.append(points)
|
pointApplyRequests.append(points)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse {
|
func pointWithdrawList(status: Int?, page: Int, pageSize: Int) async throws -> PointWithdrawListResponse {
|
||||||
if shouldFailPointWithdrawList { throw FakeWalletError.requestFailed }
|
if shouldFailPointWithdrawList { throw FakeWalletError.requestFailed }
|
||||||
PointWithdrawListResponse(total: 1, list: [PointWithdrawItem(id: page, points: 24, amount: 2, status: 1, createdAt: "2026-07-01")])
|
pointWithdrawListPages.append(page)
|
||||||
|
let count = pagedRecordTotal > 1 ? pageSize : 1
|
||||||
|
return PointWithdrawListResponse(
|
||||||
|
total: pagedRecordTotal,
|
||||||
|
list: (0 ..< count).map {
|
||||||
|
PointWithdrawItem(id: page * 100 + $0, points: 24, amount: 2, status: 1, createdAt: "2026-07-01")
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -548,6 +548,91 @@ final class WildPhotographerReportTests: XCTestCase {
|
|||||||
XCTAssertEqual(record.imageCount, 1)
|
XCTAssertEqual(record.imageCount, 1)
|
||||||
XCTAssertEqual(record.videoCount, 1)
|
XCTAssertEqual(record.videoCount, 1)
|
||||||
XCTAssertEqual(detail.supplementRecords.first?.summaryText, "补充说明 · 1张图片")
|
XCTAssertEqual(detail.supplementRecords.first?.summaryText, "补充说明 · 1张图片")
|
||||||
|
XCTAssertEqual(detail.supplementRecords.first?.evidences.first?.fileURL, "https://example.com/supplement.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSupplementRecordsPreserveMixedMediaOrderAndSummary() throws {
|
||||||
|
let evidences = [
|
||||||
|
WildReportDetailEvidence(
|
||||||
|
fileType: 1,
|
||||||
|
fileTypeText: "图片",
|
||||||
|
fileURL: "https://example.com/one.jpg",
|
||||||
|
fileName: "one.jpg"
|
||||||
|
),
|
||||||
|
WildReportDetailEvidence(
|
||||||
|
fileType: 2,
|
||||||
|
fileTypeText: "视频",
|
||||||
|
fileURL: "https://example.com/two.mp4",
|
||||||
|
fileName: "two.mp4"
|
||||||
|
),
|
||||||
|
WildReportDetailEvidence(
|
||||||
|
fileType: 5,
|
||||||
|
fileTypeText: "图片",
|
||||||
|
fileURL: "https://example.com/three.png",
|
||||||
|
fileName: "three.png"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
let detail = WildReportDetailData(
|
||||||
|
supplements: [
|
||||||
|
WildReportDetailContent(
|
||||||
|
type: "supplement",
|
||||||
|
id: 7,
|
||||||
|
desc: "补充现场材料",
|
||||||
|
time: "2026-07-13 10:00:00",
|
||||||
|
evidences: evidences
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
let record = try XCTUnwrap(detail.supplementRecords.first)
|
||||||
|
|
||||||
|
XCTAssertEqual(record.evidences.map(\.fileType), [1, 2, 5])
|
||||||
|
XCTAssertEqual(record.evidences.map(\.fileURL), [
|
||||||
|
"https://example.com/one.jpg",
|
||||||
|
"https://example.com/two.mp4",
|
||||||
|
"https://example.com/three.png"
|
||||||
|
])
|
||||||
|
XCTAssertEqual(record.imageCount, 2)
|
||||||
|
XCTAssertEqual(record.videoCount, 1)
|
||||||
|
XCTAssertTrue(record.hasMedia)
|
||||||
|
XCTAssertEqual(record.summaryText, "补充现场材料 · 2张图片 · 1段视频")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSupplementRecordsFallBackToReportContentsAndKeepMedia() throws {
|
||||||
|
let detail = WildReportDetailData(
|
||||||
|
supplements: [],
|
||||||
|
reportContents: [
|
||||||
|
WildReportDetailContent(
|
||||||
|
type: "report",
|
||||||
|
id: 1,
|
||||||
|
desc: "初始举报",
|
||||||
|
time: "2026-07-13 09:00:00"
|
||||||
|
),
|
||||||
|
WildReportDetailContent(
|
||||||
|
type: "supplement",
|
||||||
|
id: 2,
|
||||||
|
desc: "补充视频",
|
||||||
|
time: "2026-07-13 10:00:00",
|
||||||
|
evidences: [
|
||||||
|
WildReportDetailEvidence(
|
||||||
|
fileType: 2,
|
||||||
|
fileTypeText: "视频",
|
||||||
|
fileURL: "https://example.com/fallback.mp4",
|
||||||
|
fileName: "fallback.mp4"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
let record = try XCTUnwrap(detail.supplementRecords.first)
|
||||||
|
|
||||||
|
XCTAssertEqual(detail.supplementRecords.count, 1)
|
||||||
|
XCTAssertEqual(record.id, "2")
|
||||||
|
XCTAssertEqual(record.evidences.map(\.fileURL), ["https://example.com/fallback.mp4"])
|
||||||
|
XCTAssertEqual(record.imageCount, 0)
|
||||||
|
XCTAssertEqual(record.videoCount, 1)
|
||||||
|
XCTAssertEqual(record.summaryText, "补充视频 · 1段视频")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSupplementRecordSequenceTitleStartsFromDisplayOrder() {
|
func testSupplementRecordSequenceTitleStartsFromDisplayOrder() {
|
||||||
|
|||||||
Reference in New Issue
Block a user