diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index c71bd2e..115a7fb 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/Models/UploadModels.swift b/wake/Models/UploadModels.swift new file mode 100644 index 0000000..336653e --- /dev/null +++ b/wake/Models/UploadModels.swift @@ -0,0 +1,57 @@ +import SwiftUI + +/// 上传状态 +public enum UploadStatus: Equatable { + case idle + case uploading(progress: Double) + case success + case failure(Error) + + public var isUploading: Bool { + if case .uploading = self { return true } + return false + } + + public var progress: Double { + if case let .uploading(progress) = self { return progress } + return 0 + } + + public static func == (lhs: UploadStatus, rhs: UploadStatus) -> Bool { + switch (lhs, rhs) { + case (.idle, .idle): + return true + case let (.uploading(lhsProgress), .uploading(rhsProgress)): + // 使用近似比较来处理浮点数的精度问题 + return abs(lhsProgress - rhsProgress) < 0.001 + case (.success, .success): + return true + case (.failure, .failure): + // 对于错误类型,我们简单地认为它们不相等,因为比较 Error 对象比较复杂 + // 如果需要更精确的比较,可以在这里添加具体实现 + return false + default: + return false + } + } +} + +/// 上传结果 +public struct UploadResult: Identifiable, Equatable { + public let id = UUID() + public var fileId: String + public var previewFileId: String + public let image: UIImage + public var status: UploadStatus = .idle + + public init(fileId: String = "", previewFileId: String = "", image: UIImage, status: UploadStatus = .idle) { + self.fileId = fileId + self.previewFileId = previewFileId + self.image = image + self.status = status + } + + public static func == (lhs: UploadResult, rhs: UploadResult) -> Bool { + lhs.id == rhs.id + } +} diff --git a/wake/View/Components/Upload/MultiImageUploader.swift b/wake/View/Components/Upload/MultiImageUploader.swift new file mode 100644 index 0000000..3598ef4 --- /dev/null +++ b/wake/View/Components/Upload/MultiImageUploader.swift @@ -0,0 +1,259 @@ +import SwiftUI +import PhotosUI +import os.log + +@available(iOS 16.0, *) +public struct MultiImageUploader: View { + @State private var selectedImages: [UIImage] = [] + @State private var uploadResults: [UploadResult] = [] + @State private var isUploading = false + @State private var showingImagePicker = false + @State private var uploadProgress: [UUID: Double] = [:] // 跟踪每个上传任务的进度 + + private let maxSelection: Int + private let onUploadComplete: ([UploadResult]) -> Void + private let uploadService = ImageUploadService.shared + private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploader") + + public init( + maxSelection: Int = 10, + onUploadComplete: @escaping ([UploadResult]) -> Void + ) { + self.maxSelection = maxSelection + self.onUploadComplete = onUploadComplete + } + + public var body: some View { + VStack(spacing: 16) { + // 上传按钮 + Button(action: { + showingImagePicker = true + }) { + Label("选择图片", systemImage: "photo.on.rectangle") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + } + .padding(.horizontal) + .sheet(isPresented: $showingImagePicker) { + ImagePicker(images: $selectedImages, selectionLimit: maxSelection) { + // 当选择完成时,开始上传 + if !selectedImages.isEmpty { + uploadImages() + } + } + } + + // 上传进度和图片网格 + ScrollView { + LazyVStack(spacing: 16) { + ForEach($uploadResults) { $result in + VStack(spacing: 8) { + // 图片预览 + Image(uiImage: result.image) + .resizable() + .scaledToFill() + .frame(height: 200) + .frame(maxWidth: .infinity) + .clipped() + .cornerRadius(8) + + // 上传进度条 + if case .uploading = result.status { + ProgressView(value: uploadProgress[result.id] ?? 0, total: 1.0) + .progressViewStyle(LinearProgressViewStyle()) + .padding(.horizontal) + + Text("上传中: \(Int((uploadProgress[result.id] ?? 0) * 100))%") + .font(.caption) + .foregroundColor(.secondary) + } else if case .success = result.status { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("上传成功") + .font(.caption) + .foregroundColor(.green) + } + } else if case .failure = result.status { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.red) + Text("上传失败") + .font(.caption) + .foregroundColor(.red) + } + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + } + .padding() + } + + // 上传按钮 + if !selectedImages.isEmpty && !isUploading { + Button(action: uploadImages) { + Text("开始上传 (\(selectedImages.count)张)") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + } + .padding(.horizontal) + .padding(.bottom) + } + } + .background(Color(.systemGroupedBackground)) + } + + private func uploadImages() { + guard !isUploading else { return } + + isUploading = true + let group = DispatchGroup() + var results = selectedImages.map { image in + UploadResult(fileId: "", previewFileId: "", image: image, status: .uploading(progress: 0)) + } + + // 更新UI显示上传中的状态 + uploadResults = results + + // 创建并发的DispatchQueue + let uploadQueue = DispatchQueue(label: "com.wake.uploadQueue", attributes: .concurrent) + let semaphore = DispatchSemaphore(value: 3) // 限制并发数为3 + + for (index, image) in selectedImages.enumerated() { + semaphore.wait() + group.enter() + + uploadQueue.async { + let resultId = results[index].id + + // 更新状态为上传中 + DispatchQueue.main.async { + if let idx = results.firstIndex(where: { $0.id == resultId }) { + results[idx].status = .uploading(progress: 0) + uploadProgress[resultId] = 0 + uploadResults = results + } + } + + // 上传原图 + uploadService.uploadImage(image, progress: { progress in + DispatchQueue.main.async { + uploadProgress[resultId] = progress.progress + if let idx = results.firstIndex(where: { $0.id == resultId }) { + results[idx].status = .uploading(progress: progress.progress) + uploadResults = results + } + } + }) { uploadResult in + defer { + semaphore.signal() + group.leave() + } + + DispatchQueue.main.async { + if let idx = results.firstIndex(where: { $0.id == resultId }) { + switch uploadResult { + case .success(let uploadResult): + // 上传成功,更新结果 + results[idx].fileId = uploadResult.fileId + // 使用空字符串作为 previewFileId,因为 ImageUploaderGetID.UploadResult 没有这个属性 + results[idx].previewFileId = "" + results[idx].status = .success + uploadResults = results + + logger.info("图片上传成功: \(uploadResult.fileId)") + + case .failure(let error): + // 上传失败 + results[idx].status = .failure(error) + uploadResults = results + logger.error("图片上传失败: \(error.localizedDescription)") + } + } + } + } + } + } + + // 所有上传任务完成后的处理 + group.notify(queue: .main) { + self.isUploading = false + let successCount = results.filter { $0.status == .success }.count + logger.info("上传完成,成功: \(successCount)/\(results.count)") + self.onUploadComplete(results) + } + } +} + +// MARK: - 图片选择器 +@available(iOS 14.0, *) +struct ImagePicker: UIViewControllerRepresentable { + @Binding var images: [UIImage] + var selectionLimit: Int = 10 + var onDismiss: (() -> Void)? + + func makeUIViewController(context: Context) -> PHPickerViewController { + var config = PHPickerConfiguration(photoLibrary: .shared()) + config.filter = .images + config.selectionLimit = selectionLimit + config.preferredAssetRepresentationMode = .current + + let picker = PHPickerViewController(configuration: config) + 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]) { + picker.dismiss(animated: true) + + let group = DispatchGroup() + var newImages: [UIImage] = [] + + for result in results { + group.enter() + let itemProvider = result.itemProvider + + if itemProvider.canLoadObject(ofClass: UIImage.self) { + itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in + if let image = image as? UIImage { + newImages.append(image) + } + group.leave() + } + } else { + group.leave() + } + } + + group.notify(queue: .main) { + self.parent.images = newImages + self.parent.onDismiss?() + } + } + } +} \ No newline at end of file diff --git a/wake/View/Examples/ImageUploadExampleView.swift b/wake/View/Examples/ImageUploadExampleView.swift new file mode 100644 index 0000000..37393de --- /dev/null +++ b/wake/View/Examples/ImageUploadExampleView.swift @@ -0,0 +1,284 @@ +import SwiftUI +import os.log + +/// 多图上传示例视图 +/// 展示如何使用 MultiImageUploader 组件实现多图上传功能 +@available(iOS 16.0, *) +struct MultiImageUploadExampleView: View { + // MARK: - 状态属性 + + @State private var uploadResults: [UploadResult] = [] + @State private var isShowingUploader = false + @State private var showUploadAlert = false + @State private var alertMessage = "" + @State private var isUploading = false + + private let logger = Logger(subsystem: "com.yourapp.uploader", category: "MultiImageUploadExample") + + // MARK: - 视图主体 + + var body: some View { + NavigationView { + VStack(spacing: 16) { + // 上传按钮和状态 + uploadButton + + // 上传统计信息 + if !uploadResults.isEmpty { + uploadStatsView + } + + // 上传进度列表 + uploadProgressList + + Spacer() + } + .navigationTitle("多图上传示例") + .toolbar { + // 清空按钮 + if !uploadResults.isEmpty && !isUploading { + Button("清空") { + withAnimation { + uploadResults.removeAll() + } + } + } + } + .sheet(isPresented: $isShowingUploader) { + // 多图上传组件 + MultiImageUploader( + maxSelection: 10 + ) { results in + processUploadResults(results) + } + } + .alert("上传结果", isPresented: $showUploadAlert) { + Button("确定", role: .cancel) { } + } message: { + Text(alertMessage) + } + } + } + + // MARK: - 子视图 + + /// 上传按钮 + private var uploadButton: some View { + Button(action: { isShowingUploader = true }) { + Label("选择并上传图片", systemImage: "photo.on.rectangle.angled") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + } + .padding(.horizontal) + .padding(.top) + } + + /// 上传统计信息 + private var uploadStatsView: some View { + VStack(alignment: .leading, spacing: 8) { + let successCount = uploadResults.filter { $0.status == .success }.count + let inProgressCount = uploadResults.filter { + if case .uploading = $0.status { return true } + return false + }.count + let failedCount = uploadResults.count - successCount - inProgressCount + + Text("上传统计") + .font(.headline) + + HStack { + StatView( + value: "\(uploadResults.count)", + label: "总数量", + color: .blue + ) + + StatView( + value: "\(successCount)", + label: "成功", + color: .green + ) + + StatView( + value: "\(inProgressCount)", + label: "上传中", + color: .orange + ) + + StatView( + value: "\(failedCount)", + label: "失败", + color: .red + ) + } + + // 总进度条 + if inProgressCount > 0 { + let progress = uploadResults.reduce(0.0) { result, uploadResult in + if case .uploading(let progress) = uploadResult.status { + return result + progress + } else if uploadResult.status == .success { + return result + 1.0 + } + return result + } / Double(uploadResults.count) + + VStack(alignment: .leading, spacing: 4) { + Text("总进度: \(Int(progress * 100))%") + .font(.subheadline) + ProgressView(value: progress) + .progressViewStyle(LinearProgressViewStyle(tint: .blue)) + } + .padding(.top, 8) + } + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(10) + .padding(.horizontal) + } + + /// 上传进度列表 + private var uploadProgressList: some View { + List { + ForEach($uploadResults) { $result in + VStack(alignment: .leading, spacing: 8) { + // 图片缩略图和状态 + HStack { + // 图片缩略图 + Image(uiImage: result.image) + .resizable() + .scaledToFill() + .frame(width: 60, height: 60) + .clipped() + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(borderColor(for: result.status), lineWidth: 1) + ) + + // 状态和进度 + VStack(alignment: .leading, spacing: 4) { + Text("图片 \(result.id.uuidString.prefix(8))...") + .font(.subheadline) + + switch result.status { + case .uploading(let progress): + VStack(alignment: .leading, spacing: 4) { + Text("上传中: \(Int(progress * 100))%") + .font(.caption) + ProgressView(value: progress) + .progressViewStyle(LinearProgressViewStyle()) + } + case .success: + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("上传成功") + .font(.caption) + .foregroundColor(.green) + } + case .failure(let error): + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.red) + Text("上传失败: \(error.localizedDescription)") + .font(.caption) + .foregroundColor(.red) + } + case .idle: + Text("等待上传...") + .font(.caption) + .foregroundColor(.secondary) + } + } + + Spacer() + } + .padding(.vertical, 4) + } + .padding(.vertical, 8) + .listRowSeparator(.hidden) + } + } + .listStyle(PlainListStyle()) + .animation(.easeInOut, value: uploadResults) + } + + // MARK: - 辅助方法 + + /// 获取状态对应的边框颜色 + private func borderColor(for status: UploadStatus) -> Color { + switch status { + case .success: return .green + case .failure: return .red + case .uploading: return .blue + case .idle: return .gray + } + } + + /// 处理上传结果 + private func processUploadResults(_ results: [UploadResult]) { + // 更新状态 + isUploading = results.contains { result in + if case .uploading = result.status { return true } + return false + } + + // 更新结果 + withAnimation { + uploadResults = results + isShowingUploader = false + } + + // 检查是否全部完成 + let allFinished = !results.contains { result in + if case .uploading = result.status { return true } + if case .idle = result.status { return true } + return false + } + + if allFinished { + let successCount = results.filter { $0.status == .success }.count + let totalCount = results.count + alertMessage = "上传完成\n成功: \(successCount)/\(totalCount)" + showUploadAlert = true + isUploading = false + + logger.info("上传完成,成功: \(successCount)/\(totalCount)") + } + } +} + +// MARK: - 子视图 + +/// 统计信息视图 +private struct StatView: View { + let value: String + let label: String + let color: Color + + var body: some View { + VStack { + Text(value) + .font(.title3.bold()) + .foregroundColor(color) + Text(label) + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + } +} + +// MARK: - 预览 +@available(iOS 16.0, *) +struct MultiImageUploadExampleView_Previews: PreviewProvider { + static var previews: some View { + MultiImageUploadExampleView() + } +} \ No newline at end of file diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index 3d1766c..13a58cf 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -44,7 +44,9 @@ struct WakeApp: App { // 根据登录状态显示不同视图 if authState.isAuthenticated { // 已登录:显示userInfo页面 - UserInfo() + // UserInfo() + // .environmentObject(authState) + MultiImageUploadExampleView() .environmentObject(authState) } else { // 未登录:显示登录界面