新增任务模块,并接入首页任务管理路由
实现任务列表、详情与创建页面,对接 API,支持云端/本地附件,并添加单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,167 @@
|
||||
//
|
||||
// TaskManagementViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// 任务状态筛选实体,表示任务管理页支持的任务状态。
|
||||
enum TaskStatusFilter: Int, CaseIterable, Identifiable {
|
||||
case all = 0
|
||||
case unaccepted = 1
|
||||
case accepted = 2
|
||||
case completed = 3
|
||||
case rejected = 4
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
/// 返回状态筛选在页面上的展示文案。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
"全部"
|
||||
case .unaccepted:
|
||||
"待接收"
|
||||
case .accepted:
|
||||
"处理中"
|
||||
case .completed:
|
||||
"已完成"
|
||||
case .rejected:
|
||||
"已驳回"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务管理 ViewModel,负责任务列表、筛选、分页和错误状态。
|
||||
@MainActor
|
||||
@Observable
|
||||
final class TaskManagementViewModel {
|
||||
var isLoading = false
|
||||
var isLoadingMore = false
|
||||
var tasks: [PhotographerTaskItem] = []
|
||||
var total = 0
|
||||
var page = 1
|
||||
var selectedStatus: TaskStatusFilter = .all
|
||||
var searchText = ""
|
||||
var startDate: Date?
|
||||
var endDate: Date?
|
||||
var errorMessage: String?
|
||||
|
||||
private let pageSize = 10
|
||||
|
||||
/// 判断任务列表是否还有下一页。
|
||||
var hasMore: Bool {
|
||||
tasks.count < total
|
||||
}
|
||||
|
||||
/// 首屏加载任务列表;无景区时清空旧数据且不请求接口。
|
||||
func reload(api: any TaskServing, scenicId: Int?) async throws {
|
||||
guard let scenicId else {
|
||||
resetList()
|
||||
errorMessage = "请先选择景区"
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
let payload = try await requestList(api: api, scenicId: scenicId, page: 1)
|
||||
page = 1
|
||||
tasks = payload.list
|
||||
total = payload.total
|
||||
} catch {
|
||||
resetList()
|
||||
errorMessage = error.localizedDescription
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载下一页任务列表;最后一页或正在加载时不会重复请求。
|
||||
func loadMore(api: any TaskServing, scenicId: Int?) async throws {
|
||||
guard let scenicId, hasMore, !isLoadingMore else { return }
|
||||
isLoadingMore = true
|
||||
errorMessage = nil
|
||||
defer { isLoadingMore = false }
|
||||
|
||||
let nextPage = page + 1
|
||||
do {
|
||||
let payload = try await requestList(api: api, scenicId: scenicId, page: nextPage)
|
||||
page = nextPage
|
||||
total = payload.total
|
||||
tasks.append(contentsOf: payload.list)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空任务列表和分页状态。
|
||||
func resetList() {
|
||||
tasks = []
|
||||
total = 0
|
||||
page = 1
|
||||
isLoading = false
|
||||
isLoadingMore = false
|
||||
}
|
||||
|
||||
/// 组装并发送任务列表请求。
|
||||
private func requestList(api: any TaskServing, scenicId: Int, page: Int) async throws -> ListPayload<PhotographerTaskItem> {
|
||||
try await api.taskList(
|
||||
scenicId: scenicId,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
taskStatus: selectedStatus.rawValue,
|
||||
taskName: trimmedSearchText,
|
||||
startTime: Self.requestDateString(from: startDate),
|
||||
endTime: Self.requestDateString(from: endDate)
|
||||
)
|
||||
}
|
||||
|
||||
/// 搜索关键字去除空白后为空则不传给后端。
|
||||
private var trimmedSearchText: String? {
|
||||
let text = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
|
||||
/// 日期请求格式化器。
|
||||
private static let requestDateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
/// 将可选日期格式化成请求字符串。
|
||||
private static func requestDateString(from date: Date?) -> String? {
|
||||
guard let date else { return nil }
|
||||
return requestDateFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务详情 ViewModel,负责详情加载和列表摘要兜底展示。
|
||||
@MainActor
|
||||
@Observable
|
||||
final class TaskDetailViewModel {
|
||||
var isLoading = false
|
||||
var detail: TaskDetailResponse?
|
||||
var errorMessage: String?
|
||||
|
||||
/// 加载任务详情;接口失败时保留外部传入的列表摘要。
|
||||
func load(api: any TaskServing, taskId: Int) async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
detail = try await api.taskDetail(id: taskId)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user