feat: 文件上传成功

This commit is contained in:
jinyaqiu 2025-08-19 19:33:10 +08:00
parent 84db43f3b5
commit 7aea7b1535
5 changed files with 267 additions and 135 deletions

View File

@ -1,52 +1,26 @@
import SwiftUI import SwiftUI
import PhotosUI import PhotosUI
// 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 // MARK: - Photo Picker
/// ///
struct PhotoPicker: UIViewControllerRepresentable { struct PhotoPicker: UIViewControllerRepresentable {
// MARK: - Properties // MARK: - Properties
@Binding var selectedImages: [UIImage] @Binding var selectedImages: [UIImage]
let selectionLimit: Int let selectionLimit: Int
let filter: PHPickerFilter let filter: PHPickerFilter
var onImageUploaded: ((Result<UploadResults, Error>) -> Void)? var onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
var onUploadProgress: ((UploadProgress) -> Void)? var onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)?
@Environment(\.presentationMode) private var presentationMode
// MARK: - Initialization // MARK: - Initialization
init(selectedImages: Binding<[UIImage]>, init(selectedImages: Binding<[UIImage]>,
selectionLimit: Int = 1, selectionLimit: Int = 1,
filter: PHPickerFilter = .images, filter: PHPickerFilter = .images,
onImageUploaded: ((Result<UploadResults, Error>) -> Void)? = nil, onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? = nil,
onUploadProgress: ((UploadProgress) -> Void)? = nil) { onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? = nil) {
self._selectedImages = selectedImages self._selectedImages = selectedImages
self.selectionLimit = selectionLimit self.selectionLimit = selectionLimit
self.filter = filter self.filter = filter
@ -55,6 +29,7 @@ struct PhotoPicker: UIViewControllerRepresentable {
} }
// MARK: - UIViewControllerRepresentable // MARK: - UIViewControllerRepresentable
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
@ -73,109 +48,77 @@ struct PhotoPicker: UIViewControllerRepresentable {
} }
// MARK: - Coordinator // MARK: - Coordinator
class Coordinator: NSObject, PHPickerViewControllerDelegate { class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: PhotoPicker let parent: PhotoPicker
private let uploader = ImageUploaderGetID() private let uploadService = ImageUploadService.shared
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]) {
guard !results.isEmpty else {
parent.presentationMode.wrappedValue.dismiss()
return
}
parent.selectedImages.removeAll() parent.selectedImages.removeAll()
let group = DispatchGroup() let group = DispatchGroup()
var loadedImages: [Int: UIImage] = [:] var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?, var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
compressed: ImageUploaderGetID.UploadResult?)] = [:] var lastError: Error?
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) { (image, error) in
guard let self = self, let image = image as? UIImage else { if let error = error {
lastError = error
group.leave()
return
}
guard let image = image as? UIImage else {
lastError = NSError(domain: "com.wake.upload", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to load image"])
group.leave() group.leave()
return return
} }
loadedImages[index] = image loadedImages[index] = image
guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else { // Upload the image
group.leave() self.uploadService.uploadOriginalAndCompressedImage(
return
}
self.uploader.uploadImage(
image, image,
progress: { [weak self] progress in compressionQuality: 0.5,
let progressInfo = UploadProgress( progress: { progress in
current: Int(progress * 100),
total: 100,
progress: progress,
isOriginal: true
)
DispatchQueue.main.async { DispatchQueue.main.async {
self?.parent.onUploadProgress?(progressInfo) self.parent.onUploadProgress?(progress)
} }
print("📤 原图上传进度: \(Int(progress * 100))%")
}, },
completion: { [weak self] originalResult in completion: { result in
guard let self = self else { defer { group.leave() }
group.leave()
return
}
switch originalResult { switch result {
case .success(let originalUploadResult): case .success(let results):
self.uploader.uploadImage( uploadResults[index] = results
compressedImage,
progress: { [weak self] progress in // Upload file info to backend
let progressInfo = UploadProgress( MaterialService.shared.uploadMaterialInfo(
current: Int(progress * 100), fileId: results.original.fileId,
total: 100, previewFileId: results.compressed.fileId
progress: progress, ) { success, errorMessage in
isOriginal: false if success {
) print("✅ 文件信息上传成功")
DispatchQueue.main.async { } else if let errorMessage = errorMessage {
self?.parent.onUploadProgress?(progressInfo) print("❌ 文件信息上传失败: \(errorMessage)")
}
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)") lastError = error
group.leave() print("❌ 图片上传失败: \(error.localizedDescription)")
} }
} }
) )
@ -188,23 +131,24 @@ struct PhotoPicker: UIViewControllerRepresentable {
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 }
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value } if let error = lastError {
self.parent.selectedImages.append(contentsOf: sortedImages) self.parent.onImageUploaded?(.failure(error))
if let firstResult = uploadResults.first?.value,
let original = firstResult.original,
let compressed = firstResult.compressed {
let results = UploadResults(original: original, compressed: compressed)
self.parent.onImageUploaded?(.success(results))
} else { } else {
self.parent.onImageUploaded?(.failure(NSError( let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
domain: "com.wake.upload", self.parent.selectedImages.append(contentsOf: sortedImages)
code: -1,
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"] if let firstResult = uploadResults.first?.value {
))) self.parent.onImageUploaded?(.success(firstResult))
} else {
self.parent.onImageUploaded?(.failure(NSError(
domain: "com.wake.upload",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]
)))
}
} }
picker.dismiss(animated: true) self.parent.presentationMode.wrappedValue.dismiss()
} }
} }
} }
@ -216,7 +160,7 @@ struct PhotoPicker: UIViewControllerRepresentable {
struct AvatarUploader: View { struct AvatarUploader: View {
@Binding var selectedImage: UIImage? @Binding var selectedImage: UIImage?
let size: CGFloat let size: CGFloat
var onUploadComplete: ((Result<UploadResults, Error>) -> Void)? var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
@State private var isImagePickerPresented = false @State private var isImagePickerPresented = false
@ -260,7 +204,7 @@ struct AvatarUploader: View {
onUploadComplete?(result) onUploadComplete?(result)
}, },
onUploadProgress: { progress in onUploadProgress: { progress in
print("上传进度:\(progress.current)/\(progress.total),进度:\(progress.progress * 100)%") print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%")
} }
) )
} }

