Files
suixinkan_uikit/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumEntryViewModel.swift
汉秋 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

253 lines
7.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
}