// // 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?() } }