2025-08-21 19:37:37 +08:00

537 lines
23 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
// 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: )
///
/// 使UIViewControllerRepresentablePHPickerViewControllerSwiftUI
struct PhotoPicker: UIViewControllerRepresentable {
<<<<<<< HEAD
=======
// MARK: - Properties
///
>>>>>>> a207b78 (feat: )
@Binding var selectedImages: [UIImage]
/// 1
let selectionLimit: Int
///
let filter: PHPickerFilter
<<<<<<< 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: )
self._selectedImages = selectedImages
self.selectionLimit = selectionLimit
self.filter = filter
self.onImageUploaded = onImageUploaded
<<<<<<< HEAD
self.onUploadProgress = onUploadProgress
}
// MARK: - UIViewControllerRepresentable
// MARK: - UIViewControllerRepresentable
=======
}
// MARK: - UIViewControllerRepresentable
>>>>>>> a207b78 (feat: )
/// PHPickerViewController
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) {}
/// PHPickerViewController
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: - Coordinator
<<<<<<< HEAD
// MARK: -
=======
>>>>>>> a207b78 (feat: )
/// PHPickerViewController
class Coordinator: NSObject, PHPickerViewControllerDelegate {
///
let parent: PhotoPicker
private let uploadService = ImageUploadService.shared
///
private let uploader = ImageUploaderGetID()
init(_ parent: PhotoPicker) {
self.parent = parent
}
///
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
//
parent.selectedImages.removeAll()
// 使DispatchGroup
let group = DispatchGroup()
<<<<<<< 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: )
//
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
<<<<<<< HEAD
if let image = image as? UIImage {
<<<<<<< HEAD
=======
//
>>>>>>> a207b78 (feat: )
loadedImages[index] = image
=======
guard let self = self, let image = image as? UIImage else {
group.leave()
return
}
// 1.
loadedImages[index] = image
// 2. 50%
guard let compressedImage = image.jpegData(compressionQuality: 0.5).flatMap(UIImage.init(data:)) else {
group.leave()
return
}
// 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)
}
}
case .failure(let error):
print("❌ 原图上传失败: \(error.localizedDescription)")
group.leave()
}
}
}
} else {
group.leave()
}
}
<<<<<<< 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: )
=======
//
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
// 1.
let sortedImages = loadedImages.sorted { $0.key < $1.key }.map { $0.value }
self.parent.selectedImages.append(contentsOf: sortedImages)
// 2.
if let firstResult = uploadResults.first?.value,
let original = firstResult.original,
let compressed = firstResult.compressed {
// 3.
let results = UploadResults(original: original, compressed: compressed)
self.parent.onImageUploaded?(.success(results))
} else {
// 4.
self.parent.onImageUploaded?(.failure(NSError(domain: "com.wake.upload",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "上传过程中出现错误"])))
}
// 5.
>>>>>>> 5611df8 (feat: )
picker.dismiss(animated: true)
}
}
}
}
<<<<<<< HEAD
// MARK: - Avatar Uploader Component
=======
// MARK: - AvatarUploader
///
///
>>>>>>> a207b78 (feat: )
struct AvatarUploader: View {
// MARK: - Properties
///
@Binding var selectedImage: UIImage?
///
let size: CGFloat
var onUploadComplete: ((Result<ImageUploadService.UploadResults, Error>) -> Void)?
///
var onUploadComplete: ((Result<UploadResults, Error>) -> Void)?
// MARK: - State
///
@State private var isImagePickerPresented = false
// MARK: - Body
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
// Default avatar container
=======
>>>>>>> a4890a4 (feat: url)
=======
//
>>>>>>> a207b78 (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
.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: )
.sheet(isPresented: $isImagePickerPresented) {
//
PhotoPicker(
selectedImages: Binding(
get: { [selectedImage].compactMap { $0 } },
set: { images in
selectedImage = images.first
}
),
selectionLimit: 1, //
onImageUploaded: { result in
//
onUploadComplete?(result)
}
)
}
}
}