feat: 确认上传

This commit is contained in:
jinyaqiu 2025-08-19 18:28:49 +08:00
parent 53aa3f04cc
commit 13f458fff6
4 changed files with 195 additions and 21 deletions

View File

@ -3,6 +3,7 @@ import Foundation
/// API /// API
public enum APIConfig { public enum APIConfig {
/// API URL /// API URL
<<<<<<< HEAD
public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1" public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1"
/// token - Keychain /// token - Keychain
@ -16,6 +17,13 @@ public enum APIConfig {
return token 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] { public static var authHeaders: [String: String] {
return [ return [
@ -24,4 +32,8 @@ public enum APIConfig {
"Accept": "application/json" "Accept": "application/json"
] ]
} }
} <<<<<<< HEAD
}
=======
}
>>>>>>> a207b78 (feat: )

View File

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import PhotosUI import PhotosUI
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// MARK: - Photo Picker // MARK: - Photo Picker
======= =======
@ -162,37 +163,72 @@ class ImageUploader: ObservableObject {
} }
>>>>>>> a4890a4 (feat: url) >>>>>>> a4890a4 (feat: url)
=======
>>>>>>> a207b78 (feat: )
/// ///
/// 使UIViewControllerRepresentablePHPickerViewControllerSwiftUI
struct PhotoPicker: UIViewControllerRepresentable { struct PhotoPicker: UIViewControllerRepresentable {
<<<<<<< HEAD
=======
// MARK: - Properties
///
>>>>>>> a207b78 (feat: )
@Binding var selectedImages: [UIImage] @Binding var selectedImages: [UIImage]
/// /// 1
let selectionLimit: Int let selectionLimit: Int
/// ///
let filter: PHPickerFilter let filter: PHPickerFilter
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
======= =======
/// ///
private let uploader = ImageUploader() private let uploader = ImageUploader()
=======
///
var onImageUploaded: ((Result<ImageUploaderGetID.UploadResult, Error>) -> Void)?
>>>>>>> a207b78 (feat: )
// MARK: - // MARK: - Initialization
/// ///
<<<<<<< HEAD
>>>>>>> a4890a4 (feat: url) >>>>>>> a4890a4 (feat: url)
init(selectedImages: Binding<[UIImage]>, selectionLimit: Int = 1, filter: PHPickerFilter = .images) { 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._selectedImages = selectedImages
self.selectionLimit = selectionLimit self.selectionLimit = selectionLimit
self.filter = filter self.filter = filter
self.onImageUploaded = onImageUploaded self.onImageUploaded = onImageUploaded
<<<<<<< HEAD
self.onUploadProgress = onUploadProgress self.onUploadProgress = onUploadProgress
} }
// MARK: - UIViewControllerRepresentable // MARK: - UIViewControllerRepresentable
// MARK: - UIViewControllerRepresentable // MARK: - UIViewControllerRepresentable
=======
}
// MARK: - UIViewControllerRepresentable
>>>>>>> a207b78 (feat: )
/// PHPickerViewController
func makeUIViewController(context: Context) -> PHPickerViewController { func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = filter configuration.filter = filter
@ -204,54 +240,100 @@ struct PhotoPicker: UIViewControllerRepresentable {
return picker return picker
} }
///
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
/// PHPickerViewController
func makeCoordinator() -> Coordinator { func makeCoordinator() -> Coordinator {
Coordinator(self) Coordinator(self)
} }
// MARK: - Coordinator // MARK: - Coordinator
<<<<<<< HEAD
// MARK: - // MARK: -
=======
>>>>>>> a207b78 (feat: )
/// PHPickerViewController
class Coordinator: NSObject, PHPickerViewControllerDelegate { class Coordinator: NSObject, PHPickerViewControllerDelegate {
///
let parent: PhotoPicker let parent: PhotoPicker
private let uploadService = ImageUploadService.shared private let uploadService = ImageUploadService.shared
///
private let uploader = ImageUploaderGetID()
init(_ parent: PhotoPicker) { init(_ parent: PhotoPicker) {
self.parent = parent self.parent = parent
} }
///
/// - Parameters:
/// - picker:
/// - results:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
//
parent.selectedImages.removeAll() parent.selectedImages.removeAll()
// 使DispatchGroup
let group = DispatchGroup() let group = DispatchGroup()
<<<<<<< HEAD
var loadedImages: [Int: UIImage] = [:] var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: ImageUploadService.UploadResults] = [:] var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
var lastError: Error? var lastError: Error?
=======
var loadedImages: [Int: UIImage] = [:] //
>>>>>>> a207b78 (feat: )
//
for (index, result) in results.enumerated() { for (index, result) in results.enumerated() {
group.enter() group.enter() //
//
if result.itemProvider.canLoadObject(ofClass: UIImage.self) { 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 { if let image = image as? UIImage {
<<<<<<< HEAD
=======
//
>>>>>>> a207b78 (feat: )
loadedImages[index] = image 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 { } else {
group.leave() group.leave() //
} }
} }
//
group.notify(queue: .main) { group.notify(queue: .main) {
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// Sort the images by their original index to maintain selection order // Sort the images by their original index to maintain selection order
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } 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 } let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
self.parent.selectedImages.append(contentsOf: sortedImages) self.parent.selectedImages.append(contentsOf: sortedImages)
>>>>>>> a4890a4 (feat: url) >>>>>>> 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) picker.dismiss(animated: true)
} }
} }
} }
} }
<<<<<<< HEAD
// MARK: - Avatar Uploader Component // MARK: - Avatar Uploader Component
=======
// MARK: - AvatarUploader
///
///
>>>>>>> a207b78 (feat: )
struct AvatarUploader: View { struct AvatarUploader: View {
// MARK: - // MARK: - Properties
/// ///
@Binding var selectedImage: UIImage? @Binding var selectedImage: UIImage?
/// ///
let size: CGFloat let size: CGFloat
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
// MARK: - ///
var onUploadComplete: ((Result<ImageUploaderGetID.UploadResult, Error>) -> Void)?
/// // MARK: - State
///
@State private var isImagePickerPresented = false @State private var isImagePickerPresented = false
// MARK: - // MARK: - Body
var body: some View { var body: some View {
Button(action: { //
isImagePickerPresented = true Button(action: { isImagePickerPresented = true }) {
}) {
ZStack { ZStack {
<<<<<<< HEAD <<<<<<< HEAD
// Avatar Image or Placeholder // Avatar Image or Placeholder
======= =======
>>>>>>> a4890a4 (feat: url) >>>>>>> a4890a4 (feat: url)
if let selectedImage = selectedImage { if let selectedImage = selectedImage {
//
Image(uiImage: selectedImage) Image(uiImage: selectedImage)
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: size, height: size) .frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: size * 0.1)) .clipShape(RoundedRectangle(cornerRadius: size * 0.1))
} else { } else {
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// Default avatar container // Default avatar container
======= =======
>>>>>>> a4890a4 (feat: url) >>>>>>> a4890a4 (feat: url)
=======
//
>>>>>>> a207b78 (feat: )
Color.gray.opacity(0.1) Color.gray.opacity(0.1)
.frame(width: size, height: size) .frame(width: size, height: size)
.overlay( .overlay(
@ -320,6 +423,7 @@ struct AvatarUploader: View {
} }
} }
.frame(width: size, height: size) .frame(width: size, height: size)
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
.contentShape(Rectangle()) // Make the entire area tappable .contentShape(Rectangle()) // Make the entire area tappable
} }
@ -329,7 +433,13 @@ struct AvatarUploader: View {
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
>>>>>>> a4890a4 (feat: url) >>>>>>> a4890a4 (feat: url)
=======
.contentShape(Rectangle()) //
}
.buttonStyle(PlainButtonStyle()) // 使
>>>>>>> a207b78 (feat: )
.sheet(isPresented: $isImagePickerPresented) { .sheet(isPresented: $isImagePickerPresented) {
//
PhotoPicker( PhotoPicker(
selectedImages: Binding( selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } }, get: { [selectedImage].compactMap { $0 } },
@ -337,7 +447,11 @@ struct AvatarUploader: View {
selectedImage = images.first selectedImage = images.first
} }
), ),
selectionLimit: 1 selectionLimit: 1, //
onImageUploaded: { result in
//
onUploadComplete?(result)
}
) )
} }
} }

