diff --git a/wake/View/Components/Upload/Avatar.swift b/wake/View/Components/Upload/Avatar.swift index b084772..d3f3925 100644 --- a/wake/View/Components/Upload/Avatar.swift +++ b/wake/View/Components/Upload/Avatar.swift @@ -1,7 +1,166 @@ import SwiftUI import PhotosUI +<<<<<<< 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) /// 照片选择器,封装了系统相册选择功能 struct PhotoPicker: UIViewControllerRepresentable { @@ -13,6 +172,15 @@ struct PhotoPicker: UIViewControllerRepresentable { /// 图片过滤器,默认为图片类型 let filter: PHPickerFilter +<<<<<<< HEAD +======= + /// 图片上传管理器 + private let uploader = ImageUploader() + + // MARK: - 初始化方法 + + /// 初始化照片选择器 +>>>>>>> a4890a4 (feat: 图片上传互获取url) init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) { self._selectedImages = selectedImages self.selectionLimit = selectionLimit @@ -25,11 +193,7 @@ struct PhotoPicker: UIViewControllerRepresentable { // MARK: - UIViewControllerRepresentable 协议方法 - /// 创建并返回PHPickerViewController实例 - /// - Parameter context: 上下文 - /// - Returns: 配置好的PHPickerViewController func makeUIViewController(context: Context) -> PHPickerViewController { - // 配置照片选择器 var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) configuration.filter = filter configuration.selectionLimit = selectionLimit @@ -40,14 +204,8 @@ struct PhotoPicker: UIViewControllerRepresentable { return picker } - /// 更新视图控制器 - /// - Parameters: - /// - uiViewController: 要更新的视图控制器 - /// - context: 上下文 func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} - /// 创建协调器 - /// - Returns: 协调器实例 func makeCoordinator() -> Coordinator { Coordinator(self) } @@ -56,22 +214,14 @@ struct PhotoPicker: UIViewControllerRepresentable { // MARK: - 协调器类 - /// 协调器,处理PHPickerViewController的代理方法 class Coordinator: NSObject, PHPickerViewControllerDelegate { - /// 父视图引用 let parent: PhotoPicker private let uploadService = ImageUploadService.shared - /// 初始化方法 - /// - Parameter parent: 父视图 init(_ parent: PhotoPicker) { self.parent = parent } - /// 用户完成图片选择时调用 - /// - Parameters: - /// - picker: 图片选择器 - /// - results: 选中的图片结果 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { parent.selectedImages.removeAll() @@ -80,16 +230,19 @@ struct PhotoPicker: UIViewControllerRepresentable { 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 image = image as? UIImage { loadedImages[index] = image + + // 上传图片 + self.parent.uploader.uploadImage(image) { result in + // 这里可以添加上传完成后的处理 + // 目前按需求暂时不处理 + } } group.leave() } @@ -99,11 +252,16 @@ struct PhotoPicker: UIViewControllerRepresentable { } group.notify(queue: .main) { +<<<<<<< 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) picker.dismiss(animated: true) } } @@ -133,16 +291,21 @@ struct AvatarUploader: View { 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 // Default avatar container +======= +>>>>>>> a4890a4 (feat: 图片上传互获取url) Color.gray.opacity(0.1) .frame(width: size, height: size) .overlay( @@ -157,12 +320,17 @@ struct AvatarUploader: View { } } .frame(width: size, height: size) +<<<<<<< HEAD .contentShape(Rectangle()) // Make the entire area tappable } .buttonStyle(PlainButtonStyle()) // Remove button highlight effect +======= + .contentShape(Rectangle()) + } + .buttonStyle(PlainButtonStyle()) +>>>>>>> a4890a4 (feat: 图片上传互获取url) .sheet(isPresented: $isImagePickerPresented) { PhotoPicker( - // 绑定选中的图片 selectedImages: Binding( get: { [selectedImage].compactMap { $0 } }, set: { images in diff --git a/wake/View/Components/Upload/Upload.swift b/wake/View/Components/Upload/Upload.swift index 402ac0f..e69de29 100644 --- a/wake/View/Components/Upload/Upload.swift +++ b/wake/View/Components/Upload/Upload.swift @@ -1,354 +0,0 @@ -import SwiftUI -import PhotosUI - -/// 上传响应数据结构体,用于解析服务器返回的上传URL和表单字段 -struct UploadResponse: Codable { - let url: String // 上传文件的目标URL - let fields: [String: String] // 上传所需的表单字段 -} - -/// 图片上传管理器,负责处理图片上传相关逻辑 -class ImageUploader: ObservableObject { - // MARK: - 发布属性 - @Published var isUploading = false // 是否正在上传 - @Published var uploadProgress: Double = 0 // 上传进度 - @Published var error: Error? // 上传错误信息 - - // 基础API地址 - private let baseURL = "https://api.memorywake.com/api/v1" - - /// 上传图片到服务器 - /// - Parameters: - /// - image: 要上传的图片 - /// - completion: 完成回调,返回上传结果 - func uploadImage(_ image: UIImage, completion: @escaping (Result) -> Void) { - // 1. 将图片转换为JPEG数据 - guard let imageData = image.jpegData(compressionQuality: 0.8) else { - completion(.failure(NSError(domain: "", code: -1, - userInfo: [NSLocalizedDescriptionKey: "图片数据转换失败"]))) - return - } - - // 2. 构建获取上传URL的请求 - guard let url = URL(string: "\(baseURL)/iam/file/generate-upload-url") else { - completion(.failure(NSError(domain: "", code: -1, - userInfo: [NSLocalizedDescriptionKey: "无效的URL"]))) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - // 3. 发起获取上传URL的请求 - URLSession.shared.dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - - // 处理网络错误 - if let error = error { - DispatchQueue.main.async { completion(.failure(error)) } - return - } - - // 检查响应数据 - guard let data = data else { - let error = NSError(domain: "", code: -1, - userInfo: [NSLocalizedDescriptionKey: "未收到服务器响应"]) - DispatchQueue.main.async { completion(.failure(error)) } - return - } - - do { - // 4. 解析上传URL响应 - let uploadResponse = try JSONDecoder().decode(UploadResponse.self, from: data) - // 5. 开始实际上传文件 - self.uploadFile(to: uploadResponse.url, - with: uploadResponse.fields, - imageData: imageData, - completion: completion) - } catch { - DispatchQueue.main.async { completion(.failure(error)) } - } - }.resume() - } - - /// 实际上传文件到指定的URL - /// - Parameters: - /// - url: 上传URL - /// - fields: 上传所需的表单字段 - /// - imageData: 图片数据 - /// - completion: 完成回调 - private func uploadFile(to url: String, - with fields: [String: String], - imageData: Data, - completion: @escaping (Result) -> Void) { - - // 验证上传URL - guard let uploadURL = URL(string: url) else { - completion(.failure(NSError(domain: "", code: -1, - userInfo: [NSLocalizedDescriptionKey: "无效的上传URL"]))) - return - } - - // 配置上传请求 - var request = URLRequest(url: uploadURL) - request.httpMethod = "POST" - - // 设置multipart/form-data边界 - let boundary = "Boundary-\(UUID().uuidString)" - request.setValue("multipart/form-data; boundary=\(boundary)", - forHTTPHeaderField: "Content-Type") - - // 构建请求体 - var body = Data() - - // 添加表单字段 - for (key, value) in fields { - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - body.append("\(value)\r\n".data(using: .utf8)!) - } - - // 添加图片数据 - let filename = "\(UUID().uuidString).jpg" - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) - body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) - body.append(imageData) - body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) - - request.httpBody = body - - // 执行上传任务 - URLSession.shared.dataTask(with: request) { _, _, error in - DispatchQueue.main.async { - if let error = error { - completion(.failure(error)) - } else { - completion(.success("上传成功")) - } - } - }.resume() - } -} - -/// 照片选择器,封装了系统相册选择功能 -struct PhotoPicker: UIViewControllerRepresentable { - // MARK: - 属性 - @Binding var selectedImages: [UIImage] // 绑定的已选图片数组 - let selectionLimit: Int // 最多可选图片数量 - let filter: PHPickerFilter // 图片过滤器 - var onUploadComplete: ((Result) -> Void)? = nil // 上传完成回调 - private let uploader = ImageUploader() // 图片上传器 - - // MARK: - 初始化方法 - init(selectedImages: Binding<[UIImage]>, - selectionLimit: Int = 1, - filter: PHPickerFilter = .images, - onUploadComplete: ((Result) -> Void)? = nil) { - self._selectedImages = selectedImages - self.selectionLimit = selectionLimit - self.filter = filter - self.onUploadComplete = onUploadComplete - } - - // MARK: - UIViewControllerRepresentable 协议方法 - - /// 创建并返回PHPickerViewController实例 - 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: - 协调器类 - - /// 协调器,处理PHPickerViewController的代理方法 - class Coordinator: NSObject, PHPickerViewControllerDelegate { - let parent: PhotoPicker - - init(_ parent: PhotoPicker) { - self.parent = parent - } - - /// 用户完成图片选择时调用 - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - parent.selectedImages.removeAll() - - let group = DispatchGroup() - var loadedImages: [Int: UIImage] = [:] - - // 加载选中的图片 - 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 - - // 立即上传图片 - self?.parent.uploader.uploadImage(image) { result in - self?.parent.onUploadComplete?(result) - } - } - group.leave() - } - } else { - group.leave() - } - } - - // 所有图片加载完成后的处理 - group.notify(queue: .main) { - // 按原始顺序排序图片 - let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } - self.parent.selectedImages.append(contentsOf: sortedImages) - - // 关闭选择器 - picker.dismiss(animated: true) - } - } - } -} - -// MARK: - 头像上传组件 - -/// 头像上传视图,提供头像选择、上传和显示功能 -struct AvatarUploader: View { - // MARK: - 属性 - @Binding var selectedImage: UIImage? // 当前选中的头像图片 - let size: CGFloat // 头像尺寸 - - // MARK: - 状态 - @State private var isImagePickerPresented = false // 是否显示图片选择器 - @State private var isUploading = false // 是否正在上传 - @State private var uploadError: Error? // 上传错误信息 - - // MARK: - 视图 - var body: some View { - Button(action: showImagePicker) { - 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) - ) - } - - // 上传加载指示器 - if isUploading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(1.5) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: size * 0.1)) - } - } - .frame(width: size, height: size) - .contentShape(Rectangle()) // 使整个区域可点击 - } - .buttonStyle(PlainButtonStyle()) // 移除按钮默认样式 - .alert("上传失败", isPresented: .constant(uploadError != nil)) { - Button("确定") { - uploadError = nil - } - } message: { - if let error = uploadError { - Text(error.localizedDescription) - } else { - Text("请重试") - } - } - .sheet(isPresented: $isImagePickerPresented) { - // 图片选择器 - PhotoPicker( - selectedImages: Binding( - get: { [selectedImage].compactMap { $0 } }, - set: { images in - selectedImage = images.first - } - ), - selectionLimit: 1, - onUploadComplete: handleUploadResult - ) - .onAppear { - isUploading = true - } - } - } - - // MARK: - 私有方法 - - /// 显示图片选择器 - private func showImagePicker() { - isImagePickerPresented = true - } - - /// 处理上传结果 - private func handleUploadResult(_ result: Result) { - isUploading = false - - switch result { - case .success(let message): - print("上传成功: \(message)") - case .failure(let error): - uploadError = error - print("上传失败: \(error.localizedDescription)") - } - } -} - -// MARK: - 预览提供程序 -#if DEBUG -struct AvatarUploader_Previews: PreviewProvider { - static var previews: some View { - VStack(spacing: 20) { - // 默认状态预览 - AvatarUploader(selectedImage: .constant(nil), size: 100) - .padding() - .previewDisplayName("默认状态") - - // 已选择图片状态预览 - AvatarUploader( - selectedImage: .constant(UIImage(systemName: "person.crop.circle.fill")), - size: 120 - ) - .padding() - .previewDisplayName("已选择图片") - } - } -} -#endif \ No newline at end of file