// // TaskSelectionViews.swift // suixinkan // // Created by Codex on 2026/6/23. // import SwiftUI /// 任务订单选择页面,供发布任务时关联已有订单。 struct TaskOrderSelectionView: View { @Environment(\.dismiss) private var dismiss let orders: [AvailableOrderResponse] let selectedOrder: AvailableOrderResponse? let onSelect: (AvailableOrderResponse?) -> Void var body: some View { List { Button { onSelect(nil) dismiss() } label: { Label("不关联订单", systemImage: selectedOrder == nil ? "checkmark.circle.fill" : "circle") } ForEach(orders) { order in Button { onSelect(order) dismiss() } label: { HStack(spacing: AppMetrics.Spacing.small) { Image(systemName: selectedOrder?.orderNumber == order.orderNumber ? "checkmark.circle.fill" : "circle") .foregroundStyle(AppDesign.primary) VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) { Text(order.projectName.isEmpty ? "未命名项目" : order.projectName) .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) Text("订单号:\(order.orderNumber)") Text("手机号:\(order.userPhone.isEmpty ? "暂无" : order.userPhone)") } .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textSecondary) } } } } .navigationTitle("选择订单") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("关闭") { dismiss() } } } } } /// 任务云盘文件选择页面,供发布任务时选择云盘附件。 struct TaskCloudFileSelectionView: View { @Environment(TaskAPI.self) private var taskAPI @Environment(ToastCenter.self) private var toastCenter @Environment(\.dismiss) private var dismiss let onConfirm: ([TaskCloudSelectionItem]) -> Void @State private var viewModel = TaskCloudFileSelectionViewModel() var body: some View { VStack(spacing: 0) { filterBar breadcrumbBar fileList confirmBar } .background(Color(hex: 0xF5F7FA)) .navigationTitle("选择云盘文件") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("关闭") { dismiss() } } } .task { await reload() } } /// 搜索与类型筛选区域。 private var filterBar: some View { VStack(spacing: AppMetrics.Spacing.small) { HStack(spacing: AppMetrics.Spacing.small) { Image(systemName: "magnifyingglass") .foregroundStyle(AppDesign.placeholder) TextField("搜索文件名", text: $viewModel.searchText) .submitLabel(.search) .onSubmit { Task { await reload() } } Button("搜索") { Task { await reload() } } .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(AppDesign.primary) } .appInputFieldStyle(cornerRadius: AppMetrics.CornerRadius.input, minHeight: AppMetrics.ControlSize.inputHeight) Picker("文件类型", selection: $viewModel.selectedFilter) { ForEach(TaskCloudFileFilter.allCases) { filter in Text(filter.title).tag(filter) } } .pickerStyle(.segmented) .onChange(of: viewModel.selectedFilter) { _, _ in Task { await reload() } } } .padding(AppMetrics.Spacing.pageHorizontal) .background(.white) } /// 云盘路径面包屑区域。 private var breadcrumbBar: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: AppMetrics.Spacing.xSmall) { ForEach(Array(viewModel.path.enumerated()), id: \.element.id) { index, folder in Button(folder.name) { Task { await popToFolder(index) } } .font(.system(size: AppMetrics.FontSize.footnote, weight: .semibold)) .foregroundStyle(index == viewModel.path.count - 1 ? AppDesign.textPrimary : AppDesign.primary) if index < viewModel.path.count - 1 { Image(systemName: "chevron.right") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(AppDesign.placeholder) } } } .padding(.horizontal, AppMetrics.Spacing.pageHorizontal) .padding(.vertical, AppMetrics.Spacing.small) } .background(Color(hex: 0xF8FAFC)) } /// 云盘文件列表区域。 private var fileList: some View { List { if viewModel.files.isEmpty { Text(viewModel.errorMessage ?? "暂无文件") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.textSecondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, AppMetrics.Spacing.xxLarge) .listRowBackground(Color.clear) } else { ForEach(viewModel.files) { file in Button { Task { await handleTap(file) } } label: { CloudFileRow(file: file, isSelected: viewModel.isSelected(file)) } .onAppear { guard file.id == viewModel.files.last?.id else { return } Task { await loadMore() } } } if viewModel.isLoadingMore { Text("加载更多...") .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textSecondary) } } } .listStyle(.plain) } /// 底部确认区域。 private var confirmBar: some View { HStack(spacing: AppMetrics.Spacing.medium) { Text("已选 \(viewModel.selectedFiles.count) 个文件") .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.textSecondary) Spacer() Button { onConfirm(viewModel.makeSelectionItems()) dismiss() } label: { Text("确认") .font(.system(size: AppMetrics.FontSize.body, weight: .semibold)) .foregroundStyle(.white) .frame(width: 108, height: 44) .background(AppDesign.primary, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button)) } } .padding(AppMetrics.Spacing.pageHorizontal) .background(.white) } /// 重新加载当前云盘目录。 private func reload() async { do { try await viewModel.reload(api: taskAPI) } catch { toastCenter.show(error.localizedDescription) } } /// 加载下一页云盘文件。 private func loadMore() async { do { try await viewModel.loadMore(api: taskAPI) } catch { toastCenter.show(error.localizedDescription) } } /// 处理云盘列表点击,文件夹进入目录,文件切换选中。 private func handleTap(_ file: CloudDriveFile) async { if file.isFolder { do { try await viewModel.enterFolder(file, api: taskAPI) } catch { toastCenter.show(error.localizedDescription) } } else { viewModel.toggleSelection(file) } } /// 回到指定面包屑目录。 private func popToFolder(_ index: Int) async { do { try await viewModel.popToFolder(at: index, api: taskAPI) } catch { toastCenter.show(error.localizedDescription) } } } /// 云盘文件列表行,展示文件或文件夹图标和选择状态。 private struct CloudFileRow: View { let file: CloudDriveFile let isSelected: Bool var body: some View { HStack(spacing: AppMetrics.Spacing.small) { Image(systemName: file.isFolder ? "folder.fill" : (file.isVideo ? "video.fill" : "photo.fill")) .font(.system(size: 22, weight: .semibold)) .foregroundStyle(file.isFolder ? AppDesign.warning : AppDesign.primary) .frame(width: AppMetrics.ControlSize.iconTapArea, height: AppMetrics.ControlSize.iconTapArea) VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) { Text(file.name.isEmpty ? "未命名文件" : file.name) .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) .lineLimit(1) Text(file.isFolder ? "\(file.childNum) 项" : "\(file.fileSize / 1024) KB") .font(.system(size: AppMetrics.FontSize.caption)) .foregroundStyle(AppDesign.textSecondary) } Spacer() if !file.isFolder { Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .foregroundStyle(isSelected ? AppDesign.primary : AppDesign.placeholder) } else { Image(systemName: "chevron.right") .foregroundStyle(AppDesign.placeholder) } } } }