2025-08-21 19:38:06 +08:00

704 lines
28 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import PhotosUI
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
// MARK: - Photo Picker
=======
///
///
class ImageUploader: ObservableObject {
private let baseURL = "https://api.memorywake.com/api/v1/file/generate-upload-url"
func uploadImage(_ image: UIImage, completion: @escaping (Result<String, Error>) -> Void) {
print("🔄 开始准备上传图片...")
// 1. Data
guard let imageData = image.jpegData(compressionQuality: 0.7) else {
let error = NSError(domain: "ImageError", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片数据转换失败"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
// 2. URL
guard let url = URL(string: baseURL) else {
let error = NSError(domain: "URLError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJqdGkiOjczNjM0ODY2MTE1MDc2NDY0NjQsImlkZW50aXR5IjoiNzM1MDQzOTY2MzExNjYxOTg4OCIsImV4cCI6MTc1NjE5NjgxNX0.hRC_So6LHuR6Gx-bDyO8aliVOd-Xumul8M7cydi2pTxHPweBx4421AfZ5BjGoEEwRZPIXJ5z7a1aDB7qvjpLCA", forHTTPHeaderField: "Authorization")
// 3.
let fileName = "avatar_\(UUID().uuidString).jpg"
let parameters: [String: Any] = [
"filename": fileName,
"content_type": "image/jpeg",
"file_size": imageData.count
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
print("📤 准备上传请求,文件名: \(fileName), 大小: \(Double(imageData.count) / 1024.0) KB")
print("📡 请求参数: \(parameters)")
} catch {
print("❌ 序列化请求参数失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
// 4.
print("🌐 请求URL: \(url.absoluteString)")
print("📋 请求头: \(request.allHTTPHeaderFields ?? [:])")
// 5. URL
print("🌐 正在获取上传链接...")
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
// 线
DispatchQueue.main.async {
if let error = error {
print("❌ 请求失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
print("📊 响应状态码: \(httpResponse.statusCode)")
print("📦 响应头: \(httpResponse.allHeaderFields)")
guard let data = data else {
let error = NSError(domain: "NetworkError", code: -1, userInfo: [NSLocalizedDescriptionKey: "没有接收到数据"])
print("❌ 错误:\(error.localizedDescription)")
completion(.failure(error))
return
}
//
if let responseString = String(data: data, encoding: .utf8) {
print("📡 原始响应数据: \(responseString)")
}
// 5. URL
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw NSError(domain: "ParseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应数据格式错误"])
}
print("📋 解析后的JSON: \(json)")
// API
if let uploadUrlString = json["url"] as? String,
let uploadUrl = URL(string: uploadUrlString) {
print("✅ 成功获取上传链接: \(uploadUrlString)")
self?.uploadImageData(imageData, to: uploadUrl, fileName: fileName, completion: completion)
} else {
throw NSError(domain: "APIError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取上传链接: \(json)"])
}
} catch {
print("❌ 解析响应数据失败: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
task.resume()
}
private func uploadImageData(_ data: Data, to url: URL, fileName: String, completion: @escaping (Result<String, Error>) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
request.httpBody = data
print("🚀 开始上传图片数据...")
let task = URLSession.shared.dataTask(with: request) { _, response, error in
DispatchQueue.main.async {
if let error = error {
print("❌ 上传失败: \(error.localizedDescription)")
completion(.failure(error))
return
}
//
if let httpResponse = response as? HTTPURLResponse {
print("📊 上传响应状态码: \(httpResponse.statusCode)")
if (200...299).contains(httpResponse.statusCode) {
let fileUrl = "https://your-cdn-domain.com/\(fileName)" // CDN
print("✅ 上传成功文件URL: \(fileUrl)")
completion(.success(fileUrl))
} else {
let errorMessage = "上传失败,状态码: \(httpResponse.statusCode)"
print("\(errorMessage)")
let error = NSError(domain: "UploadError",
code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: errorMessage])
completion(.failure(error))
}
} else {
let errorMessage = "无效的服务器响应"
print("\(errorMessage)")
let error = NSError(domain: "UploadError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: errorMessage])
completion(.failure(error))
}
}
}
task.resume()
}
}
>>>>>>> a4890a4 (feat: url)
=======
>>>>>>> a207b78 (feat: )
=======
///
struct UploadResults {
let original: ImageUploaderGetID.UploadResult
let compressed: ImageUploaderGetID.UploadResult
}
>>>>>>> 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
}
}
=======
>>>>>>> 5edee64 (feat: )
// MARK: - Photo Picker
>>>>>>> 8e641fd (feat: )
///
struct PhotoPicker: UIViewControllerRepresentable {
<<<<<<< HEAD
=======
// MARK: - Properties
<<<<<<< HEAD
<<<<<<< HEAD
///
>>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
@Binding var selectedImages: [UIImage]
let selectionLimit: Int
let filter: PHPickerFilter
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
=======
///
private let uploader = ImageUploader()
=======
///
<<<<<<< HEAD
var onImageUploaded: ((Result<ImageUploaderGetID.UploadResult, Error>) -> Void)?
>>>>>>> a207b78 (feat: )
=======
var onImageUploaded: ((Result<UploadResults, Error>) -> Void)?
>>>>>>> 5611df8 (feat: )
// 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<UploadResults, Error>) -> Void)? = nil
) {
>>>>>>> a207b78 (feat: )
=======
var onImageUploaded: ((Result<UploadResults, Error>) -> Void)?
var onUploadProgress: ((UploadProgress) -> Void)?
=======
@Binding var selectedImages: [UIImage]
let selectionLimit: Int
let filter: PHPickerFilter
var onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
var onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)?
@Environment(\.presentationMode) private var presentationMode
>>>>>>> 5edee64 (feat: )
// MARK: - Initialization
init(selectedImages: Binding<[UIImage]>,
selectionLimit: Int = 1,
filter: PHPickerFilter = .images,
<<<<<<< HEAD
onImageUploaded: ((Result<UploadResults, Error>) -> Void)? = nil,
onUploadProgress: ((UploadProgress) -> Void)? = nil) {
>>>>>>> 8e641fd (feat: )
=======
onImageUploaded: ((Result<ImageUploadService.UploadResults, Error>) -> Void)? = nil,
onUploadProgress: ((ImageUploadService.UploadProgress) -> Void)? = nil) {
>>>>>>> 5edee64 (feat: )
self._selectedImages = selectedImages
self.selectionLimit = selectionLimit
self.filter = filter
self.onImageUploaded = onImageUploaded
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 8e641fd (feat: )
self.onUploadProgress = onUploadProgress
}
// MARK: - UIViewControllerRepresentable
<<<<<<< HEAD
<<<<<<< HEAD
// MARK: - UIViewControllerRepresentable
=======
}
// MARK: - UIViewControllerRepresentable
>>>>>>> a207b78 (feat: )
/// PHPickerViewController
=======
>>>>>>> 8e641fd (feat: )
=======
>>>>>>> 5edee64 (feat: )
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = filter
configuration.selectionLimit = selectionLimit
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: - Coordinator
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
// MARK: -
=======
>>>>>>> a207b78 (feat: )
/// PHPickerViewController
=======
>>>>>>> 8e641fd (feat: )
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: PhotoPicker
<<<<<<< HEAD
private let uploadService = ImageUploadService.shared
///
=======
>>>>>>> 8e641fd (feat: )
private let uploader = ImageUploaderGetID()
=======
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: PhotoPicker
private let uploadService = ImageUploadService.shared
>>>>>>> 5edee64 (feat: )
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()
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
var lastError: Error?
=======
var loadedImages: [Int: UIImage] = [:] //
<<<<<<< HEAD
>>>>>>> a207b78 (feat: )
=======
var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?,
compressed: ImageUploaderGetID.UploadResult?)] = [:]
>>>>>>> 5611df8 (feat: )
=======
var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: (original: ImageUploaderGetID.UploadResult?,
compressed: ImageUploaderGetID.UploadResult?)] = [:]
>>>>>>> 8e641fd (feat: )
=======
var loadedImages: [Int: UIImage] = [:]
var uploadResults: [Int: ImageUploadService.UploadResults] = [:]
var lastError: Error?
>>>>>>> 5edee64 (feat: )
for (index, result) in results.enumerated() {
group.enter()
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
<<<<<<< HEAD
result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
<<<<<<< HEAD
if let image = image as? UIImage {
<<<<<<< HEAD
=======
//
>>>>>>> a207b78 (feat: )
loadedImages[index] = image
=======
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"])
>>>>>>> 5edee64 (feat: )
group.leave()
return
}
loadedImages[index] = image
<<<<<<< HEAD
guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else {
group.leave()
return
}
<<<<<<< HEAD
// 3.
self.uploader.uploadImage(image) { [weak self] originalResult in
guard let self = self else {
group.leave()
return
}
>>>>>>> 5611df8 (feat: )
switch originalResult {
case .success(let originalUploadResult):
// 4.
self.uploader.uploadImage(compressedImage) { 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
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)
}
=======
self.uploader.uploadImage(
=======
// Upload the image
self.uploadService.uploadOriginalAndCompressedImage(
>>>>>>> 5edee64 (feat: )
image,
compressionQuality: 0.5,
progress: { progress in
DispatchQueue.main.async {
self.parent.onUploadProgress?(progress)
}
},
<<<<<<< HEAD
completion: { [weak self] originalResult in
guard let self = self else {
group.leave()
return
>>>>>>> 8e641fd (feat: )
}
=======
completion: { result in
defer { group.leave() }
>>>>>>> 5edee64 (feat: )
switch result {
case .success(let results):
uploadResults[index] = results
// Upload file info to backend
MaterialService.shared.uploadMaterialInfo(
fileId: results.original.fileId,
previewFileId: results.compressed.fileId
) { success, errorMessage in
if success {
print("✅ 文件信息上传成功")
} else if let errorMessage = errorMessage {
print("❌ 文件信息上传失败: \(errorMessage)")
}
}
case .failure(let error):
lastError = error
print("❌ 图片上传失败: \(error.localizedDescription)")
}
}
)
}
} else {
group.leave()
}
}
<<<<<<< HEAD
<<<<<<< HEAD
//
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 }
self.parent.selectedImages.append(contentsOf: sortedImages)
// Dismiss the picker
=======
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: )
=======
//
=======
>>>>>>> 8e641fd (feat: )
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 {
self.parent.onImageUploaded?(.success(firstResult))
} else {
self.parent.onImageUploaded?(.failure(NSError(
domain: "com.wake.upload",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"]
)))
}
}
<<<<<<< HEAD
<<<<<<< HEAD
// 5.
>>>>>>> 5611df8 (feat: )
=======
>>>>>>> 8e641fd (feat: )
picker.dismiss(animated: true)
=======
self.parent.presentationMode.wrappedValue.dismiss()
>>>>>>> 5edee64 (feat: )
}
}
}
}
<<<<<<< HEAD
<<<<<<< HEAD
// MARK: - Avatar Uploader Component
=======
// MARK: - AvatarUploader
///
///
>>>>>>> a207b78 (feat: )
=======
// MARK: - Avatar Uploader
///
>>>>>>> 8e641fd (feat: )
struct AvatarUploader: View {
@Binding var selectedImage: UIImage?
let size: CGFloat
<<<<<<< HEAD
<<<<<<< HEAD
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
///
=======
>>>>>>> 8e641fd (feat: )
var onUploadComplete: ((Result<UploadResults, Error>) -> Void)?
=======
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
>>>>>>> 5edee64 (feat: )
@State private var isImagePickerPresented = false
var body: some View {
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
<<<<<<< HEAD
// Default avatar container
=======
>>>>>>> a4890a4 (feat: url)
=======
//
>>>>>>> a207b78 (feat: )
=======
>>>>>>> 8e641fd (feat: )
Color.gray.opacity(0.1)
.frame(width: size, height: size)
.overlay(
SVGImage(svgName: "Avatar")
.frame(width: size * 0.8, height: size * 0.8)
)
.clipShape(RoundedRectangle(cornerRadius: size * 0.1))
.overlay(
RoundedRectangle(cornerRadius: size * 0.1)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
}
}
.frame(width: size, height: size)
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
.contentShape(Rectangle()) // Make the entire area tappable
}
.buttonStyle(PlainButtonStyle()) // Remove button highlight effect
=======
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
>>>>>>> a4890a4 (feat: url)
=======
.contentShape(Rectangle()) //
}
.buttonStyle(PlainButtonStyle()) // 使
>>>>>>> a207b78 (feat: )
=======
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
>>>>>>> 8e641fd (feat: )
.sheet(isPresented: $isImagePickerPresented) {
PhotoPicker(
selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } },
set: { images in
selectedImage = images.first
}
),
selectionLimit: 1,
onImageUploaded: { result in
onUploadComplete?(result)
},
onUploadProgress: { progress in
print("上传进度:\(progress.current)/\(progress.total),进度:\(Int(progress.progress * 100))%")
}
)
}
}
}