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>
272 lines
10 KiB
Swift
272 lines
10 KiB
Swift
//
|
||
// 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) private var taskAPI
|
||
@EnvironmentObject private var toastCenter: ToastCenter
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
let onConfirm: ([TaskCloudSelectionItem]) -> Void
|
||
|
||
@StateObject 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)
|
||
}
|
||
}
|
||
}
|
||
}
|