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