Files
suixinkan_ios_new/suixinkan/Features/Tasks/Views/TaskSelectionViews.swift
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +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) 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)
}
}
}
}