diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 314d409..64481b2 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/View/Components/Upload/Avatar.swift b/wake/View/Components/Upload/Avatar.swift index 07f115d..2757c5d 100644 --- a/wake/View/Components/Upload/Avatar.swift +++ b/wake/View/Components/Upload/Avatar.swift @@ -1,52 +1,26 @@ import SwiftUI import PhotosUI -// MARK: - Data Models - -/// 上传进度信息 -public struct UploadProgress { - public let current: Int - public let total: Int - public let progress: Double - public let isOriginal: Bool - - public init(current: Int, total: Int, progress: Double, isOriginal: Bool) { - self.current = current - self.total = total - self.progress = progress - self.isOriginal = isOriginal - } -} - -/// 上传结果,包含原图和压缩图的上传信息 -public struct UploadResults { - public let original: ImageUploaderGetID.UploadResult - public let compressed: ImageUploaderGetID.UploadResult - - public init(original: ImageUploaderGetID.UploadResult, - compressed: ImageUploaderGetID.UploadResult) { - self.original = original - self.compressed = compressed - } -} - // MARK: - Photo Picker /// 照片选择器,封装了系统相册选择功能 struct PhotoPicker: UIViewControllerRepresentable { // MARK: - Properties + @Binding var selectedImages: [UIImage] let selectionLimit: Int let filter: PHPickerFilter - var onImageUploaded: ((Result) -> Void)? - var onUploadProgress: ((UploadProgress) -> Void)? + var onImageUploaded: ((Result) -> Void)? + var onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? + @Environment(\.presentationMode) private var presentationMode // MARK: - Initialization + init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images, - onImageUploaded: ((Result) -> Void)? = nil, - onUploadProgress: ((UploadProgress) -> Void)? = nil) { + onImageUploaded: ((Result) -> Void)? = nil, + onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? = nil) { self._selectedImages = selectedImages self.selectionLimit = selectionLimit self.filter = filter @@ -55,6 +29,7 @@ struct PhotoPicker: UIViewControllerRepresentable { } // MARK: - UIViewControllerRepresentable + func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) configuration.filter = filter @@ -73,109 +48,77 @@ struct PhotoPicker: UIViewControllerRepresentable { } // MARK: - Coordinator + class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: PhotoPicker - private let uploader = ImageUploaderGetID() + private let uploadService = ImageUploadService.shared init(_ parent: PhotoPicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + guard !results.isEmpty else { + parent.presentationMode.wrappedValue.dismiss() + return + } + parent.selectedImages.removeAll() let group = DispatchGroup() - var loadedImages: [Int: UIImage] = [:] - var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?, - compressed: ImageUploaderGetID.UploadResult?)] = [:] + var loadedImages: [Int: UIImage] = [:] + var uploadResults: [Int: ImageUploadService.UploadResults] = [:] + var lastError: Error? for (index, result) in results.enumerated() { group.enter() if result.itemProvider.canLoadObject(ofClass: UIImage.self) { - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in - guard let self = self, let image = image as? UIImage else { + result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in + if let error = error { + lastError = error + group.leave() + return + } + + guard let image = image as? UIImage else { + lastError = NSError(domain: "com.wake.upload", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to load image"]) group.leave() return } loadedImages[index] = image - guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else { - group.leave() - return - } - - self.uploader.uploadImage( + // Upload the image + self.uploadService.uploadOriginalAndCompressedImage( image, - progress: { [weak self] progress in - let progressInfo = UploadProgress( - current: Int(progress * 100), - total: 100, - progress: progress, - isOriginal: true - ) + compressionQuality: 0.5, + progress: { progress in DispatchQueue.main.async { - self?.parent.onUploadProgress?(progressInfo) + self.parent.onUploadProgress?(progress) } - print("📤 原图上传进度: \(Int(progress * 100))%") }, - completion: { [weak self] originalResult in - guard let self = self else { - group.leave() - return - } + completion: { result in + defer { group.leave() } - switch originalResult { - case .success(let originalUploadResult): - self.uploader.uploadImage( - compressedImage, - progress: { [weak self] progress in - let progressInfo = UploadProgress( - current: Int(progress * 100), - total: 100, - progress: progress, - isOriginal: false - ) - DispatchQueue.main.async { - self?.parent.onUploadProgress?(progressInfo) - } - print("📊 压缩图上传进度: \(Int(progress * 100))%") - }, - completion: { compressedResult in - defer { group.leave() } - - switch compressedResult { - case .success(let compressedUploadResult): - uploadResults[index] = (originalUploadResult, compressedUploadResult) - print("✅ 原图和压缩图上传成功!") - print("📂 原图信息:") - print(" - 文件ID: \(originalUploadResult.fileId)") - print(" - 文件大小: \(originalUploadResult.fileSize) 字节") - print("📦 压缩图信息:") - print(" - 文件ID: \(compressedUploadResult.fileId)") - print(" - 文件大小: \(compressedUploadResult.fileSize) 字节") - - MaterialService.shared.uploadMaterialInfo( - fileId: originalUploadResult.fileId, - previewFileId: compressedUploadResult.fileId - ) { success, errorMessage in - if success { - print("✅ 文件信息上传成功 素材上传成功!!!!!") - } else if let errorMessage = errorMessage { - print("❌ 文件信息上传失败: \(errorMessage)") - } - } - - case .failure(let error): - print("❌ 压缩图上传失败: \(error.localizedDescription)") - uploadResults[index] = (originalUploadResult, nil) - } + switch result { + case .success(let results): + uploadResults[index] = results + + // Upload file info to backend + MaterialService.shared.uploadMaterialInfo( + fileId: results.original.fileId, + previewFileId: results.compressed.fileId + ) { success, errorMessage in + if success { + print("✅ 文件信息上传成功") + } else if let errorMessage = errorMessage { + print("❌ 文件信息上传失败: \(errorMessage)") } - ) + } case .failure(let error): - print("❌ 原图上传失败: \(error.localizedDescription)") - group.leave() + lastError = error + print("❌ 图片上传失败: \(error.localizedDescription)") } } ) @@ -188,23 +131,24 @@ struct PhotoPicker: UIViewControllerRepresentable { group.notify(queue: .main) { [weak self] in guard let self = self else { return } - let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } - self.parent.selectedImages.append(contentsOf: sortedImages) - - if let firstResult = uploadResults.first?.value, - let original = firstResult.original, - let compressed = firstResult.compressed { - let results = UploadResults(original: original, compressed: compressed) - self.parent.onImageUploaded?(.success(results)) + if let error = lastError { + self.parent.onImageUploaded?(.failure(error)) } else { - self.parent.onImageUploaded?(.failure(NSError( - domain: "com.wake.upload", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"] - ))) + let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } + self.parent.selectedImages.append(contentsOf: sortedImages) + + if let firstResult = uploadResults.first?.value { + self.parent.onImageUploaded?(.success(firstResult)) + } else { + self.parent.onImageUploaded?(.failure(NSError( + domain: "com.wake.upload", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"] + ))) + } } - picker.dismiss(animated: true) + self.parent.presentationMode.wrappedValue.dismiss() } } } @@ -216,7 +160,7 @@ struct PhotoPicker: UIViewControllerRepresentable { struct AvatarUploader: View { @Binding var selectedImage: UIImage? let size: CGFloat - var onUploadComplete: ((Result) -> Void)? + var onUploadComplete: ((Result) -> Void)? @State private var isImagePickerPresented = false @@ -260,7 +204,7 @@ struct AvatarUploader: View { onUploadComplete?(result) }, onUploadProgress: { progress in - print("上传进度:\(progress.current)/\(progress.total),进度:\(progress.progress * 100)%") + print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%") } ) } diff --git a/wake/View/Components/Upload/ImageUploadService.swift b/wake/View/Components/Upload/ImageUploadService.swift new file mode 100644 index 0000000..085919a --- /dev/null +++ b/wake/View/Components/Upload/ImageUploadService.swift @@ -0,0 +1,166 @@ +import Foundation +import UIKit + +/// 图片上传服务,封装了图片上传和进度跟踪功能 +public class ImageUploadService { + + // MARK: - Shared Instance + + public static let shared = ImageUploadService() + + // MARK: - Properties + + private let uploader: ImageUploaderGetID + + // MARK: - Initialization + + public init(uploader: ImageUploaderGetID = ImageUploaderGetID()) { + self.uploader = uploader + } + + // MARK: - Public Methods + + /// 上传图片并返回上传结果 + /// - Parameters: + /// - image: 要上传的图片 + /// - progressHandler: 上传进度回调 (0.0 到 1.0) + /// - completion: 完成回调,返回上传结果或错误 + public func uploadImage( + _ image: UIImage, + progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void, + completion: @escaping (Result) -> Void + ) { + uploader.uploadImage( + image, + progress: { progress in + let progressInfo = ImageUploadService.UploadProgress( + current: Int(progress * 100), + total: 100, + progress: progress, + isOriginal: true + ) + DispatchQueue.main.async { + progressHandler(progressInfo) + } + }, + completion: { result in + DispatchQueue.main.async { + completion(result) + } + } + ) + } + + /// 上传压缩图片并返回上传结果 + /// - Parameters: + /// - image: 要上传的图片 + /// - compressionQuality: 压缩质量 (0.0 到 1.0) + /// - progressHandler: 上传进度回调 (0.0 到 1.0) + /// - completion: 完成回调,返回上传结果或错误 + public func uploadCompressedImage( + _ image: UIImage, + compressionQuality: CGFloat = 0.5, + progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void, + completion: @escaping (Result) -> Void + ) { + guard let compressedImage = image.jpegData(compressionQuality: compressionQuality).flatMap(UIImage.init(data:)) else { + completion(.failure(NSError(domain: "com.wake.upload", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片压缩失败"]))) + return + } + + uploadImage( + compressedImage, + progress: progressHandler, + completion: completion + ) + } + + /// 上传原图和压缩图 + /// - Parameters: + /// - image: 原始图片 + /// - compressionQuality: 压缩质量 (0.0 到 1.0) + /// - progressHandler: 上传进度回调 + /// - completion: 完成回调,返回原始图和压缩图的上传结果 + public func uploadOriginalAndCompressedImage( + _ image: UIImage, + compressionQuality: CGFloat = 0.5, + progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void, + completion: @escaping (Result) -> Void + ) { + // 上传原图 + uploadImage(image, progress: { progress in + let originalProgress = ImageUploadService.UploadProgress( + current: Int(progress.progress * 100), + total: 200, // 总进度为200(原图100 + 压缩图100) + progress: progress.progress * 0.5, // 原图占50% + isOriginal: true + ) + progressHandler(originalProgress) + }) { [weak self] originalResult in + guard let self = self else { return } + + switch originalResult { + case .success(let originalUploadResult): + // 原图上传成功,上传压缩图 + self.uploadCompressedImage( + image, + compressionQuality: compressionQuality, + progress: { progress in + let compressedProgress = ImageUploadService.UploadProgress( + current: 100 + Int(progress.progress * 100), // 从100开始 + total: 200, // 总进度为200(原图100 + 压缩图100) + progress: 0.5 + (progress.progress * 0.5), // 压缩图占后50% + isOriginal: false + ) + progressHandler(compressedProgress) + }, + completion: { compressedResult in + switch compressedResult { + case .success(let compressedUploadResult): + let results = ImageUploadService.UploadResults( + original: originalUploadResult, + compressed: compressedUploadResult + ) + completion(.success(results)) + + case .failure(let error): + completion(.failure(error)) + } + } + ) + + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: - Supporting Types + + /// 上传进度信息 + public struct UploadProgress { + public let current: Int + public let total: Int + public let progress: Double + public let isOriginal: Bool + + public init(current: Int, total: Int, progress: Double, isOriginal: Bool) { + self.current = current + self.total = total + self.progress = progress + self.isOriginal = isOriginal + } + } + + /// 上传结果,包含原图和压缩图的上传信息 + public struct UploadResults { + public let original: ImageUploaderGetID.UploadResult + public let compressed: ImageUploaderGetID.UploadResult + + public init(original: ImageUploaderGetID.UploadResult, + compressed: ImageUploaderGetID.UploadResult) { + self.original = original + self.compressed = compressed + } + } +} diff --git a/wake/View/Components/Upload/ImageUploader.swift b/wake/View/Components/Upload/ImageUploader.swift deleted file mode 100644 index e69de29..0000000 diff --git a/wake/View/Components/Upload/ImageUploaderGetID.swift b/wake/View/Components/Upload/ImageUploaderGetID.swift index 6eb056b..7699858 100644 --- a/wake/View/Components/Upload/ImageUploaderGetID.swift +++ b/wake/View/Components/Upload/ImageUploaderGetID.swift @@ -7,7 +7,7 @@ public class ImageUploaderGetID: ObservableObject { // MARK: - 类型定义 /// 上传结果 - public struct UploadResult { + public struct UploadResult: Codable { public let fileUrl: String public let fileName: String public let fileSize: Int @@ -29,6 +29,7 @@ public class ImageUploaderGetID: ObservableObject { case invalidResponse case uploadFailed(Error?) case invalidFileId + case invalidResponseData public var errorDescription: String? { switch self { @@ -44,6 +45,8 @@ public class ImageUploaderGetID: ObservableObject { return "上传失败: \(error?.localizedDescription ?? "未知错误")" case .invalidFileId: return "无效的文件ID" + case .invalidResponseData: + return "无效的响应数据" } } } @@ -197,8 +200,8 @@ public class ImageUploaderGetID: ObservableObject { /// 确认上传 private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result) -> Void) { - let urlString = "\(apiConfig.baseURL)/file/confirm-upload" - guard let url = URL(string: urlString) else { + let endpoint = "\(apiConfig.baseURL)/file/confirm-upload" + guard let url = URL(string: endpoint) else { completion(.failure(UploadError.invalidURL)) return } @@ -207,24 +210,39 @@ public class ImageUploaderGetID: ObservableObject { request.httpMethod = "POST" request.allHTTPHeaderFields = apiConfig.authHeaders - let requestBody: [String: Any] = ["file_id": fileId] + let body: [String: Any] = [ + "file_id": fileId, + "file_name": fileName, + "file_size": fileSize + ] do { - request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) + request.httpBody = try JSONSerialization.data(withJSONObject: body) + print("📤 确认上传请求,fileId: \(fileId), 文件名: \(fileName)") } catch { + print("❌ 序列化确认上传参数失败: \(error.localizedDescription)") completion(.failure(error)) return } let task = session.dataTask(with: request) { data, response, error in if let error = error { + print("❌ 确认上传请求失败: \(error.localizedDescription)") completion(.failure(UploadError.uploadFailed(error))) return } - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - completion(.failure(UploadError.serverError("确认上传失败,状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)"))) + guard let httpResponse = response as? HTTPURLResponse else { + print("❌ 无效的服务器响应") + completion(.failure(UploadError.invalidResponse)) + return + } + + guard (200...299).contains(httpResponse.statusCode) else { + let statusCode = httpResponse.statusCode + let errorMessage = "确认上传失败,状态码: \(statusCode)" + print("❌ \(errorMessage)") + completion(.failure(UploadError.serverError(errorMessage))) return } @@ -270,9 +288,13 @@ public class ImageUploaderGetID: ObservableObject { return } - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(UploadError.invalidResponse)) + return + } + + guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else { + let statusCode = httpResponse.statusCode completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)"))) return } @@ -293,7 +315,7 @@ public class ImageUploaderGetID: ObservableObject { task?.progress.cancel() } } else { - // Fallback for earlier iOS versions + // iOS 11 以下版本使用通知 var lastProgress: Double = 0 let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in let bytesSent = task.countOfBytesSent