Files
suixinkan_ios_new/suixinkan/Features/Tasks/ViewModels/TaskCreateViewModel.swift
汉秋 8e34a45d50 移除 MJRefresh 依赖,恢复 SwiftUI 系统下拉刷新。
同时收紧各 ViewModel 内部 isLoading 可见性,避免不必要的视图刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 10:55:45 +08:00

251 lines
8.9 KiB
Swift
Raw Permalink 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.

//
// TaskCreateViewModel.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import Foundation
import Combine
///
struct TaskCloudSelectionItem: Identifiable, Hashable {
let id: Int
let fileName: String
let fileType: Int
var remark: String
}
///
struct TaskLocalUploadItem: Identifiable, Hashable {
let id: UUID
let data: Data
let fileName: String
let fileType: Int
var remark: String
var uploadedURL: String?
var progress: Int
var errorMessage: String?
///
var isUploading: Bool {
progress > 0 && progress < 100 && uploadedURL == nil && errorMessage == nil
}
///
var statusText: String {
if let errorMessage { return errorMessage }
if uploadedURL != nil { return "已上传" }
if progress > 0 { return "上传中 \(progress)%" }
return "待上传"
}
}
/// ViewModel
@MainActor
final class TaskCreateViewModel: ObservableObject {
@Published var taskName = ""
@Published var remark = ""
@Published var urgentHourText = "0"
@Published var selectedOrder: AvailableOrderResponse?
@Published var availableOrders: [AvailableOrderResponse] = []
@Published var selectedCloudFiles: [TaskCloudSelectionItem] = []
@Published var selectedLocalFiles: [TaskLocalUploadItem] = []
private(set) var isLoadingOrders = false
@Published var isSubmitting = false
@Published var errorMessage: String?
@Published var didSubmitSuccessfully = false
///
var canSubmit: Bool {
validateForm(scenicId: 1, shouldSetError: false)
&& !isSubmitting
&& !selectedLocalFiles.contains(where: \.isUploading)
}
///
func loadAvailableOrders(api: any TaskServing, scenicId: Int?) async {
guard let scenicId else {
availableOrders = []
return
}
isLoadingOrders = true
defer { isLoadingOrders = false }
do {
availableOrders = try await api.availableOrderList(scenicId: scenicId)
} catch {
availableOrders = []
errorMessage = error.localizedDescription
}
}
///
func selectOrder(_ order: AvailableOrderResponse?) {
selectedOrder = order
}
/// ID
func mergeCloudFiles(_ files: [TaskCloudSelectionItem]) {
var next = selectedCloudFiles
for file in files where !next.contains(where: { $0.id == file.id }) {
next.append(file)
}
selectedCloudFiles = next
}
///
func removeCloudFile(id: Int) {
selectedCloudFiles.removeAll { $0.id == id }
}
///
func addLocalFile(data: Data, fileName: String) {
let fileType = Self.fileType(for: fileName)
selectedLocalFiles.append(
TaskLocalUploadItem(
id: UUID(),
data: data,
fileName: fileName,
fileType: fileType,
remark: "",
uploadedURL: nil,
progress: 0,
errorMessage: nil
)
)
}
///
func removeLocalFile(id: UUID) {
selectedLocalFiles.removeAll { $0.id == id }
}
/// OSS URL
@discardableResult
func submit(api: any TaskServing, uploadService: any OSSUploadServing, scenicId: Int?) async throws -> Bool {
guard !isSubmitting else { return false }
guard validateForm(scenicId: scenicId, shouldSetError: true) else { return false }
guard let scenicId, let urgentHour = Int(urgentHourText.trimmingCharacters(in: .whitespacesAndNewlines)) else { return false }
isSubmitting = true
errorMessage = nil
didSubmitSuccessfully = false
defer { isSubmitting = false }
do {
let uploadedFiles = try await uploadLocalFiles(uploadService: uploadService, scenicId: scenicId)
let request = AddTaskRequest(
scenicId: "\(scenicId)",
name: taskName.trimmingCharacters(in: .whitespacesAndNewlines),
orderNumber: selectedOrder?.orderNumber,
remark: remark.trimmingCharacters(in: .whitespacesAndNewlines),
urgentHour: urgentHour,
cloudFile: selectedCloudFiles.map { CloudFileItem(fileId: $0.id, remark: $0.remark) },
uploadFile: uploadedFiles
)
try await api.addTask(request)
didSubmitSuccessfully = true
resetForm()
return true
} catch {
errorMessage = error.localizedDescription
throw error
}
}
///
func resetForm() {
taskName = ""
remark = ""
urgentHourText = "0"
selectedOrder = nil
selectedCloudFiles = []
selectedLocalFiles = []
}
///
private func validateForm(scenicId: Int?, shouldSetError: Bool) -> Bool {
func fail(_ message: String) -> Bool {
if shouldSetError { errorMessage = message }
return false
}
guard scenicId != nil else {
return fail("请先选择景区")
}
guard !taskName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return fail("请输入任务名称")
}
guard !remark.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return fail("请输入任务备注")
}
guard let urgentHour = Int(urgentHourText.trimmingCharacters(in: .whitespacesAndNewlines)), urgentHour >= 0 else {
return fail("请输入正确的紧急小时")
}
guard !selectedLocalFiles.contains(where: \.isUploading) else {
return fail("文件上传中,请稍后提交")
}
return true
}
///
private func uploadLocalFiles(uploadService: any OSSUploadServing, scenicId: Int) async throws -> [UploadFileItem] {
var uploadedFiles: [UploadFileItem] = []
for index in selectedLocalFiles.indices {
if let uploadedURL = selectedLocalFiles[index].uploadedURL {
uploadedFiles.append(
UploadFileItem(
fileUrl: uploadedURL,
fileName: selectedLocalFiles[index].fileName,
remark: selectedLocalFiles[index].remark
)
)
continue
}
selectedLocalFiles[index].errorMessage = nil
let fileID = selectedLocalFiles[index].id
do {
let url = try await uploadService.uploadTaskFile(
data: selectedLocalFiles[index].data,
fileName: selectedLocalFiles[index].fileName,
fileType: selectedLocalFiles[index].fileType,
scenicId: scenicId,
onProgress: { [weak self] progress in
Task { @MainActor in
self?.updateLocalFileProgress(id: fileID, progress: progress)
}
}
)
selectedLocalFiles[index].uploadedURL = url
selectedLocalFiles[index].progress = 100
uploadedFiles.append(
UploadFileItem(
fileUrl: url,
fileName: selectedLocalFiles[index].fileName,
remark: selectedLocalFiles[index].remark
)
)
} catch {
selectedLocalFiles[index].progress = 0
selectedLocalFiles[index].errorMessage = error.localizedDescription
throw error
}
}
return uploadedFiles
}
///
private func updateLocalFileProgress(id: UUID, progress: Int) {
guard let index = selectedLocalFiles.firstIndex(where: { $0.id == id }) else { return }
selectedLocalFiles[index].progress = progress
}
///
private static func fileType(for fileName: String) -> Int {
let ext = URL(fileURLWithPath: fileName).pathExtension.lowercased()
return ["mp4", "mov", "m4v", "avi"].contains(ext) ? 1 : 2
}
}