// // 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.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 { Set(selectedFiles.compactMap { $0.cloudFile?.id }) } private func notifyStateChange() { onStateChange?() } }