View File

@ -7,7 +7,11 @@ public class ImageUploaderGetID: ObservableObject {
// MARK: - // MARK: -
/// ///
<<<<<<< HEAD
public struct UploadResult: Codable { public struct UploadResult: Codable {
=======
public struct UploadResult {
>>>>>>> a207b78 (feat: )
public let fileUrl: String public let fileUrl: String
public let fileName: String public let fileName: String
public let fileSize: Int public let fileSize: Int
@ -29,7 +33,10 @@ public class ImageUploaderGetID: ObservableObject {
case invalidResponse case invalidResponse
case uploadFailed(Error?) case uploadFailed(Error?)
case invalidFileId case invalidFileId
<<<<<<< HEAD
case invalidResponseData case invalidResponseData
=======
>>>>>>> a207b78 (feat: )
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
@ -45,8 +52,11 @@ public class ImageUploaderGetID: ObservableObject {
return "上传失败: \(error?.localizedDescription ?? "未知错误")" return "上传失败: \(error?.localizedDescription ?? "未知错误")"
case .invalidFileId: case .invalidFileId:
return "无效的文件ID" return "无效的文件ID"
<<<<<<< HEAD
case .invalidResponseData: case .invalidResponseData:
return "无效的响应数据" return "无效的响应数据"
=======
>>>>>>> a207b78 (feat: )
} }
} }
} }
@ -72,6 +82,7 @@ public class ImageUploaderGetID: ObservableObject {
/// ///
/// - Parameters: /// - Parameters:
/// - image: /// - image:
<<<<<<< HEAD
/// - progress: (0.0 1.0) /// - progress: (0.0 1.0)
/// - completion: /// - completion:
public func uploadImage( public func uploadImage(
@ -79,6 +90,10 @@ public class ImageUploaderGetID: ObservableObject {
progress: @escaping (Double) -> Void, progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, Error>) -> Void completion: @escaping (Result<UploadResult, Error>) -> Void
) { ) {
=======
/// - completion: Result
public func uploadImage(_ image: UIImage, completion: @escaping (Result<UploadResult, Error>) -> Void) {
>>>>>>> a207b78 (feat: )
print("🔄 开始准备上传图片...") print("🔄 开始准备上传图片...")
// 1. Data // 1. Data
@ -93,6 +108,7 @@ public class ImageUploaderGetID: ObservableObject {
getUploadURL(for: imageData) { [weak self] result in getUploadURL(for: imageData) { [weak self] result in
switch result { switch result {
case .success((let fileId, let uploadURL)): case .success((let fileId, let uploadURL)):
<<<<<<< HEAD
print("📤 获取到上传URL开始上传文件...") print("📤 获取到上传URL开始上传文件...")
// 3. // 3.
@ -124,6 +140,13 @@ public class ImageUploaderGetID: ObservableObject {
case .failure(let error): case .failure(let error):
print("❌ 获取上传URL失败: \(error.localizedDescription)") 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)) 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) { private func confirmUpload(fileId: String, fileName: String, fileSize: Int, completion: @escaping (Result<UploadResult, Error>) -> Void) {
<<<<<<< HEAD
let endpoint = "\(apiConfig.baseURL)/file/confirm-upload" let endpoint = "\(apiConfig.baseURL)/file/confirm-upload"
guard let url = URL(string: endpoint) else { 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)) completion(.failure(UploadError.invalidURL))
return return
} }
@ -210,6 +238,7 @@ public class ImageUploaderGetID: ObservableObject {
request.httpMethod = "POST" request.httpMethod = "POST"
request.allHTTPHeaderFields = apiConfig.authHeaders request.allHTTPHeaderFields = apiConfig.authHeaders
<<<<<<< HEAD
let body: [String: Any] = [ let body: [String: Any] = [
"file_id": fileId, "file_id": fileId,
"file_name": fileName, "file_name": fileName,
@ -221,17 +250,28 @@ public class ImageUploaderGetID: ObservableObject {
print("📤 确认上传请求fileId: \(fileId), 文件名: \(fileName)") print("📤 确认上传请求fileId: \(fileId), 文件名: \(fileName)")
} catch { } catch {
print("❌ 序列化确认上传参数失败: \(error.localizedDescription)") print("❌ 序列化确认上传参数失败: \(error.localizedDescription)")
=======
let requestBody: [String: Any] = ["file_id": fileId]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
} catch {
>>>>>>> a207b78 (feat: )
completion(.failure(error)) completion(.failure(error))
return return
} }
let task = session.dataTask(with: request) { data, response, error in let task = session.dataTask(with: request) { data, response, error in
if let error = error { if let error = error {
<<<<<<< HEAD
print("❌ 确认上传请求失败: \(error.localizedDescription)") print("❌ 确认上传请求失败: \(error.localizedDescription)")
=======
>>>>>>> a207b78 (feat: )
completion(.failure(UploadError.uploadFailed(error))) completion(.failure(UploadError.uploadFailed(error)))
return return
} }
<<<<<<< HEAD
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
print("❌ 无效的服务器响应") print("❌ 无效的服务器响应")
completion(.failure(UploadError.invalidResponse)) completion(.failure(UploadError.invalidResponse))
@ -243,6 +283,11 @@ public class ImageUploaderGetID: ObservableObject {
let errorMessage = "确认上传失败,状态码: \(statusCode)" let errorMessage = "确认上传失败,状态码: \(statusCode)"
print("\(errorMessage)") print("\(errorMessage)")
completion(.failure(UploadError.serverError(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 return
} }
@ -260,6 +305,7 @@ public class ImageUploaderGetID: ObservableObject {
task.resume() task.resume()
} }
<<<<<<< HEAD
/// URL /// URL
/// - Parameters: /// - Parameters:
@ -440,6 +486,8 @@ private extension URLSessionTask {
} }
} }
} }
=======
>>>>>>> a207b78 (feat: )
} }
// MARK: - // MARK: -