193 lines
6.7 KiB
Swift
193 lines
6.7 KiB
Swift
//
|
|
// 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?()
|
|
}
|
|
}
|