From 13f458fff6f467d228ba0432bcbb371b7e50a64f Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Tue, 19 Aug 2025 18:28:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A1=AE=E8=AE=A4=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/Utils/APIConfig.swift | 14 +- wake/View/Components/Upload/Avatar.swift | 154 +++++++++++++++--- wake/View/Components/Upload/Compression.swift | 0 .../Upload/ImageUploaderGetID.swift | 48 ++++++ 4 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 wake/View/Components/Upload/Compression.swift diff --git a/wake/Utils/APIConfig.swift b/wake/Utils/APIConfig.swift index a65ae2a..3d3d33b 100644 --- a/wake/Utils/APIConfig.swift +++ b/wake/Utils/APIConfig.swift @@ -3,6 +3,7 @@ import Foundation /// API 配置信息 public enum APIConfig { /// API 基础 URL +<<<<<<< HEAD public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1" /// 认证 token - 从 Keychain 中获取 @@ -16,6 +17,13 @@ public enum APIConfig { return token } +======= + public static let baseURL = "https://api.memorywake.com/api/v1" + + /// 认证 token - 生产环境中应该存储在 Keychain 中 + public static let authToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA" + +>>>>>>> a207b78 (feat: 确认上传) /// 认证请求头 public static var authHeaders: [String: String] { return [ @@ -24,4 +32,8 @@ public enum APIConfig { "Accept": "application/json" ] } -} \ No newline at end of file +<<<<<<< HEAD +} +======= +} +>>>>>>> a207b78 (feat: 确认上传) diff --git a/wake/View/Components/Upload/Avatar.swift b/wake/View/Components/Upload/Avatar.swift index d3f3925..9f665fb 100644 --- a/wake/View/Components/Upload/Avatar.swift +++ b/wake/View/Components/Upload/Avatar.swift @@ -1,6 +1,7 @@ import SwiftUI import PhotosUI +<<<<<<< HEAD <<<<<<< HEAD // MARK: - Photo Picker ======= @@ -162,37 +163,72 @@ class ImageUploader: ObservableObject { } >>>>>>> a4890a4 (feat: 图片上传互获取url) +======= +>>>>>>> a207b78 (feat: 确认上传) /// 照片选择器,封装了系统相册选择功能 +/// 使用UIViewControllerRepresentable包装PHPickerViewController,提供SwiftUI兼容的图片选择界面 struct PhotoPicker: UIViewControllerRepresentable { +<<<<<<< HEAD +======= + // MARK: - Properties + + /// 绑定的已选图片数组,用于存储用户选择的图片 +>>>>>>> a207b78 (feat: 确认上传) @Binding var selectedImages: [UIImage] - /// 最多可选图片数量 + /// 最多可选图片数量,默认为1 let selectionLimit: Int - /// 图片过滤器,默认为图片类型 + /// 图片过滤器,默认为图片类型,可过滤特定类型的媒体 let filter: PHPickerFilter +<<<<<<< HEAD <<<<<<< HEAD ======= /// 图片上传管理器 private let uploader = ImageUploader() +======= + /// 图片上传完成回调,返回上传结果或错误 + var onImageUploaded: ((Result) -> Void)? +>>>>>>> a207b78 (feat: 确认上传) - // MARK: - 初始化方法 + // 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: 确认上传) self._selectedImages = selectedImages self.selectionLimit = selectionLimit self.filter = filter self.onImageUploaded = onImageUploaded +<<<<<<< HEAD self.onUploadProgress = onUploadProgress } // MARK: - UIViewControllerRepresentable // MARK: - UIViewControllerRepresentable 协议方法 +======= + } + // MARK: - UIViewControllerRepresentable +>>>>>>> a207b78 (feat: 确认上传) + + /// 创建并返回配置好的PHPickerViewController func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) configuration.filter = filter @@ -204,54 +240,100 @@ struct PhotoPicker: UIViewControllerRepresentable { return picker } + /// 更新视图控制器(空实现,因为不需要更新) func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} + /// 创建协调器,用于处理PHPickerViewController的代理方法 func makeCoordinator() -> Coordinator { Coordinator(self) } // MARK: - Coordinator +<<<<<<< HEAD // MARK: - 协调器类 +======= +>>>>>>> a207b78 (feat: 确认上传) + /// 协调器类,处理PHPickerViewController的代理方法 class Coordinator: NSObject, PHPickerViewControllerDelegate { + /// 对父视图的弱引用 let parent: PhotoPicker private let uploadService = ImageUploadService.shared + /// 图片上传器实例 + private let uploader = ImageUploaderGetID() + init(_ parent: PhotoPicker) { self.parent = parent } + /// 当用户完成图片选择时调用 + /// - Parameters: + /// - picker: 图片选择器实例 + /// - results: 用户选择的图片结果数组 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + // 清空已选图片 parent.selectedImages.removeAll() + // 使用DispatchGroup管理多个异步图片加载任务 let group = DispatchGroup() +<<<<<<< HEAD var loadedImages: [Int: UIImage] = [:] var uploadResults: [Int: ImageUploadService.UploadResults] = [:] var lastError: Error? +======= + var loadedImages: [Int: UIImage] = [:] // 用于保持图片顺序的字典 +>>>>>>> a207b78 (feat: 确认上传) + // 遍历所有选中的图片 for (index, result) in results.enumerated() { - group.enter() + group.enter() // 进入组 + // 检查是否可以加载图片 if result.itemProvider.canLoadObject(ofClass: UIImage.self) { - result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in + // 异步加载图片 + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in if let image = image as? UIImage { +<<<<<<< HEAD +======= + // 将加载的图片存入字典,保持原始顺序 +>>>>>>> a207b78 (feat: 确认上传) loadedImages[index] = image // 上传图片 - self.parent.uploader.uploadImage(image) { result in - // 这里可以添加上传完成后的处理 - // 目前按需求暂时不处理 + 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)") + + // 调用上传完成回调 + self?.parent.onImageUploaded?(.success(uploadResult)) + + case .failure(let error): + print("❌ 上传失败: \(error.localizedDescription)") + // 调用上传完成回调,传递错误 + self?.parent.onImageUploaded?(.failure(error)) + } + } } } - group.leave() + group.leave() // 离开组 } } else { - group.leave() + group.leave() // 如果无法加载图片,也离开组 } } + // 所有图片加载完成后的处理 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 } @@ -262,50 +344,71 @@ struct PhotoPicker: UIViewControllerRepresentable { 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: 确认上传) picker.dismiss(animated: true) } } } } +<<<<<<< HEAD // MARK: - Avatar Uploader Component +======= +// MARK: - AvatarUploader + +/// 头像上传视图,提供头像选择功能 +/// 封装了头像显示和选择逻辑,支持点击选择新头像 +>>>>>>> a207b78 (feat: 确认上传) struct AvatarUploader: View { - // MARK: - 属性 + // MARK: - Properties - /// 当前选中的头像图片 + /// 绑定的当前选中头像图片 @Binding var selectedImage: UIImage? - /// 头像尺寸 + /// 头像显示尺寸 let size: CGFloat var onUploadComplete: ((Result) -> Void)? - // MARK: - 状态 + /// 上传完成回调,返回上传结果或错误 + var onUploadComplete: ((Result) -> Void)? - /// 是否显示图片选择器 + // MARK: - State + + /// 控制图片选择器的显示状态 @State private var isImagePickerPresented = false - // MARK: - 视图 + // MARK: - Body var body: some View { - Button(action: { - isImagePickerPresented = true - }) { + // 头像按钮,点击后显示图片选择器 + 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 // Default avatar container ======= >>>>>>> a4890a4 (feat: 图片上传互获取url) +======= + // 默认头像占位视图 +>>>>>>> a207b78 (feat: 确认上传) Color.gray.opacity(0.1) .frame(width: size, height: size) .overlay( @@ -320,6 +423,7 @@ struct AvatarUploader: View { } } .frame(width: size, height: size) +<<<<<<< HEAD <<<<<<< HEAD .contentShape(Rectangle()) // Make the entire area tappable } @@ -329,7 +433,13 @@ struct AvatarUploader: View { } .buttonStyle(PlainButtonStyle()) >>>>>>> a4890a4 (feat: 图片上传互获取url) +======= + .contentShape(Rectangle()) // 确保整个区域都可点击 + } + .buttonStyle(PlainButtonStyle()) // 使用无样式按钮 +>>>>>>> a207b78 (feat: 确认上传) .sheet(isPresented: $isImagePickerPresented) { + // 显示图片选择器 PhotoPicker( selectedImages: Binding( get: { [selectedImage].compactMap { $0 } }, @@ -337,7 +447,11 @@ struct AvatarUploader: View { selectedImage = images.first } ), - selectionLimit: 1 + selectionLimit: 1, // 限制只能选择一张图片 + onImageUploaded: { result in + // 图片上传完成后的处理 + onUploadComplete?(result) + } ) } } diff --git a/wake/View/Components/Upload/Compression.swift b/wake/View/Components/Upload/Compression.swift new file mode 100644 index 0000000..e69de29 diff --git a/wake/View/Components/Upload/ImageUploaderGetID.swift b/wake/View/Components/Upload/ImageUploaderGetID.swift index 7699858..38b4a9b 100644 --- a/wake/View/Components/Upload/ImageUploaderGetID.swift +++ b/wake/View/Components/Upload/ImageUploaderGetID.swift @@ -7,7 +7,11 @@ public class ImageUploaderGetID: ObservableObject { // MARK: - 类型定义 /// 上传结果 +<<<<<<< HEAD public struct UploadResult: Codable { +======= + public struct UploadResult { +>>>>>>> a207b78 (feat: 确认上传) public let fileUrl: String public let fileName: String public let fileSize: Int @@ -29,7 +33,10 @@ public class ImageUploaderGetID: ObservableObject { case invalidResponse case uploadFailed(Error?) case invalidFileId +<<<<<<< HEAD case invalidResponseData +======= +>>>>>>> a207b78 (feat: 确认上传) public var errorDescription: String? { switch self { @@ -45,8 +52,11 @@ public class ImageUploaderGetID: ObservableObject { return "上传失败: \(error?.localizedDescription ?? "未知错误")" case .invalidFileId: return "无效的文件ID" +<<<<<<< HEAD case .invalidResponseData: return "无效的响应数据" +======= +>>>>>>> a207b78 (feat: 确认上传) } } } @@ -72,6 +82,7 @@ public class ImageUploaderGetID: ObservableObject { /// 上传图片到服务器 /// - Parameters: /// - image: 要上传的图片 +<<<<<<< HEAD /// - progress: 上传进度回调 (0.0 到 1.0) /// - completion: 完成回调 public func uploadImage( @@ -79,6 +90,10 @@ public class ImageUploaderGetID: ObservableObject { progress: @escaping (Double) -> Void, completion: @escaping (Result) -> Void ) { +======= + /// - completion: 完成回调,返回Result类型的结果 + public func uploadImage(_ image: UIImage, completion: @escaping (Result) -> Void) { +>>>>>>> a207b78 (feat: 确认上传) print("🔄 开始准备上传图片...") // 1. 转换图片为Data @@ -93,6 +108,7 @@ public class ImageUploaderGetID: ObservableObject { getUploadURL(for: imageData) { [weak self] result in switch result { case .success((let fileId, let uploadURL)): +<<<<<<< HEAD print("📤 获取到上传URL,开始上传文件...") // 3. 上传文件 @@ -124,6 +140,13 @@ public class ImageUploaderGetID: ObservableObject { case .failure(let error): print("❌ 获取上传URL失败: \(error.localizedDescription)") +======= + // 3. 确认上传 + self?.confirmUpload(fileId: fileId, fileName: "avatar_\(UUID().uuidString).jpg", fileSize: imageData.count) { confirmResult in + completion(confirmResult) + } + case .failure(let error): +>>>>>>> a207b78 (feat: 确认上传) completion(.failure(error)) } } @@ -200,8 +223,13 @@ public class ImageUploaderGetID: ObservableObject { /// 确认上传 private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result) -> Void) { +<<<<<<< HEAD let endpoint = "\(apiConfig.baseURL)/file/confirm-upload" guard let url = URL(string: endpoint) else { +======= + let urlString = "\(apiConfig.baseURL)/file/confirm-upload" + guard let url = URL(string: urlString) else { +>>>>>>> a207b78 (feat: 确认上传) completion(.failure(UploadError.invalidURL)) return } @@ -210,6 +238,7 @@ public class ImageUploaderGetID: ObservableObject { request.httpMethod = "POST" request.allHTTPHeaderFields = apiConfig.authHeaders +<<<<<<< HEAD let body: [String: Any] = [ "file_id": fileId, "file_name": fileName, @@ -221,17 +250,28 @@ public class ImageUploaderGetID: ObservableObject { print("📤 确认上传请求,fileId: \(fileId), 文件名: \(fileName)") } catch { print("❌ 序列化确认上传参数失败: \(error.localizedDescription)") +======= + let requestBody: [String: Any] = ["file_id": fileId] + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) + } catch { +>>>>>>> a207b78 (feat: 确认上传) completion(.failure(error)) return } let task = session.dataTask(with: request) { data, response, error in if let error = error { +<<<<<<< HEAD print("❌ 确认上传请求失败: \(error.localizedDescription)") +======= +>>>>>>> a207b78 (feat: 确认上传) completion(.failure(UploadError.uploadFailed(error))) return } +<<<<<<< HEAD guard let httpResponse = response as? HTTPURLResponse else { print("❌ 无效的服务器响应") completion(.failure(UploadError.invalidResponse)) @@ -243,6 +283,11 @@ public class ImageUploaderGetID: ObservableObject { let errorMessage = "确认上传失败,状态码: \(statusCode)" print("❌ \(errorMessage)") completion(.failure(UploadError.serverError(errorMessage))) +======= + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + completion(.failure(UploadError.serverError("确认上传失败,状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)"))) +>>>>>>> a207b78 (feat: 确认上传) return } @@ -260,6 +305,7 @@ public class ImageUploaderGetID: ObservableObject { task.resume() } +<<<<<<< HEAD /// 上传文件到指定URL /// - Parameters: @@ -440,6 +486,8 @@ private extension URLSessionTask { } } } +======= +>>>>>>> a207b78 (feat: 确认上传) } // MARK: - 响应模型