feat: add AI retouch task center
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import Foundation
|
||||
|
||||
/// AI 修图任务列表状态,负责筛选、游标分页、去重和静默刷新。
|
||||
final class TravelAlbumAIJobListViewModel {
|
||||
private(set) var items: [TravelAlbumAIJobSummary] = []
|
||||
private(set) var selectedFilter: TravelAlbumAIJobFilter = .all
|
||||
private(set) var isLoading = false
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isLoadingMore = false
|
||||
private(set) var errorMessage: String?
|
||||
private var nextCursor: String?
|
||||
private var hasMore = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
var containsInProgressJobs: Bool { items.contains { $0.status.isInProgress } }
|
||||
var canLoadMore: Bool { hasMore && !isLoadingMore }
|
||||
|
||||
/// 选择筛选项并重新加载第一页。
|
||||
func selectFilter(_ filter: TravelAlbumAIJobFilter, api: any TravelAlbumServing) async {
|
||||
guard selectedFilter != filter else { return }
|
||||
selectedFilter = filter
|
||||
items = []
|
||||
notify()
|
||||
await loadFirstPage(api: api)
|
||||
}
|
||||
|
||||
/// 首次加载或下拉刷新第一页。
|
||||
func loadFirstPage(api: any TravelAlbumServing, refreshing: Bool = false, silent: Bool = false) async {
|
||||
guard !isLoading && !isRefreshing else { return }
|
||||
if refreshing { isRefreshing = true } else if !silent { isLoading = true }
|
||||
errorMessage = nil
|
||||
notify()
|
||||
defer {
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
notify()
|
||||
}
|
||||
await request(cursor: nil, reset: true, api: api, allowCursorRecovery: false, silent: silent)
|
||||
}
|
||||
|
||||
/// 加载下一页。
|
||||
func loadMore(api: any TravelAlbumServing) async {
|
||||
guard canLoadMore, let cursor = nextCursor else { return }
|
||||
isLoadingMore = true
|
||||
notify()
|
||||
defer { isLoadingMore = false; notify() }
|
||||
await request(cursor: cursor, reset: false, api: api, allowCursorRecovery: true, silent: false)
|
||||
}
|
||||
|
||||
private func request(
|
||||
cursor: String?,
|
||||
reset: Bool,
|
||||
api: any TravelAlbumServing,
|
||||
allowCursorRecovery: Bool,
|
||||
silent: Bool
|
||||
) async {
|
||||
do {
|
||||
let response = try await api.aiRetouchJobList(
|
||||
statusGroup: selectedFilter,
|
||||
limit: 20,
|
||||
cursor: cursor
|
||||
)
|
||||
if reset {
|
||||
items = response.items
|
||||
} else {
|
||||
var known = Set(items.map(\.id))
|
||||
items += response.items.filter { known.insert($0.id).inserted }
|
||||
}
|
||||
nextCursor = response.nextCursor
|
||||
hasMore = response.hasMore && response.nextCursor?.isEmpty == false
|
||||
errorMessage = nil
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
let message = error.localizedDescription
|
||||
if allowCursorRecovery && message.localizedCaseInsensitiveContains("cursor") {
|
||||
nextCursor = nil
|
||||
hasMore = false
|
||||
await request(cursor: nil, reset: true, api: api, allowCursorRecovery: false, silent: silent)
|
||||
return
|
||||
}
|
||||
if items.isEmpty { errorMessage = message.isEmpty ? "任务加载失败" : message }
|
||||
if !silent { onShowMessage?(message.isEmpty ? "任务加载失败" : message) }
|
||||
}
|
||||
}
|
||||
|
||||
private func notify() { onStateChange?() }
|
||||
}
|
||||
|
||||
/// AI 修图任务详情状态,负责刷新、404 识别和终态判断。
|
||||
final class TravelAlbumAIJobDetailViewModel {
|
||||
private(set) var detail: TravelAlbumAIJobDetail?
|
||||
private(set) var isLoading = false
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var errorMessage: String?
|
||||
private(set) var isNotFound = false
|
||||
|
||||
let batchId: Int
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onNotFound: (() -> Void)?
|
||||
|
||||
init(batchId: Int) { self.batchId = batchId }
|
||||
|
||||
var shouldPoll: Bool { detail?.status.isInProgress == true }
|
||||
|
||||
/// 拉取任务详情;静默刷新失败时保留现有内容。
|
||||
func load(api: any TravelAlbumServing, refreshing: Bool = false, silent: Bool = false) async {
|
||||
guard batchId > 0, !isLoading && !isRefreshing else {
|
||||
if batchId <= 0 { markNotFound() }
|
||||
return
|
||||
}
|
||||
if refreshing { isRefreshing = true } else if detail == nil && !silent { isLoading = true }
|
||||
errorMessage = nil
|
||||
notify()
|
||||
defer {
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
notify()
|
||||
}
|
||||
do {
|
||||
detail = try await api.aiRetouchJobInfo(batchId: batchId)
|
||||
isNotFound = false
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch APIError.httpStatus(let status, _) where status == 404 {
|
||||
markNotFound()
|
||||
} catch {
|
||||
let message = error.localizedDescription.isEmpty ? "任务详情加载失败" : error.localizedDescription
|
||||
if detail == nil { errorMessage = message }
|
||||
if !silent { onShowMessage?(message) }
|
||||
}
|
||||
}
|
||||
|
||||
private func markNotFound() {
|
||||
guard !isNotFound else { return }
|
||||
isNotFound = true
|
||||
onNotFound?()
|
||||
}
|
||||
|
||||
private func notify() { onStateChange?() }
|
||||
}
|
||||
Reference in New Issue
Block a user