diff --git a/docs/design/ai-retouch-v1/00-flow-overview.png b/docs/design/ai-retouch-v1/00-flow-overview.png new file mode 100644 index 0000000..2216d6f Binary files /dev/null and b/docs/design/ai-retouch-v1/00-flow-overview.png differ diff --git a/docs/design/ai-retouch-v1/01-album-multi-selection.png b/docs/design/ai-retouch-v1/01-album-multi-selection.png new file mode 100644 index 0000000..d35af61 Binary files /dev/null and b/docs/design/ai-retouch-v1/01-album-multi-selection.png differ diff --git a/docs/design/ai-retouch-v1/02-template-selection.png b/docs/design/ai-retouch-v1/02-template-selection.png new file mode 100644 index 0000000..a40ed96 Binary files /dev/null and b/docs/design/ai-retouch-v1/02-template-selection.png differ diff --git a/docs/design/ai-retouch-v1/03-template-preview.png b/docs/design/ai-retouch-v1/03-template-preview.png new file mode 100644 index 0000000..8fd965b Binary files /dev/null and b/docs/design/ai-retouch-v1/03-template-preview.png differ diff --git a/docs/design/ai-retouch-v1/04-result-success.png b/docs/design/ai-retouch-v1/04-result-success.png new file mode 100644 index 0000000..d9b613e Binary files /dev/null and b/docs/design/ai-retouch-v1/04-result-success.png differ diff --git a/docs/design/ai-retouch-v1/05-result-processing.png b/docs/design/ai-retouch-v1/05-result-processing.png new file mode 100644 index 0000000..61d23e3 Binary files /dev/null and b/docs/design/ai-retouch-v1/05-result-processing.png differ diff --git a/docs/design/ai-retouch-v1/06-result-failed.png b/docs/design/ai-retouch-v1/06-result-failed.png new file mode 100644 index 0000000..e9e311b Binary files /dev/null and b/docs/design/ai-retouch-v1/06-result-failed.png differ diff --git a/docs/design/ai-retouch-v1/README.md b/docs/design/ai-retouch-v1/README.md new file mode 100644 index 0000000..560cca4 --- /dev/null +++ b/docs/design/ai-retouch-v1/README.md @@ -0,0 +1,85 @@ +# AI 修图完整链路设计稿 V1 + +## 交付内容 + +| 文件 | 页面 / 状态 | +|---|---| +| `00-flow-overview.png` | 六张关键屏稿总览 | +| `01-album-multi-selection.png` | 相册管理 · 已选择 4 张照片 | +| `02-template-selection.png` | AI 修图模板选择 Sheet | +| `03-template-preview.png` | 模板全屏预览 | +| `04-result-success.png` | 精修结果 · 成功 | +| `05-result-processing.png` | 精修结果 · 生成新版本中 | +| `06-result-failed.png` | 精修结果 · 新版本生成失败 | + +独立屏稿均为 853 × 1844 像素(约 9:19.5),面向 iOS 16+、iPhone 竖屏、浅色模式。 + +## 设计系统 + +- 主色:`#1677FF`;辅助渐变:`#5CA8FF`、`#A7D8FF`。 +- 页面底色:`#F7FAFF`;主文字:`#172033`;次文字:`#667085`。 +- 失败色:`#EF4444`;赠送/免费标签使用橙色,避免与主要操作竞争。 +- 字体:SF Pro / 苹方风格;圆角 12–16 pt;以 8 pt 为基础间距。 +- 正文置于高不透明度表面;极光光晕只用于背景与重点操作,不承载关键信息。 +- 结果页只通过“原图 / 精修后 / 氛围感”标签切换,不使用对比滑杆。 +- 重新修图期间及失败后保留上一版成功图片;状态反馈只占局部,不遮挡照片。 + +## 可复用 ImageGen 提示词 + +以下提示词使用内置 ImageGen。首屏以 `../travel-album-management-redesign-v2.png` 为风格参考;后续页面以上一张已确认屏稿作为参考,确保视觉连续。 + +### 通用前缀 + +```text +Use case: ui-mockup +Asset type: shippable high-fidelity iOS app screen, full portrait canvas, no device frame +Style/medium: realistic production UIKit UI, light Aurora blue, crisp SF Pro / PingFang-like typography, SF Symbols-style icons, 12–16 pt corners, 8 pt spacing grid, subtle borders and restrained shadows. +Color palette: primary #1677FF, gradient accents #5CA8FF and #A7D8FF, background #F7FAFF, text #172033 and #667085, danger #EF4444. +Constraints: iPhone portrait 9:19.5; iOS status bar and home indicator; practical UIKit-buildable hierarchy; accessible contrast; minimum 44 pt touch targets; no dark mode; no watermark; no decorative device frame; no annotations. +Text constraints: render specified Chinese text exactly once; no gibberish, duplicate labels or unrelated copy. +``` + +### 01 · 相册多选 + +```text +Create the album management screen in multi-selection mode. Top navigation: “相册管理” and “完成”. Album summary: “旅拍相册”, “138****8000”. Filter row: “全部 128”, “已购 36”, “时间 ↓”. Use a precise three-column grid of nine travel photos. Four photos have blue checkmarks, blue outlines and subtle selected overlays; other photos show empty selectors. Fixed bottom actions: red outline “删除 4 张” and blue gradient “AI修图 4 张”. +``` + +### 02 · 模板选择 + +```text +Create a large native page sheet titled “AI修图” with subtitle “已选择 4 张照片”. Show the note “每张照片生成精修结果,氛围感可选;本次可免费生成 1 张封面”. Include three horizontal sections: “原图精修” + “必选”, “氛围感修图” + “选填”, “封面风格” + “赠送 · 不占额度”. Each section has three photographic cards, a clear single selected state and “预览” buttons. Cost card: “AI精修 · 按张收费”, “预计消耗 8 张”, “剩余 12 张”. Fixed actions: “取消” and “确定”. +``` + +### 03 · 模板预览 + +```text +Create an immersive full-screen preview for the selected “自然通透” template. Use a dark navy photographic canvas, a circular back button, title “自然通透” and subtitle “双指缩放查看细节”. The large photo shows a woman in a straw hat overlooking Santorini. Add “模板效果预览”. Bottom information panel: “自然通透”, “提亮肤色与画面通透感,保留自然质感”, “当前已选择”. Primary action: “使用此模板”. Do not add a comparison slider. +``` + +### 04 · 正常结果 + +```text +Create “照片预览” with counter “1 / 9”. Add exactly three tabs: “原图”, “精修后”, “氛围感”; select “精修后”. Show the same Santorini photo with badge “AI已修”, filename “IMG_8291.JPG” and “自然通透 · 2026/08/13”. Information card: “精修结果”, “自然通透模板 · 已生成最新版本”. Fixed actions: “AI修图”, “删除”, “刷新”. Use tabs only; no slider or press-and-hold comparison. +``` + +### 05 · 处理中状态编辑 + +```text +Use case: precise-object-edit. Keep the successful result screen, photo, tabs and layout unchanged. Replace “AI已修” with spinner + “修图中”. Replace the result card with “正在生成新版本” and “当前仍展示上一版结果,完成后自动更新”, plus an indeterminate shimmer line with no percentage. Keep the previous photo unobscured. Disable only the “AI修图” button; leave “删除” and “刷新” available. +``` + +### 06 · 失败状态编辑 + +```text +Use case: precise-object-edit. Keep the successful result screen, photo, tabs and layout unchanged. Replace “AI已修” with a local red badge “生成失败”. Replace the result card with pale-red recovery content: “新版本生成失败”, “上一版结果已保留,可重新修图”, and an outline action “重新修图”. Keep the previous photo unobscured and restore the bottom “AI修图” button to its primary blue state. Do not show a modal, stack trace or error code. +``` + +## QA 记录 + +- 六张独立屏稿尺寸一致,使用同一旅拍照片体系与组件语言。 +- 精修必选、氛围感选填、四张照片触发封面、封面不占额度均已表达。 +- 成功、处理中、失败三种状态均保留同一照片与结果标签,便于直接比较。 +- 处理中不展示虚构百分比;重复提交按钮禁用,刷新仍可用。 +- 失败状态提供“重新修图”恢复路径,且上一版结果保持可见。 +- 本目录只包含设计稿与说明,未修改 UIKit、业务逻辑或 Asset Catalog。 diff --git a/docs/design/ai-retouch-v2/00-flow-overview-v2.png b/docs/design/ai-retouch-v2/00-flow-overview-v2.png new file mode 100644 index 0000000..d8e4f50 Binary files /dev/null and b/docs/design/ai-retouch-v2/00-flow-overview-v2.png differ diff --git a/docs/design/ai-retouch-v2/01-album-multi-selection.png b/docs/design/ai-retouch-v2/01-album-multi-selection.png new file mode 100644 index 0000000..d35af61 Binary files /dev/null and b/docs/design/ai-retouch-v2/01-album-multi-selection.png differ diff --git a/docs/design/ai-retouch-v2/02-template-selection.png b/docs/design/ai-retouch-v2/02-template-selection.png new file mode 100644 index 0000000..a40ed96 Binary files /dev/null and b/docs/design/ai-retouch-v2/02-template-selection.png differ diff --git a/docs/design/ai-retouch-v2/03-template-preview-dark.png b/docs/design/ai-retouch-v2/03-template-preview-dark.png new file mode 100644 index 0000000..3555ae3 Binary files /dev/null and b/docs/design/ai-retouch-v2/03-template-preview-dark.png differ diff --git a/docs/design/ai-retouch-v2/04-result-success-dark.png b/docs/design/ai-retouch-v2/04-result-success-dark.png new file mode 100644 index 0000000..61af5da Binary files /dev/null and b/docs/design/ai-retouch-v2/04-result-success-dark.png differ diff --git a/docs/design/ai-retouch-v2/05-result-processing-dark.png b/docs/design/ai-retouch-v2/05-result-processing-dark.png new file mode 100644 index 0000000..f79d328 Binary files /dev/null and b/docs/design/ai-retouch-v2/05-result-processing-dark.png differ diff --git a/docs/design/ai-retouch-v2/06-result-failed-dark.png b/docs/design/ai-retouch-v2/06-result-failed-dark.png new file mode 100644 index 0000000..9ef198d Binary files /dev/null and b/docs/design/ai-retouch-v2/06-result-failed-dark.png differ diff --git a/docs/design/ai-retouch-v2/README.md b/docs/design/ai-retouch-v2/README.md new file mode 100644 index 0000000..2b4057c --- /dev/null +++ b/docs/design/ai-retouch-v2/README.md @@ -0,0 +1,76 @@ +# AI 修图完整链路设计稿 V2 + +## 本轮调整 + +- 第 1、2 张沿用 V1 的浅色“轻盈极光蓝”风格。 +- 第 3–6 张改为统一的黑色专业影像工作区,突出照片本身。 +- 暗色页面不做简单反色:背景、浮层、边框、文字和按钮均使用独立暗色语义层级。 +- V1 完整保留在 `../ai-retouch-v1/`,便于对比和回退。 + +## 交付内容 + +| 文件 | 页面 / 状态 | 风格 | +|---|---|---| +| `00-flow-overview-v2.png` | 六张关键屏稿总览 | 混合流程 | +| `01-album-multi-selection.png` | 相册管理 · 已选择 4 张 | 浅色 | +| `02-template-selection.png` | AI 修图模板选择 Sheet | 浅色 | +| `03-template-preview-dark.png` | 模板全屏预览 | 暗色 | +| `04-result-success-dark.png` | 精修结果 · 成功 | 暗色 | +| `05-result-processing-dark.png` | 精修结果 · 生成中 | 暗色 | +| `06-result-failed-dark.png` | 精修结果 · 生成失败 | 暗色 | + +## 暗色设计 Token + +- 页面背景:`#05070D`,允许向深海军蓝 `#07111F` 做非常轻微的层次过渡。 +- 抬升表面:`#111827`;边框:`#263244`。 +- 主文字:`#F8FAFC`;次文字:`#9CA3AF`;未选中标签:`#C7CED9`。 +- 品牌主色:`#1677FF`;蓝色高光:`#5CA8FF`。 +- 失败色:`#EF4444`,仅用于失败徽标、恢复入口和局部边框。 +- 图片保持明亮自然,不叠加全屏蒙层;信息区和操作区不出现白色表面。 + +## 可复用 ImageGen 暗色编辑提示词 + +以下提示词使用内置 ImageGen,以对应 V1 页面作为 edit target;第 3 张暗色稿确定后,也作为第 4–6 张的风格参考。 + +### 通用暗色编辑约束 + +```text +Use case: precise-object-edit +Restyle the target into a cohesive dark professional photography workspace. +Preserve the exact iPhone portrait dimensions, photo content and crop, person identity and pose, all Chinese copy, hierarchy, controls and safe-area spacing. +Use background #05070D, elevated surface #111827, border #263244, primary #1677FF, text #F8FAFC and secondary #9CA3AF. +Keep the bright photo unchanged as the focal point. Use white text and SF Symbols-style white icons. Use brand blue only for selected state and primary actions. +No white page areas, no comparison slider, no device frame, no annotations, no watermark and no additional copy. +``` + +### 模板预览 + +```text +Keep “自然通透”, “双指缩放查看细节”, the Santorini photo and “模板效果预览”. Change the lower information panel to charcoal with white heading, muted description and blue “当前已选择”. Change the complete bottom safe area to black and retain the blue “使用此模板” action. +``` + +### 修图结果 + +```text +Keep “照片预览”, “1 / 9” and the three tabs “原图 / 精修后 / 氛围感”. Use a charcoal segmented control with “精修后” selected in blue. Keep “AI已修”, the filename metadata, the photo and the result information. Change the result card and secondary actions “删除 / 刷新” to dark surfaces; keep “AI修图” blue. +``` + +### 处理中状态 + +```text +Based on the approved dark result screen, change only the badge to spinner + “修图中”. Keep the previous photo visible. Change the card to “正在生成新版本” and “当前仍展示上一版结果,完成后自动更新”, with an indeterminate shimmer line and no percentage. Disable only “AI修图”. +``` + +### 失败状态 + +```text +Based on the approved dark result screen, change only the badge to “生成失败”. Keep the previous photo visible. Use a charcoal recovery card with restrained red border, “新版本生成失败”, “上一版结果已保留,可重新修图” and “重新修图”. Keep the main “AI修图” button blue. Do not use a white or pale-red card. +``` + +## QA 记录 + +- 第 1、2 张与 V1 像素级复用,没有重新生成。 +- 第 3–6 张均为 853 × 1844,暗色背景、表面、文字和操作层级一致。 +- 成功、处理中、失败三态保留同一结果标签、照片和底部操作布局。 +- 处理中无虚构百分比;失败状态有明确恢复动作;旧结果始终可见。 +- 本目录只新增设计稿与说明,未修改 UIKit、业务逻辑或 Asset Catalog。 diff --git a/suixinkan/Features/TravelAlbum/Models/TravelAlbumModels.swift b/suixinkan/Features/TravelAlbum/Models/TravelAlbumModels.swift index d6c26df..576d488 100644 --- a/suixinkan/Features/TravelAlbum/Models/TravelAlbumModels.swift +++ b/suixinkan/Features/TravelAlbum/Models/TravelAlbumModels.swift @@ -358,7 +358,7 @@ enum TravelAlbumAIRetouchTemplateCategory: Int, Sendable, Hashable { switch self { case .refined: "原图精修" case .atmosphere: "氛围感修图" - case .cover: "封面风格模板" + case .cover: "封面风格" } } } diff --git a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumAIRetouchTemplateViewModel.swift b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumAIRetouchTemplateViewModel.swift index 312800a..666bf18 100644 --- a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumAIRetouchTemplateViewModel.swift +++ b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumAIRetouchTemplateViewModel.swift @@ -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 "每张照片生成精修结果,氛围感可选" } /// 当前分组是否为选填。 diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumAIRetouchTemplateViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumAIRetouchTemplateViewController.swift index 7bebfbc..b58f2fc 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumAIRetouchTemplateViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumAIRetouchTemplateViewController.swift @@ -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(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) } diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift index d11ea6e..b770127 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift @@ -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() diff --git a/suixinkanTests/TravelAlbumAIRetouchTemplateViewModelTests.swift b/suixinkanTests/TravelAlbumAIRetouchTemplateViewModelTests.swift index d0daf2c..404e55d 100644 --- a/suixinkanTests/TravelAlbumAIRetouchTemplateViewModelTests.swift +++ b/suixinkanTests/TravelAlbumAIRetouchTemplateViewModelTests.swift @@ -25,7 +25,7 @@ final class TravelAlbumAIRetouchTemplateViewModelTests: XCTestCase { XCTAssertTrue(viewModel.shouldShowInitialTips) XCTAssertEqual( viewModel.initialTipsText, - "Tips:本次显示 3 类风格。每张照片生成「精修后 + 氛围感」2 个结果,第一张照片另生成所选封面结果。" + "每张照片生成精修结果,氛围感可选;本次可免费生成 1 张封面" ) XCTAssertEqual(viewModel.visibleCategories, [.refined, .atmosphere, .cover]) XCTAssertEqual(viewModel.selectedRefinedTemplateId, 11) diff --git a/suixinkanTests/TravelAlbumDetailViewControllerTests.swift b/suixinkanTests/TravelAlbumDetailViewControllerTests.swift index 6bf3bfb..3b493f5 100644 --- a/suixinkanTests/TravelAlbumDetailViewControllerTests.swift +++ b/suixinkanTests/TravelAlbumDetailViewControllerTests.swift @@ -206,7 +206,7 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { XCTAssertFalse(imageView.constraintsAffectingLayout(for: .vertical).isEmpty) } - func testPreviewUsesScreenshotChromeAndConstrainedImageViewport() throws { + func testPreviewUsesScreenshotChromeOverFullScreenImageViewport() throws { UIView.setAnimationsEnabled(false) defer { UIView.setAnimationsEnabled(true) } @@ -287,8 +287,10 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { XCTAssertEqual(segmentedControl.selectedSegmentIndex, 0) XCTAssertEqual(backButton.bounds.size, CGSize(width: 44, height: 44)) XCTAssertNil(backButton.configuration?.baseBackgroundColor) - XCTAssertEqual(collectionView.frame.minY, topChrome.frame.maxY, accuracy: 0.5) - XCTAssertEqual(collectionView.frame.maxY, bottomChrome.frame.minY, accuracy: 0.5) + XCTAssertEqual(collectionView.frame, controller.view.bounds) + XCTAssertLessThan(collectionView.frame.minY, topChrome.frame.maxY) + XCTAssertGreaterThan(collectionView.frame.maxY, bottomChrome.frame.minY) + XCTAssertEqual(collectionView.contentInsetAdjustmentBehavior, .never) XCTAssertTrue(controller.view.allLabels().contains { $0.text == "IMG_0002.jpeg" }) XCTAssertTrue(controller.view.allLabels().contains { $0.text == "2.4MB" }) XCTAssertTrue(controller.view.allLabels().contains { $0.text == "1/10" }) @@ -345,7 +347,8 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { XCTAssertTrue(segmentedControl.isHidden) XCTAssertTrue(highResolutionButton.isHidden) - XCTAssertEqual(bottomChrome.frame.minY - collectionView.frame.maxY, 56, accuracy: 0.5) + XCTAssertEqual(collectionView.frame, controller.view.bounds) + XCTAssertGreaterThan(collectionView.frame.maxY, bottomChrome.frame.minY) } func testSelectionModeShowsAIRetouchOnLeftAndDeleteOnRight() async throws { @@ -459,17 +462,17 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { XCTAssertEqual(selectionCountLabel.text, "已选择 4 张照片") XCTAssertEqual( tipsLabel.text, - "Tips:本次显示 3 类风格。每张照片生成「精修后 + 氛围感」2 个结果,第一张照片另生成所选封面结果。" + "每张照片生成精修结果,氛围感可选;本次可免费生成 1 张封面" ) XCTAssertFalse(tipsContainer.isHidden) - XCTAssertEqual(tipsContainer.backgroundColor?.travelAlbumTestHexRGB, 0xEAF3FF) + XCTAssertEqual(tipsContainer.backgroundColor?.travelAlbumTestHexRGB, 0xF4F8FF) XCTAssertEqual(tipsLabel.textColor.travelAlbumTestHexRGB, 0x385778) - XCTAssertEqual(tipsContainer.layer.cornerRadius, 10) + XCTAssertEqual(tipsContainer.layer.cornerRadius, 12) XCTAssertTrue(tipsContainer.isDescendant(of: collectionView)) XCTAssertFalse(bottomBar.isDescendant(of: collectionView)) - XCTAssertEqual(cancelButton.configuration?.title, "取消") - XCTAssertEqual(confirmButton.configuration?.title, "确定") - XCTAssertEqual(cancelButton.bounds.width, confirmButton.bounds.width, accuracy: 0.5) + XCTAssertEqual(cancelButton.currentTitle, "取消") + XCTAssertEqual(confirmButton.currentTitle, "确定") + XCTAssertLessThan(cancelButton.bounds.width, confirmButton.bounds.width) XCTAssertTrue(confirmButton.isEnabled) } @@ -502,9 +505,9 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { } as? UICollectionView ) let accessibleLabels = controller.view.allAccessibilityLabels() - XCTAssertTrue(accessibleLabels.contains("原图精修")) + XCTAssertTrue(accessibleLabels.contains("原图精修,必选")) XCTAssertTrue(accessibleLabels.contains("氛围感修图,选填")) - XCTAssertTrue(accessibleLabels.contains("封面风格模板,赠送 · 不占精修张数")) + XCTAssertTrue(accessibleLabels.contains("封面风格,赠送 · 不占额度")) collectionView.scrollToItem( at: IndexPath(item: 0, section: 4), @@ -517,6 +520,13 @@ final class TravelAlbumDetailViewControllerTests: XCTestCase { $0.accessibilityIdentifier == "travelAlbum.aiRetouchRemainingQuotaLabel" } as? UILabel ) + let requiredQuotaLabel = try XCTUnwrap( + controller.view.findSubview { + $0.accessibilityIdentifier == "travelAlbum.aiRetouchRequiredQuotaLabel" + } as? UILabel + ) + XCTAssertEqual(requiredQuotaLabel.text, "预计消耗 4 张") + XCTAssertEqual(requiredQuotaLabel.accessibilityLabel, "预计消耗4张修图额度") XCTAssertEqual(quotaLabel.text, "剩余 8 张") XCTAssertEqual(quotaLabel.accessibilityLabel, "剩余8张修图额度") }