Files
suixinkan_uikit/suixinkan/Features/Task/ViewModels/TaskAddViewModel.swift
汉秋 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

309 lines
9.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TaskAddViewModel.swift
// suixinkan
//
import Foundation
/// ViewModel Android `TaskAddViewModel`
final class TaskAddViewModel {
private(set) var taskName = ""
private(set) var taskDetails = ""
private(set) var selectedOrderNumber: String?
private(set) var urgentHour = 0
private(set) var selectedFiles: [TaskFileItem] = []
private(set) var isUploading = false
private(set) var showSuccessDialog = false
private(set) var showRemarkDialog = false
private(set) var remarkDialogText = ""
private(set) var remarkTargetFileID: String?
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private let appStore: AppStore
private var pendingRemarkFileID: String?
init(appStore: AppStore = .shared) {
self.appStore = appStore
}
///
func updateTaskName(_ value: String) {
taskName = String(value.prefix(20))
notifyStateChange()
}
///
func updateTaskDetails(_ value: String) {
taskDetails = String(value.prefix(200))
notifyStateChange()
}
///
func setSelectedOrderNumber(_ orderNumber: String?) {
let trimmed = orderNumber?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
selectedOrderNumber = trimmed.isEmpty ? nil : trimmed
notifyStateChange()
}
///
func setUrgentHour(_ hour: Int) {
urgentHour = max(0, hour)
notifyStateChange()
}
///
func updateCustomUrgentHourText(_ text: String) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let hour = Int(trimmed), hour > 0 else { return }
urgentHour = hour
notifyStateChange()
}
///
func addCloudFiles(_ files: [CloudFile]) {
let existingIDs = Set(selectedFiles.compactMap { $0.cloudFile?.id })
let newItems = files
.filter { !existingIDs.contains($0.id) }
.map {
TaskFileItem(source: .cloud, cloudFile: $0)
}
guard !newItems.isEmpty else { return }
selectedFiles.append(contentsOf: newItems)
if newItems.count == 1 {
pendingRemarkFileID = newItems[0].id
}
notifyStateChange()
}
/// ID pending
func consumePendingRemarkFileID() -> String? {
defer { pendingRemarkFileID = nil }
return pendingRemarkFileID
}
///
@discardableResult
func addUploadPlaceholder(fileName: String, fileType: Int, uploadTaskId: String) -> TaskFileItem {
let item = TaskFileItem(
source: .upload,
uploadFileName: fileName,
uploadFileType: fileType,
uploadTaskId: uploadTaskId,
isUploading: true,
uploadProgress: 0
)
selectedFiles.append(item)
isUploading = true
notifyStateChange()
return item
}
///
func updateUploadProgress(uploadTaskId: String, progress: Int) {
selectedFiles = selectedFiles.map { item in
guard item.uploadTaskId == uploadTaskId else { return item }
var updated = item
updated.uploadProgress = progress
updated.isUploading = progress < 100
return updated
}
notifyStateChange()
}
///
func prepareUpload(uploadTaskId: String, fileName: String, localPreviewData: Data?) {
selectedFiles = selectedFiles.map { item in
guard item.uploadTaskId == uploadTaskId else { return item }
var updated = item
updated.uploadFileName = fileName
updated.localPreviewData = localPreviewData
return updated
}
notifyStateChange()
}
/// URL
func markUploadSucceeded(
uploadTaskId: String,
fileURL: String,
fileName: String? = nil,
coverURL: String? = nil,
localPreviewData: Data? = nil,
autoRemarkWhenSingle: Bool
) {
selectedFiles = selectedFiles.map { item in
guard item.uploadTaskId == uploadTaskId else { return item }
var updated = item
updated.uploadFileUrl = fileURL
if let fileName, !fileName.isEmpty {
updated.uploadFileName = fileName
}
updated.uploadCoverUrl = coverURL
if let localPreviewData {
updated.localPreviewData = localPreviewData
}
updated.isUploading = false
updated.uploadProgress = 100
return updated
}
isUploading = selectedFiles.contains(where: \.isUploading)
if autoRemarkWhenSingle, let item = selectedFiles.first(where: { $0.uploadTaskId == uploadTaskId }) {
pendingRemarkFileID = item.id
}
notifyStateChange()
}
///
func markUploadFailed(uploadTaskId: String) {
selectedFiles.removeAll { $0.uploadTaskId == uploadTaskId }
isUploading = selectedFiles.contains(where: \.isUploading)
notifyStateChange()
}
///
func removeTaskFile(id: String) {
selectedFiles.removeAll { $0.id == id }
isUploading = selectedFiles.contains(where: \.isUploading)
notifyStateChange()
}
///
func presentRemarkDialog(for fileID: String) {
guard let file = selectedFiles.first(where: { $0.id == fileID }) else { return }
remarkTargetFileID = fileID
remarkDialogText = file.remark
showRemarkDialog = true
notifyStateChange()
}
///
func updateRemarkDialogText(_ text: String) {
remarkDialogText = String(text.prefix(50))
notifyStateChange()
}
///
func saveRemarkDialog() {
guard let fileID = remarkTargetFileID else { return }
selectedFiles = selectedFiles.map { item in
guard item.id == fileID else { return item }
var updated = item
updated.remark = remarkDialogText
return updated
}
hideRemarkDialog()
}
///
func hideRemarkDialog() {
showRemarkDialog = false
remarkTargetFileID = nil
remarkDialogText = ""
notifyStateChange()
}
///
var remarkTargetFile: TaskFileItem? {
guard let fileID = remarkTargetFileID else { return nil }
return selectedFiles.first { $0.id == fileID }
}
///
func validateAndSubmit(api: TaskAPI) async {
if isUploading {
onShowMessage?("文件正在上传中,请稍后添加!")
return
}
let trimmedName = taskName.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedName.isEmpty {
onShowMessage?("请填写任务名称!")
return
}
if trimmedName.count > 20 {
onShowMessage?("任务名称不能超过20字")
return
}
if taskDetails.count > 200 {
onShowMessage?("备注信息不能超过200字")
return
}
let request = buildAddTaskRequest(name: trimmedName)
do {
try await api.createTask(request)
showSuccessDialog = true
notifyStateChange()
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription)
}
}
///
func buildAddTaskRequest(name: String? = nil) -> AddTaskRequest {
let cloudItems = selectedFiles
.filter { $0.source == .cloud }
.compactMap { item -> TaskCloudFileItem? in
guard let fileId = item.cloudFile?.id else { return nil }
return TaskCloudFileItem(fileId: fileId, remark: item.remark)
}
let uploadItems = selectedFiles
.filter { $0.source == .upload }
.compactMap { item -> TaskUploadFileItem? in
guard let url = item.uploadFileUrl, !url.isEmpty else { return nil }
return TaskUploadFileItem(
fileUrl: url,
fileName: item.uploadFileName ?? "",
remark: item.remark
)
}
return AddTaskRequest(
scenicId: appStore.session.currentScenicId,
name: name ?? taskName,
orderNumber: selectedOrderNumber,
remark: taskDetails,
urgentHour: urgentHour,
cloudFile: cloudItems,
uploadFile: uploadItems
)
}
///
func handleContinueAdding() {
resetAllState()
}
///
func dismissSuccessDialog() {
showSuccessDialog = false
notifyStateChange()
}
///
func resetAllState() {
taskName = ""
taskDetails = ""
selectedOrderNumber = nil
urgentHour = 0
selectedFiles = []
isUploading = false
showSuccessDialog = false
hideRemarkDialog()
pendingRemarkFileID = nil
notifyStateChange()
}
/// ID
var importedCloudFileIDs: Set<Int> {
Set(selectedFiles.compactMap { $0.cloudFile?.id })
}
private func notifyStateChange() {
onStateChange?()
}
}