新增线下收款记录和 ai 修图优化
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user