diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 8deb0a0..79e8ee0 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 bf7e0f9..e85ba83 100644 --- a/wake/View/Components/Upload/Avatar.swift +++ b/wake/View/Components/Upload/Avatar.swift @@ -1,6 +1,12 @@ import SwiftUI import PhotosUI +/// 上传结果,包含原图和压缩图的上传信息 +struct UploadResults { + let original: ImageUploaderGetID.UploadResult + let compressed: ImageUploaderGetID.UploadResult +} + /// 照片选择器,封装了系统相册选择功能 /// 使用UIViewControllerRepresentable包装PHPickerViewController,提供SwiftUI兼容的图片选择界面 struct PhotoPicker: UIViewControllerRepresentable { @@ -16,7 +22,7 @@ struct PhotoPicker: UIViewControllerRepresentable { let filter: PHPickerFilter /// 图片上传完成回调,返回上传结果或错误 - var onImageUploaded: ((Result) -> Void)? + var onImageUploaded: ((Result) -> Void)? // MARK: - Initialization @@ -30,7 +36,7 @@ struct PhotoPicker: UIViewControllerRepresentable { selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images, - onImageUploaded: ((Result) -> Void)? = nil + onImageUploaded: ((Result) -> Void)? = nil ) { self._selectedImages = selectedImages self.selectionLimit = selectionLimit @@ -75,66 +81,113 @@ struct PhotoPicker: UIViewControllerRepresentable { } /// 当用户完成图片选择时调用 - /// - Parameters: - /// - picker: 图片选择器实例 - /// - results: 用户选择的图片结果数组 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { // 清空已选图片 parent.selectedImages.removeAll() - // 使用DispatchGroup管理多个异步图片加载任务 + // 使用DispatchGroup管理多个异步任务 let group = DispatchGroup() var loadedImages: [Int: UIImage] = [:] // 用于保持图片顺序的字典 + var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?, + compressed: ImageUploaderGetID.UploadResult?)] = [:] // 遍历所有选中的图片 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 - if let image = image as? UIImage { - // 将加载的图片存入字典,保持原始顺序 - loadedImages[index] = image + guard let self = self, let image = image as? UIImage else { + group.leave() + return + } + + // 1. 保存原始图片 + loadedImages[index] = image + + // 2. 压缩图片(质量压缩到50%) + guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else { + group.leave() + return + } + + // 3. 上传原图 + self.uploader.uploadImage(image) { [weak self] originalResult in + guard let self = self else { + group.leave() + return + } - // 上传图片 - self?.uploader.uploadImage(image) { result in - // 在主线程中调用上传完成回调 - DispatchQueue.main.async { - switch result { - case .success(let uploadResult): - print("✅ 上传成功!fileId: \(uploadResult.fileId)") - print("📂 文件信息:") - print(" - 文件名: \(uploadResult.fileName)") - print(" - 文件大小: \(uploadResult.fileSize) 字节") - print(" - 文件URL: \(uploadResult.fileUrl)") + switch originalResult { + case .success(let originalUploadResult): + // 4. 原图上传成功后上传压缩图 + self.uploader.uploadImage(compressedImage) { 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) 字节") - // 调用上传完成回调 - self?.parent.onImageUploaded?(.success(uploadResult)) + // 使用MaterialService上传文件信息到后端 + 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)") - // 调用上传完成回调,传递错误 - self?.parent.onImageUploaded?(.failure(error)) + print("❌ 压缩图上传失败: \(error.localizedDescription)") + uploadResults[index] = (originalUploadResult, nil) } } + + case .failure(let error): + print("❌ 原图上传失败: \(error.localizedDescription)") + group.leave() } } - group.leave() // 离开组 } } else { - group.leave() // 如果无法加载图片,也离开组 + group.leave() } } - // 所有图片加载完成后的处理 - group.notify(queue: .main) { - // 按原始顺序排序并更新图片数组 + // 所有上传任务完成后的处理 + group.notify(queue: .main) { [weak self] in + guard let self = self else { return } + + // 1. 更新选中的图片(只显示原图) let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } self.parent.selectedImages.append(contentsOf: sortedImages) - // 关闭图片选择器 + // 2. 检查是否所有上传都成功 + if let firstResult = uploadResults.first?.value, + let original = firstResult.original, + let compressed = firstResult.compressed { + // 3. 如果成功,返回上传结果 + let results = UploadResults(original: original, compressed: compressed) + self.parent.onImageUploaded?(.success(results)) + } else { + // 4. 如果失败,返回错误 + self.parent.onImageUploaded?(.failure(NSError(domain: "com.wake.upload", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]))) + } + + // 5. 关闭图片选择器 picker.dismiss(animated: true) } } @@ -155,7 +208,7 @@ struct AvatarUploader: View { let size: CGFloat /// 上传完成回调,返回上传结果或错误 - var onUploadComplete: ((Result) -> Void)? + var onUploadComplete: ((Result) -> Void)? // MARK: - State diff --git a/wake/View/Components/Upload/MaterialService.swift b/wake/View/Components/Upload/MaterialService.swift new file mode 100644 index 0000000..6453d5a --- /dev/null +++ b/wake/View/Components/Upload/MaterialService.swift @@ -0,0 +1,62 @@ +import Foundation + +/// 素材服务,处理与素材相关的网络请求 +class MaterialService { + + /// 单例实例 + static let shared = MaterialService() + + private init() {} + + /// 上传素材信息 + /// - Parameters: + /// - fileId: 原文件ID + /// - previewFileId: 预览文件ID + /// - completion: 完成回调,返回是否成功 + func uploadMaterialInfo(fileId: String, + previewFileId: String, + completion: @escaping (Bool, String?) -> Void) { + + let materialData: [[String: String]] = [[ + "file_id": fileId, + "preview_file_id": previewFileId + ]] + + guard let url = URL(string: "\(APIConfig.baseURL)/material") else { + completion(false, "无效的URL") + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = APIConfig.authHeaders + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: materialData) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + completion(false, "上传失败: \(error.localizedDescription)") + return + } + + if let httpResponse = response as? HTTPURLResponse { + if (200...299).contains(httpResponse.statusCode) { + completion(true, nil) + } else { + let statusCode = httpResponse.statusCode + completion(false, "服务器返回错误状态码: \(statusCode)") + } + } else { + completion(false, "无效的服务器响应") + } + } + + task.resume() + + } catch { + completion(false, "请求数据序列化失败: \(error.localizedDescription)") + } + } +}