import SwiftUI import PhotosUI <<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD // MARK: - Photo Picker ======= /// 上传管理器,处理图片上传 /// 图片上传管理器 class ImageUploader: ObservableObject { private let baseURL = "https://api.memorywake.com/api/v1/file/generate-upload-url" func uploadImage(_ image: UIImage, completion: @escaping (Result) -> Void) { print("🔄 开始准备上传图片...") // 1. 将图片转换为Data guard let imageData = image.jpegData(compressionQuality: 0.7) else { let error = NSError(domain: "ImageError", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片数据转换失败"]) print("❌ 错误:\(error.localizedDescription)") completion(.failure(error)) return } // 2. 检查URL是否有效 guard let url = URL(string: baseURL) else { let error = NSError(domain: "URLError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"]) print("❌ 错误:\(error.localizedDescription)") completion(.failure(error)) return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA", forHTTPHeaderField: "Authorization") // 3. 准备请求参数 let fileName = "avatar_\(UUID().uuidString).jpg" let parameters: [String: Any] = [ "filename": fileName, "content_type": "image/jpeg", "file_size": imageData.count ] do { request.httpBody = try JSONSerialization.data(withJSONObject: parameters) print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB") print("📡 请求参数: \(parameters)") } catch { print("❌ 序列化请求参数失败: \(error.localizedDescription)") completion(.failure(error)) return } // 4. 打印调试信息 print("🌐 请求URL: \(url.absoluteString)") print("📋 请求头: \(request.allHTTPHeaderFields ?? [:])") // 5. 发起获取上传URL的请求 print("🌐 正在获取上传链接...") let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in // 切换到主线程处理响应 DispatchQueue.main.async { if let error = error { print("❌ 请求失败: \(error.localizedDescription)") completion(.failure(error)) return } guard let httpResponse = response as? HTTPURLResponse else { let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"]) print("❌ 错误:\(error.localizedDescription)") completion(.failure(error)) return } print("📊 响应状态码: \(httpResponse.statusCode)") print("📦 响应头: \(httpResponse.allHeaderFields)") guard let data = data else { let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "没有接收到数据"]) print("❌ 错误:\(error.localizedDescription)") completion(.failure(error)) return } // 打印原始响应数据 if let responseString = String(data: data, encoding: .utf8) { print("📡 原始响应数据: \(responseString)") } // 5. 解析获取上传URL的响应 do { guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw NSError(domain: "ParseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应数据格式错误"]) } print("📋 解析后的JSON: \(json)") // 根据实际API响应结构调整这里的键名 if let uploadUrlString = json["url"] as? String, let uploadUrl = URL(string: uploadUrlString) { print("✅ 成功获取上传链接: \(uploadUrlString)") self?.uploadImageData(imageData, to: uploadUrl, fileName: fileName, completion: completion) } else { throw NSError(domain: "APIError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取上传链接: \(json)"]) } } catch { print("❌ 解析响应数据失败: \(error.localizedDescription)") completion(.failure(error)) } } } task.resume() } private func uploadImageData(_ data: Data, to url: URL, fileName: String, completion: @escaping (Result) -> Void) { var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type") request.httpBody = data print("🚀 开始上传图片数据...") let task = URLSession.shared.dataTask(with: request) { _, response, error in DispatchQueue.main.async { if let error = error { print("❌ 上传失败: \(error.localizedDescription)") completion(.failure(error)) return } // 处理上传响应 if let httpResponse = response as? HTTPURLResponse { print("📊 上传响应状态码: \(httpResponse.statusCode)") if (200...299).contains(httpResponse.statusCode) { let fileUrl = "https://your-cdn-domain.com/\(fileName)" // 替换为实际的CDN域名 print("✅ 上传成功!文件URL: \(fileUrl)") completion(.success(fileUrl)) } else { let errorMessage = "上传失败,状态码: \(httpResponse.statusCode)" print("❌ \(errorMessage)") let error = NSError(domain: "UploadError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]) completion(.failure(error)) } } else { let errorMessage = "无效的服务器响应" print("❌ \(errorMessage)") let error = NSError(domain: "UploadError", code: -1, userInfo: [NSLocalizedDescriptionKey: errorMessage]) completion(.failure(error)) } } } task.resume() } } >>>>>>> a4890a4 (feat: 图片上传互获取url) ======= >>>>>>> a207b78 (feat: 确认上传) ======= /// 上传结果,包含原图和压缩图的上传信息 struct UploadResults { let original: ImageUploaderGetID.UploadResult let compressed: ImageUploaderGetID.UploadResult } >>>>>>> 5611df8 (feat: 素材上传成) ======= // 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 >>>>>>> 8e641fd (feat: 上传进度) /// 照片选择器,封装了系统相册选择功能 struct PhotoPicker: UIViewControllerRepresentable { <<<<<<< HEAD ======= // MARK: - Properties <<<<<<< HEAD /// 绑定的已选图片数组,用于存储用户选择的图片 >>>>>>> a207b78 (feat: 确认上传) ======= >>>>>>> 8e641fd (feat: 上传进度) @Binding var selectedImages: [UIImage] let selectionLimit: Int let filter: PHPickerFilter <<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD ======= /// 图片上传管理器 private let uploader = ImageUploader() ======= /// 图片上传完成回调,返回上传结果或错误 <<<<<<< HEAD var onImageUploaded: ((Result) -> Void)? >>>>>>> a207b78 (feat: 确认上传) ======= var onImageUploaded: ((Result) -> Void)? >>>>>>> 5611df8 (feat: 素材上传成) // MARK: - Initialization /// 初始化照片选择器 <<<<<<< HEAD >>>>>>> a4890a4 (feat: 图片上传互获取url) init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) { ======= /// - Parameters: /// - selectedImages: 绑定的图片数组,用于接收用户选择的图片 /// - selectionLimit: 最多可选图片数量,默认为1 /// - filter: 媒体类型过滤器,默认为图片 /// - onImageUploaded: 图片上传完成后的回调闭包 init( selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images, onImageUploaded: ((Result) -> Void)? = nil ) { >>>>>>> a207b78 (feat: 确认上传) ======= var onImageUploaded: ((Result) -> Void)? var onUploadProgress: ((UploadProgress) -> Void)? // MARK: - Initialization init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images, onImageUploaded: ((Result) -> Void)? = nil, onUploadProgress: ((UploadProgress) -> Void)? = nil) { >>>>>>> 8e641fd (feat: 上传进度) self._selectedImages = selectedImages self.selectionLimit = selectionLimit self.filter = filter self.onImageUploaded = onImageUploaded <<<<<<< HEAD <<<<<<< HEAD ======= >>>>>>> 8e641fd (feat: 上传进度) self.onUploadProgress = onUploadProgress } // MARK: - UIViewControllerRepresentable <<<<<<< HEAD // MARK: - UIViewControllerRepresentable 协议方法 ======= } // MARK: - UIViewControllerRepresentable >>>>>>> a207b78 (feat: 确认上传) /// 创建并返回配置好的PHPickerViewController ======= >>>>>>> 8e641fd (feat: 上传进度) 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 <<<<<<< HEAD <<<<<<< HEAD // MARK: - 协调器类 ======= >>>>>>> a207b78 (feat: 确认上传) /// 协调器类,处理PHPickerViewController的代理方法 ======= >>>>>>> 8e641fd (feat: 上传进度) class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: PhotoPicker <<<<<<< HEAD private let uploadService = ImageUploadService.shared /// 图片上传器实例 ======= >>>>>>> 8e641fd (feat: 上传进度) private let uploader = ImageUploaderGetID() init(_ parent: PhotoPicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { parent.selectedImages.removeAll() let group = DispatchGroup() <<<<<<< HEAD <<<<<<< HEAD var loadedImages: [Int: UIImage] = [:] var uploadResults: [Int: ImageUploadService.UploadResults] = [:] var lastError: Error? ======= var loadedImages: [Int: UIImage] = [:] // 用于保持图片顺序的字典 <<<<<<< HEAD >>>>>>> a207b78 (feat: 确认上传) ======= var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?, compressed: ImageUploaderGetID.UploadResult?)] = [:] >>>>>>> 5611df8 (feat: 素材上传成) ======= var loadedImages: [Int: UIImage] = [:] var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?, compressed: ImageUploaderGetID.UploadResult?)] = [:] >>>>>>> 8e641fd (feat: 上传进度) 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 <<<<<<< HEAD if let image = image as? UIImage { <<<<<<< HEAD ======= // 将加载的图片存入字典,保持原始顺序 >>>>>>> a207b78 (feat: 确认上传) loadedImages[index] = image ======= guard let self = self, let image = image as? UIImage else { group.leave() return } loadedImages[index] = image guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else { group.leave() return } <<<<<<< HEAD // 3. 上传原图 self.uploader.uploadImage(image) { [weak self] originalResult in guard let self = self else { group.leave() return } >>>>>>> 5611df8 (feat: 素材上传成) 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) 字节") // 使用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)") uploadResults[index] = (originalUploadResult, nil) } ======= self.uploader.uploadImage( image, progress: { [weak self] progress in let progressInfo = UploadProgress( current: Int(progress * 100), total: 100, progress: progress, isOriginal: true ) DispatchQueue.main.async { self?.parent.onUploadProgress?(progressInfo) } print("📤 原图上传进度: \(Int(progress * 100))%") }, completion: { [weak self] originalResult in guard let self = self else { group.leave() return >>>>>>> 8e641fd (feat: 上传进度) } 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) } } ) case .failure(let error): print("❌ 原图上传失败: \(error.localizedDescription)") group.leave() } } ) } } else { group.leave() } } <<<<<<< HEAD <<<<<<< HEAD // 所有图片加载完成后的处理 group.notify(queue: .main) { <<<<<<< HEAD <<<<<<< HEAD // Sort the images by their original index to maintain selection order let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } self.parent.selectedImages.append(contentsOf: sortedImages) // Dismiss the picker ======= let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } self.parent.selectedImages.append(contentsOf: sortedImages) >>>>>>> a4890a4 (feat: 图片上传互获取url) ======= // 按原始顺序排序并更新图片数组 let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } self.parent.selectedImages.append(contentsOf: sortedImages) // 关闭图片选择器 >>>>>>> a207b78 (feat: 确认上传) ======= // 所有上传任务完成后的处理 ======= >>>>>>> 8e641fd (feat: 上传进度) 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)) } else { self.parent.onImageUploaded?(.failure(NSError( domain: "com.wake.upload", code: -1, userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"] ))) } <<<<<<< HEAD // 5. 关闭图片选择器 >>>>>>> 5611df8 (feat: 素材上传成) ======= >>>>>>> 8e641fd (feat: 上传进度) picker.dismiss(animated: true) } } } } <<<<<<< HEAD <<<<<<< HEAD // MARK: - Avatar Uploader Component ======= // MARK: - AvatarUploader /// 头像上传视图,提供头像选择功能 /// 封装了头像显示和选择逻辑,支持点击选择新头像 >>>>>>> a207b78 (feat: 确认上传) ======= // MARK: - Avatar Uploader /// 头像上传视图,提供头像选择功能 >>>>>>> 8e641fd (feat: 上传进度) struct AvatarUploader: View { @Binding var selectedImage: UIImage? let size: CGFloat <<<<<<< HEAD var onUploadComplete: ((Result) -> Void)? /// 上传完成回调,返回上传结果或错误 ======= >>>>>>> 8e641fd (feat: 上传进度) var onUploadComplete: ((Result) -> Void)? @State private var isImagePickerPresented = false var body: some View { Button(action: { isImagePickerPresented = true }) { ZStack { <<<<<<< HEAD // Avatar Image or Placeholder ======= >>>>>>> a4890a4 (feat: 图片上传互获取url) if let selectedImage = selectedImage { Image(uiImage: selectedImage) .resizable() .scaledToFill() .frame(width: size, height: size) .clipShape(RoundedRectangle(cornerRadius: size * 0.1)) } else { <<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD // Default avatar container ======= >>>>>>> a4890a4 (feat: 图片上传互获取url) ======= // 默认头像占位视图 >>>>>>> a207b78 (feat: 确认上传) ======= >>>>>>> 8e641fd (feat: 上传进度) 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) <<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD .contentShape(Rectangle()) // Make the entire area tappable } .buttonStyle(PlainButtonStyle()) // Remove button highlight effect ======= .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) >>>>>>> a4890a4 (feat: 图片上传互获取url) ======= .contentShape(Rectangle()) // 确保整个区域都可点击 } .buttonStyle(PlainButtonStyle()) // 使用无样式按钮 >>>>>>> a207b78 (feat: 确认上传) ======= .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) >>>>>>> 8e641fd (feat: 上传进度) .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),进度:\(progress.progress * 100)%") } ) } } }