Files
suixinkan_ios_new/suixinkan/Features/Tasks/Views/TaskSelectionViews.swift
汉秋 3e898d93d0 Add Tasks module with home routing for task management flows.
Implement task list, detail, and create pages with API integration, cloud/local attachment support, and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:26:13 +08:00

272 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.

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