Files
suixinkan_uikit/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumDetailViewModel.swift
T
2026-07-31 16:48:28 +08:00

285 lines
9.4 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: "文件名倒序"
}
}
}
private(set) var album: TravelAlbum?
private(set) var materials: [TravelAlbumMaterial] = []
private(set) var allPhotoCount = 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 isUsingPurchasedDemoData = 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 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
}
}
/// 切换素材 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
isUsingPurchasedDemoData = 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
)
if reset,
selectedTab == .purchased,
response.total == 0,
response.list.isEmpty {
applyPurchasedDemoMaterials()
} else {
materials = reset ? response.list : materials + response.list
canLoadMore = materials.count < response.total
}
if selectedTab == .all {
allPhotoCount = response.total
}
isLoadingMore = false
notifyStateChange()
} catch is CancellationError {
return
} catch {
if !reset && currentPage > 1 {
currentPage -= 1
}
isLoadingMore = false
if reset, selectedTab == .purchased {
// 已购接口暂不可用时继续提供稳定的本地演示数据,保证产品流程可演示。
applyPurchasedDemoMaterials()
notifyStateChange()
return
}
onShowMessage?(error.localizedDescription)
}
}
/// 根据滚动位置触发加载更多。
func loadMoreIfNeeded(lastVisibleIndex: Int, api: any TravelAlbumServing) async {
if lastVisibleIndex >= materials.count - 6 {
await loadMaterials(reset: false, api: api)
}
}
/// 切换选择模式。
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()
}
/// 删除已选素材。
func deleteSelectedMaterials(api: any TravelAlbumServing) async {
let ids = Array(selectedMaterialIds)
guard !ids.isEmpty else {
onShowMessage?("请选择要删除的素材")
return
}
do {
for id in ids {
try await api.deleteMaterial(id: id)
}
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 applyPurchasedDemoMaterials() {
let demoItems: [(fileName: String, fileSize: Int, assetName: String, createdAt: String)] = [
("IMG_0724_0001.jpeg", 2_684_928, "purchased_alpine_lake", "2026-07-24 11:25:18"),
("IMG_0724_0002.jpeg", 3_215_360, "purchased_forest_waterfall", "2026-07-24 11:27:46"),
("IMG_0724_0003.jpeg", 2_936_832, "purchased_grassland_path", "2026-07-24 11:31:09"),
("IMG_0724_0004.jpeg", 3_481_600, "purchased_lakeside_flowers", "2026-07-24 11:34:22"),
]
var materials = demoItems.enumerated().map { index, item in
TravelAlbumMaterial(
id: -((albumId * 10) + index + 1),
userEquityTravelId: albumId,
status: 2,
fileName: item.fileName,
fileType: 1,
fileUrl: TravelAlbumMaterial.bundledImageURL(assetName: item.assetName),
fileSize: item.fileSize,
coverUrl: TravelAlbumMaterial.bundledImageURL(assetName: item.assetName),
isPurchased: true,
createdAt: item.createdAt,
updatedAt: item.createdAt
)
}
switch sortOption {
case .createdAsc:
materials.sort { $0.createdAt < $1.createdAt }
case .createdDesc:
materials.sort { $0.createdAt > $1.createdAt }
case .fileNameAsc:
materials.sort { $0.fileName.localizedStandardCompare($1.fileName) == .orderedAscending }
case .fileNameDesc:
materials.sort { $0.fileName.localizedStandardCompare($1.fileName) == .orderedDescending }
}
self.materials = materials
canLoadMore = false
isUsingPurchasedDemoData = true
}
private func notifyStateChange() {
onStateChange?()
}
}