Files
suixinkan_ios_new/suixinkan/Features/Tasks/Views/TaskManagementView.swift
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

273 lines
10 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.

//
// TaskManagementView.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import SwiftUI
///
struct TaskManagementView: View {
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var router: RouterPath
@Environment(\.taskAPI) private var taskAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel = TaskManagementViewModel()
var body: some View {
ScrollView {
LazyVStack(spacing: AppMetrics.Spacing.medium) {
filterSection
contentSection
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.vertical, AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("任务管理")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
router.navigate(to: .home(.taskCreate))
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("发布任务")
}
}
.refreshable { await reload(showLoading: false) }
.task {
guard viewModel.tasks.isEmpty else { return }
await reload(showLoading: true)
}
}
///
private var filterSection: some View {
VStack(spacing: AppMetrics.Spacing.small) {
Picker("任务状态", selection: $viewModel.selectedStatus) {
ForEach(TaskStatusFilter.allCases) { status in
Text(status.title).tag(status)
}
}
.pickerStyle(.segmented)
.onChange(of: viewModel.selectedStatus) { _ in
Task { await reload(showLoading: true) }
}
HStack(spacing: AppMetrics.Spacing.small) {
Image(systemName: "magnifyingglass")
.foregroundStyle(AppDesign.placeholder)
TextField("搜索任务名称", text: $viewModel.searchText)
.textInputAutocapitalization(.never)
.submitLabel(.search)
.onSubmit { Task { await reload(showLoading: true) } }
Button("搜索") {
Task { await reload(showLoading: true) }
}
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
.appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight)
VStack(spacing: AppMetrics.Spacing.xSmall) {
TaskDatePickerRow(title: "开始日期", date: $viewModel.startDate)
TaskDatePickerRow(title: "结束日期", date: $viewModel.endDate)
HStack {
Button("清除日期") {
viewModel.startDate = nil
viewModel.endDate = nil
Task { await reload(showLoading: true) }
}
.foregroundStyle(AppDesign.textSecondary)
Spacer()
Button("应用日期") {
Task { await reload(showLoading: true) }
}
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
.font(.system(size: AppMetrics.FontSize.subheadline))
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}
///
@ViewBuilder
private var contentSection: some View {
if accountContext.currentScenic?.id == nil {
TaskEmptyState(title: "缺少景区上下文", message: "请先在首页选择景区后再查看任务。")
} else if viewModel.tasks.isEmpty {
TaskEmptyState(title: "暂无任务", message: viewModel.errorMessage ?? "当前筛选条件下没有任务。")
} else {
ForEach(viewModel.tasks) { task in
Button {
router.navigate(to: .home(.taskDetail(id: task.id, summary: task)))
} label: {
TaskCardView(task: task)
}
.buttonStyle(.plain)
.onAppear {
guard task.id == viewModel.tasks.last?.id else { return }
Task { await loadMore() }
}
}
if viewModel.isLoadingMore {
Text("加载更多...")
.font(.system(size: AppMetrics.FontSize.footnote))
.foregroundStyle(AppDesign.textSecondary)
.padding(.vertical, AppMetrics.Spacing.small)
} else if viewModel.hasMore {
Button("加载更多") {
Task { await loadMore() }
}
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.padding(.vertical, AppMetrics.Spacing.small)
}
}
}
///
private func reload(showLoading: Bool) async {
do {
try await globalLoading.withOptionalLoading(showLoading && accountContext.currentScenic?.id != nil) {
try await viewModel.reload(api: taskAPI, scenicId: accountContext.currentScenic?.id)
}
} catch {
toastCenter.show(error.localizedDescription)
}
}
///
private func loadMore() async {
do {
try await viewModel.loadMore(api: taskAPI, scenicId: accountContext.currentScenic?.id)
} catch {
toastCenter.show(error.localizedDescription)
}
}
}
///
private struct TaskCardView: View {
let task: PhotographerTaskItem
var body: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
HStack(alignment: .top, spacing: AppMetrics.Spacing.small) {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
Text(task.name.isEmpty ? "未命名任务" : task.name)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
.lineLimit(2)
if !task.orderNumber.isEmpty {
Text("订单号:\(task.orderNumber)")
.font(.system(size: AppMetrics.FontSize.footnote))
.foregroundStyle(AppDesign.textSecondary)
}
}
Spacer(minLength: AppMetrics.Spacing.small)
Text(task.statusName.isEmpty ? "\(task.taskStatus)" : task.statusName)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(statusColor)
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.xxSmall)
.background(statusColor.opacity(0.12), in: Capsule())
}
if !task.photogRemark.isEmpty {
Text(task.photogRemark)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
.lineLimit(2)
}
HStack(spacing: AppMetrics.Spacing.small) {
Label(task.editorName.isEmpty ? "暂无编辑" : task.editorName, systemImage: "person.crop.circle")
Spacer()
Text(task.createdAt.isEmpty ? task.updatedAt : task.createdAt)
}
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.placeholder)
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
///
private var statusColor: Color {
switch task.taskStatus {
case 3:
AppDesign.success
case 4:
Color(hex: 0xEF4444)
case 1, 2:
AppDesign.warning
default:
AppDesign.primary
}
}
}
///
private struct TaskDatePickerRow: View {
let title: String
@Binding var date: Date?
var body: some View {
HStack {
Text(title)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
Spacer()
DatePicker(
title,
selection: Binding(
get: { date ?? Date() },
set: { date = $0 }
),
displayedComponents: .date
)
.labelsHidden()
}
}
}
///
private struct TaskEmptyState: View {
let title: String
let message: String
var body: some View {
VStack(spacing: AppMetrics.Spacing.small) {
Image(systemName: "tray")
.font(.system(size: 36, weight: .semibold))
.foregroundStyle(AppDesign.placeholder)
Text(title)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
Text(message)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, AppMetrics.Spacing.xxLarge)
.padding(.horizontal, AppMetrics.Spacing.large)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
}