145 lines
5.1 KiB
Swift
145 lines
5.1 KiB
Swift
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?() }
|
|
}
|