Files
suixinkan_ios_new/suixinkan/Features/Tasks/Views/TaskManagementView.swift
汉秋 26f4d0e671 Migrate from iOS 17 Observation to iOS 16-compatible Combine architecture.
Lower deployment target to iOS 16, replace @Observable with ObservableObject, add navigation and UI compatibility shims, and include login smoke UI tests.

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))
}
}