feat: 文件上传成功
This commit is contained in:
parent
84db43f3b5
commit
7aea7b1535
Binary file not shown.
@ -1,52 +1,26 @@
|
||||
import SwiftUI
|
||||
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
|
||||
|
||||
/// 照片选择器,封装了系统相册选择功能
|
||||
struct PhotoPicker: UIViewControllerRepresentable {
|
||||
// MARK: - Properties
|
||||
|
||||
@Binding var selectedImages: [UIImage]
|
||||
let selectionLimit: Int
|
||||
let filter: PHPickerFilter
|
||||
var onImageUploaded: ((Result<UploadResults, Error>) -> Void)?
|
||||
var onUploadProgress: ((UploadProgress) -> Void)?
|
||||
var onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
|
||||
var onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)?
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(selectedImages: Binding<[UIImage]>,
|
||||
selectionLimit: Int = 1,
|
||||
filter: PHPickerFilter = .images,
|
||||
onImageUploaded: ((Result<UploadResults, Error>) -> Void)? = nil,
|
||||
onUploadProgress: ((UploadProgress) -> Void)? = nil) {
|
||||
onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? = nil,
|
||||
onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? = nil) {
|
||||
self._selectedImages = selectedImages
|
||||
self.selectionLimit = selectionLimit
|
||||
self.filter = filter
|
||||
@ -55,6 +29,7 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
// MARK: - UIViewControllerRepresentable
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.filter = filter
|
||||
@ -73,109 +48,77 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let parent: PhotoPicker
|
||||
private let uploader = ImageUploaderGetID()
|
||||
private let uploadService = ImageUploadService.shared
|
||||
|
||||
init(_ parent: PhotoPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard !results.isEmpty else {
|
||||
parent.presentationMode.wrappedValue.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
parent.selectedImages.removeAll()
|
||||
let group = DispatchGroup()
|
||||
var loadedImages: [Int: UIImage] = [:]
|
||||
var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?,
|
||||
compressed: ImageUploaderGetID.UploadResult?)] = [:]
|
||||
var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
|
||||
var lastError: Error?
|
||||
|
||||
for (index, result) in results.enumerated() {
|
||||
group.enter()
|
||||
|
||||
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
|
||||
guard let self = self, let image = image as? UIImage else {
|
||||
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
||||
loadedImages[index] = image
|
||||
|
||||
guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else {
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
self.uploader.uploadImage(
|
||||
// Upload the image
|
||||
self.uploadService.uploadOriginalAndCompressedImage(
|
||||
image,
|
||||
progress: { [weak self] progress in
|
||||
let progressInfo = UploadProgress(
|
||||
current: Int(progress * 100),
|
||||
total: 100,
|
||||
progress: progress,
|
||||
isOriginal: true
|
||||
)
|
||||
compressionQuality: 0.5,
|
||||
progress: { progress in
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.onUploadProgress?(progressInfo)
|
||||
self.parent.onUploadProgress?(progress)
|
||||
}
|
||||
print("📤 原图上传进度: \(Int(progress * 100))%")
|
||||
},
|
||||
completion: { [weak self] originalResult in
|
||||
guard let self = self else {
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
completion: { result 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) 字节")
|
||||
switch result {
|
||||
case .success(let results):
|
||||
uploadResults[index] = results
|
||||
|
||||
// Upload file info to backend
|
||||
MaterialService.shared.uploadMaterialInfo(
|
||||
fileId: originalUploadResult.fileId,
|
||||
previewFileId: compressedUploadResult.fileId
|
||||
fileId: results.original.fileId,
|
||||
previewFileId: results.compressed.fileId
|
||||
) { success, errorMessage in
|
||||
if success {
|
||||
print("✅ 文件信息上传成功 素材上传成功!!!!!")
|
||||
print("✅ 文件信息上传成功")
|
||||
} else if let errorMessage = errorMessage {
|
||||
print("❌ 文件信息上传失败: \(errorMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
print("❌ 压缩图上传失败: \(error.localizedDescription)")
|
||||
uploadResults[index] = (originalUploadResult, nil)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
case .failure(let error):
|
||||
print("❌ 原图上传失败: \(error.localizedDescription)")
|
||||
group.leave()
|
||||
lastError = error
|
||||
print("❌ 图片上传失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -188,14 +131,14 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
||||
group.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = lastError {
|
||||
self.parent.onImageUploaded?(.failure(error))
|
||||
} else {
|
||||
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
|
||||
self.parent.selectedImages.append(contentsOf: sortedImages)
|
||||
|
||||
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))
|
||||
if let firstResult = uploadResults.first?.value {
|
||||
self.parent.onImageUploaded?(.success(firstResult))
|
||||
} else {
|
||||
self.parent.onImageUploaded?(.failure(NSError(
|
||||
domain: "com.wake.upload",
|
||||
@ -203,8 +146,9 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
||||
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
picker.dismiss(animated: true)
|
||||
self.parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,7 +160,7 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
||||
struct AvatarUploader: View {
|
||||
@Binding var selectedImage: UIImage?
|
||||
let size: CGFloat
|
||||
var onUploadComplete: ((Result<UploadResults, Error>) -> Void)?
|
||||
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
|
||||
|
||||
@State private var isImagePickerPresented = false
|
||||
|
||||
@ -260,7 +204,7 @@ struct AvatarUploader: View {
|
||||
onUploadComplete?(result)
|
||||
},
|
||||
onUploadProgress: { progress in
|
||||
print("上传进度:\(progress.current)/\(progress.total),进度:\(progress.progress * 100)%")
|
||||
print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
166
wake/View/Components/Upload/ImageUploadService.swift
Normal file
166
wake/View/Components/Upload/ImageUploadService.swift
Normal 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, // 总进度为200(原图100 + 压缩图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, // 总进度为200(原图100 + 压缩图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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ public class ImageUploaderGetID: ObservableObject {
|
||||
// MARK: - 类型定义
|
||||
|
||||
/// 上传结果
|
||||
public struct UploadResult {
|
||||
public struct UploadResult: Codable {
|
||||
public let fileUrl: String
|
||||
public let fileName: String
|
||||
public let fileSize: Int
|
||||
@ -29,6 +29,7 @@ public class ImageUploaderGetID: ObservableObject {
|
||||
case invalidResponse
|
||||
case uploadFailed(Error?)
|
||||
case invalidFileId
|
||||
case invalidResponseData
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
@ -44,6 +45,8 @@ public class ImageUploaderGetID: ObservableObject {
|
||||
return "上传失败: \(error?.localizedDescription ?? "未知错误")"
|
||||
case .invalidFileId:
|
||||
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) {
|
||||
let urlString = "\(apiConfig.baseURL)/file/confirm-upload"
|
||||
guard let url = URL(string: urlString) else {
|
||||
let endpoint = "\(apiConfig.baseURL)/file/confirm-upload"
|
||||
guard let url = URL(string: endpoint) else {
|
||||
completion(.failure(UploadError.invalidURL))
|
||||
return
|
||||
}
|
||||
@ -207,24 +210,39 @@ public class ImageUploaderGetID: ObservableObject {
|
||||
request.httpMethod = "POST"
|
||||
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 {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
print("📤 确认上传请求,fileId: \(fileId), 文件名: \(fileName)")
|
||||
} catch {
|
||||
print("❌ 序列化确认上传参数失败: \(error.localizedDescription)")
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
let task = session.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
print("❌ 确认上传请求失败: \(error.localizedDescription)")
|
||||
completion(.failure(UploadError.uploadFailed(error)))
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
completion(.failure(UploadError.serverError("确认上传失败,状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)")))
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
print("❌ 无效的服务器响应")
|
||||
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
|
||||
}
|
||||
|
||||
@ -270,9 +288,13 @@ public class ImageUploaderGetID: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
completion(.failure(UploadError.invalidResponse))
|
||||
return
|
||||
}
|
||||
|
||||
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
|
||||
let statusCode = httpResponse.statusCode
|
||||
completion(.failure(UploadError.serverError("上传失败,状态码: \(statusCode)")))
|
||||
return
|
||||
}
|
||||
@ -293,7 +315,7 @@ public class ImageUploaderGetID: ObservableObject {
|
||||
task?.progress.cancel()
|
||||
}
|
||||
} else {
|
||||
// Fallback for earlier iOS versions
|
||||
// iOS 11 以下版本使用通知
|
||||
var lastProgress: Double = 0
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
let bytesSent = task.countOfBytesSent
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user