feat: 确认上传
This commit is contained in:
parent
53aa3f04cc
commit
13f458fff6
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
}
|
||||
=======
|
||||
}
|
||||
>>>>>>> a207b78 (feat: 确认上传)
|
||||
|
||||
@ -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<ImageUploaderGetID.UploadResult, Error>) -> 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<ImageUploaderGetID.UploadResult, Error>) -> 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<ImageUploadService.UploadResults, Error>) -> Void)?
|
||||
|
||||
// MARK: - 状态
|
||||
/// 上传完成回调,返回上传结果或错误
|
||||
var onUploadComplete: ((Result<ImageUploaderGetID.UploadResult, Error>) -> 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
0
wake/View/Components/Upload/Compression.swift
Normal file
0
wake/View/Components/Upload/Compression.swift
Normal file
@ -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<UploadResult, Error>) -> Void
|
||||
) {
|
||||
=======
|
||||
/// - completion: 完成回调,返回Result类型的结果
|
||||
public func uploadImage(_ image: UIImage, completion: @escaping (Result<UploadResult, Error>) -> 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<UploadResult, Error>) -> 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: - 响应模型
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user