新增ai修图

This commit is contained in:
XHYourName
2026-07-31 16:48:28 +08:00
parent 7ccbc5568d
commit 6129a1632f
42 changed files with 3696 additions and 54 deletions
@@ -0,0 +1,316 @@
//
// TravelAlbumPhotoPreviewModels.swift
// suixinkan
//
import Foundation
/// 单张照片的 AI 修图状态,控制处理中禁用、结果对比以及再次修图覆盖。
struct TravelAlbumAIEditResultState: Equatable, Sendable {
/// 相册缩略图需要展示的 AI 修图状态。
enum ThumbnailStatus: Equatable, Sendable {
case none
case processing
case edited
case cover
case failed
}
/// 当前预览展示的版本。
enum DisplayMode: Equatable, Sendable {
case original
case edited
case atmosphere
case cover
}
private(set) var isProcessing = false
private(set) var didFailLastAttempt = false
private(set) var appliedPresetID: String?
private(set) var appliedAtmosphereOptionID: String?
private(set) var appliedCoverTemplateID: String?
private(set) var displayMode: DisplayMode = .original
/// 已经存在可查看的修图结果。
var hasEditedResult: Bool {
appliedPresetID != nil
}
/// 处理期间禁止再次发起 AI 修图。
var canStartEditing: Bool {
!isProcessing
}
/// 当前结果是否为封面模板生成的封面图。
var isCover: Bool {
appliedCoverTemplateID != nil
}
/// 当前是否已经生成可独立查看的氛围感修图结果。
var hasAtmosphereResult: Bool {
appliedAtmosphereOptionID != nil
}
/// 根据修图任务状态生成缩略图角标,处理中和失败优先于历史成功结果。
var thumbnailStatus: ThumbnailStatus {
if isProcessing {
return .processing
}
if didFailLastAttempt {
return .failed
}
if isCover {
return .cover
}
return hasEditedResult ? .edited : .none
}
/// 开始修图;已有结果保留到新结果完成,避免处理中画面回退。
mutating func startProcessing() {
guard !isProcessing else { return }
isProcessing = true
didFailLastAttempt = false
}
/// 完成修图并覆盖旧预设结果,默认展示新的修图后版本。
mutating func complete(
presetID: String,
atmosphereOptionID: String? = nil,
coverTemplateID: String? = nil
) {
isProcessing = false
didFailLastAttempt = false
appliedPresetID = presetID
appliedAtmosphereOptionID = atmosphereOptionID
appliedCoverTemplateID = coverTemplateID
displayMode = coverTemplateID == nil ? .edited : .cover
}
/// 处理失败时恢复入口;已有成功结果继续保留。
mutating func failProcessing() {
isProcessing = false
didFailLastAttempt = true
}
/// 选择还原原图后清除历史修图结果和失败状态。
mutating func restoreOriginal() {
isProcessing = false
didFailLastAttempt = false
appliedPresetID = nil
appliedAtmosphereOptionID = nil
appliedCoverTemplateID = nil
displayMode = .original
}
/// 在已有结果时切换原图或修图后预览。
mutating func selectDisplayMode(_ mode: DisplayMode) {
guard hasEditedResult else {
displayMode = .original
return
}
if mode == .atmosphere, !hasAtmosphereResult {
displayMode = .edited
return
}
if mode == .cover, !isCover {
displayMode = .edited
return
}
displayMode = mode
}
}
/// 相册管理底部操作栏状态,用于在上传与多选操作之间切换。
struct TravelAlbumSelectionToolbarState: Equatable, Sendable {
let isSelectionMode: Bool
let selectedCount: Int
/// 普通状态显示上传照片。
var showsUpload: Bool {
!isSelectionMode
}
/// 多选状态显示 AI 修图和删除。
var showsSelectionActions: Bool {
isSelectionMode
}
/// 至少选中一张照片后才允许执行批量操作。
var selectionActionsEnabled: Bool {
isSelectionMode && selectedCount > 0
}
}
/// 相册照片预览状态,维护当前照片位置并处理左右切换。
struct TravelAlbumPhotoPreviewState: Equatable, Sendable {
private(set) var materials: [TravelAlbumMaterial]
private(set) var currentIndex: Int
init(materials: [TravelAlbumMaterial], initialIndex: Int) {
self.materials = materials
currentIndex = min(max(initialIndex, 0), max(materials.count - 1, 0))
}
/// 当前正在预览的照片。
var currentMaterial: TravelAlbumMaterial? {
materials.indices.contains(currentIndex) ? materials[currentIndex] : nil
}
/// 切换至下一张;已经是最后一张时保持不变。
@discardableResult
mutating func moveNext() -> Bool {
guard currentIndex + 1 < materials.count else { return false }
currentIndex += 1
return true
}
/// 切换至上一张;已经是第一张时保持不变。
@discardableResult
mutating func movePrevious() -> Bool {
guard currentIndex > 0 else { return false }
currentIndex -= 1
return true
}
/// 删除当前照片,并自动定位到相邻照片。
@discardableResult
mutating func removeCurrent() -> Bool {
guard materials.indices.contains(currentIndex) else { return false }
materials.remove(at: currentIndex)
if currentIndex >= materials.count {
currentIndex = max(materials.count - 1, 0)
}
return !materials.isEmpty
}
}
/// 相册照片 AI 修图预设,当前为本地演示数据,后续可替换为服务端下发的预设列表。
struct TravelAlbumEditPreset: Hashable, Sendable, Identifiable {
/// 图像效果类型,用于在预览页生成对应效果图。
enum Effect: String, Hashable, Sendable {
case original
case portrait
case vintage
case brocade
case distantMountain
case mist
case summer
case rich
}
let id: String
let title: String
let effect: Effect
/// AI 修图底部弹窗展示的默认预设,暂无接口时用于完整演示选择流程。
static let defaultOptions: [TravelAlbumEditPreset] = [
TravelAlbumEditPreset(id: "original", title: "还原为原图", effect: .original),
TravelAlbumEditPreset(id: "portrait", title: "写真-简约肖像", effect: .portrait),
TravelAlbumEditPreset(id: "vintage", title: "写真-清冷古风", effect: .vintage),
TravelAlbumEditPreset(id: "brocade", title: "旅拍-锦绣", effect: .brocade),
TravelAlbumEditPreset(id: "distant_mountain", title: "旅拍-远山", effect: .distantMountain),
TravelAlbumEditPreset(id: "mist", title: "旅拍-薄雾", effect: .mist),
TravelAlbumEditPreset(id: "summer", title: "油画-夏日", effect: .summer),
TravelAlbumEditPreset(id: "rich", title: "油画-浓郁", effect: .rich),
]
}
/// 可选的氛围感修图样式;选中后在原图精修之外额外生成一张独立结果。
struct TravelAlbumAtmosphereEditOption: Hashable, Sendable, Identifiable {
let id: String
let title: String
let subtitle: String
let effect: TravelAlbumEditPreset.Effect
/// 暂无接口时用于演示横向滚动单选的氛围感样式。
static let defaultOptions: [TravelAlbumAtmosphereEditOption] = [
TravelAlbumAtmosphereEditOption(
id: "warm_sun",
title: "日落暖阳",
subtitle: "温暖柔和",
effect: .summer
),
TravelAlbumAtmosphereEditOption(
id: "retro_film",
title: "复古胶片",
subtitle: "怀旧颗粒感",
effect: .vintage
),
TravelAlbumAtmosphereEditOption(
id: "clear_blue",
title: "清透蓝调",
subtitle: "通透轻盈",
effect: .mist
),
TravelAlbumAtmosphereEditOption(
id: "forest_mist",
title: "森系薄雾",
subtitle: "低饱和自然感",
effect: .distantMountain
),
TravelAlbumAtmosphereEditOption(
id: "vivid_story",
title: "浓郁故事",
subtitle: "高饱和电影感",
effect: .rich
),
]
/// 兼容现有结果测试与默认演示数据的首个氛围感样式。
static let defaultOption = defaultOptions[0]
}
/// 多图 AI 修图使用的封面风格模板,当前为本地演示数据,后续可替换为服务端配置。
struct TravelAlbumCoverTemplate: Hashable, Sendable, Identifiable {
/// 封面预览的排版样式,用于生成可辨识的本地效果图。
enum PreviewStyle: String, Hashable, Sendable {
case travelMemoir
case minimal
case scenicStory
case film
}
/// 触发展示封面模板选择的最少照片数量。
static let minimumPhotoCount = 4
let id: String
let title: String
let previewStyle: PreviewStyle
/// 当前消耗的精修额度;免费阶段为 0,后续收费可由服务端改为正数。
let quotaCost: Int
/// 当前模板是否为赠送且不占用精修张数。
var isFreeGift: Bool {
quotaCost == 0
}
/// 判断本次多选是否需要展示封面模板模块。
static func shouldShow(for selectedPhotoCount: Int) -> Bool {
selectedPhotoCount >= minimumPhotoCount
}
/// 暂无接口时用于完整演示单选流程的封面模板。
static let defaultOptions: [TravelAlbumCoverTemplate] = [
TravelAlbumCoverTemplate(id: "travel_memoir", title: "旅行纪念册", previewStyle: .travelMemoir, quotaCost: 0),
TravelAlbumCoverTemplate(id: "minimal", title: "简约留白", previewStyle: .minimal, quotaCost: 0),
TravelAlbumCoverTemplate(id: "scenic_story", title: "景区故事", previewStyle: .scenicStory, quotaCost: 0),
TravelAlbumCoverTemplate(id: "film", title: "电影胶片", previewStyle: .film, quotaCost: 0),
]
}
/// AI 修图弹窗最终提交的样式组合,统一承载原图精修、氛围感和封面模板。
struct TravelAlbumAIEditSelection: Equatable, Sendable {
let refinedPreset: TravelAlbumEditPreset
let atmosphereOption: TravelAlbumAtmosphereEditOption?
let coverTemplate: TravelAlbumCoverTemplate?
/// 每张普通照片需要生成的修图结果数量,不包含原图。
var resultCountPerPhoto: Int {
atmosphereOption == nil ? 1 : 2
}
/// 弹窗当前展示并提交的样式类别数量。
var selectedStyleCount: Int {
1 + (atmosphereOption == nil ? 0 : 1) + (coverTemplate == nil ? 0 : 1)
}
}