From 86ac98a465d92bdcc273565b1fc3a23ca8b0c4df Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Tue, 26 Aug 2025 12:29:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9A=82=E6=8F=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Components/Upload/MediaUpload.swift | 214 +++++++++++------- wake/View/Examples/MediaDemo.swift | 16 +- wake/View/Owner/UserInfo/AvatarPicker.swift | 11 +- wake/View/Upload/MediaUploadView.swift | 11 +- 4 files changed, 156 insertions(+), 96 deletions(-) diff --git a/wake/View/Components/Upload/MediaUpload.swift b/wake/View/Components/Upload/MediaUpload.swift index b1881ce..99616f4 100644 --- a/wake/View/Components/Upload/MediaUpload.swift +++ b/wake/View/Components/Upload/MediaUpload.swift @@ -2,18 +2,27 @@ import SwiftUI import os.log /// 媒体上传状态 -public enum MediaUploadStatus: Equatable { +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 lhsProgress == rhsProgress + return abs(lhsProgress - rhsProgress) < 0.01 case (.completed(let lhsId), .completed(let rhsId)): return lhsId == rhsId case (.failed, .failed): @@ -27,95 +36,96 @@ public enum MediaUploadStatus: Equatable { switch self { case .pending: return "等待上传" case .uploading(let progress): return "上传中 \(Int(progress * 100))%" - case .completed(let fileId): return "上传完成 (ID: \(fileId.prefix(8))...)" + 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 var selectedMedia: [MediaType] = [] + @Published public private(set) var selectedMedia: [MediaType] = [] /// 上传状态 - @Published public var uploadStatus: [String: MediaUploadStatus] = [:] + @Published public private(set) var uploadStatus: [String: MediaUploadStatus] = [:] private let uploader = ImageUploadService() + private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager") public init() {} /// 添加上传媒体 public func addMedia(_ media: [MediaType]) { - print("Adding \(media.count) media items") let newMedia = media.filter { newItem in - !selectedMedia.contains { $0.id == newItem.id } - } - print("After filtering duplicates: \(newMedia.count) new items") - - let isFirstMedia = selectedMedia.isEmpty - selectedMedia.append(contentsOf: newMedia) - - for item in newMedia { - uploadStatus[item.id] = .pending + !self.selectedMedia.contains { $0.id == newItem.id } } - // 如果是第一次添加媒体,发送通知 - if isFirstMedia, let firstMedia = newMedia.first { - NotificationCenter.default.post( - name: .didAddFirstMedia, - object: nil, - userInfo: ["media": firstMedia] - ) + // 使用任务组处理多个媒体项的添加 + Task { [weak self] in + guard let self = self else { return } + 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] + ) + } } } - /// 移除指定索引的媒体 - public func removeMedia(at index: Int) { - guard index < selectedMedia.count else { return } - selectedMedia.remove(at: index) - // 更新状态字典 - var newStatus: [String: MediaUploadStatus] = [:] - uploadStatus.forEach { key, value in - if let keyInt = Int(key), keyInt < index { - newStatus[key] = value - } else if let keyInt = Int(key), keyInt > index { - newStatus["\(keyInt - 1)"] = value - } + /// 移除指定ID的媒体 + public func removeMedia(id: String) { + Task { @MainActor in + self.selectedMedia.removeAll { $0.id == id } + self.uploadStatus.removeValue(forKey: id) } - uploadStatus = newStatus } /// 清空所有媒体 public func clearAllMedia() { - selectedMedia.removeAll() - uploadStatus.removeAll() + Task { @MainActor in + self.selectedMedia.removeAll() + self.uploadStatus.removeAll() + } } /// 开始上传所有选中的媒体 public func startUpload() { - print("🔄 开始批量上传 \(selectedMedia.count) 个文件") - // 重置上传状态 - uploadStatus.removeAll() + logger.info("🔄 开始批量上传 \(self.selectedMedia.count) 个文件") - for (index, media) in selectedMedia.enumerated() { - let id = "\(index)" - uploadStatus[id] = .pending - - // Convert MediaType to ImageUploadService.MediaType - let uploadMediaType: ImageUploadService.MediaType - switch media { - case .image(let image): - uploadMediaType = .image(image) - case .video(let url, let thumbnail): - uploadMediaType = .video(url, thumbnail) - } - uploadMedia(uploadMediaType, id: id) + // 只处理状态为pending或failed的媒体 + let mediaToUpload = self.selectedMedia.filter { media in + guard let status = self.uploadStatus[media.id] else { return true } + return !status.isCompleted && !status.isUploading + } + + for media in mediaToUpload { + self.uploadMedia(media) } } /// 获取上传结果 public func getUploadResults() -> [String: String] { - var results: [String: String] = [:] + var results: [String: String] = [:] for (id, status) in uploadStatus { if case .completed(let fileId) = status { results[id] = fileId @@ -126,42 +136,59 @@ public class MediaUploadManager: ObservableObject { /// 检查是否所有上传都已完成 public var isAllUploaded: Bool { - guard !selectedMedia.isEmpty else { return false } - return uploadStatus.allSatisfy { _, status in - if case .completed = status { return true } + 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 - private func uploadMedia(_ media: ImageUploadService.MediaType, id: String) { - print("🔄 开始处理媒体: \(id)") - uploadStatus[id] = .uploading(progress: 0) + private func uploadMedia(_ media: MediaType) { + logger.info("🔄 开始处理媒体: \(media.id)") + // 更新状态为上传中 + updateStatus(for: media.id, status: .uploading(progress: 0)) + + // 转换媒体类型 + let uploadMediaType: ImageUploadService.MediaType + switch media { + case .image(let image): + uploadMediaType = .image(image) + case .video(let url, let thumbnail): + uploadMediaType = .video(url, thumbnail) + } + + // 开始上传 uploader.uploadMedia( - media, - progress: { progress in - print("📊 上传进度 (\(id)): \(progress.current)%") - DispatchQueue.main.async { - self.uploadStatus[id] = .uploading(progress: progress.progress) + uploadMediaType, + progress: { [weak self] progress in + guard let self = self else { return } + Task { @MainActor in + self.updateStatus(for: media.id, status: .uploading(progress: progress.progress)) } }, completion: { [weak self] result in guard let self = self else { return } - DispatchQueue.main.async { + Task { @MainActor in switch result { case .success(let uploadResult): - print("✅ 上传成功 (\(id)): \(uploadResult.fileId)") - self.uploadStatus[id] = .completed(fileId: uploadResult.fileId) + self.logger.info("✅ 上传成功 (\(media.id)): \(uploadResult.fileId)") + self.updateStatus(for: media.id, status: .completed(fileId: uploadResult.fileId)) case .failure(let error): - print("❌ 上传失败 (\(id)): \(error.localizedDescription)") - self.uploadStatus[id] = .failed(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: - Preview Helper @@ -171,7 +198,6 @@ struct MediaUploadExample: View { @StateObject private var uploadManager = MediaUploadManager() @State private var showMediaPicker = false - // 添加图片和视频选择限制参数 let imageSelectionLimit: Int let videoSelectionLimit: Int @@ -231,7 +257,15 @@ struct MediaUploadExample: View { .navigationTitle("媒体上传") .sheet(isPresented: $showMediaPicker) { MediaPicker( - selectedMedia: $uploadManager.selectedMedia, + 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 } @@ -253,15 +287,15 @@ struct MediaSelectionView: View { // 显示媒体缩略图和上传状态 List { - ForEach(0..