// // TravelAlbumPreviewModels.swift // suixinkan // import Foundation /// 相册预览页横向滑动策略,由调用方通过配置注入。 enum TravelAlbumPreviewSwipeMode: Sendable, Equatable { /// 横滑只切换原图项目,关联图通过 Tab 切换。 case projectsOnly /// 横滑依次切换当前项目的关联图,并在边界进入相邻项目原图。 case includeVariants } /// 相册预览页配置。 struct TravelAlbumPreviewConfiguration: Sendable { let swipeMode: TravelAlbumPreviewSwipeMode /// 创建预览配置,默认只按原图项目分页。 init(swipeMode: TravelAlbumPreviewSwipeMode = .projectsOnly) { self.swipeMode = swipeMode } } /// 预览图片类型,顺序同时决定 Tab 与关联图浏览顺序。 enum TravelAlbumPreviewAssetKind: Int, CaseIterable, Sendable, Hashable { case original case retouched case atmosphere case cover var title: String { switch self { case .original: "原图" case .retouched: "精修后" case .atmosphere: "氛围感" case .cover: "封面" } } } /// 预览页使用的单张图片信息,与后端关联图字段解耦。 struct TravelAlbumPreviewAsset: Identifiable, Sendable, Hashable { let id: String let kind: TravelAlbumPreviewAssetKind let fileURL: String let coverURL: String let fileName: String let fileSize: Int /// 实际用于展示的地址,优先使用原图地址。 var displayURL: String { let trimmedFileURL = fileURL.trimmingCharacters(in: .whitespacesAndNewlines) return trimmedFileURL.isEmpty ? coverURL.trimmingCharacters(in: .whitespacesAndNewlines) : trimmedFileURL } /// 横滑预览地址,优先使用体积更小的封面图以降低解码开销。 var previewURL: String { let trimmedCoverURL = coverURL.trimmingCharacters(in: .whitespacesAndNewlines) return trimmedCoverURL.isEmpty ? fileURL.trimmingCharacters(in: .whitespacesAndNewlines) : trimmedCoverURL } } /// 一张原图及其所有关联图片组成的预览项目。 struct TravelAlbumPreviewProject: Identifiable, Sendable, Hashable { let originalMaterialId: Int let assets: [TravelAlbumPreviewAsset] var id: Int { originalMaterialId } /// 按产品约定顺序返回实际存在的图片。 var orderedAssets: [TravelAlbumPreviewAsset] { TravelAlbumPreviewAssetKind.allCases.compactMap(asset(for:)) } /// 是否存在原图之外的关联图片。 var hasVariants: Bool { orderedAssets.contains { $0.kind != .original } } /// 返回指定类型的图片。 func asset(for kind: TravelAlbumPreviewAssetKind) -> TravelAlbumPreviewAsset? { assets.first { $0.kind == kind } } /// 将当前素材映射为仅含原图的预览项目;新接口接入后替换此适配层即可。 init(material: TravelAlbumMaterial) { originalMaterialId = material.id assets = [ TravelAlbumPreviewAsset( id: "original-\(material.id)", kind: .original, fileURL: material.fileUrl, coverURL: material.coverUrl, fileName: material.fileName, fileSize: material.fileSize ), ] } /// 创建包含关联图片的项目,主要供适配器和测试使用。 init(originalMaterialId: Int, assets: [TravelAlbumPreviewAsset]) { self.originalMaterialId = originalMaterialId var seenKinds = Set() self.assets = assets.filter { seenKinds.insert($0.kind).inserted } } } /// 预览分页节点,记录当前图片所属项目及类型。 struct TravelAlbumPreviewNode: Sendable, Hashable { let projectIndex: Int let kind: TravelAlbumPreviewAssetKind } /// 预览页纯状态工具,负责构建节点和处理分组滑动边界。 enum TravelAlbumPreviewNavigator { /// 根据滑动模式生成页面节点。 static func nodes( projects: [TravelAlbumPreviewProject], mode: TravelAlbumPreviewSwipeMode ) -> [TravelAlbumPreviewNode] { projects.enumerated().flatMap { index, project in switch mode { case .projectsOnly: [TravelAlbumPreviewNode(projectIndex: index, kind: .original)] case .includeVariants: project.orderedAssets.map { TravelAlbumPreviewNode(projectIndex: index, kind: $0.kind) } } } } /// 处理反向滑动边界:从项目原图向右滑时直接进入上一项目原图。 static func backwardTargetIndex( nodes: [TravelAlbumPreviewNode], currentIndex: Int ) -> Int { guard nodes.indices.contains(currentIndex), currentIndex > 0 else { return max(0, currentIndex) } let current = nodes[currentIndex] guard current.kind == .original, current.projectIndex > 0 else { return currentIndex - 1 } return nodes.firstIndex { $0.projectIndex == current.projectIndex - 1 && $0.kind == .original } ?? currentIndex - 1 } } /// 预览操作执行结果,便于后续替换真实业务接口。 enum TravelAlbumPreviewActionResult: Sendable, Equatable { case success(String?) case failure(String) case unavailable(String) } /// 预览页底部操作协议,所有操作均以原素材项目 ID 为目标。 protocol TravelAlbumPreviewActionHandling: AnyObject { func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult func deleteProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult func refreshProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult } /// 新接口接入前的占位操作实现,不修改任何业务数据。 final class PlaceholderTravelAlbumPreviewActionHandler: TravelAlbumPreviewActionHandling { func requestAIRetouch(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { .unavailable("AI修复功能待接口接入") } func deleteProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { .unavailable("项目删除接口待接入") } func refreshProject(originalMaterialId: Int) async -> TravelAlbumPreviewActionResult { .unavailable("关联图片刷新接口待接入") } }