import SwiftUI import os.log /// 媒体上传状态 public enum MediaUploadStatus: Equatable, Identifiable { case pending case uploading(progress: Double) case completed(fileId: String) case failed(Error) public var id: String { switch self { case .pending: return "pending" case .uploading: return "uploading" case .completed(let fileId): return "completed_\(fileId)" case .failed: return "failed" } } public static func == (lhs: MediaUploadStatus, rhs: MediaUploadStatus) -> Bool { switch (lhs, rhs) { case (.pending, .pending): return true case (.uploading(let lhsProgress), .uploading(let rhsProgress)): return abs(lhsProgress - rhsProgress) < 0.01 case (.completed(let lhsId), .completed(let rhsId)): return lhsId == rhsId case (.failed, .failed): return false // Errors don't need to be equatable default: return false } } public var description: String { switch self { case .pending: return "等待上传" case .uploading(let progress): return "上传中 \(Int(progress * 100))%" case .completed(let fileId): return "上传完成 (ID: \(fileId.prefix(8)))..." case .failed(let error): return "上传失败: \(error.localizedDescription)" } } public var isCompleted: Bool { if case .completed = self { return true } return false } public var isUploading: Bool { if case .uploading = self { return true } return false } } /// 媒体上传管理器 @MainActor public class MediaUploadManager: ObservableObject { /// 已选媒体文件 @Published public private(set) var selectedMedia: [MediaType] = [] /// 上传状态 @Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:] /// 上传结果 @Published public private(set) var uploadResults: [String: UploadResult] = [:] private let uploader = ImageUploadService() // Use ImageUploadService private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager") public init() {} /// 添加上传媒体 public func addMedia(_ media: [MediaType]) { let newMedia = media.filter { newItem in !self.selectedMedia.contains { $0.id == newItem.id } } var updatedMedia = self.selectedMedia for item in newMedia { updatedMedia.append(item) self.uploadStatus[item.id] = .pending } self.selectedMedia = updatedMedia // 如果是第一次添加媒体,发送通知 if !newMedia.isEmpty, let firstMedia = newMedia.first, self.selectedMedia.count == newMedia.count { NotificationCenter.default.post( name: .didAddFirstMedia, object: nil, userInfo: ["media": firstMedia] ) } } /// 移除指定ID的媒体 public func removeMedia(id: String) { Task { @MainActor in self.selectedMedia.removeAll { $0.id == id } self.uploadStatus.removeValue(forKey: id) self.uploadResults.removeValue(forKey: id) } } /// 清空所有媒体 public func clearAllMedia() { selectedMedia.removeAll() uploadStatus.removeAll() uploadResults.removeAll() } /// 开始上传所有选中的媒体 public func startUpload() { logger.info("🔄 开始批量上传 \(self.selectedMedia.count) 个文件") // 只处理状态为pending或failed的媒体 let mediaToUpload = self.selectedMedia.filter { media in guard let status = self.uploadStatus[media.id] else { return true } return !status.isCompleted && !status.isUploading } // 清空之前的上传结果 self.uploadResults.removeAll() for media in mediaToUpload { self.uploadMedia(media) } } /// 获取上传结果 public func getUploadResults() -> [String: UploadResult] { return uploadResults } /// 检查是否所有上传都已完成 public var isAllUploaded: Bool { guard !self.selectedMedia.isEmpty else { return false } return self.selectedMedia.allSatisfy { media in if case .completed = self.uploadStatus[media.id] { return true } return false } } // MARK: - Private Methods /// 上传结果 public struct UploadResult: Codable, Equatable { public let fileId: String public let thumbnailId: String? public init(fileId: String, thumbnailId: String? = nil) { self.fileId = fileId self.thumbnailId = thumbnailId } public static func == (lhs: UploadResult, rhs: UploadResult) -> Bool { return lhs.fileId == rhs.fileId && lhs.thumbnailId == rhs.thumbnailId } } private func uploadMedia(_ media: MediaType) { logger.info("🔄 开始处理媒体: \(media.id)") // 更新状态为上传中 updateStatus(for: media.id, status: .uploading(progress: 0)) // 转换媒体类型 let uploadMedia: ImageUploadService.MediaType switch media { case .image(let uiImage): uploadMedia = .image(uiImage) case .video(let url, let thumbnail): uploadMedia = .video(url as URL, thumbnail) } // 上传媒体文件 uploader.uploadMedia(uploadMedia, progress: { progress in // 更新上传进度 Task { @MainActor in self.updateStatus(for: media.id, status: .uploading(progress: progress.progress)) } }, completion: { [weak self] result in guard let self = self else { return } Task { @MainActor in switch result { case .success(let uploadResult): // 处理上传结果 let fileId: String let thumbnailId: String? switch uploadResult { case .file(let result): fileId = result.fileId thumbnailId = nil case .video(let video, let thumbnail): fileId = video.fileId thumbnailId = thumbnail?.fileId } // 保存上传结果 let result = UploadResult(fileId: fileId, thumbnailId: thumbnailId) self.uploadResults[media.id] = result self.logger.info("✅ 上传成功 (\(media.id)): \(fileId), 缩略图ID: \(thumbnailId ?? "无")") self.updateStatus(for: media.id, status: .completed(fileId: fileId)) // 打印上传结果 if self.isAllUploaded { self.printUploadResults() } case .failure(let error): self.logger.error("❌ 上传失败 (\(media.id)): \(error.localizedDescription)") self.updateStatus(for: media.id, status: .failed(error)) } } }) } @MainActor private func updateStatus(for mediaId: String, status: MediaUploadStatus) { uploadStatus[mediaId] = status } // MARK: - Upload Results /// 打印上传结果 private func printUploadResults() { let results = self.selectedMedia.compactMap { media -> [String: String]? in guard let result = self.uploadResults[media.id] else { return nil } return [ "file_id": result.fileId, "preview_file_id": result.thumbnailId ?? result.fileId ] } do { let jsonData = try JSONSerialization.data(withJSONObject: results, options: [.prettyPrinted, .sortedKeys]) if let jsonString = String(data: jsonData, encoding: .utf8) { print("📦 上传完成,文件ID列表:") print(jsonString) } } catch { print("❌ 无法序列化上传结果: \(error)") } } } // MARK: - Preview Helper /// 示例视图,展示如何使用 MediaUploadManager struct MediaUploadExample: View { @StateObject private var uploadManager = MediaUploadManager() @State private var showMediaPicker = false let imageSelectionLimit: Int let videoSelectionLimit: Int init(imageSelectionLimit: Int = 10, videoSelectionLimit: Int = 10) { self.imageSelectionLimit = imageSelectionLimit self.videoSelectionLimit = videoSelectionLimit } var body: some View { VStack(spacing: 20) { // 选择媒体按钮 Button(action: { showMediaPicker = true }) { Label("选择媒体", systemImage: "photo.on.rectangle") .font(.headline) .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) } .padding(.horizontal) // 显示选择限制信息 VStack(alignment: .leading, spacing: 4) { Text("选择限制:") .font(.subheadline) .foregroundColor(.secondary) Text("• 最多选择 \(imageSelectionLimit) 张图片") .font(.caption) .foregroundColor(.secondary) Text("• 最多选择 \(videoSelectionLimit) 个视频") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) // 显示已选媒体 MediaSelectionView(uploadManager: uploadManager) // 上传按钮 Button(action: { uploadManager.startUpload() }) { Text("开始上传") .font(.headline) .frame(maxWidth: .infinity) .padding() .background(uploadManager.selectedMedia.isEmpty ? Color.gray : Color.green) .foregroundColor(.white) .cornerRadius(10) } .padding(.horizontal) .disabled(uploadManager.selectedMedia.isEmpty) Spacer() } .sheet(isPresented: $showMediaPicker) { MediaPicker( selectedMedia: Binding( get: { self.uploadManager.selectedMedia }, set: { newMedia in Task { @MainActor in self.uploadManager.clearAllMedia() self.uploadManager.addMedia(newMedia) } } ), imageSelectionLimit: imageSelectionLimit, videoSelectionLimit: videoSelectionLimit, onDismiss: { showMediaPicker = false } ) } } } /// 媒体选择视图组件 struct MediaSelectionView: View { @ObservedObject var uploadManager: MediaUploadManager var body: some View { if !uploadManager.selectedMedia.isEmpty { VStack(spacing: 10) { Text("已选择 \(uploadManager.selectedMedia.count) 个媒体文件") .font(.headline) // 显示媒体缩略图和上传状态 List { ForEach(uploadManager.selectedMedia, id: \.id) { media in let status = uploadManager.uploadStatus[media.id] ?? .pending HStack { // 缩略图 MediaThumbnailView(media: media, onDelete: { uploadManager.removeMedia(id: media.id) }) .frame(width: 60, height: 60) VStack(alignment: .leading, spacing: 4) { Text(media.isVideo ? "视频" : "图片") .font(.subheadline) // 上传状态 Text(status.description) .font(.caption) .foregroundColor(statusColor(status)) // 上传进度条 if case .uploading(let progress) = status { ProgressView(value: progress, total: 1.0) .progressViewStyle(LinearProgressViewStyle()) .frame(height: 4) } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 8) } } .frame(height: 300) } .padding(.top) } else { Text("未选择任何媒体") .foregroundColor(.secondary) .padding(.top, 50) } } private func statusColor(_ status: MediaUploadStatus) -> Color { switch status { case .pending: return .secondary case .uploading: return .blue case .completed: return .green case .failed: return .red } } } /// 媒体缩略图视图 private struct MediaThumbnailView: View { let media: MediaType let onDelete: (() -> Void)? var body: some View { ZStack(alignment: .topTrailing) { if let thumbnail = media.thumbnail { Image(uiImage: thumbnail) .resizable() .scaledToFill() .frame(width: 60, height: 60) .cornerRadius(8) .clipped() if media.isVideo { Image(systemName: "video.fill") .foregroundColor(.white) .font(.system(size: 12)) .padding(4) .background(Color.black.opacity(0.6)) .clipShape(Circle()) .padding(4) } // 删除按钮 if let onDelete = onDelete { Button(action: onDelete) { Image(systemName: "xmark.circle.fill") .foregroundColor(.white) .font(.system(size: 16)) .background(Color.black.opacity(0.8)) .clipShape(Circle()) } .padding(4) } } else { Color.gray.opacity(0.3) .frame(width: 60, height: 60) .cornerRadius(8) } } } } #Preview { // 在预览中显示自定义限制 MediaUploadExample( imageSelectionLimit: 5, videoSelectionLimit: 2 ) .environmentObject(AuthState.shared) }