import SwiftUI import PhotosUI // MARK: - Photo Picker /// 照片选择器,封装了系统相册选择功能 struct PhotoPicker: UIViewControllerRepresentable { // MARK: - Properties @Binding var selectedImages: [UIImage] let selectionLimit: Int let filter: PHPickerFilter 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: ((ImageUploadService.UploadProgress) -> Void)? = nil) { self._selectedImages = selectedImages self.selectionLimit = selectionLimit self.filter = filter self.onImageUploaded = onImageUploaded self.onUploadProgress = onUploadProgress } // MARK: - UIViewControllerRepresentable func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) configuration.filter = filter configuration.selectionLimit = selectionLimit configuration.preferredAssetRepresentationMode = .current let picker = PHPickerViewController(configuration: configuration) picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } // MARK: - Coordinator class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: PhotoPicker 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: 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) { (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 // Upload the image self.uploadService.uploadOriginalAndCompressedImage( image, compressionQuality: 0.5, progress: { progress in DispatchQueue.main.async { self.parent.onUploadProgress?(progress) } }, completion: { result in defer { group.leave() } switch result { case .success(let results): uploadResults[index] = results // 使用 NetworkService 上传文件信息 let parameters: [String: Any] = [ "file_id": results.original.fileId, "preview_file_id": results.compressed.fileId ] // 将参数包装在数组中 let requestBody: [[String: Any]] = [parameters] // 定义响应模型 struct MaterialResponse: Decodable { let success: Bool let message: String? } // 直接使用 URLSession 发送请求,确保数据格式正确 do { let jsonData = try JSONSerialization.data(withJSONObject: requestBody) var request = URLRequest(url: URL(string: "\(APIConfig.baseURL)/material")!) request.httpMethod = "POST" request.allHTTPHeaderFields = APIConfig.authHeaders request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = jsonData let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { print("❌ 文件信息上传失败: \(error.localizedDescription)") return } if let httpResponse = response as? HTTPURLResponse { if (200...299).contains(httpResponse.statusCode) { print("✅ 文件信息上传成功") } else { let statusCode = httpResponse.statusCode let errorMessage = String(data: data ?? Data(), encoding: .utf8) ?? "" print("❌ 服务器返回错误状态码: \(statusCode), 响应: \(errorMessage)") } } } task.resume() } catch { print("❌ 请求数据序列化失败: \(error.localizedDescription)") } case .failure(let error): lastError = error print("❌ 图片上传失败: \(error.localizedDescription)") } } ) } } else { group.leave() } } group.notify(queue: .main) { [weak self] in guard let self = self else { return } if let error = lastError { self.parent.onImageUploaded?(.failure(error)) } else { 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: "上传过程中出现错误"] ))) } } self.parent.presentationMode.wrappedValue.dismiss() } } } } // MARK: - Avatar Uploader /// 头像上传视图,提供头像选择功能 struct AvatarUploader: View { @Binding var selectedImage: UIImage? let size: CGFloat var onUploadComplete: ((Result) -> Void)? @State private var isImagePickerPresented = false var body: some View { Button(action: { isImagePickerPresented = true }) { ZStack { if let selectedImage = selectedImage { Image(uiImage: selectedImage) .resizable() .scaledToFill() .frame(width: size, height: size) .clipShape(RoundedRectangle(cornerRadius: size * 0.1)) } else { Color.gray.opacity(0.1) .frame(width: size, height: size) .overlay( SVGImage(svgName: "Avatar") .frame(width: size * 0.8, height: size * 0.8) ) .clipShape(RoundedRectangle(cornerRadius: size * 0.1)) .overlay( RoundedRectangle(cornerRadius: size * 0.1) .stroke(Color.gray.opacity(0.3), lineWidth: 1) ) } } .frame(width: size, height: size) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) .sheet(isPresented: $isImagePickerPresented) { PhotoPicker( selectedImages: Binding( get: { [selectedImage].compactMap { $0 } }, set: { images in selectedImage = images.first } ), selectionLimit: 1, onImageUploaded: { result in onUploadComplete?(result) }, onUploadProgress: { progress in print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%") } ) } } }