feat: add AI retouch task center
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
import Foundation
|
||||
|
||||
/// AI 修图任务筛选分组,对应任务列表接口的 `status_group`。
|
||||
enum TravelAlbumAIJobFilter: String, CaseIterable, Sendable, Hashable {
|
||||
case all
|
||||
case inProgress = "in_progress"
|
||||
case completed
|
||||
case failed
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all: "全部"
|
||||
case .inProgress: "进行中"
|
||||
case .completed: "已完成"
|
||||
case .failed: "失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图任务及子任务状态;未知值安全降级,避免新增后端状态导致整页解码失败。
|
||||
enum TravelAlbumAIJobStatus: Sendable, Hashable {
|
||||
case queued
|
||||
case processing
|
||||
case succeeded
|
||||
case partiallySucceeded
|
||||
case failed
|
||||
case canceled
|
||||
case unknown(String)
|
||||
|
||||
init(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "queued": self = .queued
|
||||
case "processing": self = .processing
|
||||
case "succeeded": self = .succeeded
|
||||
case "partially_succeeded": self = .partiallySucceeded
|
||||
case "failed": self = .failed
|
||||
case "canceled": self = .canceled
|
||||
default: self = .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .queued: "queued"
|
||||
case .processing: "processing"
|
||||
case .succeeded: "succeeded"
|
||||
case .partiallySucceeded: "partially_succeeded"
|
||||
case .failed: "failed"
|
||||
case .canceled: "canceled"
|
||||
case .unknown(let value): value
|
||||
}
|
||||
}
|
||||
|
||||
var isInProgress: Bool { self == .queued || self == .processing }
|
||||
var isTerminal: Bool { !isInProgress && !isUnknown }
|
||||
private var isUnknown: Bool { if case .unknown = self { true } else { false } }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .queued: "排队中"
|
||||
case .processing: "修图中"
|
||||
case .succeeded: "已完成"
|
||||
case .partiallySucceeded: "部分完成"
|
||||
case .failed: "失败"
|
||||
case .canceled: "已取消"
|
||||
case .unknown: "状态更新中"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TravelAlbumAIJobStatus: Decodable {
|
||||
init(from decoder: Decoder) throws {
|
||||
self.init(rawValue: (try? decoder.singleValueContainer().decode(String.self)) ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图输出类型。
|
||||
enum TravelAlbumAIJobOutputType: Sendable, Hashable {
|
||||
case refined
|
||||
case atmosphere
|
||||
case cover
|
||||
case unknown(String)
|
||||
|
||||
init(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "refined": self = .refined
|
||||
case "atmosphere": self = .atmosphere
|
||||
case "cover": self = .cover
|
||||
default: self = .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .refined: "原图精修"
|
||||
case .atmosphere: "氛围感"
|
||||
case .cover: "封面"
|
||||
case .unknown: "其他结果"
|
||||
}
|
||||
}
|
||||
|
||||
var previewKind: TravelAlbumPreviewAssetKind? {
|
||||
switch self {
|
||||
case .refined: .retouched
|
||||
case .atmosphere: .atmosphere
|
||||
case .cover: .cover
|
||||
case .unknown: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TravelAlbumAIJobOutputType: Decodable {
|
||||
init(from decoder: Decoder) throws {
|
||||
self.init(rawValue: (try? decoder.singleValueContainer().decode(String.self)) ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图子任务数量进度。
|
||||
struct TravelAlbumAIJobProgress: Decodable, Sendable, Equatable, Hashable {
|
||||
let total: Int
|
||||
let queued: Int
|
||||
let processing: Int
|
||||
let succeeded: Int
|
||||
let failed: Int
|
||||
let canceled: Int
|
||||
|
||||
var completed: Int { min(total, succeeded + failed + canceled) }
|
||||
var fraction: Double { total > 0 ? min(1, Double(completed) / Double(total)) : 0 }
|
||||
}
|
||||
|
||||
/// AI 修图提交成功后返回的任务摘要。
|
||||
struct TravelAlbumAIJobSubmission: Decodable, Sendable, Equatable {
|
||||
let aiRetouchBatchId: Int
|
||||
let userEquityTravelId: Int
|
||||
let status: TravelAlbumAIJobStatus
|
||||
let progress: TravelAlbumAIJobProgress
|
||||
let createdAt: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case aiRetouchBatchId = "ai_retouch_batch_id"
|
||||
case userEquityTravelId = "user_equity_travel_id"
|
||||
case status, progress
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务所属相册摘要。
|
||||
struct TravelAlbumAIJobAlbum: Decodable, Sendable, Equatable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let userPhone: String
|
||||
let coverURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name
|
||||
case userPhone = "user_phone"
|
||||
case coverURL = "cover_url"
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务计划输出数量。
|
||||
struct TravelAlbumAIJobOutput: Decodable, Sendable, Equatable, Hashable {
|
||||
let type: TravelAlbumAIJobOutputType
|
||||
let count: Int
|
||||
}
|
||||
|
||||
/// 任务列表缩略图。
|
||||
struct TravelAlbumAIJobPreviewImage: Decodable, Sendable, Equatable, Hashable {
|
||||
let materialId: Int
|
||||
let thumbnailURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case materialId = "material_id"
|
||||
case thumbnailURL = "thumbnail_url"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图任务列表项。
|
||||
struct TravelAlbumAIJobSummary: Decodable, Sendable, Equatable, Hashable, Identifiable {
|
||||
var id: Int { aiRetouchBatchId }
|
||||
let aiRetouchBatchId: Int
|
||||
let userEquityTravelId: Int
|
||||
let scope: String
|
||||
let status: TravelAlbumAIJobStatus
|
||||
let album: TravelAlbumAIJobAlbum
|
||||
let sourceCount: Int
|
||||
let outputs: [TravelAlbumAIJobOutput]
|
||||
let previewImages: [TravelAlbumAIJobPreviewImage]
|
||||
let progress: TravelAlbumAIJobProgress
|
||||
let estimatedFinishAt: String?
|
||||
let failureSummary: String?
|
||||
let createdAt: String
|
||||
let startedAt: String?
|
||||
let finishedAt: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case aiRetouchBatchId = "ai_retouch_batch_id"
|
||||
case userEquityTravelId = "user_equity_travel_id"
|
||||
case scope, status, album, outputs, progress
|
||||
case sourceCount = "source_count"
|
||||
case previewImages = "preview_images"
|
||||
case estimatedFinishAt = "estimated_finish_at"
|
||||
case failureSummary = "failure_summary"
|
||||
case createdAt = "created_at"
|
||||
case startedAt = "started_at"
|
||||
case finishedAt = "finished_at"
|
||||
}
|
||||
|
||||
var displayFailureSummary: String? {
|
||||
guard status == .failed || status == .partiallySucceeded || progress.failed > 0 else { return nil }
|
||||
let value = failureSummary?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return value.isEmpty ? "部分照片处理失败,点击查看原因" : value
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图任务游标分页响应。
|
||||
struct TravelAlbumAIJobListResponse: Decodable, Sendable, Equatable {
|
||||
let items: [TravelAlbumAIJobSummary]
|
||||
let nextCursor: String?
|
||||
let hasMore: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case items
|
||||
case nextCursor = "next_cursor"
|
||||
case hasMore = "has_more"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图额度结算信息。
|
||||
struct TravelAlbumAIJobQuotaSettlement: Decodable, Sendable, Equatable {
|
||||
let status: String
|
||||
let reservedUnits: Int
|
||||
let consumedUnits: Int
|
||||
let releasedUnits: Int
|
||||
let coverUnits: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case reservedUnits = "reserved_units"
|
||||
case consumedUnits = "consumed_units"
|
||||
case releasedUnits = "released_units"
|
||||
case coverUnits = "cover_units"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图目标的来源素材。
|
||||
struct TravelAlbumAIJobSourceMaterial: Decodable, Sendable, Equatable, Hashable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
let thumbnailURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case fileName = "file_name"
|
||||
case thumbnailURL = "thumbnail_url"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图目标使用的模板摘要。
|
||||
struct TravelAlbumAIJobTemplate: Decodable, Sendable, Equatable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
}
|
||||
|
||||
/// AI 修图成功结果资源。
|
||||
struct TravelAlbumAIJobResultAsset: Decodable, Sendable, Equatable, Hashable {
|
||||
let id: Int
|
||||
let materialId: Int
|
||||
let url: String
|
||||
let thumbnailURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case materialId = "material_id"
|
||||
case url
|
||||
case thumbnailURL = "thumbnail_url"
|
||||
}
|
||||
}
|
||||
|
||||
/// 可直接展示给用户的 AI 修图失败信息。
|
||||
struct TravelAlbumAIJobError: Decodable, Sendable, Equatable, Hashable {
|
||||
let code: String
|
||||
let message: String
|
||||
let retryable: Bool
|
||||
|
||||
var displayMessage: String {
|
||||
let value = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return value.isEmpty ? "处理失败,请前往相册重新修图" : value
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个输出目标的处理明细。
|
||||
struct TravelAlbumAIJobTarget: Decodable, Sendable, Equatable, Hashable, Identifiable {
|
||||
var id: Int { targetId }
|
||||
let targetId: Int
|
||||
let sourceMaterial: TravelAlbumAIJobSourceMaterial?
|
||||
let inputMaterialIds: [Int]
|
||||
let outputType: TravelAlbumAIJobOutputType
|
||||
let template: TravelAlbumAIJobTemplate?
|
||||
let status: TravelAlbumAIJobStatus
|
||||
let resultAsset: TravelAlbumAIJobResultAsset?
|
||||
let error: TravelAlbumAIJobError?
|
||||
let createdAt: String
|
||||
let startedAt: String?
|
||||
let finishedAt: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case targetId = "target_id"
|
||||
case sourceMaterial = "source_material"
|
||||
case inputMaterialIds = "input_material_ids"
|
||||
case outputType = "output_type"
|
||||
case template, status, error
|
||||
case resultAsset = "result_asset"
|
||||
case createdAt = "created_at"
|
||||
case startedAt = "started_at"
|
||||
case finishedAt = "finished_at"
|
||||
}
|
||||
|
||||
var displayFailureMessage: String? {
|
||||
guard status == .failed else { return nil }
|
||||
return error?.displayMessage ?? "处理失败,请前往相册重新修图"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图任务完整详情。
|
||||
struct TravelAlbumAIJobDetail: Decodable, Sendable, Equatable {
|
||||
let aiRetouchBatchId: Int
|
||||
let userEquityTravelId: Int
|
||||
let scope: String
|
||||
let status: TravelAlbumAIJobStatus
|
||||
let album: TravelAlbumAIJobAlbum
|
||||
let sourceCount: Int
|
||||
let outputs: [TravelAlbumAIJobOutput]
|
||||
let progress: TravelAlbumAIJobProgress
|
||||
let quotaSettlement: TravelAlbumAIJobQuotaSettlement
|
||||
let targets: [TravelAlbumAIJobTarget]
|
||||
let estimatedFinishAt: String?
|
||||
let createdAt: String
|
||||
let startedAt: String?
|
||||
let finishedAt: String?
|
||||
let durationSeconds: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case aiRetouchBatchId = "ai_retouch_batch_id"
|
||||
case userEquityTravelId = "user_equity_travel_id"
|
||||
case scope, status, album, outputs, progress, targets
|
||||
case sourceCount = "source_count"
|
||||
case quotaSettlement = "quota_settlement"
|
||||
case estimatedFinishAt = "estimated_finish_at"
|
||||
case createdAt = "created_at"
|
||||
case startedAt = "started_at"
|
||||
case finishedAt = "finished_at"
|
||||
case durationSeconds = "duration_seconds"
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 修图任务日期展示工具。
|
||||
enum TravelAlbumAIJobDateFormatter {
|
||||
private static let internetFormatter = ISO8601DateFormatter()
|
||||
private static let preciseFormatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return formatter
|
||||
}()
|
||||
|
||||
static func date(_ value: String?) -> Date? {
|
||||
guard let value, !value.isEmpty else { return nil }
|
||||
return preciseFormatter.date(from: value) ?? internetFormatter.date(from: value)
|
||||
}
|
||||
|
||||
static func display(_ value: String?) -> String {
|
||||
guard let date = date(value) else { return "--" }
|
||||
return date.formatted(.dateTime.month().day().hour().minute())
|
||||
}
|
||||
|
||||
static func time(_ value: String?) -> String {
|
||||
guard let date = date(value) else { return "--" }
|
||||
return date.formatted(.dateTime.hour().minute())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user