为资产模块新增相册管理与片花上传

为 album_list 与 album_trailer 接入首页路由,在 Profile 中新增 DEBUG 首页菜单预览,并扩展 Assets 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 09:43:46 +08:00
parent cac22fcc56
commit ae0bc116d8
17 changed files with 2018 additions and 26 deletions

View File

@ -578,3 +578,367 @@ final class MediaLibraryEditorViewModel {
.joined(separator: ",")
}
}
/// ViewModel
@MainActor
@Observable
final class AlbumListViewModel {
var folders: [AlbumFolderItem] = []
var total = 0
var page = 1
var searchText = ""
var startTime = ""
var endTime = ""
var isLoading = false
var isLoadingMore = false
var isMutating = false
var errorMessage: String?
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
@Observable
final class AlbumDetailViewModel {
let folderId: Int
var folder: AlbumFolderItem?
var files: [AlbumFileItem] = []
var selectedTab: AlbumFileTab = .image
var total = 0
var page = 1
var isLoading = false
var isLoadingMore = false
var isMutating = false
var errorMessage: String?
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
@Observable
final class AlbumTrailerViewModel {
var folders: [AlbumFolderItem] = []
var selectedFolderId: Int?
var localFiles: [AlbumLocalUploadFile] = []
var uploadProgress = 0
var isLoadingFolders = false
var isSubmitting = false
var errorMessage: String?
var didUploadSuccessfully = false
///
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
}
}