添加提交任务流程并抽取资料编辑页
This commit is contained in:
@ -0,0 +1,164 @@
|
||||
//
|
||||
// CloudStoragePickForTaskViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 任务云盘多选 ViewModel,对齐 Android 任务模式云盘浏览与多选逻辑。
|
||||
final class CloudStoragePickForTaskViewModel {
|
||||
|
||||
private(set) var pathStack: [CloudPathItem] = [CloudPathItem(id: 0, name: "全部文件")]
|
||||
private(set) var files: [CloudFile] = []
|
||||
private(set) var selectedFiles: [Int: CloudFile] = [:]
|
||||
private(set) var searchText = ""
|
||||
private(set) var filterType = 0
|
||||
private(set) var sortType = 2
|
||||
private(set) var isLoading = false
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var canLoadMore = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
private var currentPage = 1
|
||||
private var totalCount = 0
|
||||
private let pageSize = 20
|
||||
private let importedFileIDs: Set<Int>
|
||||
|
||||
init(importedFileIDs: Set<Int> = []) {
|
||||
self.importedFileIDs = importedFileIDs
|
||||
}
|
||||
|
||||
/// 当前文件夹 ID。
|
||||
var currentFolderID: Int {
|
||||
pathStack.last?.id ?? 0
|
||||
}
|
||||
|
||||
/// 已选文件列表。
|
||||
var selectedFileList: [CloudFile] {
|
||||
Array(selectedFiles.values)
|
||||
}
|
||||
|
||||
/// 文件是否已在任务页导入。
|
||||
func isImported(_ fileID: Int) -> Bool {
|
||||
importedFileIDs.contains(fileID)
|
||||
}
|
||||
|
||||
/// 文件是否已选中。
|
||||
func isSelected(_ fileID: Int) -> Bool {
|
||||
selectedFiles[fileID] != nil
|
||||
}
|
||||
|
||||
/// 更新搜索词。
|
||||
func updateSearchText(_ text: String) {
|
||||
searchText = text
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 更新类型筛选。
|
||||
func updateFilterType(_ type: Int) {
|
||||
filterType = type
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 更新排序方式。
|
||||
func updateSortType(_ type: Int) {
|
||||
sortType = type
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 首次或刷新加载。
|
||||
func refresh(api: TaskAPI) async {
|
||||
currentPage = 1
|
||||
isRefreshing = true
|
||||
notifyStateChange()
|
||||
await loadPage(api: api, reset: true)
|
||||
isRefreshing = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 加载更多。
|
||||
func loadMore(api: TaskAPI) async {
|
||||
guard canLoadMore, !isLoading else { return }
|
||||
currentPage += 1
|
||||
await loadPage(api: api, reset: false)
|
||||
}
|
||||
|
||||
/// 点击文件或文件夹。
|
||||
func handleItemTap(_ file: CloudFile, api: TaskAPI) async {
|
||||
if file.isFolder {
|
||||
pathStack.append(CloudPathItem(id: file.id, name: file.name))
|
||||
searchText = ""
|
||||
await refresh(api: api)
|
||||
return
|
||||
}
|
||||
guard file.isSelectableMedia else { return }
|
||||
if isImported(file.id) {
|
||||
onShowMessage?("已选择")
|
||||
return
|
||||
}
|
||||
if selectedFiles[file.id] != nil {
|
||||
selectedFiles.removeValue(forKey: file.id)
|
||||
} else {
|
||||
selectedFiles[file.id] = file
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 跳转面包屑路径。
|
||||
func navigateToPathIndex(_ index: Int, api: TaskAPI) async {
|
||||
guard index >= 0, index < pathStack.count else { return }
|
||||
pathStack = Array(pathStack.prefix(index + 1))
|
||||
searchText = ""
|
||||
await refresh(api: api)
|
||||
}
|
||||
|
||||
/// 返回上级目录。
|
||||
func navigateBack(api: TaskAPI) async -> Bool {
|
||||
guard pathStack.count > 1 else { return false }
|
||||
pathStack.removeLast()
|
||||
searchText = ""
|
||||
await refresh(api: api)
|
||||
return true
|
||||
}
|
||||
|
||||
private func loadPage(api: TaskAPI, reset: Bool) async {
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await api.cloudFileList(
|
||||
parentFolderId: currentFolderID,
|
||||
name: searchText,
|
||||
type: filterType,
|
||||
orderBy: sortType,
|
||||
page: currentPage,
|
||||
pageSize: pageSize
|
||||
)
|
||||
totalCount = response.total
|
||||
if reset {
|
||||
files = response.list
|
||||
} else {
|
||||
files.append(contentsOf: response.list)
|
||||
}
|
||||
canLoadMore = files.count < totalCount
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
if reset {
|
||||
files = []
|
||||
}
|
||||
canLoadMore = false
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
287
suixinkan/Features/Task/ViewModels/TaskAddViewModel.swift
Normal file
287
suixinkan/Features/Task/ViewModels/TaskAddViewModel.swift
Normal file
@ -0,0 +1,287 @@
|
||||
//
|
||||
// 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?()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
//
|
||||
// TaskOrderSelectViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 任务关联订单选择 ViewModel,对齐 Android `OrderSelectViewModel`。
|
||||
final class TaskOrderSelectViewModel {
|
||||
|
||||
private(set) var orders: [AvailableOrder] = []
|
||||
private(set) var selectedOrderNumber: String?
|
||||
private(set) var isLoading = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
private let appStore: AppStore
|
||||
|
||||
init(appStore: AppStore = .shared, initialOrderNumber: String? = nil) {
|
||||
self.appStore = appStore
|
||||
let trimmed = initialOrderNumber?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
selectedOrderNumber = trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
/// 拉取可选订单列表。
|
||||
func loadOrders(api: TaskAPI) async {
|
||||
isLoading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLoading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
let scenicId = appStore.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
orders = []
|
||||
onShowMessage?("请先选择景区")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
orders = try await api.availableOrders(scenicId: scenicId)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
orders = []
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换订单选中状态,再次点击同一项则取消。
|
||||
func toggleSelection(orderNumber: String) {
|
||||
if selectedOrderNumber == orderNumber {
|
||||
selectedOrderNumber = nil
|
||||
} else {
|
||||
selectedOrderNumber = orderNumber
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 确认选择并返回订单号。
|
||||
func confirmedOrderNumber() -> String? {
|
||||
selectedOrderNumber
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user