Files
suixinkan_uikit/suixinkan/Features/TravelAlbum/ViewModels/TravelAlbumEntryViewModel.swift
T
lujiuyin d04641b623 feat: 新增相册自动修图与OTG状态预览
支持相册修图配置、模板选择和传输模式选择;上传后自动提交修图并展示状态角标及精修预览。补充接口文档与相关测试。
2026-08-27 16:03:40 +08:00

271 lines
8.5 KiB
Swift

//
// 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,
preservingContentOnFailure: Bool = false
) 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 {
if !preservingContentOnFailure {
albums = []
albumTotal = 0
}
onShowMessage?(error.localizedDescription)
}
}
/// 用户下拉刷新相册列表;失败时保留当前内容,避免页面瞬间清空。
func refreshAlbums(api: any TravelAlbumServing) async {
guard !isLoading else { return }
await loadAlbums(api: api, preservingContentOnFailure: true)
}
/// 打开创建相册弹窗。
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,
autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration = .disabled,
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
}
if autoRetouchConfiguration.enabled && !autoRetouchConfiguration.isValid {
onShowMessage?("请选择有效的 AI 修图模板")
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,
autoRetouchConfiguration: autoRetouchConfiguration
)
case .preOrder:
request = TravelAlbumCreateRequest(
name: albumName,
type: mode.apiType,
orderNumber: order?.orderNumber,
materialNum: nil,
materialPrice: nil,
materialPackagePrice: nil,
photoPrice: nil,
autoRetouchConfiguration: autoRetouchConfiguration
)
}
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
}
}