feat: 新增相册自动修图与OTG状态预览

支持相册修图配置、模板选择和传输模式选择;上传后自动提交修图并展示状态角标及精修预览。补充接口文档与相关测试。
This commit is contained in:
2026-08-27 16:03:40 +08:00
parent 9fce6ef713
commit d04641b623
17 changed files with 2523 additions and 26 deletions
@@ -260,7 +260,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertFalse(GlobalLoadingManager.shared.isShowing)
}
func testAIJobEntryOnlyAppearsOnAlbumManagementNavigationBar() {
func testAlbumManagementShowsAIJobEntryButAlbumEntryDoesNot() {
let detail = TravelAlbumDetailViewController(albumId: 2, api: TravelAlbumMockAPI())
detail.setupNavigationBar()
let entry = TravelAlbumEntryViewController(api: TravelAlbumMockAPI())
@@ -1394,6 +1394,111 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase {
XCTAssertFalse(hiddenCell.accessibilityLabel?.contains("修图状态:") == true)
}
func testWiredTransferProgressUsesOnlyUploadStatusAndRealProgress() throws {
let cell = WiredTransferPhotoCell(frame: CGRect(x: 0, y: 0, width: 300, height: 66))
let progressView = try XCTUnwrap(cell.findSubview {
$0.accessibilityIdentifier == "wiredTransfer.uploadProgress"
} as? UIProgressView)
let uploadStates: [TravelAlbumOTGUploadStatus] = [.pending, .transferring, .uploading, .uploaded, .failed]
let retouchStates: [TravelAlbumAutoRetouchState] = [.none, .pendingSubmission, .submitting, .processing, .completed, .failed]
for uploadState in uploadStates {
for retouchState in retouchStates {
var item = makeWiredTransferPhotoItem(state: retouchState)
item.status = uploadState
item.progress = uploadState == .uploaded ? 100 : 37
cell.apply(item: item, selectionMode: false, selected: false)
XCTAssertEqual(progressView.isHidden, uploadState != .transferring && uploadState != .uploading)
XCTAssertEqual(progressView.progress, Float(item.progress) / 100, accuracy: 0.001)
}
}
}
func testWiredTransferRetouchBadgeShowsStateColorsAndKeepsCircularThumbnailLayout() throws {
let cases: [(TravelAlbumAutoRetouchState, String, UIColor)] = [
(.pendingSubmission, "修图中", UIColor(hex: 0x7C3AED)),
(.submitting, "修图中", UIColor(hex: 0x7C3AED)),
(.processing, "修图中", UIColor(hex: 0x7C3AED)),
(.completed, "修图成功", UIColor(hex: 0x047857)),
(.failed, "修图失败", AppColor.danger),
]
let cell = WiredTransferPhotoCell(frame: CGRect(x: 0, y: 0, width: 300, height: 66))
let badge = try XCTUnwrap(cell.findSubview {
$0.accessibilityIdentifier == "wiredTransfer.retouchBadge"
} as? UILabel)
let thumbnail = try XCTUnwrap(cell.findSubview {
$0.accessibilityIdentifier == "wiredTransfer.thumbnail"
})
for (state, expectedStatus, expectedColor) in cases {
cell.apply(item: makeWiredTransferPhotoItem(state: state), selectionMode: false, selected: false)
cell.layoutIfNeeded()
XCTAssertFalse(badge.isHidden)
XCTAssertEqual(badge.text, "修")
XCTAssertEqual(badge.backgroundColor?.travelAlbumTestHexRGB, expectedColor.travelAlbumTestHexRGB)
XCTAssertEqual(badge.textColor, .white)
XCTAssertEqual(badge.frame.size, CGSize(width: 16, height: 16))
// Auto Layout 对齐 alignmentRect,系统符号占位图的 frame 可能带有留白。
let badgeAlignmentRect = badge.alignmentRect(forFrame: badge.frame)
let thumbnailAlignmentRect = thumbnail.alignmentRect(forFrame: thumbnail.frame)
XCTAssertEqual(badgeAlignmentRect.minY, thumbnailAlignmentRect.minY + 2, accuracy: 0.1)
XCTAssertEqual(badgeAlignmentRect.maxX, thumbnailAlignmentRect.maxX - 2, accuracy: 0.1)
XCTAssertEqual(badge.layer.cornerRadius, 8)
XCTAssertEqual(badge.accessibilityLabel, expectedStatus)
XCTAssertTrue(cell.accessibilityLabel?.contains("修图状态:\(expectedStatus)") == true)
}
cell.prepareForReuse()
cell.apply(item: makeWiredTransferPhotoItem(state: .none), selectionMode: false, selected: false)
XCTAssertTrue(badge.isHidden)
XCTAssertNil(badge.accessibilityLabel)
XCTAssertFalse(cell.accessibilityLabel?.contains("修图状态:") == true)
}
func testWiredTransferUploadLabelIgnoresAllRetouchStates() throws {
let cell = WiredTransferPhotoCell(frame: CGRect(x: 0, y: 0, width: 300, height: 66))
let label = try XCTUnwrap(cell.findSubview {
$0.accessibilityIdentifier == "wiredTransfer.uploadStatus"
} as? UILabel)
let successColor = UIColor(hex: 0x16A34A)
let cases: [(TravelAlbumOTGUploadStatus, String, UIColor, UIColor)] = [
(.pending, "待上传", AppColor.textSecondary, AppColor.pageBackground),
(.transferring, "传输中", AppColor.primary, AppColor.primary.withAlphaComponent(0.12)),
(.uploading, "上传中", AppColor.primary, AppColor.primary.withAlphaComponent(0.12)),
(.uploaded, "已上传", successColor, successColor.withAlphaComponent(0.12)),
(.failed, "上传失败", AppColor.danger, AppColor.danger.withAlphaComponent(0.12)),
]
let retouchStates: [TravelAlbumAutoRetouchState] = [
.none, .pendingSubmission, .submitting, .processing, .completed, .failed,
]
for (uploadStatus, text, foreground, background) in cases {
for retouchState in retouchStates {
var item = makeWiredTransferPhotoItem(state: retouchState)
item.status = uploadStatus
cell.apply(item: item, selectionMode: false, selected: false)
XCTAssertFalse(label.isHidden)
XCTAssertEqual(label.text, text)
XCTAssertEqual(label.textColor, foreground)
XCTAssertEqual(label.backgroundColor, background)
XCTAssertTrue(cell.accessibilityLabel?.hasPrefix("photo.JPG,\(text)") == true)
}
}
}
private func makeWiredTransferPhotoItem(state: TravelAlbumAutoRetouchState) -> TravelAlbumOTGPhotoItem {
TravelAlbumOTGPhotoItem(
id: "photo", sourceId: "photo", fileName: "photo.JPG", thumbnailURL: nil,
capturedAt: "2026-08-27 12:00:00", fileSizeText: "1 KB", fileSizeBytes: 1024,
status: .uploaded, progress: 100, errorMessage: nil, localPath: "", remoteUrl: "",
autoRetouchState: state
)
}
private func waitUntil(_ condition: @escaping () -> Bool) async {
for _ in 0 ..< 200 {
if condition() { return }