feat: add AI retouch and album preview actions
This commit is contained in:
@@ -12,6 +12,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
private let viewModel: TravelAlbumDetailViewModel
|
||||
private let api: any TravelAlbumServing
|
||||
private let previewConfiguration: TravelAlbumPreviewConfiguration
|
||||
private let scenicIdProvider: () -> Int
|
||||
|
||||
private let contentView = UIView()
|
||||
private let infoCard = TravelAlbumInfoCard()
|
||||
@@ -25,17 +26,21 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumMaterial>!
|
||||
private let bottomBar = UIView()
|
||||
private let bottomDivider = UIView()
|
||||
private let selectionActionStack = UIStackView()
|
||||
private let aiRetouchButton = UIButton(type: .system)
|
||||
private let deleteSelectedButton = UIButton(type: .system)
|
||||
private let uploadButton = UIButton(type: .system)
|
||||
|
||||
init(
|
||||
albumId: Int,
|
||||
api: (any TravelAlbumServing)? = nil,
|
||||
previewConfiguration: TravelAlbumPreviewConfiguration = .init()
|
||||
previewConfiguration: TravelAlbumPreviewConfiguration = .init(),
|
||||
scenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }
|
||||
) {
|
||||
viewModel = TravelAlbumDetailViewModel(albumId: albumId)
|
||||
self.api = api ?? NetworkServices.shared.travelAlbumAPI
|
||||
self.previewConfiguration = previewConfiguration
|
||||
self.scenicIdProvider = scenicIdProvider
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@@ -118,8 +123,20 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
uploadConfiguration?.imagePadding = 8
|
||||
uploadButton.configuration = uploadConfiguration
|
||||
uploadButton.accessibilityLabel = "上传照片"
|
||||
configureBottomButton(deleteSelectedButton, title: "删除选中(0)", color: TravelAlbumDetailStyle.danger)
|
||||
deleteSelectedButton.isHidden = true
|
||||
selectionActionStack.axis = .horizontal
|
||||
selectionActionStack.alignment = .fill
|
||||
selectionActionStack.distribution = .fillEqually
|
||||
selectionActionStack.spacing = 12
|
||||
selectionActionStack.isHidden = true
|
||||
selectionActionStack.accessibilityIdentifier = "travelAlbum.selectionActionStack"
|
||||
configureBottomButton(aiRetouchButton, title: "AI修图", color: TravelAlbumDetailStyle.primary)
|
||||
aiRetouchButton.accessibilityLabel = "AI修图"
|
||||
aiRetouchButton.accessibilityIdentifier = "travelAlbum.aiRetouchButton"
|
||||
configureBottomButton(deleteSelectedButton, title: "删除", color: TravelAlbumDetailStyle.danger)
|
||||
deleteSelectedButton.accessibilityLabel = "删除"
|
||||
deleteSelectedButton.accessibilityIdentifier = "travelAlbum.deleteButton"
|
||||
selectionActionStack.addArrangedSubview(aiRetouchButton)
|
||||
selectionActionStack.addArrangedSubview(deleteSelectedButton)
|
||||
|
||||
view.addSubview(contentView)
|
||||
contentView.addSubview(infoCard)
|
||||
@@ -133,7 +150,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(bottomDivider)
|
||||
bottomBar.addSubview(uploadButton)
|
||||
bottomBar.addSubview(deleteSelectedButton)
|
||||
bottomBar.addSubview(selectionActionStack)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
@@ -150,7 +167,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
make.height.equalTo(48)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
}
|
||||
deleteSelectedButton.snp.makeConstraints { make in
|
||||
selectionActionStack.snp.makeConstraints { make in
|
||||
make.edges.equalTo(uploadButton)
|
||||
}
|
||||
|
||||
@@ -205,6 +222,7 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
allTabButton.addTarget(self, action: #selector(allTabTapped), for: .touchUpInside)
|
||||
purchasedTabButton.addTarget(self, action: #selector(purchasedTabTapped), for: .touchUpInside)
|
||||
selectButton.addTarget(self, action: #selector(selectTapped), for: .touchUpInside)
|
||||
aiRetouchButton.addTarget(self, action: #selector(aiRetouchTapped), for: .touchUpInside)
|
||||
deleteSelectedButton.addTarget(self, action: #selector(deleteSelectedTapped), for: .touchUpInside)
|
||||
uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
@@ -255,10 +273,14 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
: "已购照片不可删除"
|
||||
|
||||
let selectedCount = viewModel.selectedMaterialIds.count
|
||||
deleteSelectedButton.setTitle("删除选中(\(selectedCount))", for: .normal)
|
||||
let hasSelection = selectedCount > 0
|
||||
aiRetouchButton.isEnabled = hasSelection
|
||||
aiRetouchButton.alpha = hasSelection ? 1 : 0.45
|
||||
aiRetouchButton.accessibilityValue = "已选择 \(selectedCount) 张照片"
|
||||
deleteSelectedButton.isEnabled = selectedCount > 0
|
||||
deleteSelectedButton.alpha = selectedCount > 0 ? 1 : 0.45
|
||||
deleteSelectedButton.isHidden = !viewModel.isSelectionMode
|
||||
deleteSelectedButton.alpha = hasSelection ? 1 : 0.45
|
||||
deleteSelectedButton.accessibilityValue = "已选择 \(selectedCount) 张照片"
|
||||
selectionActionStack.isHidden = !viewModel.isSelectionMode
|
||||
uploadButton.isHidden = viewModel.isSelectionMode
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbumMaterial>()
|
||||
@@ -396,6 +418,31 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
viewModel.toggleSelectionMode()
|
||||
}
|
||||
|
||||
@objc private func aiRetouchTapped() {
|
||||
let materialIds = Array(viewModel.selectedMaterialIds)
|
||||
guard !materialIds.isEmpty else { return }
|
||||
let scenicId = scenicIdProvider()
|
||||
guard scenicId > 0 else {
|
||||
showToast("请先选择景区")
|
||||
return
|
||||
}
|
||||
|
||||
let controller = TravelAlbumAIRetouchTemplateViewController(
|
||||
viewModel: TravelAlbumAIRetouchTemplateViewModel(
|
||||
albumId: viewModel.albumId,
|
||||
scenicId: scenicId,
|
||||
materialIds: materialIds
|
||||
),
|
||||
api: api,
|
||||
onSubmitted: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.viewModel.completeAIRetouchSubmission()
|
||||
self.showToast("AI修图任务已提交")
|
||||
}
|
||||
)
|
||||
present(controller, animated: true)
|
||||
}
|
||||
|
||||
@objc private func deleteSelectedTapped() {
|
||||
let count = viewModel.selectedMaterialIds.count
|
||||
guard count > 0 else { return }
|
||||
@@ -429,12 +476,16 @@ final class TravelAlbumDetailViewController: BaseViewController {
|
||||
totalCount: viewModel.currentPhotoCount,
|
||||
startProjectIndex: startIndex,
|
||||
configuration: previewConfiguration,
|
||||
actionHandler: TravelAlbumPreviewActionHandler(api: previewAPI),
|
||||
loadMore: {
|
||||
await previewViewModel.loadMaterials(reset: false, api: previewAPI)
|
||||
return (
|
||||
previewViewModel.materials.map(TravelAlbumPreviewProject.init(material:)),
|
||||
previewViewModel.currentPhotoCount
|
||||
)
|
||||
},
|
||||
onProjectDeleted: { materialId in
|
||||
previewViewModel.removeMaterialAfterPreviewDeletion(id: materialId)
|
||||
}
|
||||
)
|
||||
present(controller, animated: true)
|
||||
@@ -465,6 +516,17 @@ private enum TravelAlbumDetailStyle {
|
||||
static let textSecondary = UIColor(hex: 0x7F8A9E)
|
||||
static let border = UIColor(hex: 0xDCE4EF)
|
||||
static let danger = UIColor(hex: 0xE53935)
|
||||
|
||||
static func badgeColor(for kind: TravelAlbumMaterialBadgeKind) -> UIColor {
|
||||
switch kind {
|
||||
case .purchased: UIColor(hex: 0x475569)
|
||||
case .pending: UIColor(hex: 0xB45309)
|
||||
case .processing: UIColor(hex: 0x1D4ED8)
|
||||
case .retouched: UIColor(hex: 0x047857)
|
||||
case .cover: UIColor(hex: 0x6D28D9)
|
||||
case .failed: UIColor(hex: 0xB91C1C)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册摘要卡,展示封面、名称、用户手机号与创建时间。
|
||||
@@ -583,11 +645,13 @@ private final class TravelAlbumInfoCard: UIView {
|
||||
}
|
||||
|
||||
/// 旅拍相册素材网格单元,展示正方形缩略图、文件名、大小和选择状态。
|
||||
private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
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 nameLabel = UILabel()
|
||||
private let sizeLabel = UILabel()
|
||||
|
||||
@@ -600,6 +664,19 @@ private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
checkImageView.tintColor = .white
|
||||
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)
|
||||
nameLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
nameLabel.textColor = TravelAlbumDetailStyle.textPrimary
|
||||
nameLabel.lineBreakMode = .byTruncatingMiddle
|
||||
@@ -607,6 +684,8 @@ private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
sizeLabel.textColor = TravelAlbumDetailStyle.textSecondary
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
imageView.addSubview(badgeView)
|
||||
badgeView.addSubview(badgeLabel)
|
||||
imageView.addSubview(checkImageView)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(sizeLabel)
|
||||
@@ -618,6 +697,15 @@ private final class TravelAlbumMaterialCell: UICollectionViewCell {
|
||||
make.top.trailing.equalToSuperview().inset(6)
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
badgeView.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)
|
||||
)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(imageView.snp.bottom).offset(6)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
@@ -646,9 +734,14 @@ private 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) }
|
||||
nameLabel.text = material.fileName.isEmpty ? "未命名照片" : material.fileName
|
||||
sizeLabel.text = TravelAlbumDisplayFormatter.fileSizeText(material.fileSize)
|
||||
accessibilityLabel = "\(nameLabel.text ?? "照片"),\(sizeLabel.text ?? "")"
|
||||
let badgeAccessibilityText = badge.map { ",状态:\($0.text)" } ?? ""
|
||||
accessibilityLabel = "\(nameLabel.text ?? "照片"),\(sizeLabel.text ?? "")\(badgeAccessibilityText)"
|
||||
accessibilityValue = selectionMode ? (selected ? "已选择" : "未选择") : nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user