Files
suixinkan_uikit/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift
T

305 lines
9.6 KiB
Swift

//
// TravelAlbumDetailViewModel.swift
// suixinkan
//
import Foundation
/// 旅拍相册详情 ViewModel,负责相册信息、素材分页筛选和删除状态。
final class TravelAlbumDetailViewModel {
enum Tab: Int, Sendable {
case all = 0
case purchased = 1
}
/// 排序方式。
enum SortOption: Int, CaseIterable, Sendable {
case createdAsc = 1
case createdDesc = 2
case fileNameAsc = 3
case fileNameDesc = 4
var title: String {
switch self {
case .createdAsc: "创建时间正序"
case .createdDesc: "创建时间倒序"
case .fileNameAsc: "文件名正序"
case .fileNameDesc: "文件名倒序"
}
}
var compactTitle: String {
switch self {
case .createdAsc: "时间 ↑"
case .createdDesc: "时间 ↓"
case .fileNameAsc: "名称 ↑"
case .fileNameDesc: "名称 ↓"
}
}
}
private(set) var album: TravelAlbum?
private(set) var materials: [TravelAlbumMaterial] = []
private(set) var allPhotoCount = 0
private(set) var purchasedPhotoCount = 0
private(set) var selectedTab: Tab = .all
private(set) var sortOption: SortOption = .createdDesc
private(set) var isLoading = true
private(set) var isRefreshing = false
private(set) var isSelectionMode = false
private(set) var selectedMaterialIds: Set<Int> = []
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
var onDeletedAlbum: ((Int) -> Void)?
let albumId: Int
private var currentPage = 1
private var canLoadMore = false
private var isLoadingMore = false
private let pageSize = 30
init(albumId: Int) {
self.albumId = albumId
}
/// 重新加载详情与素材。
func refreshAll(api: any TravelAlbumServing) async {
guard albumId > 0 else {
isLoading = false
onShowMessage?("相册不存在")
notifyStateChange()
return
}
isRefreshing = true
notifyStateChange()
await loadAlbumInfo(api: api)
await loadAllPhotoCount(api: api)
await loadPurchasedPhotoCount(api: api)
await loadMaterials(reset: true, api: api)
isRefreshing = false
isLoading = false
notifyStateChange()
}
/// 拉取相册详情。
func loadAlbumInfo(api: any TravelAlbumServing) async {
do {
album = try await api.info(id: albumId)
notifyStateChange()
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription)
}
}
/// 拉取全部照片数量。
func loadAllPhotoCount(api: any TravelAlbumServing) async {
do {
let response = try await api.materialList(
userEquityTravelId: albumId,
page: 1,
pageSize: 1,
orderBy: sortOption.rawValue,
isPurchased: nil
)
allPhotoCount = response.total
notifyStateChange()
} catch is CancellationError {
return
} catch {
return
}
}
/// 拉取已购照片数量;失败时保留已有数量,不影响素材列表加载。
func loadPurchasedPhotoCount(api: any TravelAlbumServing) async {
do {
let response = try await api.materialList(
userEquityTravelId: albumId,
page: 1,
pageSize: 1,
orderBy: sortOption.rawValue,
isPurchased: 1
)
purchasedPhotoCount = response.total
notifyStateChange()
} catch is CancellationError {
return
} catch {
return
}
}
/// 摘要卡封面地址,依次回退相册封面、首张素材封面和首张素材原图。
var displayCoverURL: String {
TravelAlbumDisplayFormatter.albumCoverURL(album: album, materials: materials)
}
/// 当前 Tab 对应的原图项目总数,供全屏预览索引使用。
var currentPhotoCount: Int {
selectedTab == .all ? allPhotoCount : purchasedPhotoCount
}
/// 切换素材 tab。
func selectTab(_ tab: Tab, api: any TravelAlbumServing) async {
guard selectedTab != tab else { return }
selectedTab = tab
isSelectionMode = false
selectedMaterialIds = []
notifyStateChange()
await loadMaterials(reset: true, api: api)
}
/// 切换排序。
func setSortOption(_ option: SortOption, api: any TravelAlbumServing) async {
guard sortOption != option else { return }
sortOption = option
notifyStateChange()
await loadMaterials(reset: true, api: api)
}
/// 拉取素材列表。
func loadMaterials(reset: Bool, api: any TravelAlbumServing) async {
guard albumId > 0 else { return }
if reset {
currentPage = 1
canLoadMore = false
} else {
guard canLoadMore, !isLoadingMore else { return }
isLoadingMore = true
currentPage += 1
}
do {
let response = try await api.materialList(
userEquityTravelId: albumId,
page: currentPage,
pageSize: pageSize,
orderBy: sortOption.rawValue,
isPurchased: selectedTab == .purchased ? 1 : nil
)
materials = reset ? response.list : materials + response.list
canLoadMore = materials.count < response.total
if selectedTab == .all {
allPhotoCount = response.total
} else {
purchasedPhotoCount = response.total
}
isLoadingMore = false
notifyStateChange()
} catch is CancellationError {
return
} catch {
if !reset && currentPage > 1 {
currentPage -= 1
}
isLoadingMore = false
onShowMessage?(error.localizedDescription)
}
}
/// 根据滚动位置触发加载更多。
func loadMoreIfNeeded(lastVisibleIndex: Int, api: any TravelAlbumServing) async {
if lastVisibleIndex >= materials.count - 6 {
await loadMaterials(reset: false, api: api)
}
}
/// 定向刷新单个素材,并同步替换网格列表中的对应缓存。
func refreshMaterial(id: Int, api: any TravelAlbumServing) async throws -> TravelAlbumMaterial {
let material = try await api.materialInfo(
userEquityTravelId: albumId,
materialId: id
)
guard material.id == id else { throw APIError.invalidResponse }
if let index = materials.firstIndex(where: { $0.id == id }) {
materials[index] = material
notifyStateChange()
}
return material
}
/// 切换选择模式。
func toggleSelectionMode() {
guard selectedTab == .all else { return }
isSelectionMode.toggle()
if !isSelectionMode {
selectedMaterialIds = []
}
notifyStateChange()
}
/// 切换素材选中状态。
func toggleMaterialSelection(_ material: TravelAlbumMaterial) {
guard isSelectionMode else { return }
guard material.status == 1 else {
onShowMessage?("仅未购买素材可删除")
return
}
if selectedMaterialIds.contains(material.id) {
selectedMaterialIds.remove(material.id)
} else {
selectedMaterialIds.insert(material.id)
}
notifyStateChange()
}
/// AI 修图任务提交成功后退出选择模式并清空当前选择。
func completeAIRetouchSubmission() {
isSelectionMode = false
selectedMaterialIds = []
notifyStateChange()
}
/// 预览页删除成功后移除对应素材,并同步全部/已购计数。
func removeMaterialAfterPreviewDeletion(id: Int) {
guard let index = materials.firstIndex(where: { $0.id == id }) else { return }
let material = materials.remove(at: index)
selectedMaterialIds.remove(id)
allPhotoCount = max(0, allPhotoCount - 1)
if material.isPurchased {
purchasedPhotoCount = max(0, purchasedPhotoCount - 1)
}
notifyStateChange()
}
/// 删除已选素材。
func deleteSelectedMaterials(api: any TravelAlbumServing) async {
let ids = selectedMaterialIds.sorted()
guard !ids.isEmpty else {
onShowMessage?("请选择要删除的素材")
return
}
do {
try await api.batchDeleteMaterials(ids: ids)
onShowMessage?("删除成功")
isSelectionMode = false
selectedMaterialIds = []
await refreshAll(api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "删除失败" : error.localizedDescription)
}
}
/// 删除当前相册。
func deleteAlbum(api: any TravelAlbumServing) async {
do {
try await api.deleteAlbum(id: albumId)
onShowMessage?("相册已删除")
onDeletedAlbum?(albumId)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription.isEmpty ? "删除失败" : error.localizedDescription)
}
}
private func notifyStateChange() {
onStateChange?()
}
}