2 Commits
Author SHA1 Message Date
lujiuyin 5704531f3a fix(album): rename AI retouch action label 2026-08-10 17:56:29 +08:00
lujiuyin 87819780c9 feat(album): add pull-to-refresh to management grid 2026-08-10 17:46:25 +08:00
6 changed files with 59 additions and 6 deletions
@@ -197,7 +197,7 @@ protocol TravelAlbumPreviewActionHandling: AnyObject {
/// 新接口接入前的占位操作实现,不修改任何业务数据。 /// 新接口接入前的占位操作实现,不修改任何业务数据。
final class PlaceholderTravelAlbumPreviewActionHandler: TravelAlbumPreviewActionHandling { final class PlaceholderTravelAlbumPreviewActionHandler: TravelAlbumPreviewActionHandling {
func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult {
.unavailable("AI修复功能待接口接入") .unavailable("AI修图功能待接口接入")
} }
func deleteProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { func deleteProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult {
@@ -14,7 +14,7 @@ final class TravelAlbumPreviewActionHandler: TravelAlbumPreviewActionHandling {
self.api = api self.api = api
} }
/// AI 修复仍由相册选择态的模板流程发起。 /// AI 修图仍由相册选择态的模板流程发起。
func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult {
.unavailable("请在相册中选择照片后使用AI修图") .unavailable("请在相册中选择照片后使用AI修图")
} }
@@ -22,6 +22,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
private let purchasedTabButton = UIButton(type: .system) private let purchasedTabButton = UIButton(type: .system)
private let sortButton = UIButton(type: .system) private let sortButton = UIButton(type: .system)
private let selectButton = UIButton(type: .system) private let selectButton = UIButton(type: .system)
private let refreshControl = UIRefreshControl()
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumMaterial>! private var dataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumMaterial>!
private let bottomBar = UIView() private let bottomBar = UIView()
@@ -97,6 +98,9 @@ final class TravelAlbumDetailViewController: BaseViewController {
collectionView.alwaysBounceVertical = true collectionView.alwaysBounceVertical = true
collectionView.showsVerticalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false
collectionView.delegate = self collectionView.delegate = self
refreshControl.tintColor = TravelAlbumDetailStyle.primary
refreshControl.accessibilityIdentifier = "travelAlbum.refreshControl"
collectionView.refreshControl = refreshControl
collectionView.register( collectionView.register(
TravelAlbumMaterialCell.self, TravelAlbumMaterialCell.self,
forCellWithReuseIdentifier: TravelAlbumMaterialCell.reuseIdentifier forCellWithReuseIdentifier: TravelAlbumMaterialCell.reuseIdentifier
@@ -225,6 +229,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
aiRetouchButton.addTarget(self, action: #selector(aiRetouchTapped), for: .touchUpInside) aiRetouchButton.addTarget(self, action: #selector(aiRetouchTapped), for: .touchUpInside)
deleteSelectedButton.addTarget(self, action: #selector(deleteSelectedTapped), for: .touchUpInside) deleteSelectedButton.addTarget(self, action: #selector(deleteSelectedTapped), for: .touchUpInside)
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside) uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
viewModel.onStateChange = { [weak self] in viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() } Task { @MainActor in self?.applyViewModel() }
} }
@@ -291,13 +296,20 @@ final class TravelAlbumDetailViewController: BaseViewController {
} }
dataSource.apply(snapshot, animatingDifferences: true) dataSource.apply(snapshot, animatingDifferences: true)
if viewModel.isLoading && viewModel.album == nil { if !viewModel.isRefreshing {
refreshControl.endRefreshing()
}
if viewModel.isLoading && viewModel.album == nil && !refreshControl.isRefreshing {
showLoading() showLoading()
} else { } else {
hideLoading() hideLoading()
} }
} }
@objc private func refreshPulled() {
Task { await viewModel.refreshAll(api: api) }
}
private func configureTabButtonBase(_ button: UIButton) { private func configureTabButtonBase(_ button: UIButton) {
button.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) button.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium)
button.accessibilityTraits.insert(.button) button.accessibilityTraits.insert(.button)
@@ -193,7 +193,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
} }
private func configureActions() { private func configureActions() {
let aiButton = makeActionButton(title: "AI修复", systemName: "wand.and.stars", color: UIColor(hex: 0x60A5FA)) let aiButton = makeActionButton(title: "AI修图", systemName: "wand.and.stars", color: UIColor(hex: 0x60A5FA))
let deleteConfiguration = makeActionButton( let deleteConfiguration = makeActionButton(
title: "删除", title: "删除",
systemName: "trash", systemName: "trash",
@@ -7,9 +7,50 @@ import UIKit
import XCTest import XCTest
@testable import suixinkan @testable import suixinkan
/// 相册管理页选择态底部操作区测试。 /// 相册管理页刷新、预览与选择态交互测试。
@MainActor @MainActor
final class TravelAlbumDetailViewControllerTests: XCTestCase { final class TravelAlbumDetailViewControllerTests: XCTestCase {
func testPullToRefreshReloadsGridAndEndsRefreshing() async throws {
let api = TravelAlbumMockAPI()
api.infoResponse = TravelAlbum(id: 2, name: "测试相册")
api.materialListResponses = [
TravelAlbumListResponse(total: 1, list: []),
TravelAlbumListResponse(total: 0, list: []),
TravelAlbumListResponse(total: 1, list: [TravelAlbumMaterial(id: 1)]),
TravelAlbumListResponse(total: 2, list: []),
TravelAlbumListResponse(total: 1, list: []),
TravelAlbumListResponse(
total: 2,
list: [TravelAlbumMaterial(id: 1), TravelAlbumMaterial(id: 2)]
),
]
let controller = TravelAlbumDetailViewController(albumId: 2, api: api)
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
window.rootViewController = controller
window.makeKeyAndVisible()
defer { window.isHidden = true }
controller.loadViewIfNeeded()
controller.view.layoutIfNeeded()
let collectionView = try XCTUnwrap(
controller.view.findSubview { $0 is UICollectionView } as? UICollectionView
)
let refreshControl = try XCTUnwrap(collectionView.refreshControl)
await waitUntil { api.materialRequests.count == 3 }
refreshControl.beginRefreshing()
XCTAssertTrue(refreshControl.isRefreshing)
refreshControl.sendActions(for: .valueChanged)
await waitUntil {
api.materialRequests.count == 6
&& !refreshControl.isRefreshing
&& collectionView.numberOfItems(inSection: 0) == 2
}
XCTAssertEqual(refreshControl.accessibilityIdentifier, "travelAlbum.refreshControl")
XCTAssertEqual(collectionView.numberOfItems(inSection: 0), 2)
XCTAssertFalse(refreshControl.isRefreshing)
}
func testPreviewDeleteHandlerCallsMaterialAPIAndPreservesServerFailure() async { func testPreviewDeleteHandlerCallsMaterialAPIAndPreservesServerFailure() async {
let api = TravelAlbumMockAPI() let api = TravelAlbumMockAPI()
let handler = TravelAlbumPreviewActionHandler(api: api) let handler = TravelAlbumPreviewActionHandler(api: api)
+1 -1
View File
@@ -169,7 +169,7 @@ final class TravelAlbumModelsTests: XCTestCase {
XCTAssertEqual( XCTAssertEqual(
aiResult, aiResult,
.unavailable("AI修复功能待接口接入") .unavailable("AI修图功能待接口接入")
) )
XCTAssertEqual( XCTAssertEqual(
deleteResult, deleteResult,