feat: add AI retouch before-after comparison

This commit is contained in:
2026-08-13 17:46:06 +08:00
parent e190b5958b
commit 0f3b26991e
12 changed files with 953 additions and 145 deletions
@@ -403,23 +403,45 @@ enum TravelAlbumAIRetouchWorkflow: Sendable, Equatable {
}
}
/// AI 修图模板,包含业务 ID、展示名称和预览图地址。
/// AI 修图模板,包含业务 ID、展示名称、卡片预览图及前后对比图片地址。
struct TravelAlbumAIRetouchTemplate: Decodable, Sendable, Equatable, Hashable, Identifiable {
let id: Int
let name: String
let previewURL: String
let beforeURL: String
let afterURL: String
enum CodingKeys: String, CodingKey {
case id
case name
case previewURL = "preview_url"
case beforeURL = "before_url"
case afterURL = "after_url"
}
/// 创建 AI 修图模板。
init(id: Int, name: String, previewURL: String) {
init(
id: Int,
name: String,
previewURL: String,
beforeURL: String = "",
afterURL: String = ""
) {
self.id = id
self.name = name
self.previewURL = previewURL
self.beforeURL = beforeURL
self.afterURL = afterURL
}
/// 解码模板;前后对比字段缺失或为 null 时按空字符串兼容旧接口响应。
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
previewURL = try container.decode(String.self, forKey: .previewURL)
beforeURL = (try? container.decodeIfPresent(String.self, forKey: .beforeURL)) ?? ""
afterURL = (try? container.decodeIfPresent(String.self, forKey: .afterURL)) ?? ""
}
}
@@ -5,6 +5,32 @@
import Foundation
/// 前后图片对比页所需的通用内容,与具体入口和 UI 框架解耦。
struct BeforeAfterComparisonContent: Sendable, Equatable {
let beforeURL: String
let afterURL: String
let beforeLabel: String
let afterLabel: String
/// 创建前后对比内容,默认使用 AI 修图模块的中文角标。
init(
beforeURL: String,
afterURL: String,
beforeLabel: String = "原图",
afterLabel: String = "效果图"
) {
self.beforeURL = beforeURL.trimmingCharacters(in: .whitespacesAndNewlines)
self.afterURL = afterURL.trimmingCharacters(in: .whitespacesAndNewlines)
self.beforeLabel = beforeLabel
self.afterLabel = afterLabel
}
/// 两张图片地址均存在时才允许进入对比页。
var isValid: Bool {
!beforeURL.isEmpty && !afterURL.isEmpty
}
}
/// 相册预览页横向滑动策略,由调用方通过配置注入。
enum TravelAlbumPreviewSwipeMode: Sendable, Equatable {
/// 横滑只切换原图项目,关联图通过 Tab 切换。
@@ -85,6 +111,19 @@ struct TravelAlbumPreviewProject: Identifiable, Sendable, Hashable {
assets.first { $0.kind == kind }
}
/// 为精修或氛围感结果构建与原图的对比内容;其他类型或无效地址返回 nil。
func comparisonContent(for kind: TravelAlbumPreviewAssetKind) -> BeforeAfterComparisonContent? {
guard kind == .retouched || kind == .atmosphere,
let original = asset(for: .original),
let result = asset(for: kind)
else { return nil }
let content = BeforeAfterComparisonContent(
beforeURL: original.displayURL,
afterURL: result.displayURL
)
return content.isValid ? content : nil
}
/// 将素材映射为原图及实际存在的 AI 精修、氛围感结果图。
init(material: TravelAlbumMaterial) {
originalMaterialId = material.id
@@ -157,6 +196,14 @@ struct TravelAlbumPreviewProject: Identifiable, Sendable, Hashable {
}
}
extension TravelAlbumAIRetouchTemplate {
/// 使用接口返回的模板示例前后图构建对比页内容。
var comparisonContent: BeforeAfterComparisonContent? {
let content = BeforeAfterComparisonContent(beforeURL: beforeURL, afterURL: afterURL)
return content.isValid ? content : nil
}
}
/// 预览分页节点,记录当前图片所属项目及类型。
struct TravelAlbumPreviewNode: Sendable, Hashable {
let projectIndex: Int