// // TravelAlbumEntryViewModel.swift // suixinkan // import Foundation /// 旅拍相册入口页 ViewModel,负责列表、创建相册和相册码状态。 final class TravelAlbumEntryViewModel { enum CreateMode: Int, Sendable { case preShoot = 0 case preOrder = 1 var apiType: Int { switch self { case .preShoot: 1 case .preOrder: 2 } } } /// 相册码弹窗状态。 struct AlbumCodeState: Sendable, Equatable { let albumId: Int let albumName: String var qrImageUrl: String var isLoading: Bool var isDownloading: Bool } private(set) var albums: [TravelAlbum] = [] private(set) var albumTotal = 0 private(set) var availableOrders: [TravelAlbumAvailableOrder] = [] private(set) var isLoading = false private(set) var isCreating = false private(set) var isCreateSheetVisible = false private(set) var albumCodeState: AlbumCodeState? var onStateChange: (() -> Void)? var onShowMessage: ((String) -> Void)? var onCreatedAlbum: ((TravelAlbumCreateResponse) -> Void)? private let currentScenicIdProvider: () -> Int private let dateProvider: () -> Date init( currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }, dateProvider: @escaping () -> Date = Date.init ) { self.currentScenicIdProvider = currentScenicIdProvider self.dateProvider = dateProvider } /// 重新拉取相册列表。 func loadAlbums(api: any TravelAlbumServing) async { isLoading = true notifyStateChange() defer { isLoading = false notifyStateChange() } do { let response = try await api.list(page: 1, pageSize: 20) albumTotal = response.total albums = response.list } catch is CancellationError { return } catch { albums = [] albumTotal = 0 onShowMessage?(error.localizedDescription) } } /// 打开创建相册弹窗。 func openCreateSheet(api: any TravelAlbumServing) async { guard !isCreating else { return } guard currentScenicIdProvider() > 0 else { onShowMessage?("请先选择景区") return } isCreateSheetVisible = true notifyStateChange() await loadAvailableOrders(api: api) } /// 关闭创建相册弹窗。 func dismissCreateSheet() { isCreateSheetVisible = false notifyStateChange() } /// 拉取可绑定订单。 func loadAvailableOrders(api: any TravelAlbumServing) async { do { availableOrders = try await api.availableOrders() notifyStateChange() } catch is CancellationError { return } catch { availableOrders = [] notifyStateChange() } } /// 校验并创建相册任务。 func confirmCreate( mode: CreateMode, freeCount: String, singlePrice: String, packagePrice: String, order: TravelAlbumAvailableOrder?, api: any TravelAlbumServing ) async { guard !isCreating else { return } guard currentScenicIdProvider() > 0 else { onShowMessage?("请先选择景区") return } if mode == .preOrder && order == nil { onShowMessage?("请选择绑定订单") return } if mode == .preShoot && singlePrice.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { onShowMessage?("请输入单张照片价格") return } let materialPrice = Double(singlePrice) if mode == .preShoot && materialPrice == nil { onShowMessage?("请输入有效的单张照片价格") return } let albumName: String switch mode { case .preOrder: albumName = order?.projectName.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? buildTodayAlbumName() case .preShoot: albumName = buildTodayAlbumName() } let request: TravelAlbumCreateRequest switch mode { case .preShoot: request = TravelAlbumCreateRequest( name: albumName, type: mode.apiType, orderNumber: nil, materialNum: Int(freeCount) ?? 0, materialPrice: materialPrice, materialPackagePrice: Double(packagePrice) ?? 0, photoPrice: 0 ) case .preOrder: request = TravelAlbumCreateRequest( name: albumName, type: mode.apiType, orderNumber: order?.orderNumber, materialNum: nil, materialPrice: nil, materialPackagePrice: nil, photoPrice: nil ) } isCreating = true notifyStateChange() defer { isCreating = false notifyStateChange() } do { let response = try await api.create(request) isCreateSheetVisible = false onShowMessage?("任务创建成功") onCreatedAlbum?(response) await loadAlbums(api: api) } catch is CancellationError { return } catch { let message = error.localizedDescription if message.contains("订单已关联") { onShowMessage?("该订单已关联旅拍相册,请刷新后重新选择") await loadAvailableOrders(api: api) } else { onShowMessage?(message) } } } /// 打开相册码弹窗并加载二维码。 func openAlbumCode(album: TravelAlbum, api: any TravelAlbumServing) async { albumCodeState = AlbumCodeState( albumId: album.id, albumName: album.name, qrImageUrl: "", isLoading: true, isDownloading: false ) notifyStateChange() do { let response = try await api.mpCode(id: album.id) guard albumCodeState?.albumId == album.id else { return } albumCodeState?.qrImageUrl = response.mpCodeOssUrl.trimmingCharacters(in: .whitespacesAndNewlines) albumCodeState?.isLoading = false notifyStateChange() } catch is CancellationError { return } catch { guard albumCodeState?.albumId == album.id else { return } albumCodeState?.isLoading = false notifyStateChange() onShowMessage?(error.localizedDescription.isEmpty ? "获取小程序码失败" : error.localizedDescription) } } /// 关闭相册码弹窗。 func dismissAlbumCode() { albumCodeState = nil notifyStateChange() } /// 设置二维码下载状态。 func setDownloadingQRCode(_ isDownloading: Bool) { albumCodeState?.isDownloading = isDownloading notifyStateChange() } /// 生成 Android 同款今日相册名。 func buildTodayAlbumName() -> String { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd" let todayText = formatter.string(from: dateProvider()) let todayCount = albums.filter { $0.createdAt.hasPrefix(todayText) }.count return "\(todayText)-\(String(format: "%03d", todayCount + 1))" } private func notifyStateChange() { onStateChange?() } } private extension String { var nonEmpty: String? { isEmpty ? nil : self } }