import SwiftUI import ComposableArchitecture import PhotosUI // MARK: - COS 上传组件 /// COS 上传组件 /// 提供图片选择、预览和上传功能 public struct COSUploadView: View { // MARK: - Properties let store: StoreOf @State private var selectedImage: UIImage? @State private var showingImagePicker = false // MARK: - Initialization public init(store: StoreOf) { self.store = store } // MARK: - Body public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in VStack(spacing: 20) { // 图片选择区域 ImageSelectionArea( selectedImage: $selectedImage, showingImagePicker: $showingImagePicker ) // 上传进度区域 if let uploadState = viewStore.uploadState, uploadState.isUploading || uploadState.result != nil || uploadState.error != nil { UploadProgressArea(uploadState: uploadState) } // 上传按钮 UploadButton( selectedImage: selectedImage, isUploading: viewStore.uploadState?.isUploading == true, isServiceReady: isServiceReady(viewStore), onUpload: { uploadImage(viewStore) } ) Spacer() } .padding() .sheet(isPresented: $showingImagePicker) { ImagePicker(selectedImage: $selectedImage) } } } // MARK: - 私有方法 /// 检查服务是否就绪 private func isServiceReady(_ viewStore: ViewStore) -> Bool { let isInitialized = viewStore.configurationState?.serviceStatus.isInitialized == true let hasValidToken = viewStore.tokenState?.currentToken?.isValid == true return isInitialized && hasValidToken } /// 上传图片 private func uploadImage(_ viewStore: ViewStore) { guard let image = selectedImage else { return } let fileName = "image_\(Date().timeIntervalSince1970).jpg" viewStore.send(.upload(.uploadUIImage(image, fileName))) } } // MARK: - 图片选择区域 /// 图片选择区域组件 private struct ImageSelectionArea: View { @Binding var selectedImage: UIImage? @Binding var showingImagePicker: Bool var body: some View { VStack(spacing: 16) { if let image = selectedImage { // 显示选中的图片 VStack(spacing: 12) { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: 200) .cornerRadius(8) HStack(spacing: 12) { Button("重新选择") { showingImagePicker = true } .buttonStyle(.bordered) Button("清除") { selectedImage = nil } .buttonStyle(.bordered) .foregroundColor(.red) } } } else { // 显示选择按钮 VStack(spacing: 12) { Image(systemName: "photo.badge.plus") .font(.system(size: 48)) .foregroundColor(.blue) Text("选择图片") .font(.headline) Text("点击选择要上传的图片") .font(.caption) .foregroundColor(.secondary) Button("选择图片") { showingImagePicker = true } .buttonStyle(.borderedProminent) } .frame(maxWidth: .infinity) .padding(40) .background(Color(.systemGray6)) .cornerRadius(12) } } } } // MARK: - 上传进度区域 /// 上传进度区域组件 private struct UploadProgressArea: View { let uploadState: UploadState var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: progressIcon) .foregroundColor(progressColor) Text("上传进度") .font(.headline) Spacer() if uploadState.isUploading { Button("取消") { // TODO: 实现取消上传 } .font(.caption) .foregroundColor(.red) } } if let task = uploadState.currentTask { VStack(alignment: .leading, spacing: 4) { Text("文件: \(task.fileName)") .font(.caption) .foregroundColor(.secondary) Text("大小: \(ByteCountFormatter.string(fromByteCount: Int64(task.imageData.count), countStyle: .file))") .font(.caption) .foregroundColor(.secondary) } } if uploadState.isUploading { VStack(spacing: 8) { ProgressView(value: uploadState.progress) .progressViewStyle(LinearProgressViewStyle(tint: .blue)) HStack { Text("\(Int(uploadState.progress * 100))%") .font(.caption) .foregroundColor(.secondary) Spacer() Text(estimatedTimeRemaining) .font(.caption) .foregroundColor(.secondary) } } } if let result = uploadState.result { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) Text("上传成功") .font(.headline) .foregroundColor(.green) } Text("URL: \(result)") .font(.caption) .foregroundColor(.secondary) .lineLimit(3) Button("复制链接") { UIPasteboard.general.string = result } .buttonStyle(.bordered) .font(.caption) } .padding() .background(Color(.systemGreen).opacity(0.1)) .cornerRadius(8) } if let error = uploadState.error { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) Text("上传失败") .font(.headline) .foregroundColor(.red) } Text(error) .font(.caption) .foregroundColor(.red) } .padding() .background(Color(.systemRed).opacity(0.1)) .cornerRadius(8) } } .padding() .background(Color(.systemGray6)) .cornerRadius(12) } private var progressIcon: String { if uploadState.isUploading { return "arrow.up.circle.fill" } else if uploadState.result != nil { return "checkmark.circle.fill" } else if uploadState.error != nil { return "xmark.circle.fill" } else { return "arrow.up.circle" } } private var progressColor: Color { if uploadState.isUploading { return .blue } else if uploadState.result != nil { return .green } else if uploadState.error != nil { return .red } else { return .gray } } private var estimatedTimeRemaining: String { // 简单的剩余时间估算 let remainingProgress = 1.0 - uploadState.progress if remainingProgress > 0 { let estimatedSeconds = Int(remainingProgress * 30) // 假设总时间30秒 return "预计剩余 \(estimatedSeconds) 秒" } else { return "即将完成" } } } // MARK: - 上传按钮 /// 上传按钮组件 private struct UploadButton: View { let selectedImage: UIImage? let isUploading: Bool let isServiceReady: Bool let onUpload: () -> Void var body: some View { VStack(spacing: 12) { if !isServiceReady { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .foregroundColor(.orange) Text("服务未就绪") .font(.headline) .foregroundColor(.orange) Text("请确保 Token 有效且服务已初始化") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .padding() .background(Color(.systemOrange).opacity(0.1)) .cornerRadius(8) } Button(action: onUpload) { HStack { if isUploading { ProgressView() .scaleEffect(0.8) .tint(.white) } else { Image(systemName: "arrow.up.circle.fill") } Text(buttonTitle) } .frame(maxWidth: .infinity) .padding() } .buttonStyle(.borderedProminent) .disabled(selectedImage == nil || isUploading || !isServiceReady) } } private var buttonTitle: String { if isUploading { return "上传中..." } else if selectedImage == nil { return "请先选择图片" } else if !isServiceReady { return "服务未就绪" } else { return "开始上传" } } } // MARK: - 图片选择器 /// 图片选择器组件 private struct ImagePicker: UIViewControllerRepresentable { @Binding var selectedImage: UIImage? @Environment(\.presentationMode) var presentationMode func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration() configuration.filter = .images configuration.selectionLimit = 1 let picker = PHPickerViewController(configuration: configuration) picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: ImagePicker init(_ parent: ImagePicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { parent.presentationMode.wrappedValue.dismiss() guard let provider = results.first?.itemProvider else { return } if provider.canLoadObject(ofClass: UIImage.self) { provider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in // 在回调中立即进行类型转换,避免数据竞争 guard let uiImage = image as? UIImage else { return } // 在主线程中安全地设置图片 DispatchQueue.main.async { guard let self = self else { return } self.parent.selectedImage = uiImage } } } } } } // MARK: - 扩展 extension COSServiceStatus { var isInitialized: Bool { switch self { case .initialized: return true default: return false } } } // MARK: - 预览 #Preview { COSUploadView( store: Store( initialState: COSFeature.State(), reducer: { COSFeature() } ) ) }