模块化 AppStore 并完善素材管理与个人空间设置。

将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 11:01:08 +08:00
parent ceca780ab3
commit 005349f8e6
128 changed files with 2953 additions and 1123 deletions

View File

@ -62,13 +62,6 @@ enum MaterialMediaType: Int, CaseIterable, Sendable, Equatable {
}
}
/// 1 2
var cloudFileType: Int {
switch self {
case .image: 2
case .video: 1
}
}
}
/// Android `MaterialItemEntity`

View File

@ -141,7 +141,12 @@ final class MaterialListViewModel {
)
totalCount = response.total
orderInfo = response.order ?? MaterialOrderInfo()
items = reset ? response.list : items + response.list
if reset {
items = response.list
} else {
let existingIDs = Set(items.map(\.id))
items.append(contentsOf: response.list.filter { !existingIDs.contains($0.id) })
}
canLoadMore = items.count < totalCount
} catch is CancellationError {
return
@ -244,6 +249,7 @@ final class MaterialFormViewModel {
private(set) var coverImage: MaterialUploadedMediaItem?
private(set) var tagList: [MaterialTagItem] = []
private(set) var tagInput = ""
private(set) var pendingDeleteTag: MaterialTagItem?
private(set) var uploadDialogState: MaterialUploadDialogState?
private(set) var isSubmitting = false
private(set) var isLoadingDetail = false
@ -259,7 +265,7 @@ final class MaterialFormViewModel {
/// ViewModel
init(
mode: Mode = .create,
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.currentScenicId }
currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }
) {
self.mode = mode
self.currentScenicIdProvider = currentScenicIdProvider
@ -295,7 +301,7 @@ final class MaterialFormViewModel {
///
func updateTagInput(_ text: String) {
tagInput = text
tagInput = String(text.prefix(15))
notifyStateChange()
}
@ -327,9 +333,23 @@ final class MaterialFormViewModel {
}
}
///
func deleteTag(_ tag: MaterialTagItem) {
///
func requestDeleteTag(_ tag: MaterialTagItem) {
pendingDeleteTag = tag
notifyStateChange()
}
///
func cancelDeleteTag() {
pendingDeleteTag = nil
notifyStateChange()
}
///
func confirmDeleteTag() {
guard let tag = pendingDeleteTag else { return }
tagList.removeAll { $0.id == tag.id }
pendingDeleteTag = nil
notifyStateChange()
}
@ -354,48 +374,6 @@ final class MaterialFormViewModel {
notifyStateChange()
}
/// 使 URL
func addCloudMediaFiles(_ files: [CloudFile]) {
guard !files.isEmpty else { return }
let remaining = mediaType.maxCount - uploadedMediaList.count
guard remaining > 0 else {
onShowMessage?(mediaType == .video ? "视频只能上传1个" : "图片最多上传9张")
return
}
var seenCloudIDs = importedCloudFileIDs
var accepted: [MaterialUploadedMediaItem] = []
for file in files where accepted.count < remaining {
let fileURL = file.fileUrl.trimmingCharacters(in: .whitespacesAndNewlines)
guard file.type == mediaType.cloudFileType, !fileURL.isEmpty, !seenCloudIDs.contains(file.id) else {
continue
}
seenCloudIDs.insert(file.id)
accepted.append(
MaterialUploadedMediaItem(
id: "cloud_\(file.id)",
data: Data(),
thumbnailData: nil,
previewURL: file.coverUrl.isEmpty ? fileURL : file.coverUrl,
fileName: file.name.isEmpty ? "cloud_file_\(file.id)" : file.name,
size: file.fileSize,
width: 0,
height: 0,
ossUrl: fileURL,
isUploading: false,
uploadProgress: 100
)
)
}
guard !accepted.isEmpty else {
onShowMessage?(mediaType == .video ? "请选择视频素材" : "请选择图片素材")
return
}
uploadedMediaList.append(contentsOf: accepted)
notifyStateChange()
}
///
func setCoverImage(_ item: MaterialUploadedMediaItem, uploader: any MaterialOSSUploading) async {
coverImage = item
@ -418,16 +396,6 @@ final class MaterialFormViewModel {
notifyStateChange()
}
/// ID
var importedCloudFileIDs: Set<Int> {
Set(
uploadedMediaList.compactMap { item in
guard item.id.hasPrefix("cloud_") else { return nil }
return Int(item.id.dropFirst("cloud_".count))
}
)
}
///
func submit(api: any MaterialManagementServing) async {
guard !materialName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {