// // 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 = [] 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?() } }