Implement task creation with cloud/local media, order linking, and OSS upload; wire home entry and move profile nickname/avatar editing to ProfileEditViewController. Co-authored-by: Cursor <cursoragent@cursor.com>
288 lines
9.1 KiB
Swift
288 lines
9.1 KiB
Swift
//
|
||
// 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 {
|
||
urgentHour = 1
|
||
notifyStateChange()
|
||
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()
|
||
}
|
||
|
||
/// 上传成功后更新文件 URL。
|
||
func markUploadSucceeded(uploadTaskId: String, fileURL: String, coverURL: String? = nil, autoRemarkWhenSingle: Bool) {
|
||
selectedFiles = selectedFiles.map { item in
|
||
guard item.uploadTaskId == uploadTaskId else { return item }
|
||
var updated = item
|
||
updated.uploadFileUrl = fileURL
|
||
updated.uploadCoverUrl = coverURL
|
||
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.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?()
|
||
}
|
||
}
|