添加提交任务流程并抽取资料编辑页

This commit is contained in:
2026-07-07 10:12:20 +08:00
parent af450fb4cc
commit ef1d3b4af5
23 changed files with 3186 additions and 170 deletions

View File

@ -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?()
}
}

View 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?()
}
}

View File

@ -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?()
}
}