feat: 上传进度

This commit is contained in:
jinyaqiu 2025-08-19 19:17:11 +08:00
parent d0f0b09f8a
commit a63d363001
3 changed files with 205 additions and 42 deletions

View File

@ -4,6 +4,7 @@ import PhotosUI
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
// MARK: - Photo Picker // MARK: - Photo Picker
======= =======
/// ///
@ -174,22 +175,54 @@ struct UploadResults {
} }
>>>>>>> 5611df8 (feat: ) >>>>>>> 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: )
/// ///
/// 使UIViewControllerRepresentablePHPickerViewControllerSwiftUI
struct PhotoPicker: UIViewControllerRepresentable { struct PhotoPicker: UIViewControllerRepresentable {
<<<<<<< HEAD <<<<<<< HEAD
======= =======
// MARK: - Properties // MARK: - Properties
<<<<<<< HEAD
/// ///
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (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
<<<<<<< HEAD <<<<<<< HEAD
@ -224,15 +257,30 @@ struct PhotoPicker: UIViewControllerRepresentable {
onImageUploaded: ((Result<UploadResults, Error>) -> Void)? = nil onImageUploaded: ((Result<UploadResults, Error>) -> Void)? = nil
) { ) {
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
var onImageUploaded: ((Result<UploadResults, Error>) -> Void)?
var onUploadProgress: ((UploadProgress) -> Void)?
// MARK: - Initialization
init(selectedImages: Binding<[UIImage]>,
selectionLimit: Int = 1,
filter: PHPickerFilter = .images,
onImageUploaded: ((Result<UploadResults, Error>) -> Void)? = nil,
onUploadProgress: ((UploadProgress) -> Void)? = nil) {
>>>>>>> 8e641fd (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 <<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
self.onUploadProgress = onUploadProgress self.onUploadProgress = onUploadProgress
} }
// MARK: - UIViewControllerRepresentable // MARK: - UIViewControllerRepresentable
<<<<<<< HEAD
// MARK: - UIViewControllerRepresentable // MARK: - UIViewControllerRepresentable
======= =======
@ -242,6 +290,8 @@ struct PhotoPicker: UIViewControllerRepresentable {
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
/// PHPickerViewController /// PHPickerViewController
=======
>>>>>>> 8e641fd (feat: )
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
@ -253,15 +303,14 @@ 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
<<<<<<< HEAD <<<<<<< HEAD
// MARK: - // MARK: -
@ -269,25 +318,26 @@ struct PhotoPicker: UIViewControllerRepresentable {
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
/// PHPickerViewController /// PHPickerViewController
=======
>>>>>>> 8e641fd (feat: )
class Coordinator: NSObject, PHPickerViewControllerDelegate { class Coordinator: NSObject, PHPickerViewControllerDelegate {
///
let parent: PhotoPicker let parent: PhotoPicker
<<<<<<< HEAD
private let uploadService = ImageUploadService.shared private let uploadService = ImageUploadService.shared
/// ///
=======
>>>>>>> 8e641fd (feat: )
private let uploader = ImageUploaderGetID() private let uploader = ImageUploaderGetID()
init(_ parent: PhotoPicker) { init(_ parent: PhotoPicker) {
self.parent = parent self.parent = parent
} }
///
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
<<<<<<< HEAD <<<<<<< HEAD
var loadedImages: [Int: UIImage] = [:] var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: ImageUploadService.UploadResults] = [:] var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
@ -300,10 +350,14 @@ struct PhotoPicker: UIViewControllerRepresentable {
var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?, var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?,
compressed: ImageUploaderGetID.UploadResult?)] = [:] compressed: ImageUploaderGetID.UploadResult?)] = [:]
>>>>>>> 5611df8 (feat: ) >>>>>>> 5611df8 (feat: )
=======
var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?,
compressed: ImageUploaderGetID.UploadResult?)] = [:]
>>>>>>> 8e641fd (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) { [weak self] (image, error) in result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
@ -320,15 +374,14 @@ struct PhotoPicker: UIViewControllerRepresentable {
return return
} }
// 1.
loadedImages[index] = image loadedImages[index] = image
// 2. 50%
guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else { guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else {
group.leave() group.leave()
return return
} }
<<<<<<< HEAD
// 3. // 3.
self.uploader.uploadImage(image) { [weak self] originalResult in self.uploader.uploadImage(image) { [weak self] originalResult in
guard let self = self else { guard let self = self else {
@ -371,19 +424,89 @@ struct PhotoPicker: UIViewControllerRepresentable {
print("❌ 压缩图上传失败: \(error.localizedDescription)") print("❌ 压缩图上传失败: \(error.localizedDescription)")
uploadResults[index] = (originalUploadResult, nil) 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): case .failure(let error):
print("❌ 原图上传失败: \(error.localizedDescription)") print("❌ 原图上传失败: \(error.localizedDescription)")
group.leave() group.leave()
} }
} }
)
} }
} else { } else {
group.leave() group.leave()
} }
} }
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// //
group.notify(queue: .main) { group.notify(queue: .main) {
@ -407,35 +530,39 @@ struct PhotoPicker: UIViewControllerRepresentable {
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
======= =======
// //
=======
>>>>>>> 8e641fd (feat: )
group.notify(queue: .main) { [weak self] in group.notify(queue: .main) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
// 1.
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)
// 2.
if let firstResult = uploadResults.first?.value, if let firstResult = uploadResults.first?.value,
let original = firstResult.original, let original = firstResult.original,
let compressed = firstResult.compressed { let compressed = firstResult.compressed {
// 3.
let results = UploadResults(original: original, compressed: compressed) let results = UploadResults(original: original, compressed: compressed)
self.parent.onImageUploaded?(.success(results)) self.parent.onImageUploaded?(.success(results))
} else { } else {
// 4. self.parent.onImageUploaded?(.failure(NSError(
self.parent.onImageUploaded?(.failure(NSError(domain: "com.wake.upload", domain: "com.wake.upload",
code: -1, code: -1,
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]))) userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]
)))
} }
<<<<<<< HEAD
// 5. // 5.
>>>>>>> 5611df8 (feat: ) >>>>>>> 5611df8 (feat: )
=======
>>>>>>> 8e641fd (feat: )
picker.dismiss(animated: true) picker.dismiss(animated: true)
} }
} }
} }
} }
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// MARK: - Avatar Uploader Component // MARK: - Avatar Uploader Component
======= =======
@ -444,28 +571,25 @@ struct PhotoPicker: UIViewControllerRepresentable {
/// ///
/// ///
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
// MARK: - Avatar Uploader
///
>>>>>>> 8e641fd (feat: )
struct AvatarUploader: View { struct AvatarUploader: View {
// MARK: - Properties
///
@Binding var selectedImage: UIImage? @Binding var selectedImage: UIImage?
///
let size: CGFloat let size: CGFloat
<<<<<<< HEAD
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
/// ///
=======
>>>>>>> 8e641fd (feat: )
var onUploadComplete: ((Result<UploadResults, Error>) -> Void)? var onUploadComplete: ((Result<UploadResults, Error>) -> Void)?
// MARK: - State
///
@State private var isImagePickerPresented = false @State private var isImagePickerPresented = false
// MARK: - Body
var body: some View { var body: some View {
//
Button(action: { isImagePickerPresented = true }) { Button(action: { isImagePickerPresented = true }) {
ZStack { ZStack {
<<<<<<< HEAD <<<<<<< HEAD
@ -473,7 +597,6 @@ struct AvatarUploader: View {
======= =======
>>>>>>> a4890a4 (feat: url) >>>>>>> a4890a4 (feat: url)
if let selectedImage = selectedImage { if let selectedImage = selectedImage {
//
Image(uiImage: selectedImage) Image(uiImage: selectedImage)
.resizable() .resizable()
.scaledToFill() .scaledToFill()
@ -481,6 +604,7 @@ struct AvatarUploader: View {
.clipShape(RoundedRectangle(cornerRadius: size * 0.1)) .clipShape(RoundedRectangle(cornerRadius: size * 0.1))
} else { } else {
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// Default avatar container // Default avatar container
======= =======
@ -488,6 +612,8 @@ struct AvatarUploader: View {
======= =======
// //
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
Color.gray.opacity(0.1) Color.gray.opacity(0.1)
.frame(width: size, height: size) .frame(width: size, height: size)
.overlay( .overlay(
@ -503,6 +629,7 @@ struct AvatarUploader: View {
} }
.frame(width: size, height: size) .frame(width: size, height: size)
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
.contentShape(Rectangle()) // Make the entire area tappable .contentShape(Rectangle()) // Make the entire area tappable
} }
@ -517,8 +644,12 @@ struct AvatarUploader: View {
} }
.buttonStyle(PlainButtonStyle()) // 使 .buttonStyle(PlainButtonStyle()) // 使
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
>>>>>>> 8e641fd (feat: )
.sheet(isPresented: $isImagePickerPresented) { .sheet(isPresented: $isImagePickerPresented) {
//
PhotoPicker( PhotoPicker(
selectedImages: Binding( selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } }, get: { [selectedImage].compactMap { $0 } },
@ -526,10 +657,12 @@ struct AvatarUploader: View {
selectedImage = images.first selectedImage = images.first
} }
), ),
selectionLimit: 1, // selectionLimit: 1,
onImageUploaded: { result in onImageUploaded: { result in
//
onUploadComplete?(result) onUploadComplete?(result)
},
onUploadProgress: { progress in
print("上传进度:\(progress.current)/\(progress.total),进度:\(progress.progress * 100)%")
} }
) )
} }

View File

@ -83,6 +83,9 @@ public class ImageUploaderGetID: ObservableObject {
/// - Parameters: /// - Parameters:
/// - image: /// - image:
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
/// - progress: (0.0 1.0) /// - progress: (0.0 1.0)
/// - completion: /// - completion:
public func uploadImage( public func uploadImage(
@ -90,10 +93,13 @@ public class ImageUploaderGetID: ObservableObject {
progress: @escaping (Double) -> Void, progress: @escaping (Double) -> Void,
completion: @escaping (Result<UploadResult, Error>) -> Void completion: @escaping (Result<UploadResult, Error>) -> Void
) { ) {
<<<<<<< HEAD
======= =======
/// - completion: Result /// - completion: Result
public func uploadImage(_ image: UIImage, completion: @escaping (Result<UploadResult, Error>) -> Void) { public func uploadImage(_ image: UIImage, completion: @escaping (Result<UploadResult, Error>) -> Void) {
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
print("🔄 开始准备上传图片...") print("🔄 开始准备上传图片...")
// 1. Data // 1. Data
@ -109,6 +115,9 @@ public class ImageUploaderGetID: ObservableObject {
switch result { switch result {
case .success((let fileId, let uploadURL)): case .success((let fileId, let uploadURL)):
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
print("📤 获取到上传URL开始上传文件...") print("📤 获取到上传URL开始上传文件...")
// 3. // 3.
@ -138,6 +147,7 @@ public class ImageUploaderGetID: ObservableObject {
} }
) )
<<<<<<< HEAD
case .failure(let error): case .failure(let error):
print("❌ 获取上传URL失败: \(error.localizedDescription)") print("❌ 获取上传URL失败: \(error.localizedDescription)")
======= =======
@ -147,6 +157,10 @@ public class ImageUploaderGetID: ObservableObject {
} }
case .failure(let error): case .failure(let error):
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
case .failure(let error):
print("❌ 获取上传URL失败: \(error.localizedDescription)")
>>>>>>> 8e641fd (feat: )
completion(.failure(error)) completion(.failure(error))
} }
} }
@ -306,6 +320,9 @@ public class ImageUploaderGetID: ObservableObject {
task.resume() task.resume()
} }
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
/// URL /// URL
/// - Parameters: /// - Parameters:
@ -334,6 +351,7 @@ public class ImageUploaderGetID: ObservableObject {
return return
} }
<<<<<<< HEAD
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(UploadError.invalidResponse)) completion(.failure(UploadError.invalidResponse))
return return
@ -341,6 +359,11 @@ public class ImageUploaderGetID: ObservableObject {
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else { guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
let statusCode = httpResponse.statusCode let statusCode = httpResponse.statusCode
=======
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
>>>>>>> 8e641fd (feat: )
completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)"))) completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)")))
return return
} }
@ -361,7 +384,11 @@ public class ImageUploaderGetID: ObservableObject {
task?.progress.cancel() task?.progress.cancel()
} }
} else { } else {
<<<<<<< HEAD
// iOS 11 使 // iOS 11 使
=======
// Fallback for earlier iOS versions
>>>>>>> 8e641fd (feat: )
var lastProgress: Double = 0 var lastProgress: Double = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
let bytesSent = task.countOfBytesSent let bytesSent = task.countOfBytesSent
@ -486,8 +513,11 @@ private extension URLSessionTask {
} }
} }
} }
<<<<<<< HEAD
======= =======
>>>>>>> a207b78 (feat: ) >>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
} }
// MARK: - // MARK: -