From 83afcb6deb294b0f521628cc8e08e1ad44218642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Fri, 14 Aug 2026 15:46:17 +0800 Subject: [PATCH] fix: streamline AI retouch submission feedback --- .../AI修图任务中心需求与接口设计.md | 8 +++-- .../TravelAlbumDetailViewModel.swift | 6 ++++ ...TravelAlbumAIJobDetailViewController.swift | 8 +++-- .../TravelAlbumDetailViewController.swift | 26 ++++------------- ...ravelAlbumPhotoPreviewViewController.swift | 29 ++++--------------- .../TravelAlbumViewModelTests.swift | 27 +++++++++++++++++ 6 files changed, 56 insertions(+), 48 deletions(-) diff --git a/suixinkan/Features/TravelAlbum/AI修图任务中心需求与接口设计.md b/suixinkan/Features/TravelAlbum/AI修图任务中心需求与接口设计.md index 6aabb9c..0caf01c 100644 --- a/suixinkan/Features/TravelAlbum/AI修图任务中心需求与接口设计.md +++ b/suixinkan/Features/TravelAlbum/AI修图任务中心需求与接口设计.md @@ -13,7 +13,7 @@ AI 修图属于异步长耗时任务。当前用户提交后只能等待或主 1. 当前账号全部相册的 AI 修图任务列表。 2. 单次 AI 修图任务详情。 3. AI 修图终态消息推送。 -4. 从推送、任务列表和提交成功提示进入对应任务的完整导航链路。 +4. 从推送和任务列表进入对应任务的完整导航链路;提交成功后仅 Toast 提示并返回相册管理。 本期解决“看得到进度、完成会通知、结果可直达”的问题,不增加任务取消、批量重试、历史版本管理或后台供应商诊断能力。 @@ -65,7 +65,8 @@ AI 修图推送优先跳转到对应任务详情页。 相册管理选择照片 → 提交 AI 修图 → 后端返回 ai_retouch_batch_id - → 客户端提示“AI修图任务已提交”并提供“查看任务” + → 客户端 Toast 提示“AI修图任务已提交,完成后将通过消息通知” + → 关闭模板页及可能存在的照片预览页,回到相册管理并刷新列表 → 用户可离开页面 → 任务进入终态后收到 type = 14 推送 → 点击推送进入任务详情 @@ -75,7 +76,8 @@ AI 修图推送优先跳转到对应任务详情页。 ### 3.2 页面入口 - 相册管理页导航栏右侧增加“修图任务”,进入当前账号的全局任务列表;新增相册页不展示该入口。 -- AI 修图提交成功提示提供“查看任务”,直接进入本次任务详情。 +- AI 修图提交成功仅展示自动消失的 Toast,不弹出查看任务确认框,也不提供立即跳转操作。 +- 提交成功后回到相册管理页,并刷新相册摘要、数量和当前素材列表。 - 任务列表点击卡片进入对应任务详情。 - `type = 14` 推送携带有效任务 ID 时直达详情,否则进入任务列表。 diff --git a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift index 3a724b7..0a4db7a 100644 --- a/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift +++ b/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift @@ -253,6 +253,12 @@ final class TravelAlbumDetailViewModel { notifyStateChange() } + /// AI 修图提交成功后重置选择状态,并刷新相册摘要、数量与当前素材列表。 + func refreshAfterAIRetouchSubmission(api: any TravelAlbumServing) async { + completeAIRetouchSubmission() + await refreshAll(api: api) + } + /// 预览页删除成功后移除对应素材,并同步全部/已购计数。 func removeMaterialAfterPreviewDeletion(id: Int) { guard let index = materials.firstIndex(where: { $0.id == id }) else { return } diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumAIJobDetailViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumAIJobDetailViewController.swift index ff033b1..c722f0e 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumAIJobDetailViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumAIJobDetailViewController.swift @@ -731,7 +731,7 @@ private final class AIJobDetailTargetRow: UIView { make.height.equalTo(28) } errorContainer.snp.makeConstraints { make in - make.top.equalTo(thumbnailView.snp.bottom).offset(10) + make.top.equalTo(templateLabel.snp.bottom).offset(12) make.leading.equalTo(titleLabel) make.trailing.equalToSuperview().inset(4) make.bottom.equalToSuperview().inset(12) @@ -781,7 +781,11 @@ private final class AIJobDetailTargetRow: UIView { let showsError = target.displayFailureMessage != nil errorContainer.isHidden = !showsError errorContainer.snp.remakeConstraints { make in - make.top.equalTo(thumbnailView.snp.bottom).offset(showsError ? 10 : 0) + if showsError { + make.top.equalTo(templateLabel.snp.bottom).offset(12) + } else { + make.top.equalTo(thumbnailView.snp.bottom) + } make.leading.equalTo(titleLabel) make.trailing.equalToSuperview().inset(4) make.bottom.equalToSuperview().inset(12) diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift index be89bf3..e22ed51 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumDetailViewController.swift @@ -458,29 +458,14 @@ final class TravelAlbumDetailViewController: BaseViewController { materialIds: materialIds ), api: api, - onSubmitted: { [weak self] submission in - guard let self else { return } - self.viewModel.completeAIRetouchSubmission() - self.presentAIRetouchSubmitted(submission) - } + onSubmitted: { [weak self] _ in self?.handleAIRetouchSubmitted() } ) present(controller, animated: true) } - private func presentAIRetouchSubmitted(_ submission: TravelAlbumAIJobSubmission) { - let alert = UIAlertController( - title: "AI修图任务已提交", - message: "完成后将通过消息通知,你也可以随时查看处理进度。", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "稍后查看", style: .cancel)) - alert.addAction(UIAlertAction(title: "查看任务", style: .default) { [weak self] _ in - self?.navigationController?.pushViewController( - TravelAlbumAIJobDetailViewController(batchId: submission.aiRetouchBatchId), - animated: true - ) - }) - present(alert, animated: true) + private func handleAIRetouchSubmitted() { + showToast("AI修图任务已提交,完成后将通过消息通知") + Task { await viewModel.refreshAfterAIRetouchSubmission(api: api) } } @objc private func deleteSelectedTapped() { @@ -533,7 +518,8 @@ final class TravelAlbumDetailViewController: BaseViewController { }, onProjectDeleted: { materialId in previewViewModel.removeMaterialAfterPreviewDeletion(id: materialId) - } + }, + onAIRetouchSubmitted: { [weak self] in self?.handleAIRetouchSubmitted() } ) present(controller, animated: true) } diff --git a/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift b/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift index 4d8e444..899b14d 100644 --- a/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift +++ b/suixinkan/UI/TravelAlbum/TravelAlbumPhotoPreviewViewController.swift @@ -25,6 +25,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController { private let loadMore: TravelAlbumPreviewLoadMore? private let reload: TravelAlbumPreviewReload? private let onProjectDeleted: ((Int) -> Void)? + private let onAIRetouchSubmitted: (() -> Void)? private let allowsActions: Bool private var nodes: [TravelAlbumPreviewNode] = [] private var currentNodeIndex = 0 @@ -73,7 +74,8 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController { aiRetouchAPI: (any TravelAlbumServing)? = nil, loadMore: TravelAlbumPreviewLoadMore? = nil, reload: TravelAlbumPreviewReload? = nil, - onProjectDeleted: ((Int) -> Void)? = nil + onProjectDeleted: ((Int) -> Void)? = nil, + onAIRetouchSubmitted: (() -> Void)? = nil ) { self.projects = Self.deduplicated(projects) self.totalCount = max(totalCount, projects.count) @@ -85,6 +87,7 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController { self.loadMore = loadMore self.reload = reload self.onProjectDeleted = onProjectDeleted + self.onAIRetouchSubmitted = onAIRetouchSubmitted self.allowsActions = allowsActions super.init(nibName: nil, bundle: nil) modalPresentationStyle = .fullScreen @@ -726,34 +729,14 @@ final class TravelAlbumPhotoPreviewViewController: UIViewController { workflow: workflow ), api: aiRetouchAPI, - onSubmitted: { [weak self] submission in + onSubmitted: { [weak self] _ in guard let self else { return } - self.reloadProjects(showGlobalLoading: false, forceRefreshImage: false) - self.presentAIRetouchSubmitted(submission) + self.dismiss(animated: true) { self.onAIRetouchSubmitted?() } } ) present(controller, animated: true) } - private func presentAIRetouchSubmitted(_ submission: TravelAlbumAIJobSubmission) { - let alert = UIAlertController( - title: "AI修图任务已提交", - message: "完成后将通过消息通知。", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "知道了", style: .cancel)) - alert.addAction(UIAlertAction(title: "查看任务", style: .default) { [weak self] _ in - guard let self, let navigationController = presentingViewController?.navigationController else { return } - dismiss(animated: true) { - navigationController.pushViewController( - TravelAlbumAIJobDetailViewController(batchId: submission.aiRetouchBatchId), - animated: true - ) - } - }) - present(alert, animated: true) - } - @objc private func deleteTapped() { guard currentProject != nil, !isDeletingProject else { return } let alert = UIAlertController( diff --git a/suixinkanTests/TravelAlbumViewModelTests.swift b/suixinkanTests/TravelAlbumViewModelTests.swift index 60a2b51..357d744 100644 --- a/suixinkanTests/TravelAlbumViewModelTests.swift +++ b/suixinkanTests/TravelAlbumViewModelTests.swift @@ -274,6 +274,33 @@ final class TravelAlbumDetailViewModelTests: XCTestCase { XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty) } + func testAIRetouchSubmissionRefreshesAlbumCountsAndCurrentMaterialList() async { + let api = TravelAlbumMockAPI() + api.infoResponse = TravelAlbum(id: 2, name: "刷新后的相册") + api.materialListResponses = [ + TravelAlbumListResponse(total: 3, list: []), + TravelAlbumListResponse(total: 1, list: []), + TravelAlbumListResponse(total: 3, list: [ + TravelAlbumMaterial(id: 7), + TravelAlbumMaterial(id: 8), + TravelAlbumMaterial(id: 9), + ]), + ] + let viewModel = TravelAlbumDetailViewModel(albumId: 2) + viewModel.toggleSelectionMode() + viewModel.toggleMaterialSelection(TravelAlbumMaterial(id: 7, status: 1)) + + await viewModel.refreshAfterAIRetouchSubmission(api: api) + + XCTAssertFalse(viewModel.isSelectionMode) + XCTAssertTrue(viewModel.selectedMaterialIds.isEmpty) + XCTAssertEqual(viewModel.album?.name, "刷新后的相册") + XCTAssertEqual(viewModel.allPhotoCount, 3) + XCTAssertEqual(viewModel.purchasedPhotoCount, 1) + XCTAssertEqual(viewModel.materials.map(\.id), [7, 8, 9]) + XCTAssertEqual(api.materialRequests.count, 3) + } + func testPreviewDeletionRemovesMaterialAndSynchronizesCounts() async { let api = TravelAlbumMockAPI() api.materialListResponses = [