381 lines
12 KiB
Swift
381 lines
12 KiB
Swift
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())
|
|
}
|
|
}
|