为资产模块新增样片管理与上传流程
扩展媒体库以支持样片类型及项目选择,为样片入口接入首页路由,并扩展 Assets 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材上传编辑 ViewModel,负责表单校验、OSS 上传和提交素材。
|
||||
/// 媒体库上传编辑 ViewModel,负责表单校验、OSS 上传和提交素材或样片。
|
||||
@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 {
|
||||
|
||||
Reference in New Issue
Block a user