Files
suixinkan_ios_uikit/suixinkan_ios/Features/Tasks/ViewModels/TaskCreateViewModel.swift
2026-06-26 14:33:31 +08:00

251 lines
9.1 KiB
Swift
Raw 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
///
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 {
var onChange: (() -> Void)?
var taskName = "" { didSet { onChange?() } }
var remark = "" { didSet { onChange?() } }
var urgentHourText = "0" { didSet { onChange?() } }
var selectedOrder: AvailableOrderResponse? { didSet { onChange?() } }
var availableOrders: [AvailableOrderResponse] = [] { didSet { onChange?() } }
var selectedCloudFiles: [TaskCloudSelectionItem] = [] { didSet { onChange?() } }
var selectedLocalFiles: [TaskLocalUploadItem] = [] { didSet { onChange?() } }
var isLoadingOrders = false { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
var didSubmitSuccessfully = false { didSet { onChange?() } }
///
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
}
}