fix: 优化相册素材状态与删除保护
This commit is contained in:
@@ -224,7 +224,7 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册素材网格角标类别,用于稳定映射文案优先级和语义颜色。
|
||||
/// 相册素材网格角标类别,用于稳定映射独立业务状态和语义颜色。
|
||||
enum TravelAlbumMaterialBadgeKind: Sendable, Equatable {
|
||||
case purchased
|
||||
case pending
|
||||
@@ -241,14 +241,15 @@ struct TravelAlbumMaterialBadgePresentation: Sendable, Equatable {
|
||||
}
|
||||
|
||||
extension TravelAlbumMaterial {
|
||||
/// 按 AI 修图状态和购买状态生成网格角标;返回 nil 时隐藏角标。
|
||||
var badgePresentation: TravelAlbumMaterialBadgePresentation? {
|
||||
if aiRetouchStatus == 0 {
|
||||
return isPurchased
|
||||
? TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
|
||||
: nil
|
||||
}
|
||||
/// 生成购买状态角标;购买状态与修图状态互不覆盖。
|
||||
var purchaseBadgePresentation: TravelAlbumMaterialBadgePresentation? {
|
||||
isPurchased
|
||||
? TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
|
||||
: nil
|
||||
}
|
||||
|
||||
/// 生成 AI 修图状态角标;未进入修图流程时返回 nil。
|
||||
var aiRetouchBadgePresentation: TravelAlbumMaterialBadgePresentation? {
|
||||
let statusName = aiRetouchStatusName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
switch aiRetouchStatus {
|
||||
case 1:
|
||||
|
||||
@@ -48,6 +48,7 @@ final class TravelAlbumDetailViewModel {
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isSelectionMode = false
|
||||
private(set) var selectedMaterialIds: Set<Int> = []
|
||||
private var selectedMaterialPurchaseStates: [Int: Bool] = [:]
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
@@ -142,12 +143,29 @@ final class TravelAlbumDetailViewModel {
|
||||
selectedTab == .all ? allPhotoCount : purchasedPhotoCount
|
||||
}
|
||||
|
||||
/// 当前选中素材中的已购买数量。
|
||||
var selectedPurchasedMaterialCount: Int {
|
||||
selectedMaterialIds.count - selectedDeletableMaterialIds.count
|
||||
}
|
||||
|
||||
/// 删除确认弹窗文案;选中已购素材时明确说明保护规则与实际删除数量。
|
||||
var deleteSelectedConfirmationMessage: String {
|
||||
let selectedCount = selectedMaterialIds.count
|
||||
let purchasedCount = selectedPurchasedMaterialCount
|
||||
guard purchasedCount > 0 else {
|
||||
return "确定删除选中的 \(selectedCount) 张素材吗?"
|
||||
}
|
||||
let deletableCount = selectedCount - purchasedCount
|
||||
return "已选择 \(selectedCount) 张素材,其中 \(purchasedCount) 张已购买。"
|
||||
+ "不会删除已购买的项目,只会删除 \(deletableCount) 张未购买的项目。"
|
||||
}
|
||||
|
||||
/// 切换素材 tab。
|
||||
func selectTab(_ tab: Tab, api: any TravelAlbumServing) async {
|
||||
guard selectedTab != tab else { return }
|
||||
selectedTab = tab
|
||||
isSelectionMode = false
|
||||
selectedMaterialIds = []
|
||||
clearMaterialSelection()
|
||||
notifyStateChange()
|
||||
await loadMaterials(reset: true, api: api)
|
||||
}
|
||||
@@ -223,10 +241,9 @@ final class TravelAlbumDetailViewModel {
|
||||
|
||||
/// 切换选择模式。
|
||||
func toggleSelectionMode() {
|
||||
guard selectedTab == .all else { return }
|
||||
isSelectionMode.toggle()
|
||||
if !isSelectionMode {
|
||||
selectedMaterialIds = []
|
||||
clearMaterialSelection()
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
@@ -234,14 +251,12 @@ final class TravelAlbumDetailViewModel {
|
||||
/// 切换素材选中状态。
|
||||
func toggleMaterialSelection(_ material: TravelAlbumMaterial) {
|
||||
guard isSelectionMode else { return }
|
||||
guard material.status == 1 else {
|
||||
onShowMessage?("仅未购买素材可删除")
|
||||
return
|
||||
}
|
||||
if selectedMaterialIds.contains(material.id) {
|
||||
selectedMaterialIds.remove(material.id)
|
||||
selectedMaterialPurchaseStates.removeValue(forKey: material.id)
|
||||
} else {
|
||||
selectedMaterialIds.insert(material.id)
|
||||
selectedMaterialPurchaseStates[material.id] = material.isPurchased
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
@@ -249,7 +264,7 @@ final class TravelAlbumDetailViewModel {
|
||||
/// AI 修图任务提交成功后退出选择模式并清空当前选择。
|
||||
func completeAIRetouchSubmission() {
|
||||
isSelectionMode = false
|
||||
selectedMaterialIds = []
|
||||
clearMaterialSelection()
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
@@ -264,6 +279,7 @@ final class TravelAlbumDetailViewModel {
|
||||
guard let index = materials.firstIndex(where: { $0.id == id }) else { return }
|
||||
let material = materials.remove(at: index)
|
||||
selectedMaterialIds.remove(id)
|
||||
selectedMaterialPurchaseStates.removeValue(forKey: id)
|
||||
allPhotoCount = max(0, allPhotoCount - 1)
|
||||
if material.isPurchased {
|
||||
purchasedPhotoCount = max(0, purchasedPhotoCount - 1)
|
||||
@@ -273,16 +289,20 @@ final class TravelAlbumDetailViewModel {
|
||||
|
||||
/// 删除已选素材。
|
||||
func deleteSelectedMaterials(api: any TravelAlbumServing) async {
|
||||
let ids = selectedMaterialIds.sorted()
|
||||
guard !ids.isEmpty else {
|
||||
guard !selectedMaterialIds.isEmpty else {
|
||||
onShowMessage?("请选择要删除的素材")
|
||||
return
|
||||
}
|
||||
let ids = selectedDeletableMaterialIds
|
||||
guard !ids.isEmpty else {
|
||||
onShowMessage?("选中的素材均已购买,未删除任何项目")
|
||||
return
|
||||
}
|
||||
do {
|
||||
try await api.batchDeleteMaterials(ids: ids)
|
||||
onShowMessage?("删除成功")
|
||||
isSelectionMode = false
|
||||
selectedMaterialIds = []
|
||||
clearMaterialSelection()
|
||||
await refreshAll(api: api)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
@@ -307,4 +327,18 @@ final class TravelAlbumDetailViewModel {
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
private func clearMaterialSelection() {
|
||||
selectedMaterialIds = []
|
||||
selectedMaterialPurchaseStates = [:]
|
||||
}
|
||||
|
||||
private var selectedDeletableMaterialIds: [Int] {
|
||||
selectedMaterialIds
|
||||
.filter { id in
|
||||
guard selectedMaterialPurchaseStates[id] == false else { return false }
|
||||
return materials.first(where: { $0.id == id })?.isPurchased != true
|
||||
}
|
||||
.sorted()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,14 +293,11 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
sortButton.accessibilityValue = viewModel.sortOption.title
|
||||
sortButton.menu = makeSortMenu()
|
||||
|
||||
let canSelectMaterials = viewModel.selectedTab == .all
|
||||
selectButton.isHidden = false
|
||||
selectButton.isEnabled = canSelectMaterials
|
||||
selectButton.alpha = canSelectMaterials ? 1 : 0.45
|
||||
selectButton.isEnabled = true
|
||||
selectButton.alpha = 1
|
||||
selectButton.setTitle(viewModel.isSelectionMode ? "完成" : "选择", for: .normal)
|
||||
selectButton.accessibilityValue = canSelectMaterials
|
||||
? (viewModel.isSelectionMode ? "选择模式已开启" : "选择模式已关闭")
|
||||
: "已购照片不可删除"
|
||||
selectButton.accessibilityValue = viewModel.isSelectionMode ? "选择模式已开启" : "选择模式已关闭"
|
||||
|
||||
let selectedCount = viewModel.selectedMaterialIds.count
|
||||
let hasSelection = selectedCount > 0
|
||||
@@ -487,7 +484,11 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
@objc private func deleteSelectedTapped() {
|
||||
let count = viewModel.selectedMaterialIds.count
|
||||
guard count > 0 else { return }
|
||||
let alert = UIAlertController(title: "删除素材", message: "确定删除选中的 \(count) 张素材吗?", preferredStyle: .alert)
|
||||
let alert = UIAlertController(
|
||||
title: "删除素材",
|
||||
message: viewModel.deleteSelectedConfirmationMessage,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
@@ -694,14 +695,16 @@ private final class TravelAlbumInfoCard: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册素材网格单元,展示正方形缩略图、文件名、大小和选择状态。
|
||||
/// 旅拍相册素材网格单元,独立展示购买、修图和选择状态。
|
||||
final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "TravelAlbumMaterialCell"
|
||||
|
||||
private let imageView = UIImageView()
|
||||
private let checkImageView = UIImageView()
|
||||
private let badgeView = UIView()
|
||||
private let badgeLabel = UILabel()
|
||||
private let purchaseBadgeView = UIView()
|
||||
private let purchaseBadgeLabel = UILabel()
|
||||
private let aiRetouchBadgeView = UIView()
|
||||
private let aiRetouchBadgeLabel = UILabel()
|
||||
private let nameLabel = UILabel()
|
||||
private let sizeLabel = UILabel()
|
||||
|
||||
@@ -715,18 +718,18 @@ final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
checkImageView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||||
checkImageView.layer.cornerRadius = 11
|
||||
checkImageView.accessibilityIdentifier = "travelAlbum.materialSelectionCheck"
|
||||
badgeView.layer.cornerRadius = 5
|
||||
badgeView.clipsToBounds = true
|
||||
badgeView.isHidden = true
|
||||
badgeView.isAccessibilityElement = false
|
||||
badgeView.accessibilityIdentifier = "travelAlbum.materialStatusBadge"
|
||||
badgeLabel.font = .systemFont(ofSize: 10, weight: .semibold)
|
||||
badgeLabel.textColor = .white
|
||||
badgeLabel.textAlignment = .center
|
||||
badgeLabel.isAccessibilityElement = false
|
||||
badgeLabel.accessibilityIdentifier = "travelAlbum.materialStatusBadgeLabel"
|
||||
badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
badgeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
configureBadge(
|
||||
purchaseBadgeView,
|
||||
label: purchaseBadgeLabel,
|
||||
viewIdentifier: "travelAlbum.materialPurchaseBadge",
|
||||
labelIdentifier: "travelAlbum.materialPurchaseBadgeLabel"
|
||||
)
|
||||
configureBadge(
|
||||
aiRetouchBadgeView,
|
||||
label: aiRetouchBadgeLabel,
|
||||
viewIdentifier: "travelAlbum.materialAIRetouchBadge",
|
||||
labelIdentifier: "travelAlbum.materialAIRetouchBadgeLabel"
|
||||
)
|
||||
nameLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
nameLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
nameLabel.lineBreakMode = .byTruncatingMiddle
|
||||
@@ -734,8 +737,8 @@ final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
sizeLabel.textColor = TravelAlbumDetailStyle.textSecondary
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
imageView.addSubview(badgeView)
|
||||
badgeView.addSubview(badgeLabel)
|
||||
imageView.addSubview(purchaseBadgeView)
|
||||
imageView.addSubview(aiRetouchBadgeView)
|
||||
imageView.addSubview(checkImageView)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(sizeLabel)
|
||||
@@ -747,14 +750,13 @@ final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
make.top.trailing.equalToSuperview().inset(6)
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
badgeView.snp.makeConstraints { make in
|
||||
aiRetouchBadgeView.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().inset(6)
|
||||
make.trailing.lessThanOrEqualTo(checkImageView.snp.leading).offset(-4)
|
||||
}
|
||||
badgeLabel.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(
|
||||
UIEdgeInsets(top: 3, left: 6, bottom: 3, right: 6)
|
||||
)
|
||||
purchaseBadgeView.snp.makeConstraints { make in
|
||||
make.leading.bottom.equalToSuperview().inset(6)
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(6)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(imageView.snp.bottom).offset(6)
|
||||
@@ -784,14 +786,54 @@ final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
checkImageView.isHidden = !selectionMode
|
||||
checkImageView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||
checkImageView.tintColor = selected ? TravelAlbumDetailStyle.primary : .white
|
||||
let badge = material.badgePresentation
|
||||
badgeView.isHidden = badge == nil
|
||||
badgeLabel.text = badge?.text
|
||||
badgeView.backgroundColor = badge.map { TravelAlbumDetailStyle.badgeColor(for: $0.kind) }
|
||||
let purchaseBadge = material.purchaseBadgePresentation
|
||||
let aiRetouchBadge = material.aiRetouchBadgePresentation
|
||||
applyBadge(purchaseBadge, to: purchaseBadgeView, label: purchaseBadgeLabel)
|
||||
applyBadge(aiRetouchBadge, to: aiRetouchBadgeView, label: aiRetouchBadgeLabel)
|
||||
nameLabel.text = material.fileName.isEmpty ? "未命名照片" : material.fileName
|
||||
sizeLabel.text = TravelAlbumDisplayFormatter.fileSizeText(material.fileSize)
|
||||
let badgeAccessibilityText = badge.map { ",状态:\($0.text)" } ?? ""
|
||||
accessibilityLabel = "\(nameLabel.text ?? "照片"),\(sizeLabel.text ?? "")\(badgeAccessibilityText)"
|
||||
let purchaseAccessibilityText = purchaseBadge.map { ",购买状态:\($0.text)" } ?? ""
|
||||
let aiRetouchAccessibilityText = aiRetouchBadge.map { ",修图状态:\($0.text)" } ?? ""
|
||||
accessibilityLabel = "\(nameLabel.text ?? "照片"),\(sizeLabel.text ?? "")"
|
||||
+ purchaseAccessibilityText
|
||||
+ aiRetouchAccessibilityText
|
||||
accessibilityValue = selectionMode ? (selected ? "已选择" : "未选择") : nil
|
||||
}
|
||||
|
||||
private func configureBadge(
|
||||
_ badgeView: UIView,
|
||||
label: UILabel,
|
||||
viewIdentifier: String,
|
||||
labelIdentifier: String
|
||||
) {
|
||||
badgeView.layer.cornerRadius = 5
|
||||
badgeView.clipsToBounds = true
|
||||
badgeView.isHidden = true
|
||||
badgeView.isAccessibilityElement = false
|
||||
badgeView.accessibilityIdentifier = viewIdentifier
|
||||
label.font = .systemFont(ofSize: 10, weight: .semibold)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.isAccessibilityElement = false
|
||||
label.accessibilityIdentifier = labelIdentifier
|
||||
label.setContentHuggingPriority(.required, for: .horizontal)
|
||||
label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
badgeView.addSubview(label)
|
||||
label.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(
|
||||
UIEdgeInsets(top: 3, left: 6, bottom: 3, right: 6)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyBadge(
|
||||
_ presentation: TravelAlbumMaterialBadgePresentation?,
|
||||
to badgeView: UIView,
|
||||
label: UILabel
|
||||
) {
|
||||
badgeView.isHidden = presentation == nil
|
||||
label.text = presentation?.text
|
||||
badgeView.backgroundColor = presentation.map { TravelAlbumDetailStyle.badgeColor(for: $0.kind) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1149,9 +1149,8 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertFalse(controller.view.allLabels().contains { $0.text == "刷新失败" })
|
||||
}
|
||||
|
||||
func testMaterialCellShowsSemanticStatusBadgeWithoutOverlappingSelectionCheck() throws {
|
||||
func testMaterialCellShowsIndependentPurchaseAndAIRetouchBadgesWithoutOverlappingSelectionCheck() throws {
|
||||
let cases: [(TravelAlbumMaterial, String, UInt)] = [
|
||||
(TravelAlbumMaterial(isPurchased: true), "已购", 0x475569),
|
||||
(TravelAlbumMaterial(aiRetouchStatus: 1), "待处理", 0xB45309),
|
||||
(TravelAlbumMaterial(aiRetouchStatus: 2), "修图中", 0x1D4ED8),
|
||||
(TravelAlbumMaterial(aiRetouchStatus: 3), "AI已修", 0x047857),
|
||||
@@ -1170,12 +1169,12 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
|
||||
let badgeView = try XCTUnwrap(
|
||||
cell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialStatusBadge"
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadge"
|
||||
}
|
||||
)
|
||||
let badgeLabel = try XCTUnwrap(
|
||||
cell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialStatusBadgeLabel"
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadgeLabel"
|
||||
} as? UILabel
|
||||
)
|
||||
let selectionCheck = try XCTUnwrap(
|
||||
@@ -1188,22 +1187,61 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
|
||||
XCTAssertEqual(badgeLabel.text, expectedText)
|
||||
XCTAssertEqual(badgeView.backgroundColor?.travelAlbumTestHexRGB, expectedColor)
|
||||
XCTAssertLessThan(badgeView.frame.maxX, selectionCheck.frame.minX)
|
||||
XCTAssertTrue(cell.accessibilityLabel?.contains("状态:\(expectedText)") == true)
|
||||
XCTAssertTrue(cell.accessibilityLabel?.contains("修图状态:\(expectedText)") == true)
|
||||
}
|
||||
|
||||
let combinedCell = TravelAlbumMaterialCell(frame: CGRect(x: 0, y: 0, width: 176, height: 220))
|
||||
combinedCell.apply(
|
||||
material: TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 2),
|
||||
selectionMode: true,
|
||||
selected: false
|
||||
)
|
||||
combinedCell.layoutIfNeeded()
|
||||
let purchaseBadge = try XCTUnwrap(
|
||||
combinedCell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialPurchaseBadge"
|
||||
}
|
||||
)
|
||||
let purchaseLabel = try XCTUnwrap(
|
||||
combinedCell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialPurchaseBadgeLabel"
|
||||
} as? UILabel
|
||||
)
|
||||
let aiRetouchBadge = try XCTUnwrap(
|
||||
combinedCell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadge"
|
||||
}
|
||||
)
|
||||
XCTAssertFalse(purchaseBadge.isHidden)
|
||||
XCTAssertFalse(aiRetouchBadge.isHidden)
|
||||
XCTAssertEqual(purchaseLabel.text, "已购")
|
||||
XCTAssertEqual(purchaseBadge.backgroundColor?.travelAlbumTestHexRGB, 0x475569)
|
||||
XCTAssertGreaterThan(purchaseBadge.frame.minY, aiRetouchBadge.frame.maxY)
|
||||
XCTAssertEqual(purchaseBadge.frame.minX, 6, accuracy: 0.5)
|
||||
XCTAssertEqual(purchaseBadge.frame.maxY, 170, accuracy: 0.5)
|
||||
XCTAssertTrue(combinedCell.accessibilityLabel?.contains("购买状态:已购") == true)
|
||||
XCTAssertTrue(combinedCell.accessibilityLabel?.contains("修图状态:修图中") == true)
|
||||
|
||||
let hiddenCell = TravelAlbumMaterialCell(frame: CGRect(x: 0, y: 0, width: 176, height: 220))
|
||||
hiddenCell.apply(
|
||||
material: TravelAlbumMaterial(aiRetouchStatus: 0, aiRetouchStatusName: "已上传"),
|
||||
selectionMode: false,
|
||||
selected: false
|
||||
)
|
||||
let hiddenBadge = try XCTUnwrap(
|
||||
let hiddenPurchaseBadge = try XCTUnwrap(
|
||||
hiddenCell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialStatusBadge"
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialPurchaseBadge"
|
||||
}
|
||||
)
|
||||
XCTAssertTrue(hiddenBadge.isHidden)
|
||||
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("状态:") == true)
|
||||
let hiddenAIRetouchBadge = try XCTUnwrap(
|
||||
hiddenCell.findSubview {
|
||||
$0.accessibilityIdentifier == "travelAlbum.materialAIRetouchBadge"
|
||||
}
|
||||
)
|
||||
XCTAssertTrue(hiddenPurchaseBadge.isHidden)
|
||||
XCTAssertTrue(hiddenAIRetouchBadge.isHidden)
|
||||
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("购买状态:") == true)
|
||||
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("修图状态:") == true)
|
||||
}
|
||||
|
||||
private func waitUntil(_ condition: @escaping () -> Bool) async {
|
||||
|
||||
@@ -399,34 +399,48 @@ final class TravelAlbumModelsTests: XCTestCase {
|
||||
XCTAssertEqual(material.aiAtmosphereURL, "")
|
||||
}
|
||||
|
||||
func testTravelAlbumMaterialBadgePriorityAndFallbacks() {
|
||||
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 0).badgePresentation)
|
||||
func testTravelAlbumMaterialPurchaseAndAIRetouchBadgesAreIndependent() {
|
||||
XCTAssertNil(TravelAlbumMaterial().purchaseBadgePresentation)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 0, aiRetouchStatusName: "已上传")
|
||||
.badgePresentation,
|
||||
TravelAlbumMaterial(isPurchased: true).purchaseBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
|
||||
)
|
||||
XCTAssertNil(TravelAlbumMaterial(isPurchased: true).aiRetouchBadgePresentation)
|
||||
let purchasedAndProcessing = TravelAlbumMaterial(isPurchased: true, aiRetouchStatus: 2)
|
||||
XCTAssertEqual(
|
||||
purchasedAndProcessing.purchaseBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 1, aiRetouchStatusName: " 排队中 ").badgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .pending, text: "排队中")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 2, aiRetouchStatusName: " ").badgePresentation,
|
||||
purchasedAndProcessing.aiRetouchBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .processing, text: "修图中")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 3).badgePresentation,
|
||||
TravelAlbumMaterial(aiRetouchStatus: 1, aiRetouchStatusName: " 排队中 ")
|
||||
.aiRetouchBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .pending, text: "排队中")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 2, aiRetouchStatusName: " ").aiRetouchBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .processing, text: "修图中")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 3).aiRetouchBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .retouched, text: "AI已修")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 3, aiRetouchStatusName: "AI封面").badgePresentation,
|
||||
TravelAlbumMaterial(aiRetouchStatus: 3, aiRetouchStatusName: "AI封面")
|
||||
.aiRetouchBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .cover, text: "AI封面")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 4).badgePresentation,
|
||||
TravelAlbumMaterial(aiRetouchStatus: 4).aiRetouchBadgePresentation,
|
||||
TravelAlbumMaterialBadgePresentation(kind: .failed, text: "失败")
|
||||
)
|
||||
XCTAssertNil(TravelAlbumMaterial(aiRetouchStatus: 99, aiRetouchStatusName: "未知").badgePresentation)
|
||||
XCTAssertNil(
|
||||
TravelAlbumMaterial(aiRetouchStatus: 99, aiRetouchStatusName: "未知")
|
||||
.aiRetouchBadgePresentation
|
||||
)
|
||||
}
|
||||
|
||||
func testDisplayFormatters() {
|
||||
|
||||
@@ -279,14 +279,22 @@ final class TravelAlbumDetailViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(TravelAlbumDisplayFormatter.creationTimeText(""), "--")
|
||||
}
|
||||
|
||||
func testOnlyUnpurchasedMaterialCanBeSelected() {
|
||||
func testAllMaterialsCanBeSelectedAndPurchasedTabSupportsSelectionMode() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.materialListResponses = [TravelAlbumListResponse(total: 1, list: [])]
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 2)
|
||||
await viewModel.selectTab(.purchased, api: api)
|
||||
viewModel.toggleSelectionMode()
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 1, status: 2))
|
||||
XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty)
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 1, isPurchased: true))
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 2, isPurchased: false))
|
||||
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 1, status: 1))
|
||||
XCTAssertEqual(viewModel.selectedMaterialIds, [1])
|
||||
XCTAssertTrue(viewModel.isSelectionMode)
|
||||
XCTAssertEqual(viewModel.selectedMaterialIds, [1, 2])
|
||||
XCTAssertEqual(viewModel.selectedPurchasedMaterialCount, 1)
|
||||
XCTAssertEqual(
|
||||
viewModel.deleteSelectedConfirmationMessage,
|
||||
"已选择 2 张素材,其中 1 张已购买。不会删除已购买的项目,只会删除 1 张未购买的项目。"
|
||||
)
|
||||
}
|
||||
|
||||
func testCompletingAIRetouchClearsSelectionModeAndSelectedMaterials() {
|
||||
@@ -405,6 +413,42 @@ final class TravelAlbumDetailViewModelTests: XCTestCase {
|
||||
XCTAssertFalse(viewModel.isSelectionMode)
|
||||
XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty)
|
||||
}
|
||||
|
||||
func testDeleteSelectedMaterialsFiltersPurchasedItemsFromBatchRequest() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
api.materialListResponses = [
|
||||
TravelAlbumListResponse(total: 0, list: []),
|
||||
TravelAlbumListResponse(total: 0, list: []),
|
||||
TravelAlbumListResponse(total: 0, list: []),
|
||||
]
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 5)
|
||||
viewModel.toggleSelectionMode()
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 10, isPurchased: true))
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 11, isPurchased: false))
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 12, isPurchased: true))
|
||||
|
||||
await viewModel.deleteSelectedMaterials(api: api)
|
||||
|
||||
XCTAssertEqual(api.deletedMaterialIDBatches, [[11]])
|
||||
XCTAssertFalse(viewModel.isSelectionMode)
|
||||
XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty)
|
||||
}
|
||||
|
||||
func testDeleteSelectedMaterialsDoesNotRequestAPIWhenAllSelectedItemsArePurchased() async {
|
||||
let api = TravelAlbumMockAPI()
|
||||
let viewModel = TravelAlbumDetailViewModel(albumId: 5)
|
||||
var message = ""
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
viewModel.toggleSelectionMode()
|
||||
viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 10, isPurchased: true))
|
||||
|
||||
await viewModel.deleteSelectedMaterials(api: api)
|
||||
|
||||
XCTAssertTrue(api.deletedMaterialIDBatches.isEmpty)
|
||||
XCTAssertEqual(message, "选中的素材均已购买,未删除任何项目")
|
||||
XCTAssertTrue(viewModel.isSelectionMode)
|
||||
XCTAssertEqual(viewModel.selectedMaterialIds, [10])
|
||||
}
|
||||
}
|
||||
|
||||
/// 有线传输 ViewModel 测试。
|
||||
|
||||
Reference in New Issue
Block a user