feat: add AI retouch and album preview actions
This commit is contained in:
@@ -43,6 +43,12 @@ protocol TravelAlbumServing {
|
||||
|
||||
/// 拉取相册小程序码。
|
||||
func mpCode(id: Int) async throws -> TravelAlbumMpCodeResponse
|
||||
|
||||
/// 拉取当前景区可用的 AI 修图模板。
|
||||
func aiRetouchTemplates(scenicId: Int) async throws -> TravelAlbumAIRetouchTemplatesResponse
|
||||
|
||||
/// 提交相册素材 AI 修图任务。
|
||||
func submitAIRetouch(_ request: TravelAlbumAIRetouchRequest) async throws
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -155,6 +161,24 @@ final class TravelAlbumAPI: TravelAlbumServing {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取当前景区可用的 AI 修图模板。
|
||||
func aiRetouchTemplates(scenicId: Int) async throws -> TravelAlbumAIRetouchTemplatesResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "\(basePath)/ai-retouch-templates",
|
||||
queryItems: [URLQueryItem(name: "scenic_id", value: String(scenicId))]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 提交相册素材 AI 修图任务;服务端 data 内容无需客户端消费。
|
||||
func submitAIRetouch(_ request: TravelAlbumAIRetouchRequest) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(method: .post, path: "\(basePath)/ai-retouch", body: request)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册 ID 请求体。
|
||||
|
||||
@@ -127,6 +127,10 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
|
||||
let fileSize: Int
|
||||
let coverUrl: String
|
||||
let isPurchased: Bool
|
||||
let aiRetouchStatus: Int
|
||||
let aiRetouchStatusName: String
|
||||
let aiRefinedURL: String
|
||||
let aiAtmosphereURL: String
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
@@ -142,10 +146,40 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
|
||||
case fileSize = "file_size"
|
||||
case coverUrl = "cover_url"
|
||||
case isPurchased = "is_purchased"
|
||||
case aiRetouchStatus = "ai_retouch_status"
|
||||
case aiRetouchStatusName = "ai_retouch_status_name"
|
||||
case aiRefinedURL = "ai_refined_url"
|
||||
case aiAtmosphereURL = "ai_atmosphere_url"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
|
||||
/// 从素材接口解码;AI 修图扩展字段缺失、为空或类型异常时使用安全默认值。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(Int.self, forKey: .id)
|
||||
userEquityTravelId = try container.decode(Int.self, forKey: .userEquityTravelId)
|
||||
status = try container.decode(Int.self, forKey: .status)
|
||||
orderNumber = try container.decode(String.self, forKey: .orderNumber)
|
||||
userId = try container.decode(Int.self, forKey: .userId)
|
||||
fileName = try container.decode(String.self, forKey: .fileName)
|
||||
fileType = try container.decode(Int.self, forKey: .fileType)
|
||||
fileUrl = try container.decode(String.self, forKey: .fileUrl)
|
||||
fileSize = try container.decode(Int.self, forKey: .fileSize)
|
||||
coverUrl = try container.decode(String.self, forKey: .coverUrl)
|
||||
isPurchased = try container.decode(Bool.self, forKey: .isPurchased)
|
||||
aiRetouchStatus = (try? container.decodeIfPresent(Int.self, forKey: .aiRetouchStatus)) ?? 0
|
||||
aiRetouchStatusName = (
|
||||
try? container.decodeIfPresent(String.self, forKey: .aiRetouchStatusName)
|
||||
) ?? ""
|
||||
aiRefinedURL = (try? container.decodeIfPresent(String.self, forKey: .aiRefinedURL)) ?? ""
|
||||
aiAtmosphereURL = (
|
||||
try? container.decodeIfPresent(String.self, forKey: .aiAtmosphereURL)
|
||||
) ?? ""
|
||||
createdAt = try container.decode(String.self, forKey: .createdAt)
|
||||
updatedAt = try container.decode(String.self, forKey: .updatedAt)
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
userEquityTravelId: Int = 0,
|
||||
@@ -158,6 +192,10 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
|
||||
fileSize: Int = 0,
|
||||
coverUrl: String = "",
|
||||
isPurchased: Bool = false,
|
||||
aiRetouchStatus: Int = 0,
|
||||
aiRetouchStatusName: String = "",
|
||||
aiRefinedURL: String = "",
|
||||
aiAtmosphereURL: String = "",
|
||||
createdAt: String = "",
|
||||
updatedAt: String = ""
|
||||
) {
|
||||
@@ -172,11 +210,68 @@ struct TravelAlbumMaterial: Decodable, Sendable, Equatable, Hashable, Identifiab
|
||||
self.fileSize = fileSize
|
||||
self.coverUrl = coverUrl
|
||||
self.isPurchased = isPurchased
|
||||
self.aiRetouchStatus = aiRetouchStatus
|
||||
self.aiRetouchStatusName = aiRetouchStatusName
|
||||
self.aiRefinedURL = aiRefinedURL
|
||||
self.aiAtmosphereURL = aiAtmosphereURL
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册素材网格角标类别,用于稳定映射文案优先级和语义颜色。
|
||||
enum TravelAlbumMaterialBadgeKind: Sendable, Equatable {
|
||||
case purchased
|
||||
case pending
|
||||
case processing
|
||||
case retouched
|
||||
case cover
|
||||
case failed
|
||||
}
|
||||
|
||||
/// 相册素材网格角标展示内容。
|
||||
struct TravelAlbumMaterialBadgePresentation: Sendable, Equatable {
|
||||
let kind: TravelAlbumMaterialBadgeKind
|
||||
let text: String
|
||||
}
|
||||
|
||||
extension TravelAlbumMaterial {
|
||||
/// 按 AI 修图状态和购买状态生成网格角标;返回 nil 时隐藏角标。
|
||||
var badgePresentation: TravelAlbumMaterialBadgePresentation? {
|
||||
if aiRetouchStatus == 0 {
|
||||
return isPurchased
|
||||
? TravelAlbumMaterialBadgePresentation(kind: .purchased, text: "已购")
|
||||
: nil
|
||||
}
|
||||
|
||||
let statusName = aiRetouchStatusName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
switch aiRetouchStatus {
|
||||
case 1:
|
||||
return TravelAlbumMaterialBadgePresentation(
|
||||
kind: .pending,
|
||||
text: statusName.isEmpty ? "待处理" : statusName
|
||||
)
|
||||
case 2:
|
||||
return TravelAlbumMaterialBadgePresentation(
|
||||
kind: .processing,
|
||||
text: statusName.isEmpty ? "修图中" : statusName
|
||||
)
|
||||
case 3:
|
||||
return TravelAlbumMaterialBadgePresentation(
|
||||
kind: statusName == "AI封面" ? .cover : .retouched,
|
||||
text: statusName.isEmpty ? "AI已修" : statusName
|
||||
)
|
||||
case 4:
|
||||
return TravelAlbumMaterialBadgePresentation(
|
||||
kind: .failed,
|
||||
text: statusName.isEmpty ? "失败" : statusName
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册创建请求体,对齐 Android `TravelAlbumCreateRequest`。
|
||||
struct TravelAlbumCreateRequest: Encodable, Sendable, Equatable {
|
||||
let name: String
|
||||
@@ -247,6 +342,67 @@ struct TravelAlbumMpCodeResponse: Decodable, Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图模板,包含业务 ID、展示名称和预览图地址。
|
||||
struct TravelAlbumAIRetouchTemplate: Decodable, Sendable, Equatable, Hashable, Identifiable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let previewURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case previewURL = "preview_url"
|
||||
}
|
||||
|
||||
/// 创建 AI 修图模板。
|
||||
init(id: Int, name: String, previewURL: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.previewURL = previewURL
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图模板接口响应,按原图、氛围感和封面风格分组。
|
||||
struct TravelAlbumAIRetouchTemplatesResponse: Decodable, Sendable, Equatable {
|
||||
let refinedTemplates: [TravelAlbumAIRetouchTemplate]
|
||||
let atmosphereTemplates: [TravelAlbumAIRetouchTemplate]
|
||||
let coverTemplates: [TravelAlbumAIRetouchTemplate]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case refinedTemplates = "refined_templates"
|
||||
case atmosphereTemplates = "atmosphere_templates"
|
||||
case coverTemplates = "cover_templates"
|
||||
}
|
||||
|
||||
/// 创建分组模板响应,默认各组为空。
|
||||
init(
|
||||
refinedTemplates: [TravelAlbumAIRetouchTemplate] = [],
|
||||
atmosphereTemplates: [TravelAlbumAIRetouchTemplate] = [],
|
||||
coverTemplates: [TravelAlbumAIRetouchTemplate] = []
|
||||
) {
|
||||
self.refinedTemplates = refinedTemplates
|
||||
self.atmosphereTemplates = atmosphereTemplates
|
||||
self.coverTemplates = coverTemplates
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交 AI 修图任务的请求参数。
|
||||
struct TravelAlbumAIRetouchRequest: Encodable, Sendable, Equatable {
|
||||
let userEquityTravelId: Int
|
||||
let materialIds: [Int]
|
||||
let refinedTemplateId: Int
|
||||
let atmosphereTemplateId: Int?
|
||||
let aiPreviewTabs: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case userEquityTravelId = "user_equity_travel_id"
|
||||
case materialIds = "material_ids"
|
||||
case refinedTemplateId = "refined_template_id"
|
||||
case atmosphereTemplateId = "atmosphere_template_id"
|
||||
case aiPreviewTabs = "ai_preview_tabs"
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册展示格式化工具。
|
||||
enum TravelAlbumDisplayFormatter {
|
||||
/// 脱敏手机号。
|
||||
|
||||
@@ -84,10 +84,10 @@ struct TravelAlbumPreviewProject: Identifiable, Sendable, Hashable {
|
||||
assets.first { $0.kind == kind }
|
||||
}
|
||||
|
||||
/// 将当前素材映射为仅含原图的预览项目;新接口接入后替换此适配层即可。
|
||||
/// 将素材映射为原图及实际存在的 AI 精修、氛围感结果图。
|
||||
init(material: TravelAlbumMaterial) {
|
||||
originalMaterialId = material.id
|
||||
assets = [
|
||||
var mappedAssets = [
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "original-\(material.id)",
|
||||
kind: .original,
|
||||
@@ -97,6 +97,33 @@ struct TravelAlbumPreviewProject: Identifiable, Sendable, Hashable {
|
||||
fileSize: material.fileSize
|
||||
),
|
||||
]
|
||||
let refinedURL = material.aiRefinedURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !refinedURL.isEmpty {
|
||||
mappedAssets.append(
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "retouched-\(material.id)",
|
||||
kind: .retouched,
|
||||
fileURL: refinedURL,
|
||||
coverURL: refinedURL,
|
||||
fileName: material.fileName,
|
||||
fileSize: material.fileSize
|
||||
)
|
||||
)
|
||||
}
|
||||
let atmosphereURL = material.aiAtmosphereURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !atmosphereURL.isEmpty {
|
||||
mappedAssets.append(
|
||||
TravelAlbumPreviewAsset(
|
||||
id: "atmosphere-\(material.id)",
|
||||
kind: .atmosphere,
|
||||
fileURL: atmosphereURL,
|
||||
coverURL: atmosphereURL,
|
||||
fileName: material.fileName,
|
||||
fileSize: material.fileSize
|
||||
)
|
||||
)
|
||||
}
|
||||
assets = mappedAssets
|
||||
}
|
||||
|
||||
/// 创建包含关联图片的项目,主要供适配器和测试使用。
|
||||
@@ -142,6 +169,15 @@ enum TravelAlbumPreviewNavigator {
|
||||
$0.projectIndex == current.projectIndex - 1 && $0.kind == .original
|
||||
} ?? currentIndex - 1
|
||||
}
|
||||
|
||||
/// 返回删除当前项目后应展示的项目索引;优先保持原索引以显示下一张,末项则回退上一张。
|
||||
static func projectIndexAfterDeletion(
|
||||
deletedProjectIndex: Int,
|
||||
remainingProjectCount: Int
|
||||
) -> Int? {
|
||||
guard remainingProjectCount > 0 else { return nil }
|
||||
return min(max(0, deletedProjectIndex), remainingProjectCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// 预览操作执行结果,便于后续替换真实业务接口。
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// TravelAlbumAIRetouchTemplateViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// AI 修图模板类别,决定页面分组、选择规则和提交字段。
|
||||
enum TravelAlbumAIRetouchTemplateCategory: Int, Sendable, Hashable {
|
||||
case refined
|
||||
case atmosphere
|
||||
case cover
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .refined: "原图精修"
|
||||
case .atmosphere: "氛围感修图"
|
||||
case .cover: "封面风格模板"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图模板选择状态,负责实时加载、单选规则、校验和任务提交。
|
||||
final class TravelAlbumAIRetouchTemplateViewModel {
|
||||
private(set) var refinedTemplates: [TravelAlbumAIRetouchTemplate] = []
|
||||
private(set) var atmosphereTemplates: [TravelAlbumAIRetouchTemplate] = []
|
||||
private(set) var coverTemplates: [TravelAlbumAIRetouchTemplate] = []
|
||||
private(set) var selectedRefinedTemplateId: Int?
|
||||
private(set) var selectedAtmosphereTemplateId: Int?
|
||||
private(set) var selectedCoverTemplateId: Int?
|
||||
private(set) var isLoading = false
|
||||
private(set) var isSubmitting = false
|
||||
private(set) var loadErrorMessage: String?
|
||||
|
||||
let albumId: Int
|
||||
let scenicId: Int
|
||||
let materialIds: [Int]
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onSubmitted: (() -> Void)?
|
||||
|
||||
/// 创建模板选择状态;素材 ID 会排序并去重,确保提交稳定。
|
||||
init(albumId: Int, scenicId: Int, materialIds: [Int]) {
|
||||
self.albumId = albumId
|
||||
self.scenicId = scenicId
|
||||
self.materialIds = Array(Set(materialIds)).sorted()
|
||||
}
|
||||
|
||||
/// 选中不少于四张素材时才展示并要求选择封面模板。
|
||||
var showsCoverTemplates: Bool {
|
||||
materialIds.count >= 4
|
||||
}
|
||||
|
||||
/// 当前必选模板缺失时用于底部提示的文案。
|
||||
var validationMessage: String? {
|
||||
guard !isLoading, loadErrorMessage == nil else { return nil }
|
||||
if refinedTemplates.isEmpty || selectedRefinedTemplateId == nil {
|
||||
return "暂无可用的原图精修模板"
|
||||
}
|
||||
if showsCoverTemplates, coverTemplates.isEmpty || selectedCoverTemplateId == nil {
|
||||
return "暂无可用的封面风格模板"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 是否满足提交条件。
|
||||
var canSubmit: Bool {
|
||||
!isLoading
|
||||
&& !isSubmitting
|
||||
&& loadErrorMessage == nil
|
||||
&& !materialIds.isEmpty
|
||||
&& selectedRefinedTemplateId != nil
|
||||
&& (!showsCoverTemplates || selectedCoverTemplateId != nil)
|
||||
}
|
||||
|
||||
/// 实时拉取当前景区模板并按规则设置默认选择。
|
||||
func loadTemplates(api: any TravelAlbumServing) async {
|
||||
guard scenicId > 0 else {
|
||||
loadErrorMessage = "请先选择景区"
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
guard !isLoading else { return }
|
||||
isLoading = true
|
||||
loadErrorMessage = nil
|
||||
notifyStateChange()
|
||||
|
||||
do {
|
||||
let response = try await api.aiRetouchTemplates(scenicId: scenicId)
|
||||
refinedTemplates = response.refinedTemplates
|
||||
atmosphereTemplates = response.atmosphereTemplates
|
||||
coverTemplates = response.coverTemplates
|
||||
selectedRefinedTemplateId = refinedTemplates.first?.id
|
||||
selectedAtmosphereTemplateId = nil
|
||||
selectedCoverTemplateId = showsCoverTemplates ? coverTemplates.first?.id : nil
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
} catch {
|
||||
refinedTemplates = []
|
||||
atmosphereTemplates = []
|
||||
coverTemplates = []
|
||||
selectedRefinedTemplateId = nil
|
||||
selectedAtmosphereTemplateId = nil
|
||||
selectedCoverTemplateId = nil
|
||||
isLoading = false
|
||||
loadErrorMessage = error.localizedDescription.isEmpty ? "模板加载失败" : error.localizedDescription
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回指定分组的模板。
|
||||
func templates(for category: TravelAlbumAIRetouchTemplateCategory) -> [TravelAlbumAIRetouchTemplate] {
|
||||
switch category {
|
||||
case .refined: refinedTemplates
|
||||
case .atmosphere: atmosphereTemplates
|
||||
case .cover: coverTemplates
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回指定分组当前选中的模板 ID。
|
||||
func selectedTemplateId(for category: TravelAlbumAIRetouchTemplateCategory) -> Int? {
|
||||
switch category {
|
||||
case .refined: selectedRefinedTemplateId
|
||||
case .atmosphere: selectedAtmosphereTemplateId
|
||||
case .cover: selectedCoverTemplateId
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择模板;氛围感模板再次点击时取消,必选分组保持单选。
|
||||
func toggleTemplate(id: Int, category: TravelAlbumAIRetouchTemplateCategory) {
|
||||
guard templates(for: category).contains(where: { $0.id == id }) else { return }
|
||||
switch category {
|
||||
case .refined:
|
||||
selectedRefinedTemplateId = id
|
||||
case .atmosphere:
|
||||
selectedAtmosphereTemplateId = selectedAtmosphereTemplateId == id ? nil : id
|
||||
case .cover:
|
||||
guard showsCoverTemplates else { return }
|
||||
selectedCoverTemplateId = id
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 提交 AI 修图任务,成功后通知页面退出选择态。
|
||||
func submit(api: any TravelAlbumServing) async {
|
||||
guard !isSubmitting else { return }
|
||||
guard let refinedTemplateId = selectedRefinedTemplateId else {
|
||||
onShowMessage?("请选择原图精修模板")
|
||||
return
|
||||
}
|
||||
guard !showsCoverTemplates || selectedCoverTemplateId != nil else {
|
||||
onShowMessage?("请选择封面风格模板")
|
||||
return
|
||||
}
|
||||
guard !materialIds.isEmpty else {
|
||||
onShowMessage?("请选择要修图的照片")
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isSubmitting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
do {
|
||||
try await api.submitAIRetouch(
|
||||
TravelAlbumAIRetouchRequest(
|
||||
userEquityTravelId: albumId,
|
||||
materialIds: materialIds,
|
||||
refinedTemplateId: refinedTemplateId,
|
||||
atmosphereTemplateId: selectedAtmosphereTemplateId,
|
||||
aiPreviewTabs: showsCoverTemplates ? selectedCoverTemplateId : nil
|
||||
)
|
||||
)
|
||||
onSubmitted?()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription.isEmpty ? "AI修图任务提交失败" : error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
@@ -232,6 +232,25 @@ final class TravelAlbumDetailViewModel {
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// AI 修图任务提交成功后退出选择模式并清空当前选择。
|
||||
func completeAIRetouchSubmission() {
|
||||
isSelectionMode = false
|
||||
selectedMaterialIds = []
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 预览页删除成功后移除对应素材,并同步全部/已购计数。
|
||||
func removeMaterialAfterPreviewDeletion(id: Int) {
|
||||
guard let index = materials.firstIndex(where: { $0.id == id }) else { return }
|
||||
let material = materials.remove(at: index)
|
||||
selectedMaterialIds.remove(id)
|
||||
allPhotoCount = max(0, allPhotoCount - 1)
|
||||
if material.isPurchased {
|
||||
purchasedPhotoCount = max(0, purchasedPhotoCount - 1)
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 删除已选素材。
|
||||
func deleteSelectedMaterials(api: any TravelAlbumServing) async {
|
||||
let ids = Array(selectedMaterialIds)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// TravelAlbumPreviewActionHandler.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 图片预览页业务操作实现,负责将底部操作转发到旅拍相册服务。
|
||||
final class TravelAlbumPreviewActionHandler: TravelAlbumPreviewActionHandling {
|
||||
private let api: any TravelAlbumServing
|
||||
|
||||
/// 使用旅拍相册服务创建操作处理器。
|
||||
init(api: any TravelAlbumServing) {
|
||||
self.api = api
|
||||
}
|
||||
|
||||
/// AI 修复仍由相册选择态的模板流程发起。
|
||||
func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult {
|
||||
.unavailable("请在相册中选择照片后使用AI修图")
|
||||
}
|
||||
|
||||
/// 调用素材删除接口,仅在服务端确认成功后返回成功结果。
|
||||
func deleteProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult {
|
||||
do {
|
||||
try await api.deleteMaterial(id: originalMaterialId)
|
||||
return .success("删除成功")
|
||||
} catch is CancellationError {
|
||||
return .failure("删除已取消")
|
||||
} catch {
|
||||
let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return .failure(message.isEmpty ? "删除失败" : message)
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新关联结果图接口尚未提供。
|
||||
func refreshProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult {
|
||||
.unavailable("关联图片刷新接口待接入")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user