Add sample management and upload flows to Assets module.

Extend media library for sample kind with project selection, wire home routing for sample entries, and expand Assets tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 10:06:16 +08:00
parent 607130ade0
commit c772b25897
13 changed files with 523 additions and 111 deletions

View File

@ -258,7 +258,7 @@ final class CloudStorageViewModel {
}
}
/// ViewModel
/// ViewModel
@MainActor
@Observable
final class MediaLibraryViewModel {
@ -277,7 +277,7 @@ final class MediaLibraryViewModel {
private let pageSize = 10
/// ViewModel
/// ViewModel
init(kind: MediaLibraryKind = .material) {
self.kind = kind
}
@ -287,7 +287,7 @@ final class MediaLibraryViewModel {
items.count < total
}
///
///
func reload(api: any AssetsServing) async {
isLoading = true
errorMessage = nil
@ -308,7 +308,7 @@ final class MediaLibraryViewModel {
}
}
///
///
func loadMore(api: any AssetsServing) async {
guard hasMore, !isLoadingMore else { return }
isLoadingMore = true
@ -326,20 +326,20 @@ final class MediaLibraryViewModel {
}
}
///
///
func loadDetail(id: Int, api: any AssetsServing) async {
isLoadingDetail = true
errorMessage = nil
defer { isLoadingDetail = false }
do {
selectedDetail = try await api.mediaAlbumDetail(id: id)
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 = "审核通过后才能上下架"
@ -347,11 +347,11 @@ final class MediaLibraryViewModel {
}
return await operate(api: api) {
let target = item.listing.toggled
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: item.id, listingStatus: target.rawValue))
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 = "审核通过后才能上下架"
@ -359,18 +359,18 @@ final class MediaLibraryViewModel {
}
return await operate(api: api) {
let target = detail.listing.toggled
try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: detail.id, listingStatus: target.rawValue))
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))
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()
@ -382,7 +382,7 @@ final class MediaLibraryViewModel {
}
}
///
///
private func requestList(api: any AssetsServing, page: Int) async throws -> MediaLibraryListResponse {
try await api.mediaAlbumList(
kind: kind,
@ -394,17 +394,20 @@ final class MediaLibraryViewModel {
}
}
/// ViewModelOSS
/// ViewModelOSS
@MainActor
@Observable
final class MediaLibraryEditorViewModel {
var name = ""
var description = ""
var selectedSpotId: Int?
var selectedProjectId: Int?
var projects: [PhotographerProjectItem] = []
var tagsText = ""
var coverFile: MediaLocalUploadFile?
var mediaFiles: [MediaLocalUploadFile] = []
var isSubmitting = false
var isLoadingProjects = false
var errorMessage: String?
var didSubmitSuccessfully = false
@ -422,6 +425,29 @@ final class MediaLibraryEditorViewModel {
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
@ -444,7 +470,7 @@ final class MediaLibraryEditorViewModel {
api: any AssetsServing,
uploadService: any OSSUploadServing
) async -> Bool {
guard validate(scenicId: scenicId) else { return false }
guard validate(kind: kind, scenicId: scenicId) else { return false }
guard !isSubmitting else { return false }
isSubmitting = true
defer { isSubmitting = false }
@ -478,7 +504,7 @@ final class MediaLibraryEditorViewModel {
scenicSpotId: selectedSpotId ?? 0,
description: description,
materialTag: normalizedTags(),
projectId: 0
projectId: kind == .sample ? (selectedProjectId ?? 0) : 0
)
try await api.mediaAlbumUpload(request)
}
@ -490,27 +516,31 @@ final class MediaLibraryEditorViewModel {
}
}
///
private func validate(scenicId: Int?) -> Bool {
///
private func validate(kind: MediaLibraryKind, scenicId: Int?) -> Bool {
let title = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard scenicId != nil else {
errorMessage = "缺少当前景区,无法提交素材"
errorMessage = "缺少当前景区,无法提交\(kind == .sample ? "样片" : "素材")"
return false
}
guard !title.isEmpty else {
errorMessage = "请输入素材名称"
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 = "请选择素材文件"
errorMessage = "请选择\(kind == .sample ? "样片" : "素材")文件"
return false
}
guard normalizedTags().count <= 120 else {