View File

@ -0,0 +1,166 @@
import Foundation
import UIKit
///
public class ImageUploadService {
// MARK: - Shared Instance
public static let shared = ImageUploadService()
// MARK: - Properties
private let uploader: ImageUploaderGetID
// MARK: - Initialization
public init(uploader: ImageUploaderGetID = ImageUploaderGetID()) {
self.uploader = uploader
}
// MARK: - Public Methods
///
/// - Parameters:
/// - image:
/// - progressHandler: (0.0 1.0)
/// - completion:
public func uploadImage(
_ image: UIImage,
progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void,
completion: @escaping (Result<ImageUploaderGetID.UploadResult, Error>) -> Void
) {
uploader.uploadImage(
image,
progress: { progress in
let progressInfo = ImageUploadService.UploadProgress(
current: Int(progress * 100),
total: 100,
progress: progress,
isOriginal: true
)
DispatchQueue.main.async {
progressHandler(progressInfo)
}
},
completion: { result in
DispatchQueue.main.async {
completion(result)
}
}
)
}
///
/// - Parameters:
/// - image:
/// - compressionQuality: (0.0 1.0)
/// - progressHandler: (0.0 1.0)
/// - completion:
public func uploadCompressedImage(
_ image: UIImage,
compressionQuality: CGFloat = 0.5,
progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void,
completion: @escaping (Result<ImageUploaderGetID.UploadResult, Error>) -> Void
) {
guard let compressedImage = image.jpegData(compressionQuality: compressionQuality).flatMap(UIImage.init(data:)) else {
completion(.failure(NSError(domain: "com.wake.upload", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片压缩失败"])))
return
}
uploadImage(
compressedImage,
progress: progressHandler,
completion: completion
)
}
///
/// - Parameters:
/// - image:
/// - compressionQuality: (0.0 1.0)
/// - progressHandler:
/// - completion:
public func uploadOriginalAndCompressedImage(
_ image: UIImage,
compressionQuality: CGFloat = 0.5,
progress progressHandler: @escaping (ImageUploadService.UploadProgress) -> Void,
completion: @escaping (Result<ImageUploadService.UploadResults, Error>) -> Void
) {
//
uploadImage(image, progress: { progress in
let originalProgress = ImageUploadService.UploadProgress(
current: Int(progress.progress * 100),
total: 200, // 200100 + 100
progress: progress.progress * 0.5, // 50%
isOriginal: true
)
progressHandler(originalProgress)
}) { [weak self] originalResult in
guard let self = self else { return }
switch originalResult {
case .success(let originalUploadResult):
//
self.uploadCompressedImage(
image,
compressionQuality: compressionQuality,
progress: { progress in
let compressedProgress = ImageUploadService.UploadProgress(
current: 100 + Int(progress.progress * 100), // 100
total: 200, // 200100 + 100
progress: 0.5 + (progress.progress * 0.5), // 50%
isOriginal: false
)
progressHandler(compressedProgress)
},
completion: { compressedResult in
switch compressedResult {
case .success(let compressedUploadResult):
let results = ImageUploadService.UploadResults(
original: originalUploadResult,
compressed: compressedUploadResult
)
completion(.success(results))
case .failure(let error):
completion(.failure(error))
}
}
)
case .failure(let error):
completion(.failure(error))
}
}
}
// MARK: - Supporting Types
///
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
}
}
}

View File

@ -7,7 +7,7 @@ public class ImageUploaderGetID: ObservableObject {
// MARK: - // MARK: -
/// ///
public struct UploadResult { public struct UploadResult: Codable {
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,6 +29,7 @@ public class ImageUploaderGetID: ObservableObject {
case invalidResponse case invalidResponse
case uploadFailed(Error?) case uploadFailed(Error?)
case invalidFileId case invalidFileId
case invalidResponseData
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
@ -44,6 +45,8 @@ public class ImageUploaderGetID: ObservableObject {
return "上传失败: \(error?.localizedDescription ?? "未知错误")" return "上传失败: \(error?.localizedDescription ?? "未知错误")"
case .invalidFileId: case .invalidFileId:
return "无效的文件ID" return "无效的文件ID"
case .invalidResponseData:
return "无效的响应数据"
} }
} }
} }
@ -197,8 +200,8 @@ 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) {
let urlString = "\(apiConfig.baseURL)/file/confirm-upload" let endpoint = "\(apiConfig.baseURL)/file/confirm-upload"
guard let url = URL(string: urlString) else { guard let url = URL(string: endpoint) else {
completion(.failure(UploadError.invalidURL)) completion(.failure(UploadError.invalidURL))
return return
} }
@ -207,24 +210,39 @@ public class ImageUploaderGetID: ObservableObject {
request.httpMethod = "POST" request.httpMethod = "POST"
request.allHTTPHeaderFields = apiConfig.authHeaders request.allHTTPHeaderFields = apiConfig.authHeaders
let requestBody: [String: Any] = ["file_id": fileId] let body: [String: Any] = [
"file_id": fileId,
"file_name": fileName,
"file_size": fileSize
]
do { do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) request.httpBody = try JSONSerialization.data(withJSONObject: body)
print("📤 确认上传请求fileId: \(fileId), 文件名: \(fileName)")
} catch { } catch {
print("❌ 序列化确认上传参数失败: \(error.localizedDescription)")
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 {
print("❌ 确认上传请求失败: \(error.localizedDescription)")
completion(.failure(UploadError.uploadFailed(error))) completion(.failure(UploadError.uploadFailed(error)))
return return
} }
guard let httpResponse = response as? HTTPURLResponse, guard let httpResponse = response as? HTTPURLResponse else {
(200...299).contains(httpResponse.statusCode) else { print("❌ 无效的服务器响应")
completion(.failure(UploadError.serverError("确认上传失败,状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)"))) completion(.failure(UploadError.invalidResponse))
return
}
guard (200...299).contains(httpResponse.statusCode) else {
let statusCode = httpResponse.statusCode
let errorMessage = "确认上传失败,状态码: \(statusCode)"
print("\(errorMessage)")
completion(.failure(UploadError.serverError(errorMessage)))
return return
} }
@ -270,9 +288,13 @@ public class ImageUploaderGetID: ObservableObject {
return return
} }
guard let httpResponse = response as? HTTPURLResponse, guard let httpResponse = response as? HTTPURLResponse else {
(200...299).contains(httpResponse.statusCode) else { completion(.failure(UploadError.invalidResponse))
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 return
}
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
let statusCode = httpResponse.statusCode
completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)"))) completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)")))
return return
} }
@ -293,7 +315,7 @@ public class ImageUploaderGetID: ObservableObject {
task?.progress.cancel() task?.progress.cancel()
} }
} else { } else {
// Fallback for earlier iOS versions // iOS 11 使
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