Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
252 lines
9.1 KiB
Swift
252 lines
9.1 KiB
Swift
//
|
||
// 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 {
|
||
/// 记录校验失败并返回 false。
|
||
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
|
||
}
|
||
}
|