feat: 优化 AI 修图模板与全屏预览
This commit is contained in:
@@ -358,7 +358,7 @@ enum TravelAlbumAIRetouchTemplateCategory: Int, Sendable, Hashable {
|
||||
switch self {
|
||||
case .refined: "原图精修"
|
||||
case .atmosphere: "氛围感修图"
|
||||
case .cover: "封面风格模板"
|
||||
case .cover: "封面风格"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -69,9 +69,9 @@ final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
/// 首次修图页按实际展示分组生成的规则说明。
|
||||
var initialTipsText: String {
|
||||
if visibleCategories.contains(.cover) {
|
||||
return "Tips:本次显示 3 类风格。每张照片生成「精修后 + 氛围感」2 个结果,第一张照片另生成所选封面结果。"
|
||||
return "每张照片生成精修结果,氛围感可选;本次可免费生成 1 张封面"
|
||||
}
|
||||
return "Tips:本次显示 2 类风格。氛围感修图为选填;选中后每张照片会生成「精修后 + 氛围感」2 个独立结果。"
|
||||
return "每张照片生成精修结果,氛围感可选"
|
||||
}
|
||||
|
||||
/// 当前分组是否为选填。
|
||||
|
||||
@@ -39,7 +39,7 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
private let validationLabel = UILabel()
|
||||
private let actionStack = UIStackView()
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
private let confirmButton = UIButton(type: .system)
|
||||
private let confirmButton = AIRetouchGradientButton(type: .system)
|
||||
|
||||
/// 创建 AI 修图模板选择 Sheet。
|
||||
init(
|
||||
@@ -110,7 +110,7 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
footerStack.alignment = .fill
|
||||
actionStack.axis = .horizontal
|
||||
actionStack.spacing = 12
|
||||
actionStack.distribution = .fillEqually
|
||||
actionStack.distribution = .fill
|
||||
actionStack.alignment = .fill
|
||||
configureCancelButton()
|
||||
configureConfirmButton()
|
||||
@@ -145,6 +145,9 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
actionStack.snp.makeConstraints { make in
|
||||
make.height.equalTo(56)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.width.equalTo(confirmButton.snp.width).multipliedBy(0.78)
|
||||
}
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
@@ -202,37 +205,25 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func configureCancelButton() {
|
||||
var configuration = UIButton.Configuration.filled()
|
||||
configuration.title = "取消"
|
||||
configuration.baseBackgroundColor = .white
|
||||
configuration.baseForegroundColor = AIRetouchTemplateStyle.textPrimary
|
||||
configuration.background.cornerRadius = 12
|
||||
configuration.background.strokeColor = UIColor(hex: 0xD1D1D6)
|
||||
configuration.background.strokeWidth = 1
|
||||
configuration.titleTextAttributesTransformer = buttonTitleTransformer
|
||||
cancelButton.configuration = configuration
|
||||
cancelButton.setTitle("取消", for: .normal)
|
||||
cancelButton.setTitleColor(AIRetouchTemplateStyle.primary, for: .normal)
|
||||
cancelButton.setTitleColor(AIRetouchTemplateStyle.primary.withAlphaComponent(0.45), for: .disabled)
|
||||
cancelButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
cancelButton.backgroundColor = AIRetouchTemplateStyle.cardBackground
|
||||
cancelButton.layer.cornerRadius = 12
|
||||
cancelButton.layer.borderColor = AIRetouchTemplateStyle.primary.cgColor
|
||||
cancelButton.layer.borderWidth = 1
|
||||
cancelButton.accessibilityIdentifier = "travelAlbum.aiRetouchCancelButton"
|
||||
}
|
||||
|
||||
private func configureConfirmButton() {
|
||||
var configuration = UIButton.Configuration.filled()
|
||||
configuration.title = "确定"
|
||||
configuration.baseBackgroundColor = AIRetouchTemplateStyle.primary
|
||||
configuration.baseForegroundColor = .white
|
||||
configuration.background.cornerRadius = 12
|
||||
configuration.titleTextAttributesTransformer = buttonTitleTransformer
|
||||
confirmButton.configuration = configuration
|
||||
confirmButton.setTitle("确定", for: .normal)
|
||||
confirmButton.setTitleColor(.white, for: .normal)
|
||||
confirmButton.setTitleColor(.white, for: .disabled)
|
||||
confirmButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
confirmButton.accessibilityIdentifier = "travelAlbum.aiRetouchConfirmButton"
|
||||
}
|
||||
|
||||
private var buttonTitleTransformer: UIConfigurationTextAttributesTransformer {
|
||||
UIConfigurationTextAttributesTransformer { attributes in
|
||||
var attributes = attributes
|
||||
attributes.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
return attributes
|
||||
}
|
||||
}
|
||||
|
||||
private func configureDataSource() {
|
||||
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
|
||||
[weak self] collectionView, indexPath, item in
|
||||
@@ -267,6 +258,7 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
for: indexPath
|
||||
) as! TravelAlbumAIRetouchModeCell
|
||||
cell.apply(
|
||||
requiredQuota: self.viewModel.requiredQuota,
|
||||
remainingQuota: self.viewModel.remainingQuota,
|
||||
insufficient: self.viewModel.remainingQuota.map {
|
||||
self.viewModel.requiredQuota > $0
|
||||
@@ -293,10 +285,10 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
title: category.title,
|
||||
badge: category == .cover
|
||||
? .gift
|
||||
: (self.viewModel.isOptional(category) ? .optional : nil)
|
||||
: (self.viewModel.isOptional(category) ? .optional : .required)
|
||||
)
|
||||
case .mode:
|
||||
header.apply(title: "修图模式", badge: nil)
|
||||
return nil
|
||||
case .overview:
|
||||
return nil
|
||||
}
|
||||
@@ -305,7 +297,7 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func makeLayout() -> UICollectionViewCompositionalLayout {
|
||||
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
|
||||
UICollectionViewCompositionalLayout { [weak self] sectionIndex, layoutEnvironment in
|
||||
guard let self,
|
||||
self.dataSource != nil,
|
||||
self.dataSource.snapshot().sectionIdentifiers.indices.contains(sectionIndex) else {
|
||||
@@ -315,25 +307,27 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
case .overview:
|
||||
let size = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(self.viewModel.shouldShowInitialTips ? 160 : 74)
|
||||
heightDimension: .absolute(self.viewModel.shouldShowInitialTips ? 150 : 82)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: size)
|
||||
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item])
|
||||
return NSCollectionLayoutSection(group: group)
|
||||
case .templates:
|
||||
let availableWidth = layoutEnvironment.container.effectiveContentSize.width
|
||||
let cardWidth = min(118, floor((availableWidth - 56) / 3))
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(118),
|
||||
heightDimension: .absolute(154)
|
||||
widthDimension: .absolute(cardWidth),
|
||||
heightDimension: .absolute(156)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
||||
section.interGroupSpacing = 12
|
||||
section.interGroupSpacing = 10
|
||||
section.contentInsets = NSDirectionalEdgeInsets(
|
||||
top: 4,
|
||||
leading: 18,
|
||||
bottom: 12,
|
||||
bottom: 14,
|
||||
trailing: 18
|
||||
)
|
||||
section.boundarySupplementaryItems = [
|
||||
@@ -350,27 +344,17 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
case .mode:
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(64)
|
||||
heightDimension: .absolute(78)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsets = NSDirectionalEdgeInsets(
|
||||
top: 4,
|
||||
top: 8,
|
||||
leading: 18,
|
||||
bottom: 18,
|
||||
trailing: 18
|
||||
)
|
||||
section.boundarySupplementaryItems = [
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(42)
|
||||
),
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .top
|
||||
),
|
||||
]
|
||||
return section
|
||||
}
|
||||
}
|
||||
@@ -400,10 +384,9 @@ final class TravelAlbumAIRetouchTemplateViewController: BaseViewController {
|
||||
cancelButton.isEnabled = !viewModel.isSubmitting
|
||||
confirmButton.isEnabled = viewModel.canSubmit
|
||||
confirmButton.alpha = viewModel.canSubmit ? 1 : 0.45
|
||||
var confirmConfiguration = confirmButton.configuration
|
||||
confirmConfiguration?.title = viewModel.isSubmitting ? "提交中" : "确定"
|
||||
confirmConfiguration?.showsActivityIndicator = viewModel.isSubmitting
|
||||
confirmButton.configuration = confirmConfiguration
|
||||
confirmButton.setGradientEnabled(viewModel.canSubmit)
|
||||
confirmButton.setTitle(viewModel.isSubmitting ? "提交中" : "确定", for: .normal)
|
||||
confirmButton.setLoading(viewModel.isSubmitting)
|
||||
isModalInPresentation = viewModel.isSubmitting
|
||||
applySnapshot()
|
||||
}
|
||||
@@ -478,58 +461,84 @@ final class TravelAlbumAIRetouchOverviewCell: UICollectionViewCell {
|
||||
private let titleLabel = UILabel()
|
||||
private let selectionCountLabel = UILabel()
|
||||
private let tipsContainer = UIView()
|
||||
private let tipsIconContainer = UIView()
|
||||
private let tipsIconView = UIImageView()
|
||||
private let tipsLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
titleLabel.text = "AI修图"
|
||||
titleLabel.textColor = AIRetouchTemplateStyle.textPrimary
|
||||
titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.accessibilityTraits = .header
|
||||
|
||||
selectionCountLabel.textColor = AIRetouchTemplateStyle.textSecondary
|
||||
selectionCountLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(
|
||||
for: .systemFont(ofSize: 17, weight: .regular)
|
||||
selectionCountLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(
|
||||
for: .systemFont(ofSize: 15, weight: .regular)
|
||||
)
|
||||
selectionCountLabel.adjustsFontForContentSizeCategory = true
|
||||
selectionCountLabel.textAlignment = .center
|
||||
selectionCountLabel.accessibilityIdentifier = "travelAlbum.aiRetouchSelectionCountLabel"
|
||||
|
||||
tipsContainer.backgroundColor = AIRetouchTemplateStyle.tipsBackground
|
||||
tipsContainer.layer.cornerRadius = 10
|
||||
tipsContainer.layer.cornerRadius = 12
|
||||
tipsContainer.layer.borderWidth = 1
|
||||
tipsContainer.layer.borderColor = AIRetouchTemplateStyle.tipsBorder.cgColor
|
||||
tipsContainer.accessibilityIdentifier = "travelAlbum.aiRetouchTipsContainer"
|
||||
|
||||
tipsIconContainer.backgroundColor = AIRetouchTemplateStyle.primary
|
||||
tipsIconContainer.layer.cornerRadius = 8
|
||||
tipsIconView.image = UIImage(systemName: "wand.and.stars")
|
||||
tipsIconView.tintColor = .white
|
||||
tipsIconView.contentMode = .scaleAspectFit
|
||||
|
||||
tipsLabel.textColor = AIRetouchTemplateStyle.tipsText
|
||||
tipsLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(
|
||||
for: .systemFont(ofSize: 13, weight: .regular)
|
||||
for: .systemFont(ofSize: 12, weight: .regular)
|
||||
)
|
||||
tipsLabel.adjustsFontForContentSizeCategory = true
|
||||
tipsLabel.numberOfLines = 0
|
||||
tipsLabel.numberOfLines = 2
|
||||
tipsLabel.accessibilityIdentifier = "travelAlbum.aiRetouchTipsLabel"
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.spacing = 4
|
||||
stackView.spacing = 2
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
stackView.addArrangedSubview(selectionCountLabel)
|
||||
stackView.addArrangedSubview(tipsContainer)
|
||||
stackView.setCustomSpacing(12, after: selectionCountLabel)
|
||||
stackView.setCustomSpacing(14, after: selectionCountLabel)
|
||||
|
||||
contentView.addSubview(stackView)
|
||||
tipsContainer.addSubview(tipsIconContainer)
|
||||
tipsIconContainer.addSubview(tipsIconView)
|
||||
tipsContainer.addSubview(tipsLabel)
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(10)
|
||||
make.top.equalToSuperview().offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(18)
|
||||
make.bottom.lessThanOrEqualToSuperview().offset(-10)
|
||||
make.bottom.lessThanOrEqualToSuperview().offset(-8)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.height.equalTo(30)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
selectionCountLabel.snp.makeConstraints { make in
|
||||
make.height.equalTo(24)
|
||||
make.height.equalTo(22)
|
||||
}
|
||||
tipsContainer.snp.makeConstraints { make in
|
||||
make.height.equalTo(52)
|
||||
}
|
||||
tipsIconContainer.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(32)
|
||||
}
|
||||
tipsIconView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(7)
|
||||
}
|
||||
tipsLabel.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12))
|
||||
make.leading.equalTo(tipsIconContainer.snp.trailing).offset(10)
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +552,7 @@ final class TravelAlbumAIRetouchOverviewCell: UICollectionViewCell {
|
||||
selectionCountLabel.text = "已选择 \(selectedPhotoCount) 张照片"
|
||||
tipsLabel.text = tips
|
||||
tipsContainer.isHidden = tips == nil
|
||||
tipsIconContainer.isHidden = tips == nil
|
||||
tipsLabel.isHidden = tips == nil
|
||||
}
|
||||
}
|
||||
@@ -560,22 +570,27 @@ final class TravelAlbumAIRetouchTemplateCell: UICollectionViewCell {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.backgroundColor = .white
|
||||
contentView.backgroundColor = AIRetouchTemplateStyle.cardBackground
|
||||
contentView.layer.cornerRadius = 12
|
||||
contentView.layer.borderWidth = 1
|
||||
contentView.clipsToBounds = true
|
||||
layer.shadowColor = UIColor(hex: 0x254A7A).cgColor
|
||||
layer.shadowOpacity = 0.08
|
||||
layer.shadowRadius = 7
|
||||
layer.shadowOffset = CGSize(width: 0, height: 3)
|
||||
layer.masksToBounds = false
|
||||
|
||||
previewImageView.contentMode = .scaleAspectFill
|
||||
previewImageView.clipsToBounds = true
|
||||
previewImageView.backgroundColor = UIColor(hex: 0xEDF2F8)
|
||||
nameLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
nameLabel.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
nameLabel.textColor = AIRetouchTemplateStyle.textPrimary
|
||||
nameLabel.textAlignment = .center
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
checkView.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
checkView.tintColor = AIRetouchTemplateStyle.primary
|
||||
checkView.backgroundColor = .white
|
||||
checkView.layer.cornerRadius = 10
|
||||
checkView.layer.cornerRadius = 11
|
||||
var previewConfiguration = UIButton.Configuration.filled()
|
||||
previewConfiguration.title = "预览"
|
||||
previewConfiguration.baseForegroundColor = .white
|
||||
@@ -598,7 +613,7 @@ final class TravelAlbumAIRetouchTemplateCell: UICollectionViewCell {
|
||||
contentView.addSubview(previewButton)
|
||||
previewImageView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(116)
|
||||
make.height.equalTo(118)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(previewImageView.snp.bottom)
|
||||
@@ -606,13 +621,13 @@ final class TravelAlbumAIRetouchTemplateCell: UICollectionViewCell {
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
checkView.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview().inset(8)
|
||||
make.size.equalTo(20)
|
||||
make.top.trailing.equalToSuperview().inset(7)
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
previewButton.snp.makeConstraints { make in
|
||||
make.trailing.bottom.equalTo(previewImageView).inset(8)
|
||||
make.width.equalTo(58)
|
||||
make.height.equalTo(32)
|
||||
make.trailing.bottom.equalTo(previewImageView).inset(7)
|
||||
make.width.equalTo(54)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,6 +664,7 @@ final class TravelAlbumAIRetouchTemplateCell: UICollectionViewCell {
|
||||
selected ? AIRetouchTemplateStyle.primary : AIRetouchTemplateStyle.border
|
||||
).cgColor
|
||||
contentView.layer.borderWidth = selected ? 2 : 1
|
||||
layer.shadowOpacity = selected ? 0.16 : 0.08
|
||||
checkView.isHidden = !selected
|
||||
isSelected = selected
|
||||
accessibilityLabel = template.name
|
||||
@@ -668,43 +684,138 @@ private final class AIRetouchTemplatePreviewButton: UIButton {
|
||||
}
|
||||
}
|
||||
|
||||
/// 模板选择页主操作按钮,使用品牌蓝渐变并支持禁用态切换。
|
||||
private final class AIRetouchGradientButton: UIButton {
|
||||
private let gradientLayer = CAGradientLayer()
|
||||
private let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
gradientLayer.colors = [
|
||||
AIRetouchTemplateStyle.primary.cgColor,
|
||||
AIRetouchTemplateStyle.primaryGradientEnd.cgColor,
|
||||
]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||
gradientLayer.cornerRadius = 12
|
||||
layer.insertSublayer(gradientLayer, at: 0)
|
||||
activityIndicator.color = .white
|
||||
activityIndicator.hidesWhenStopped = true
|
||||
activityIndicator.isUserInteractionEnabled = false
|
||||
addSubview(activityIndicator)
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
gradientLayer.frame = bounds
|
||||
activityIndicator.center = CGPoint(x: bounds.midX - 44, y: bounds.midY)
|
||||
}
|
||||
|
||||
/// 根据提交条件切换品牌渐变和禁用灰色。
|
||||
func setGradientEnabled(_ enabled: Bool) {
|
||||
gradientLayer.colors = enabled
|
||||
? [
|
||||
AIRetouchTemplateStyle.primary.cgColor,
|
||||
AIRetouchTemplateStyle.primaryGradientEnd.cgColor,
|
||||
]
|
||||
: [
|
||||
AIRetouchTemplateStyle.disabledStart.cgColor,
|
||||
AIRetouchTemplateStyle.disabledEnd.cgColor,
|
||||
]
|
||||
}
|
||||
|
||||
/// 切换提交中状态,并在标题左侧展示活动指示器。
|
||||
func setLoading(_ loading: Bool) {
|
||||
if loading {
|
||||
activityIndicator.startAnimating()
|
||||
} else {
|
||||
activityIndicator.stopAnimating()
|
||||
}
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图计费模式卡,展示按张收费规则与剩余额度。
|
||||
final class TravelAlbumAIRetouchModeCell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "TravelAlbumAIRetouchModeCell"
|
||||
|
||||
private let iconContainer = UIView()
|
||||
private let iconView = UIImageView()
|
||||
private let modeLabel = UILabel()
|
||||
private let quotaContainer = UIView()
|
||||
private let quotaStack = UIStackView()
|
||||
private let requiredQuotaContainer = UIView()
|
||||
private let requiredQuotaLabel = UILabel()
|
||||
private let quotaLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.backgroundColor = .white
|
||||
contentView.layer.cornerRadius = 12
|
||||
contentView.layer.borderWidth = 1
|
||||
contentView.layer.borderColor = AIRetouchTemplateStyle.border.cgColor
|
||||
contentView.layer.shadowColor = UIColor(hex: 0x254A7A).cgColor
|
||||
contentView.layer.shadowOpacity = 0.06
|
||||
contentView.layer.shadowRadius = 8
|
||||
contentView.layer.shadowOffset = CGSize(width: 0, height: 3)
|
||||
|
||||
iconContainer.backgroundColor = AIRetouchTemplateStyle.primary
|
||||
iconContainer.layer.cornerRadius = 10
|
||||
iconView.image = UIImage(systemName: "wand.and.stars")
|
||||
iconView.tintColor = .white
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
modeLabel.text = "AI精修 · 按张收费"
|
||||
modeLabel.textColor = AIRetouchTemplateStyle.textPrimary
|
||||
modeLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
quotaContainer.backgroundColor = AIRetouchTemplateStyle.primary.withAlphaComponent(0.1)
|
||||
quotaContainer.layer.cornerRadius = 15
|
||||
quotaLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .medium)
|
||||
quotaLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
quotaLabel.textAlignment = .center
|
||||
modeLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
|
||||
quotaStack.axis = .vertical
|
||||
quotaStack.alignment = .trailing
|
||||
quotaStack.spacing = 3
|
||||
requiredQuotaContainer.backgroundColor = AIRetouchTemplateStyle.primary.withAlphaComponent(0.1)
|
||||
requiredQuotaContainer.layer.cornerRadius = 12
|
||||
requiredQuotaLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .semibold)
|
||||
requiredQuotaLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
requiredQuotaLabel.textAlignment = .center
|
||||
requiredQuotaLabel.accessibilityIdentifier = "travelAlbum.aiRetouchRequiredQuotaLabel"
|
||||
quotaLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
||||
quotaLabel.textColor = AIRetouchTemplateStyle.textSecondary
|
||||
quotaLabel.textAlignment = .right
|
||||
quotaLabel.accessibilityIdentifier = "travelAlbum.aiRetouchRemainingQuotaLabel"
|
||||
|
||||
contentView.addSubview(iconContainer)
|
||||
iconContainer.addSubview(iconView)
|
||||
contentView.addSubview(modeLabel)
|
||||
contentView.addSubview(quotaContainer)
|
||||
quotaContainer.addSubview(quotaLabel)
|
||||
contentView.addSubview(quotaStack)
|
||||
quotaStack.addArrangedSubview(requiredQuotaContainer)
|
||||
requiredQuotaContainer.addSubview(requiredQuotaLabel)
|
||||
quotaStack.addArrangedSubview(quotaLabel)
|
||||
iconContainer.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(14)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(38)
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(9)
|
||||
}
|
||||
modeLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.leading.equalTo(iconContainer.snp.trailing).offset(10)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(quotaContainer.snp.leading).offset(-12)
|
||||
make.trailing.lessThanOrEqualTo(quotaStack.snp.leading).offset(-10)
|
||||
}
|
||||
quotaContainer.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
quotaStack.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-14)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.equalTo(96)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
quotaLabel.snp.makeConstraints { make in
|
||||
requiredQuotaContainer.snp.makeConstraints { make in
|
||||
make.height.equalTo(24)
|
||||
make.width.greaterThanOrEqualTo(108)
|
||||
}
|
||||
requiredQuotaLabel.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
@@ -714,20 +825,24 @@ final class TravelAlbumAIRetouchModeCell: UICollectionViewCell {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 更新剩余可精修张数,并在额度不足时切换为警示色。
|
||||
func apply(remainingQuota: Int?, insufficient: Bool) {
|
||||
/// 更新预计消耗和剩余精修张数,并在额度不足时切换为警示色。
|
||||
func apply(requiredQuota: Int, remainingQuota: Int?, insufficient: Bool) {
|
||||
requiredQuotaLabel.text = "预计消耗 \(requiredQuota) 张"
|
||||
requiredQuotaLabel.accessibilityLabel = "预计消耗\(requiredQuota)张修图额度"
|
||||
quotaLabel.text = remainingQuota.map { "剩余 \($0) 张" } ?? "剩余 -- 张"
|
||||
quotaLabel.accessibilityLabel = remainingQuota.map { "剩余\($0)张修图额度" }
|
||||
?? "剩余修图额度暂不可用"
|
||||
quotaLabel.textColor = insufficient
|
||||
requiredQuotaLabel.textColor = insufficient
|
||||
? AIRetouchTemplateStyle.danger
|
||||
: (remainingQuota == nil ? AIRetouchTemplateStyle.textSecondary : AIRetouchTemplateStyle.primary)
|
||||
quotaContainer.backgroundColor = quotaLabel.textColor.withAlphaComponent(0.1)
|
||||
: AIRetouchTemplateStyle.primary
|
||||
requiredQuotaContainer.backgroundColor = requiredQuotaLabel.textColor.withAlphaComponent(0.1)
|
||||
quotaLabel.textColor = insufficient ? AIRetouchTemplateStyle.danger : AIRetouchTemplateStyle.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
/// 模板分组标题右侧的业务标记。
|
||||
fileprivate enum AIRetouchTemplateSectionBadge {
|
||||
case required
|
||||
case optional
|
||||
case gift
|
||||
}
|
||||
@@ -773,12 +888,16 @@ final class TravelAlbumAIRetouchSectionHeader: UICollectionReusableView {
|
||||
titleLabel.text = title
|
||||
optionalLabel.isHidden = badge == nil
|
||||
switch badge {
|
||||
case .required:
|
||||
optionalLabel.text = " 必选 "
|
||||
optionalLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
optionalLabel.backgroundColor = AIRetouchTemplateStyle.primary.withAlphaComponent(0.1)
|
||||
case .optional:
|
||||
optionalLabel.text = " 选填 "
|
||||
optionalLabel.textColor = AIRetouchTemplateStyle.primary
|
||||
optionalLabel.backgroundColor = AIRetouchTemplateStyle.primary.withAlphaComponent(0.1)
|
||||
case .gift:
|
||||
optionalLabel.text = " 赠送 · 不占精修张数 "
|
||||
optionalLabel.text = " 赠送 · 不占额度 "
|
||||
optionalLabel.textColor = AIRetouchTemplateStyle.gift
|
||||
optionalLabel.backgroundColor = AIRetouchTemplateStyle.giftBackground
|
||||
case nil:
|
||||
@@ -918,13 +1037,18 @@ extension TravelAlbumAIRetouchTemplatePreviewViewController: UIScrollViewDelegat
|
||||
/// AI 修图模板页视觉常量。
|
||||
private enum AIRetouchTemplateStyle {
|
||||
static let primary = UIColor(hex: 0x1677FF)
|
||||
static let pageBackground = UIColor(hex: 0xF7F7F7)
|
||||
static let primaryGradientEnd = UIColor(hex: 0x4B9BFF)
|
||||
static let pageBackground = UIColor(hex: 0xF7FAFF)
|
||||
static let cardBackground = UIColor.white
|
||||
static let textPrimary = UIColor(hex: 0x172033)
|
||||
static let textSecondary = UIColor(hex: 0x6E6E73)
|
||||
static let tipsBackground = UIColor(hex: 0xEAF3FF)
|
||||
static let textSecondary = UIColor(hex: 0x7D8799)
|
||||
static let tipsBackground = UIColor(hex: 0xF4F8FF)
|
||||
static let tipsBorder = UIColor(hex: 0xD9E7FA)
|
||||
static let tipsText = UIColor(hex: 0x385778)
|
||||
static let gift = UIColor(hex: 0xFF9418)
|
||||
static let giftBackground = UIColor(hex: 0xFFF1D9)
|
||||
static let border = UIColor(hex: 0xDCE4EF)
|
||||
static let danger = UIColor(hex: 0xE53935)
|
||||
static let disabledStart = UIColor(hex: 0xA9C8EE)
|
||||
static let disabledEnd = UIColor(hex: 0xC6DAF3)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
collectionView.isPagingEnabled = true
|
||||
collectionView.alwaysBounceHorizontal = nodes.count > 1
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.contentInsetAdjustmentBehavior = .never
|
||||
collectionView.accessibilityIdentifier = "travelAlbum.previewCollectionView"
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
@@ -225,9 +226,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController {
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(topChrome.snp.bottom)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-126.5)
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
divider.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
|
||||
Reference in New Issue
Block a user