Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,974 @@
//
// AssetsViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import Foundation
/// App
@MainActor
final class CloudTransferStore {
var onChange: (() -> Void)?
static let shared = CloudTransferStore()
private(set) var records: [CloudTransferItem] = [] { didSet { onChange?() } }
/// ID
@discardableResult
func start(fileName: String, direction: CloudTransferDirection) -> UUID {
let id = UUID()
records.insert(CloudTransferItem(id: id, fileName: fileName, direction: direction), at: 0)
return id
}
///
func update(id: UUID, progress: Int, message: String = "") {
guard let index = records.firstIndex(where: { $0.id == id }) else { return }
records[index].progress = max(0, min(100, progress))
if !message.isEmpty {
records[index].message = message
}
}
///
func succeed(id: UUID, message: String = "已完成") {
guard let index = records.firstIndex(where: { $0.id == id }) else { return }
records[index].progress = 100
records[index].status = .success
records[index].message = message
}
///
func fail(id: UUID, message: String) {
guard let index = records.firstIndex(where: { $0.id == id }) else { return }
records[index].status = .failed
records[index].message = message
}
///
func clear() {
records = []
}
}
/// ViewModel
@MainActor
final class CloudStorageViewModel {
var onChange: (() -> Void)?
var path: [CloudDriveFile] = [CloudDriveFile(id: 0, name: "云盘")] { didSet { onChange?() } }
var files: [CloudDriveFile] = [] { didSet { onChange?() } }
var total = 0 { didSet { onChange?() } }
var page = 1 { didSet { onChange?() } }
var searchText = "" { didSet { onChange?() } }
var selectedFilter: CloudDriveFilter = .all { didSet { onChange?() } }
var selectedSort: CloudDriveSort = .updatedDesc { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isLoadingMore = false { didSet { onChange?() } }
var isMutating = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
private let pageSize = 20
/// ID
var currentFolderId: Int {
path.last?.id ?? 0
}
///
var hasMore: Bool {
files.count < total
}
///
func reload(api: any AssetsServing) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let payload = try await requestFiles(api: api, page: 1)
page = 1
files = payload.list
total = payload.total
} catch {
files = []
total = 0
page = 1
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: any AssetsServing) async {
guard hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let payload = try await requestFiles(api: api, page: nextPage)
page = nextPage
total = payload.total
files.append(contentsOf: payload.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func enterFolder(_ folder: CloudDriveFile, api: any AssetsServing) async {
guard folder.isFolder else { return }
path.append(folder)
await reload(api: api)
}
///
func popToFolder(at index: Int, api: any AssetsServing) async {
guard path.indices.contains(index) else { return }
path = Array(path.prefix(index + 1))
await reload(api: api)
}
///
func createFolder(name: String, api: any AssetsServing) async -> Bool {
let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !folderName.isEmpty else {
errorMessage = "请输入文件夹名称"
return false
}
return await mutate(api: api) {
try await api.cloudFolderCreate(CloudFolderCreateRequest(parentFolderId: currentFolderId, name: folderName))
}
}
///
func rename(_ item: CloudDriveFile, newName: String, api: any AssetsServing) async -> Bool {
let fileName = newName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !fileName.isEmpty else {
errorMessage = "请输入名称"
return false
}
return await mutate(api: api) {
if item.isFolder {
try await api.cloudFolderEdit(CloudFolderModifyRequest(id: item.id, name: fileName))
} else {
try await api.cloudFileEdit(CloudFileModifyRequest(id: item.id, fileName: fileName))
}
}
}
///
func delete(_ item: CloudDriveFile, api: any AssetsServing) async -> Bool {
await mutate(api: api) {
let action = CloudFileActionItem(id: item.id, type: item.type)
try await api.cloudFileDelete(CloudFileDeleteRequest(list: [action]))
}
}
///
func move(_ item: CloudDriveFile, targetFolderId: Int, api: any AssetsServing) async -> Bool {
await mutate(api: api) {
let action = CloudFileActionItem(id: item.id, type: item.type)
try await api.cloudFileMove(CloudFileMoveRequest(targetFolderId: targetFolderId, list: [action]))
}
}
/// OSS
func upload(
localFiles: [CloudLocalUploadFile],
scenicId: Int?,
api: any AssetsServing,
uploadService: any OSSUploadServing,
transferStore: CloudTransferStore
) async -> Bool {
guard let scenicId else {
errorMessage = "缺少当前景区,无法上传"
return false
}
guard !localFiles.isEmpty else { return true }
isMutating = true
defer { isMutating = false }
do {
let permission = try await api.checkCloudUploadPermission()
guard permission.canUpload else {
let reason = permission.reason.trimmingCharacters(in: .whitespacesAndNewlines)
errorMessage = reason.isEmpty ? "当前账号暂无上传权限" : reason
return false
}
for file in localFiles {
let transferId = transferStore.start(fileName: file.fileName, direction: .upload)
do {
let url = try await uploadService.uploadCloudFile(
data: file.data,
fileName: file.fileName,
fileType: file.fileType,
scenicId: scenicId
) { progress in
Task { @MainActor in
transferStore.update(id: transferId, progress: progress)
}
}
try await api.cloudFileUpload(
CloudFileUploadRequest(parentFolderId: currentFolderId, fileUrl: url, fileName: file.fileName)
)
transferStore.succeed(id: transferId)
} catch {
transferStore.fail(id: transferId, message: error.localizedDescription)
throw error
}
}
await reload(api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func mutate(api: any AssetsServing, operation: () async throws -> Void) async -> Bool {
isMutating = true
defer { isMutating = false }
do {
try await operation()
await reload(api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func requestFiles(api: any AssetsServing, page: Int) async throws -> ListPayload<CloudDriveFile> {
try await api.cloudFileList(
parentFolderId: currentFolderId,
name: searchText.trimmingCharacters(in: .whitespacesAndNewlines),
type: selectedFilter.rawValue,
orderBy: selectedSort.rawValue,
page: page,
pageSize: pageSize
)
}
}
/// ViewModel
@MainActor
final class MediaLibraryViewModel {
var onChange: (() -> Void)?
let kind: MediaLibraryKind
var items: [MediaLibraryItem] = [] { didSet { onChange?() } }
var selectedDetail: MediaLibraryDetail? { didSet { onChange?() } }
var orderInfo: MediaLibraryOrderInfo? { didSet { onChange?() } }
var total = 0 { didSet { onChange?() } }
var page = 1 { didSet { onChange?() } }
var keyword = "" { didSet { onChange?() } }
var selectedAuditFilter: MediaLibraryAuditFilter = .all { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isLoadingMore = false { didSet { onChange?() } }
var isLoadingDetail = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
private let pageSize = 10
/// ViewModel
init(kind: MediaLibraryKind = .material) {
self.kind = kind
}
///
var hasMore: Bool {
items.count < total
}
///
func reload(api: any AssetsServing) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let response = try await requestList(api: api, page: 1)
page = 1
items = response.list
total = response.total
orderInfo = response.order
} catch {
items = []
total = 0
orderInfo = nil
page = 1
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: any AssetsServing) async {
guard hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let response = try await requestList(api: api, page: nextPage)
page = nextPage
total = response.total
orderInfo = response.order ?? orderInfo
items.append(contentsOf: response.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func loadDetail(id: Int, api: any AssetsServing) async {
isLoadingDetail = true
errorMessage = nil
defer { isLoadingDetail = false }
do {
selectedDetail = try await api.mediaAlbumDetail(id: id, kind: kind)
} catch {
errorMessage = error.localizedDescription
}
}
///
func toggleListing(_ item: MediaLibraryItem, api: any AssetsServing) async -> Bool {
guard item.isApproved else {
errorMessage = "审核通过后才能上下架"
return false
}
return await operate(api: api) {
let target = item.listing.toggled
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: item.id, type: kind.rawValue, listingStatus: target.rawValue))
}
}
///
func toggleListing(detail: MediaLibraryDetail, api: any AssetsServing) async -> Bool {
guard detail.isApproved else {
errorMessage = "审核通过后才能上下架"
return false
}
return await operate(api: api) {
let target = detail.listing.toggled
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: detail.id, type: kind.rawValue, listingStatus: target.rawValue))
}
}
///
func delete(id: Int, api: any AssetsServing) async -> Bool {
await operate(api: api) {
try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: id, type: kind.rawValue))
}
}
///
private func operate(api: any AssetsServing, operation: () async throws -> Void) async -> Bool {
do {
try await operation()
await reload(api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func requestList(api: any AssetsServing, page: Int) async throws -> MediaLibraryListResponse {
try await api.mediaAlbumList(
kind: kind,
keyword: keyword.trimmingCharacters(in: .whitespacesAndNewlines),
status: selectedAuditFilter.rawValue >= 0 ? selectedAuditFilter.rawValue : nil,
page: page,
pageSize: pageSize
)
}
}
/// ViewModelOSS
@MainActor
final class MediaLibraryEditorViewModel {
var onChange: (() -> Void)?
var name = "" { didSet { onChange?() } }
var description = "" { didSet { onChange?() } }
var selectedSpotId: Int? { didSet { onChange?() } }
var selectedProjectId: Int? { didSet { onChange?() } }
var projects: [PhotographerProjectItem] = [] { didSet { onChange?() } }
var tagsText = "" { didSet { onChange?() } }
var coverFile: MediaLocalUploadFile? { didSet { onChange?() } }
var mediaFiles: [MediaLocalUploadFile] = [] { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var isLoadingProjects = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
var didSubmitSuccessfully = false { didSet { onChange?() } }
private let editingId: Int?
///
init(detail: MediaLibraryDetail? = nil) {
editingId = detail?.id
name = detail?.name ?? ""
description = detail?.description ?? ""
}
///
var isEditing: Bool {
editingId != nil
}
///
func loadProjects(scenicId: Int?, api: any AssetsServing) async {
guard let scenicId else {
projects = []
errorMessage = "缺少当前景区,无法加载项目"
return
}
isLoadingProjects = true
errorMessage = nil
defer { isLoadingProjects = false }
do {
let payload = try await api.projectList(scenicId: scenicId, name: nil, page: 1, pageSize: 50)
projects = payload.list
if let selectedProjectId, !projects.contains(where: { $0.id == selectedProjectId }) {
self.selectedProjectId = nil
}
} catch {
projects = []
errorMessage = error.localizedDescription
}
}
///
func setCover(_ file: MediaLocalUploadFile) {
coverFile = file
}
///
func addMediaFiles(_ files: [MediaLocalUploadFile]) {
mediaFiles.append(contentsOf: files)
}
///
func removeMediaFile(id: UUID) {
mediaFiles.removeAll { $0.id == id }
}
///
func submit(
kind: MediaLibraryKind = .material,
scenicId: Int?,
api: any AssetsServing,
uploadService: any OSSUploadServing
) async -> Bool {
guard validate(kind: kind, scenicId: scenicId) else { return false }
guard !isSubmitting else { return false }
isSubmitting = true
defer { isSubmitting = false }
do {
let actualScenicId = scenicId ?? 0
let coverUpload = try await uploadCover(scenicId: actualScenicId, uploadService: uploadService)
let uploads = try await uploadMediaFiles(scenicId: actualScenicId, uploadService: uploadService)
if let editingId {
let request = MediaAlbumEditRequest(
id: editingId,
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
type: kind.rawValue,
mediaType: resolvedMediaType(),
mediaList: uploads,
coverUrl: coverUpload.url,
coverSize: coverUpload.size,
scenicSpotId: selectedSpotId ?? 0,
description: description,
materialTag: normalizedTags()
)
try await api.mediaAlbumEdit(request)
} else {
let request = MediaAlbumUploadRequest(
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
type: kind.rawValue,
mediaType: resolvedMediaType(),
mediaList: uploads,
coverUrl: coverUpload.url,
coverSize: coverUpload.size,
scenicSpotId: selectedSpotId ?? 0,
description: description,
materialTag: normalizedTags(),
projectId: kind == .sample ? (selectedProjectId ?? 0) : 0
)
try await api.mediaAlbumUpload(request)
}
didSubmitSuccessfully = true
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func validate(kind: MediaLibraryKind, scenicId: Int?) -> Bool {
let title = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard scenicId != nil else {
errorMessage = "缺少当前景区,无法提交\(kind == .sample ? "样片" : "素材")"
return false
}
guard !title.isEmpty else {
errorMessage = "请输入\(kind == .sample ? "样片" : "素材")名称"
return false
}
guard selectedSpotId != nil else {
errorMessage = "请选择打卡点"
return false
}
guard kind != .sample || selectedProjectId != nil else {
errorMessage = "请选择关联项目"
return false
}
guard coverFile != nil else {
errorMessage = "请选择封面"
return false
}
guard !mediaFiles.isEmpty else {
errorMessage = "请选择\(kind == .sample ? "样片" : "素材")文件"
return false
}
guard normalizedTags().count <= 120 else {
errorMessage = "标签内容过长"
return false
}
return true
}
/// URL
private func uploadCover(
scenicId: Int,
uploadService: any OSSUploadServing
) async throws -> (url: String, size: MediaAlbumFileSize) {
guard let coverFile else {
throw APIError.emptyData
}
let url = try await uploadService.uploadCloudFile(
data: coverFile.data,
fileName: coverFile.fileName,
fileType: coverFile.fileType,
scenicId: scenicId,
onProgress: { _ in }
)
return (url, MediaAlbumFileSize(width: coverFile.width, height: coverFile.height))
}
///
private func uploadMediaFiles(
scenicId: Int,
uploadService: any OSSUploadServing
) async throws -> [MediaAlbumUploadItem] {
var result: [MediaAlbumUploadItem] = []
for file in mediaFiles {
let url = try await uploadService.uploadCloudFile(
data: file.data,
fileName: file.fileName,
fileType: file.fileType,
scenicId: scenicId,
onProgress: { _ in }
)
result.append(
MediaAlbumUploadItem(
originalName: file.fileName,
ossUrl: url,
size: Int64(file.data.count),
fileWidthSize: MediaAlbumFileSize(width: file.width, height: file.height)
)
)
}
return result
}
///
private func resolvedMediaType() -> Int {
mediaFiles.contains { $0.fileType == 1 } ? 1 : 2
}
///
private func normalizedTags() -> String {
tagsText
.split { $0 == "," || $0 == "" || $0 == " " || $0 == "\n" }
.map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.joined(separator: ",")
}
}
/// ViewModel
@MainActor
final class AlbumListViewModel {
var onChange: (() -> Void)?
var folders: [AlbumFolderItem] = [] { didSet { onChange?() } }
var total = 0 { didSet { onChange?() } }
var page = 1 { didSet { onChange?() } }
var searchText = "" { didSet { onChange?() } }
var startTime = "" { didSet { onChange?() } }
var endTime = "" { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isLoadingMore = false { didSet { onChange?() } }
var isMutating = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
private let pageSize = 10
///
var hasMore: Bool {
folders.count < total
}
///
func reload(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId else {
folders = []
total = 0
page = 1
errorMessage = "缺少当前景区,无法加载相册"
return
}
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let payload = try await requestFolders(api: api, scenicId: scenicId, page: 1)
self.page = 1
folders = payload.list
total = payload.total
} catch {
folders = []
total = 0
page = 1
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId, hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let payload = try await requestFolders(api: api, scenicId: scenicId, page: nextPage)
page = nextPage
total = payload.total
folders.append(contentsOf: payload.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func createFolder(name: String, remark: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard let scenicId else {
errorMessage = "缺少当前景区,无法创建相册"
return false
}
guard !folderName.isEmpty else {
errorMessage = "请输入相册名称"
return false
}
guard !isMutating else { return false }
isMutating = true
defer { isMutating = false }
do {
try await api.addAlbumFolder(scenicId: scenicId, name: folderName, remark: remark.trimmingCharacters(in: .whitespacesAndNewlines))
await reload(api: api, scenicId: scenicId)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func requestFolders(api: any AssetsServing, scenicId: Int, page: Int) async throws -> ListPayload<AlbumFolderItem> {
try await api.albumFolderList(
scenicId: scenicId,
page: page,
pageSize: pageSize,
name: searchText.trimmingCharacters(in: .whitespacesAndNewlines),
startTime: startTime.trimmingCharacters(in: .whitespacesAndNewlines).assetsNilIfEmpty,
endTime: endTime.trimmingCharacters(in: .whitespacesAndNewlines).assetsNilIfEmpty
)
}
}
/// ViewModel/
@MainActor
final class AlbumDetailViewModel {
var onChange: (() -> Void)?
let folderId: Int
var folder: AlbumFolderItem? { didSet { onChange?() } }
var files: [AlbumFileItem] = [] { didSet { onChange?() } }
var selectedTab: AlbumFileTab = .image { didSet { onChange?() } }
var total = 0 { didSet { onChange?() } }
var page = 1 { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isLoadingMore = false { didSet { onChange?() } }
var isMutating = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
private let pageSize = 20
/// ViewModel
init(folderId: Int, summary: AlbumFolderItem? = nil) {
self.folderId = folderId
folder = summary
}
///
var hasMore: Bool {
files.count < total
}
/// Tab
func reload(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId else {
files = []
total = 0
page = 1
errorMessage = "缺少当前景区,无法加载相册内容"
return
}
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
folder = try await api.albumFolderInfo(id: folderId)
let listPayload = try await api.albumFileList(
scenicId: scenicId,
folderId: folderId,
fileType: selectedTab.rawValue,
page: 1,
pageSize: pageSize
)
files = listPayload.list
total = listPayload.total
page = 1
} catch {
page = 1
errorMessage = error.localizedDescription
}
}
///
func selectTab(_ tab: AlbumFileTab, api: any AssetsServing, scenicId: Int?) async {
guard selectedTab != tab else { return }
selectedTab = tab
files = []
total = 0
page = 1
await reload(api: api, scenicId: scenicId)
}
/// Tab
func loadMore(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId, hasMore, !isLoadingMore else { return }
isLoadingMore = true
defer { isLoadingMore = false }
do {
let nextPage = page + 1
let payload = try await api.albumFileList(
scenicId: scenicId,
folderId: folderId,
fileType: selectedTab.rawValue,
page: nextPage,
pageSize: pageSize
)
page = nextPage
total = payload.total
files.append(contentsOf: payload.list)
} catch {
errorMessage = error.localizedDescription
}
}
///
func delete(file: AlbumFileItem, scenicId: Int?, api: any AssetsServing) async -> Bool {
await mutate(api: api, scenicId: scenicId) {
try await api.deleteAlbumFiles(folderId: folderId, idList: [file.id])
}
}
///
func setCover(file: AlbumFileItem, scenicId: Int?, api: any AssetsServing) async -> Bool {
await mutate(api: api, scenicId: scenicId) {
try await api.editAlbumFolder(folderId: folderId, coverFileId: file.id, name: nil, remark: nil)
}
}
///
func rename(name: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !folderName.isEmpty else {
errorMessage = "请输入相册名称"
return false
}
return await mutate(api: api, scenicId: scenicId) {
try await api.editAlbumFolder(folderId: folderId, coverFileId: nil, name: folderName, remark: nil)
}
}
///
func updateRemark(_ remark: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
await mutate(api: api, scenicId: scenicId) {
try await api.editAlbumFolder(folderId: folderId, coverFileId: nil, name: nil, remark: remark.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
///
private func mutate(api: any AssetsServing, scenicId: Int?, operation: () async throws -> Void) async -> Bool {
guard !isMutating else { return false }
guard scenicId != nil else {
errorMessage = "缺少当前景区,无法操作相册"
return false
}
isMutating = true
defer { isMutating = false }
do {
try await operation()
await reload(api: api, scenicId: scenicId)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
}
/// ViewModel
@MainActor
final class AlbumTrailerViewModel {
var onChange: (() -> Void)?
var folders: [AlbumFolderItem] = [] { didSet { onChange?() } }
var selectedFolderId: Int? { didSet { onChange?() } }
var localFiles: [AlbumLocalUploadFile] = [] { didSet { onChange?() } }
var uploadProgress = 0 { didSet { onChange?() } }
var isLoadingFolders = false { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
var didUploadSuccessfully = false { didSet { onChange?() } }
///
func loadFolders(api: any AssetsServing, scenicId: Int?) async {
guard let scenicId else {
folders = []
selectedFolderId = nil
errorMessage = "缺少当前景区,无法加载相册"
return
}
isLoadingFolders = true
errorMessage = nil
defer { isLoadingFolders = false }
do {
let payload = try await api.albumFolderList(
scenicId: scenicId,
page: 1,
pageSize: 100,
name: "",
startTime: nil,
endTime: nil
)
folders = payload.list
if selectedFolderId == nil {
selectedFolderId = folders.first?.id
}
} catch {
folders = []
selectedFolderId = nil
errorMessage = error.localizedDescription
}
}
///
func addLocalFiles(_ files: [AlbumLocalUploadFile]) {
localFiles.append(contentsOf: files)
}
///
func removeLocalFile(id: UUID) {
localFiles.removeAll { $0.id == id }
}
/// OSS
func submit(scenicId: Int?, api: any AssetsServing, uploadService: any OSSUploadServing) async -> Bool {
guard let scenicId else {
errorMessage = "缺少当前景区,无法上传"
return false
}
guard let folderId = selectedFolderId else {
errorMessage = "请选择相册"
return false
}
guard !localFiles.isEmpty else {
errorMessage = "请选择要上传的图片或视频"
return false
}
guard !isSubmitting else { return false }
isSubmitting = true
didUploadSuccessfully = false
uploadProgress = 0
defer { isSubmitting = false }
do {
let fileCount = max(localFiles.count, 1)
for (index, file) in localFiles.enumerated() {
let url = try await uploadService.uploadAlbumFile(
data: file.data,
fileName: file.fileName,
fileType: file.fileType,
scenicId: scenicId
) { progress in
Task { @MainActor in
let base = Double(index) / Double(fileCount)
let step = Double(progress) / Double(fileCount)
self.uploadProgress = min(99, Int((base + step / 100) * 100))
}
}
try await api.albumFileUploadURL(scenicId: scenicId, fileURL: url, folderId: folderId)
}
uploadProgress = 100
didUploadSuccessfully = true
localFiles = []
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
}
private extension String {
/// nil便
var assetsNilIfEmpty: String? {
isEmpty ? nil : self
}
}