新增线下收款记录和 ai 修图优化

This commit is contained in:
han xin
2026-08-21 15:51:52 +08:00
parent 6fe9928b49
commit 5cf4409bad
40 changed files with 5482 additions and 56 deletions
@@ -22,8 +22,14 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
private let freeCountField = UITextField()
private let singlePriceField = UITextField()
private let packagePriceField = UITextField()
private let autoRetouchSectionView = UIView()
private let autoRetouchTitleLabel = UILabel()
private let autoRetouchDetailLabel = UILabel()
private let noRetouchOption = TravelAlbumModeOptionView()
private let aiRetouchOption = TravelAlbumModeOptionView()
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
private var autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration = .disabled
init(viewModel: TravelAlbumEntryViewModel, api: any TravelAlbumServing) {
self.viewModel = viewModel
@@ -31,8 +37,16 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .pageSheet
if let sheetPresentationController {
sheetPresentationController.detents = [.medium(), .large()]
let formDetent = UISheetPresentationController.Detent.Identifier("createTravelAlbumForm")
sheetPresentationController.detents = [
.custom(identifier: formDetent) { context in
min(590, context.maximumDetentValue)
},
.large(),
]
sheetPresentationController.selectedDetentIdentifier = formDetent
sheetPresentationController.prefersGrabberVisible = false
sheetPresentationController.preferredCornerRadius = AppRadius.xl
}
}
@@ -62,6 +76,7 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
configureTextField(freeCountField, placeholder: "请输入免费张数", keyboardType: .numberPad)
configureTextField(singlePriceField, placeholder: "请输入单张照片价格", keyboardType: .decimalPad)
configureTextField(packagePriceField, placeholder: "请输入打包价格", keyboardType: .decimalPad)
configureAutoRetouchSection()
configureActionButton(cancelButton, title: "取消", backgroundColor: UIColor(hex: 0xF4F4F4), titleColor: AppColor.textSecondary)
configureActionButton(confirmButton, title: "确定", backgroundColor: AppColor.primary, titleColor: .white)
@@ -112,6 +127,8 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
[freeCountField, singlePriceField, packagePriceField].forEach {
$0.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged)
}
noRetouchOption.addTarget(self, action: #selector(noRetouchTapped), for: .touchUpInside)
aiRetouchOption.addTarget(self, action: #selector(aiRetouchTapped), for: .touchUpInside)
}
override func viewDidDisappear(_ animated: Bool) {
@@ -148,6 +165,45 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
fieldsStack.addArrangedSubview(makeFieldGroup(title: "免费张数", required: false, field: freeCountField))
fieldsStack.addArrangedSubview(makeFieldGroup(title: "单张照片价格(元)", required: true, field: singlePriceField))
fieldsStack.addArrangedSubview(makeFieldGroup(title: "打包价格(元)", required: false, field: packagePriceField))
fieldsStack.addArrangedSubview(autoRetouchSectionView)
}
private func configureAutoRetouchSection() {
autoRetouchSectionView.backgroundColor = .white
autoRetouchSectionView.layer.cornerRadius = AppRadius.sm
autoRetouchSectionView.layer.borderWidth = 1
autoRetouchSectionView.layer.borderColor = AppColor.border.cgColor
autoRetouchSectionView.clipsToBounds = true
autoRetouchTitleLabel.text = "修图方式"
autoRetouchTitleLabel.font = .app(.bodyMedium)
autoRetouchTitleLabel.textColor = AppColor.textPrimary
autoRetouchDetailLabel.text = "选择 AI 修图后,需要再选一个效果模板"
autoRetouchDetailLabel.font = .app(.caption)
autoRetouchDetailLabel.textColor = AppColor.textSecondary
let optionsStack = UIStackView(arrangedSubviews: [noRetouchOption, aiRetouchOption])
optionsStack.axis = .horizontal
optionsStack.spacing = AppSpacing.xs
optionsStack.distribution = .fillEqually
autoRetouchSectionView.addSubview(autoRetouchTitleLabel)
autoRetouchSectionView.addSubview(autoRetouchDetailLabel)
autoRetouchSectionView.addSubview(optionsStack)
autoRetouchTitleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
autoRetouchDetailLabel.snp.makeConstraints { make in
make.top.equalTo(autoRetouchTitleLabel.snp.bottom).offset(AppSpacing.xxs)
make.leading.trailing.equalTo(autoRetouchTitleLabel)
}
optionsStack.snp.makeConstraints { make in
make.top.equalTo(autoRetouchDetailLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.bottom.equalToSuperview().inset(AppSpacing.sm)
make.height.equalTo(72)
}
updateAutoRetouchSection()
}
private func makeFieldGroup(title: String, required: Bool, field: UITextField) -> UIView {
@@ -188,6 +244,7 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
freeCount: freeCountField.text ?? "",
singlePrice: singlePriceField.text ?? "",
packagePrice: packagePriceField.text ?? "",
autoRetouchConfiguration: autoRetouchConfiguration,
order: nil,
api: api
)
@@ -208,6 +265,43 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
}
}
@objc private func noRetouchTapped() {
autoRetouchConfiguration = .disabled
updateAutoRetouchSection()
}
@objc private func aiRetouchTapped() {
presentTemplatePicker()
}
private func presentTemplatePicker() {
guard presentedViewController == nil else { return }
let controller = TravelAlbumAutoRetouchSettingSheetViewController(
configuration: autoRetouchConfiguration,
startsWithModeSelection: false
)
controller.onConfirm = { [weak self] configuration in
guard let self else { return }
self.autoRetouchConfiguration = configuration
self.updateAutoRetouchSection()
}
present(controller, animated: true)
}
private func updateAutoRetouchSection() {
let isEnabled = autoRetouchConfiguration.isEnabled
noRetouchOption.apply(
title: TravelAlbumRetouchMode.disabled.title,
desc: "保留原图",
selected: !isEnabled
)
aiRetouchOption.apply(
title: TravelAlbumRetouchMode.aiRetouch.title,
desc: autoRetouchConfiguration.template?.title ?? "点击选模板",
selected: isEnabled
)
}
private func sanitizeMoneyInput(_ value: String) -> String {
var result = ""
var hasDot = false
@@ -0,0 +1,416 @@
//
// TravelAlbumAutoRetouchSettingSheetViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// 自动 AI 修图设置 Sheet,先选修图方式,选择 AI 修图后再选带效果图的模板。
final class TravelAlbumAutoRetouchSettingSheetViewController: BaseViewController, UITableViewDelegate {
private enum Step: Equatable {
case mode
case template
}
private enum Item: Hashable {
case mode(TravelAlbumRetouchMode)
case template(TravelAlbumEditPreset)
}
var onConfirm: ((TravelAlbumAutoRetouchConfiguration) -> Void)?
var onCancelled: (() -> Void)?
private let startsWithModeSelection: Bool
private let backButton = UIButton(type: .system)
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let tableView = UITableView(frame: .zero, style: .plain)
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
private var dataSource: UITableViewDiffableDataSource<Int, Item>!
private var step: Step
private var pendingConfiguration: TravelAlbumAutoRetouchConfiguration
private var selectedItem: Item?
private var previewImages: [TravelAlbumEditPreset.Effect: UIImage] = [:]
private var didConfirm = false
/// 创建自动修图设置 Sheet。
/// - Parameters:
/// - configuration: 当前配置,用于回显已选方式和模板。
/// - startsWithModeSelection: 是否先展示“不修图 / AI 修图”两个一级选项。
init(
configuration: TravelAlbumAutoRetouchConfiguration,
startsWithModeSelection: Bool
) {
self.startsWithModeSelection = startsWithModeSelection
step = startsWithModeSelection ? .mode : .template
pendingConfiguration = configuration
if startsWithModeSelection {
selectedItem = .mode(configuration.isEnabled ? .aiRetouch : .disabled)
} else if let template = configuration.template {
selectedItem = .template(template)
}
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .pageSheet
if let sheetPresentationController {
let identifier = UISheetPresentationController.Detent.Identifier("travelAlbumAutoRetouchSetting")
sheetPresentationController.detents = [
.custom(identifier: identifier) { context in
min(660, context.maximumDetentValue)
},
.large(),
]
sheetPresentationController.selectedDetentIdentifier = identifier
sheetPresentationController.prefersGrabberVisible = false
sheetPresentationController.preferredCornerRadius = AppRadius.xl
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {}
override func setupUI() {
view.backgroundColor = .white
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
backButton.tintColor = AppColor.textPrimary
backButton.accessibilityLabel = "返回修图方式"
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
subtitleLabel.font = .app(.caption)
subtitleLabel.textColor = AppColor.textSecondary
subtitleLabel.textAlignment = .center
subtitleLabel.numberOfLines = 0
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.delegate = self
tableView.register(
TravelAlbumAutoRetouchOptionCell.self,
forCellReuseIdentifier: TravelAlbumAutoRetouchOptionCell.reuseIdentifier
)
dataSource = UITableViewDiffableDataSource<Int, Item>(tableView: tableView) {
[weak self] tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(
withIdentifier: TravelAlbumAutoRetouchOptionCell.reuseIdentifier,
for: indexPath
) as! TravelAlbumAutoRetouchOptionCell
guard let self else { return cell }
switch item {
case .mode(let mode):
let iconName = mode == .disabled ? "photo" : "wand.and.stars"
let detail = mode == .disabled
? "保留照片原效果"
: (self.pendingConfiguration.template.map { "当前模板:\($0.title)" } ?? "选择后需再选一个修图模板")
cell.apply(
title: mode.title,
detail: detail,
previewImage: UIImage(systemName: iconName),
usesTemplateImage: true,
selected: self.selectedItem == item
)
case .template(let preset):
cell.apply(
title: preset.title,
detail: preset.autoRetouchEffectDescription,
previewImage: self.previewImage(for: preset),
usesTemplateImage: false,
selected: self.selectedItem == item
)
}
return cell
}
configureActionButton(cancelButton, title: "取消", isPrimary: false)
configureActionButton(confirmButton, title: "确定", isPrimary: true)
let buttonStack = UIStackView(arrangedSubviews: [cancelButton, confirmButton])
buttonStack.axis = .horizontal
buttonStack.spacing = AppSpacing.sm
buttonStack.distribution = .fillEqually
view.addSubview(backButton)
view.addSubview(titleLabel)
view.addSubview(subtitleLabel)
view.addSubview(tableView)
view.addSubview(buttonStack)
backButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(AppSpacing.md)
make.centerY.equalTo(titleLabel)
make.size.equalTo(AppSpacing.minTouchTarget)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.lg)
make.leading.trailing.equalToSuperview().inset(60)
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.trailing.equalToSuperview().inset(AppSpacing.lg)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(subtitleLabel.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.bottom.equalTo(buttonStack.snp.top).offset(-AppSpacing.sm)
}
buttonStack.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-AppSpacing.md)
make.height.equalTo(AppSpacing.minTouchTarget)
}
refreshContent()
}
override func bindActions() {
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isBeingDismissed, !didConfirm {
onCancelled?()
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
step == .mode ? 80 : 108
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .mode(.disabled):
pendingConfiguration = .disabled
selectedItem = item
applySnapshot()
updateConfirmButton()
case .mode(.aiRetouch):
step = .template
selectedItem = pendingConfiguration.template.map(Item.template)
refreshContent()
case .template(let preset):
pendingConfiguration = .enabled(templateID: preset.id)
selectedItem = item
applySnapshot()
updateConfirmButton()
}
}
private func refreshContent() {
switch step {
case .mode:
titleLabel.text = "选择修图方式"
subtitleLabel.text = "照片上传前可选择保留原图,或使用 AI 自动修图"
backButton.isHidden = true
case .template:
titleLabel.text = "选择修图模板"
subtitleLabel.text = "缩略图为模板实际效果,后续上传的照片将自动套用"
backButton.isHidden = !startsWithModeSelection
}
applySnapshot()
updateConfirmButton()
}
private func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
snapshot.appendSections([0])
switch step {
case .mode:
snapshot.appendItems(TravelAlbumRetouchMode.allCases.map(Item.mode))
case .template:
snapshot.appendItems(TravelAlbumEditPreset.autoRetouchOptions.map(Item.template))
}
// 选中状态存放在页面状态中,item 本身的标识不会变化。
// Diffable Data Source 不会主动重新配置相同 item,因此需显式刷新仍在当前列表中的选项。
let currentItems = Set(dataSource.snapshot().itemIdentifiers)
let retainedItems = snapshot.itemIdentifiers.filter(currentItems.contains)
snapshot.reconfigureItems(retainedItems)
dataSource.apply(snapshot, animatingDifferences: false)
}
private func updateConfirmButton() {
let isEnabled: Bool
switch selectedItem {
case .mode(.disabled):
isEnabled = step == .mode
case .mode(.aiRetouch):
isEnabled = step == .mode && pendingConfiguration.isEnabled && pendingConfiguration.isValid
case .template:
isEnabled = step == .template && pendingConfiguration.isEnabled && pendingConfiguration.isValid
case nil:
isEnabled = false
}
confirmButton.isEnabled = isEnabled
confirmButton.alpha = isEnabled ? 1 : 0.45
}
private func configureActionButton(_ button: UIButton, title: String, isPrimary: Bool) {
button.setTitle(title, for: .normal)
button.setTitleColor(isPrimary ? .white : AppColor.textSecondary, for: .normal)
button.titleLabel?.font = .app(.subtitle)
button.backgroundColor = isPrimary ? AppColor.primary : AppColor.inputBackground
button.layer.cornerRadius = AppRadius.md
}
private func previewImage(for preset: TravelAlbumEditPreset) -> UIImage? {
if let image = previewImages[preset.effect] {
return image
}
guard let source = makePreviewSourceImage() else { return nil }
let image = TravelAlbumAIEditImageProcessor.render(effect: preset.effect, source: source)
previewImages[preset.effect] = image
return image
}
private func makePreviewSourceImage() -> UIImage? {
guard let source = UIImage(named: "purchased_lakeside_flowers") else { return nil }
let size = CGSize(width: 176, height: 176)
return UIGraphicsImageRenderer(size: size).image { _ in
let scale = max(size.width / source.size.width, size.height / source.size.height)
let drawSize = CGSize(width: source.size.width * scale, height: source.size.height * scale)
let origin = CGPoint(x: (size.width - drawSize.width) / 2, y: (size.height - drawSize.height) / 2)
source.draw(in: CGRect(origin: origin, size: drawSize))
}
}
@objc private func backTapped() {
guard startsWithModeSelection else { return }
step = .mode
selectedItem = .mode(pendingConfiguration.isEnabled ? .aiRetouch : .disabled)
refreshContent()
}
@objc private func cancelTapped() {
dismiss(animated: true)
}
@objc private func confirmTapped() {
guard confirmButton.isEnabled else { return }
didConfirm = true
onConfirm?(pendingConfiguration)
dismiss(animated: true)
}
}
/// 自动修图选项行,一级方式显示图标,模板显示本地生成的真实效果缩略图。
private final class TravelAlbumAutoRetouchOptionCell: UITableViewCell {
static let reuseIdentifier = "TravelAlbumAutoRetouchOptionCell"
private let cardView = UIView()
private let previewImageView = UIImageView()
private let titleLabel = UILabel()
private let detailLabel = UILabel()
private let selectionImageView = UIImageView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
cardView.layer.cornerRadius = AppRadius.sm
cardView.layer.borderWidth = 1
previewImageView.clipsToBounds = true
previewImageView.layer.cornerRadius = AppRadius.xs
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
detailLabel.font = .app(.caption)
detailLabel.textColor = AppColor.textTertiary
detailLabel.numberOfLines = 2
selectionImageView.contentMode = .scaleAspectFit
contentView.addSubview(cardView)
cardView.addSubview(previewImageView)
cardView.addSubview(titleLabel)
cardView.addSubview(detailLabel)
cardView.addSubview(selectionImageView)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
}
previewImageView.snp.makeConstraints { make in
make.top.bottom.leading.equalToSuperview().inset(8)
make.width.equalTo(88)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(previewImageView.snp.trailing).offset(AppSpacing.sm)
make.trailing.lessThanOrEqualTo(selectionImageView.snp.leading).offset(-AppSpacing.sm)
make.bottom.equalTo(cardView.snp.centerY).offset(-2)
}
detailLabel.snp.makeConstraints { make in
make.top.equalTo(cardView.snp.centerY).offset(2)
make.leading.trailing.equalTo(titleLabel)
}
selectionImageView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-AppSpacing.md)
make.centerY.equalToSuperview()
make.size.equalTo(22)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 刷新选项文案、效果图与单选外观。
func apply(
title: String,
detail: String,
previewImage: UIImage?,
usesTemplateImage: Bool,
selected: Bool
) {
titleLabel.text = title
detailLabel.text = detail
previewImageView.image = usesTemplateImage
? previewImage?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 28, weight: .medium))
: previewImage
previewImageView.contentMode = usesTemplateImage ? .center : .scaleAspectFill
previewImageView.tintColor = AppColor.primary
previewImageView.backgroundColor = usesTemplateImage ? AppColor.primaryLight : AppColor.pageBackground
cardView.backgroundColor = selected ? AppColor.primaryLight : .white
cardView.layer.borderColor = (selected ? AppColor.primary : AppColor.border).cgColor
selectionImageView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
selectionImageView.tintColor = selected ? AppColor.primary : AppColor.textTertiary
accessibilityLabel = "\(title),\(detail)"
accessibilityValue = selected ? "已选择" : "未选择"
}
}
private extension TravelAlbumEditPreset {
/// 模板效果的简短说明,与缩略图一起帮助用户判断效果。
var autoRetouchEffectDescription: String {
switch effect {
case .original:
return "保留原图色彩"
case .portrait:
return "明亮柔和的人像质感"
case .vintage:
return "低饱和复古色调"
case .brocade:
return "鲜活通透的旅拍风格"
case .distantMountain:
return "自然淡雅的远山色调"
case .mist:
return "轻雾低对比氛围"
case .summer:
return "温暖明亮的油画色彩"
case .rich:
return "高饱和浓郁油画质感"
}
}
}
@@ -40,7 +40,7 @@ final class WiredCameraTransferViewController: BaseViewController {
private let refreshButton = UIButton(type: .system)
private let helpLabel = UILabel()
private let chipsStack = UIStackView()
private let retouchButton = WiredTransferSettingChipButton()
private let retouchButton = WiredTransferSettingChipButton(showsChevron: true)
private let formatButton = WiredTransferSettingChipButton()
private let modeButton = WiredTransferSettingChipButton(showsChevron: true)
private let settingsStatsDivider = UIView()
@@ -161,7 +161,6 @@ final class WiredCameraTransferViewController: BaseViewController {
[retouchButton, formatButton, modeButton].forEach {
chipsStack.addArrangedSubview($0)
}
retouchButton.isUserInteractionEnabled = false
formatButton.isUserInteractionEnabled = false
settingsStatsDivider.backgroundColor = AppColor.border
@@ -387,6 +386,7 @@ final class WiredCameraTransferViewController: BaseViewController {
Task { @MainActor in self?.showToast(message) }
}
refreshButton.addTarget(self, action: #selector(refreshTapped), for: .touchUpInside)
retouchButton.addTarget(self, action: #selector(retouchTapped), for: .touchUpInside)
batchButton.addTarget(self, action: #selector(batchTapped), for: .touchUpInside)
historyImportButton.addTarget(self, action: #selector(historyImportTapped), for: .touchUpInside)
albumImportButton.addTarget(self, action: #selector(albumImportTapped), for: .touchUpInside)
@@ -567,6 +567,7 @@ final class WiredCameraTransferViewController: BaseViewController {
let selected = viewModel.selectedPhotoIds.contains(item.id)
cell.apply(item: item, selectionMode: viewModel.selectUploadMode, selected: selected)
cell.onRetry = { [weak self] in self?.viewModel.retryPhoto(photoId: item.id) }
cell.onRetryRetouch = { [weak self] in self?.viewModel.retryAutoRetouch(photoId: item.id) }
cell.onDelete = { [weak self] in self?.viewModel.deletePhoto(photoId: item.id) }
}
@@ -711,6 +712,17 @@ final class WiredCameraTransferViewController: BaseViewController {
viewModel.refreshCameraFiles()
}
@objc private func retouchTapped() {
let controller = TravelAlbumAutoRetouchSettingSheetViewController(
configuration: viewModel.autoRetouchConfiguration,
startsWithModeSelection: true
)
controller.onConfirm = { [weak self] configuration in
self?.viewModel.updateAutoRetouchConfiguration(configuration)
}
present(controller, animated: true)
}
@objc private func batchTapped() {
viewModel.onBatchUploadButtonClick()
}
@@ -1126,6 +1138,7 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
private let selectionIconView = UIImageView()
private let imageView = UIImageView()
private let retouchBadgeLabel = UILabel()
private let statusLabel = UILabel()
private let titleLabel = UILabel()
private let sizeLabel = UILabel()
@@ -1134,6 +1147,7 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
private let separatorView = UIView()
var onRetry: (() -> Void)?
var onRetryRetouch: (() -> Void)?
var onDelete: (() -> Void)?
override init(frame: CGRect) {
@@ -1151,6 +1165,7 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
imageView.kf.cancelDownloadTask()
imageView.image = nil
onRetry = nil
onRetryRetouch = nil
onDelete = nil
menuButton.menu = nil
}
@@ -1178,16 +1193,21 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
titleLabel.alpha = rowAlpha
sizeLabel.text = item.fileSizeText
sizeLabel.alpha = rowAlpha
statusLabel.text = statusText(item.status)
statusLabel.textColor = statusTextColor(item.status)
statusLabel.backgroundColor = statusBackgroundColor(item.status)
progressView.isHidden = item.status != .uploading && item.status != .transferring
progressView.progress = Float(item.progress) / 100.0
statusLabel.text = statusText(item)
statusLabel.textColor = statusTextColor(item)
statusLabel.backgroundColor = statusBackgroundColor(item)
retouchBadgeLabel.isHidden = item.autoRetouchState != .completed
let isProcessing = item.autoRetouchState == .processing
progressView.isHidden = !isProcessing && item.status != .uploading && item.status != .transferring
progressView.progress = isProcessing ? 0.62 : Float(item.progress) / 100.0
selectionIconView.isHidden = !selectionMode
selectionIconView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
selectionIconView.tintColor = canSelect ? (selected ? AppColor.primary : AppColor.textTertiary) : AppColor.textTertiary.withAlphaComponent(0.5)
menuButton.isHidden = selectionMode
menuButton.menu = makeMenu(canRetry: item.status == .failed || item.status == .pending)
menuButton.menu = makeMenu(
canRetryUpload: item.status == .failed || item.status == .pending,
canRetryRetouch: item.autoRetouchState == .failed
)
updateImageConstraints(selectionMode: selectionMode)
}
@@ -1197,6 +1217,14 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 6
retouchBadgeLabel.text = "修"
retouchBadgeLabel.font = .systemFont(ofSize: 9, weight: .semibold)
retouchBadgeLabel.textColor = .white
retouchBadgeLabel.textAlignment = .center
retouchBadgeLabel.backgroundColor = UIColor(hex: 0x7C3AED)
retouchBadgeLabel.layer.cornerRadius = 8
retouchBadgeLabel.clipsToBounds = true
retouchBadgeLabel.isHidden = true
statusLabel.font = .systemFont(ofSize: 9)
statusLabel.textAlignment = .center
statusLabel.layer.cornerRadius = 3
@@ -1219,6 +1247,7 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
contentView.addSubview(selectionIconView)
contentView.addSubview(imageView)
contentView.addSubview(retouchBadgeLabel)
contentView.addSubview(titleLabel)
contentView.addSubview(statusLabel)
contentView.addSubview(sizeLabel)
@@ -1232,6 +1261,11 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
make.size.equalTo(20)
}
updateImageConstraints(selectionMode: true)
retouchBadgeLabel.snp.makeConstraints { make in
make.top.equalTo(imageView).offset(2)
make.trailing.equalTo(imageView).offset(-2)
make.size.equalTo(16)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(imageView).offset(1)
make.leading.equalTo(imageView.snp.trailing).offset(8)
@@ -1266,15 +1300,19 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
}
}
private func makeMenu(canRetry: Bool) -> UIMenu {
private func makeMenu(canRetryUpload: Bool, canRetryRetouch: Bool) -> UIMenu {
let retry = UIAction(title: "重传", image: UIImage(systemName: "arrow.clockwise")) { [weak self] _ in
self?.onRetry?()
}
retry.attributes = canRetry ? [] : [.disabled]
retry.attributes = canRetryUpload ? [] : [.disabled]
let retryRetouch = UIAction(title: "重新修图", image: UIImage(systemName: "wand.and.stars")) { [weak self] _ in
self?.onRetryRetouch?()
}
retryRetouch.attributes = canRetryRetouch ? [] : [.disabled]
let delete = UIAction(title: "删除", image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] _ in
self?.onDelete?()
}
return UIMenu(children: [retry, delete])
return UIMenu(children: [retry, retryRetouch, delete])
}
private func updateImageConstraints(selectionMode: Bool) {
@@ -1290,8 +1328,13 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
}
}
private func statusText(_ status: TravelAlbumOTGUploadStatus) -> String {
switch status {
private func statusText(_ item: TravelAlbumOTGPhotoItem) -> String {
switch item.autoRetouchState {
case .processing: return "修图中"
case .failed: return "修图失败"
case .none, .completed: break
}
switch item.status {
case .pending: return "待上传"
case .transferring: return "传输中"
case .uploading: return "上传中"
@@ -1300,8 +1343,10 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
}
}
private func statusTextColor(_ status: TravelAlbumOTGUploadStatus) -> UIColor {
switch status {
private func statusTextColor(_ item: TravelAlbumOTGPhotoItem) -> UIColor {
if item.autoRetouchState == .processing { return AppColor.primary }
if item.autoRetouchState == .failed { return AppColor.danger }
switch item.status {
case .pending: return AppColor.textSecondary
case .transferring, .uploading: return AppColor.primary
case .uploaded: return UIColor(hex: 0x16A34A)
@@ -1309,8 +1354,10 @@ private final class WiredTransferPhotoCell: UICollectionViewCell {
}
}
private func statusBackgroundColor(_ status: TravelAlbumOTGUploadStatus) -> UIColor {
switch status {
private func statusBackgroundColor(_ item: TravelAlbumOTGPhotoItem) -> UIColor {
if item.autoRetouchState == .processing { return AppColor.primary.withAlphaComponent(0.12) }
if item.autoRetouchState == .failed { return AppColor.danger.withAlphaComponent(0.12) }
switch item.status {
case .pending:
return AppColor.pageBackground
case .transferring, .uploading: