feat: 暂提

This commit is contained in:
jinyaqiu 2025-08-26 12:29:49 +08:00
parent 6229ddcd6c
commit 86ac98a465
4 changed files with 156 additions and 96 deletions

View File

@ -2,18 +2,27 @@ import SwiftUI
import os.log import os.log
/// ///
public enum MediaUploadStatus: Equatable { public enum MediaUploadStatus: Equatable, Identifiable {
case pending case pending
case uploading(progress: Double) case uploading(progress: Double)
case completed(fileId: String) case completed(fileId: String)
case failed(Error) 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 { public static func == (lhs: MediaUploadStatus, rhs: MediaUploadStatus) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.pending, .pending): case (.pending, .pending):
return true return true
case (.uploading(let lhsProgress), .uploading(let rhsProgress)): case (.uploading(let lhsProgress), .uploading(let rhsProgress)):
return lhsProgress == rhsProgress return abs(lhsProgress - rhsProgress) < 0.01
case (.completed(let lhsId), .completed(let rhsId)): case (.completed(let lhsId), .completed(let rhsId)):
return lhsId == rhsId return lhsId == rhsId
case (.failed, .failed): case (.failed, .failed):
@ -27,95 +36,96 @@ public enum MediaUploadStatus: Equatable {
switch self { switch self {
case .pending: return "等待上传" case .pending: return "等待上传"
case .uploading(let progress): return "上传中 \(Int(progress * 100))%" 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)" 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 { 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 uploader = ImageUploadService()
private let logger = Logger(subsystem: "com.yourapp.media", category: "MediaUploadManager")
public init() {} public init() {}
/// ///
public func addMedia(_ media: [MediaType]) { public func addMedia(_ media: [MediaType]) {
print("Adding \(media.count) media items")
let newMedia = media.filter { newItem in let newMedia = media.filter { newItem in
!selectedMedia.contains { $0.id == newItem.id } !self.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
} }
// // 使
if isFirstMedia, let firstMedia = newMedia.first { Task { [weak self] in
NotificationCenter.default.post( guard let self = self else { return }
name: .didAddFirstMedia, var updatedMedia = self.selectedMedia
object: nil, for item in newMedia {
userInfo: ["media": firstMedia] 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(at index: Int) { public func removeMedia(id: String) {
guard index < selectedMedia.count else { return } Task { @MainActor in
selectedMedia.remove(at: index) self.selectedMedia.removeAll { $0.id == id }
// self.uploadStatus.removeValue(forKey: id)
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
}
} }
uploadStatus = newStatus
} }
/// ///
public func clearAllMedia() { public func clearAllMedia() {
selectedMedia.removeAll() Task { @MainActor in
uploadStatus.removeAll() self.selectedMedia.removeAll()
self.uploadStatus.removeAll()
}
} }
/// ///
public func startUpload() { public func startUpload() {
print("🔄 开始批量上传 \(selectedMedia.count) 个文件") logger.info("🔄 开始批量上传 \(self.selectedMedia.count) 个文件")
//
uploadStatus.removeAll()
for (index, media) in selectedMedia.enumerated() { // pendingfailed
let id = "\(index)" let mediaToUpload = self.selectedMedia.filter { media in
uploadStatus[id] = .pending guard let status = self.uploadStatus[media.id] else { return true }
return !status.isCompleted && !status.isUploading
// Convert MediaType to ImageUploadService.MediaType }
let uploadMediaType: ImageUploadService.MediaType
switch media { for media in mediaToUpload {
case .image(let image): self.uploadMedia(media)
uploadMediaType = .image(image)
case .video(let url, let thumbnail):
uploadMediaType = .video(url, thumbnail)
}
uploadMedia(uploadMediaType, id: id)
} }
} }
/// ///
public func getUploadResults() -> [String: String] { public func getUploadResults() -> [String: String] {
var results: [String: String] = [:] var results: [String: String] = [:]
for (id, status) in uploadStatus { for (id, status) in uploadStatus {
if case .completed(let fileId) = status { if case .completed(let fileId) = status {
results[id] = fileId results[id] = fileId
@ -126,42 +136,59 @@ public class MediaUploadManager: ObservableObject {
/// ///
public var isAllUploaded: Bool { public var isAllUploaded: Bool {
guard !selectedMedia.isEmpty else { return false } guard !self.selectedMedia.isEmpty else { return false }
return uploadStatus.allSatisfy { _, status in return self.selectedMedia.allSatisfy { media in
if case .completed = status { return true } if case .completed = self.uploadStatus[media.id] { return true }
return false return false
} }
} }
// MARK: - Private Methods // MARK: - Private Methods
private func uploadMedia(_ media: ImageUploadService.MediaType, id: String) { private func uploadMedia(_ media: MediaType) {
print("🔄 开始处理媒体: \(id)") logger.info("🔄 开始处理媒体: \(media.id)")
uploadStatus[id] = .uploading(progress: 0)
//
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( uploader.uploadMedia(
media, uploadMediaType,
progress: { progress in progress: { [weak self] progress in
print("📊 上传进度 (\(id)): \(progress.current)%") guard let self = self else { return }
DispatchQueue.main.async { Task { @MainActor in
self.uploadStatus[id] = .uploading(progress: progress.progress) self.updateStatus(for: media.id, status: .uploading(progress: progress.progress))
} }
}, },
completion: { [weak self] result in completion: { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
DispatchQueue.main.async { Task { @MainActor in
switch result { switch result {
case .success(let uploadResult): case .success(let uploadResult):
print("✅ 上传成功 (\(id)): \(uploadResult.fileId)") self.logger.info("✅ 上传成功 (\(media.id)): \(uploadResult.fileId)")
self.uploadStatus[id] = .completed(fileId: uploadResult.fileId) self.updateStatus(for: media.id, status: .completed(fileId: uploadResult.fileId))
case .failure(let error): case .failure(let error):
print("❌ 上传失败 (\(id)): \(error.localizedDescription)") self.logger.error("❌ 上传失败 (\(media.id)): \(error.localizedDescription)")
self.uploadStatus[id] = .failed(error) self.updateStatus(for: media.id, status: .failed(error))
} }
} }
} }
) )
} }
@MainActor
private func updateStatus(for mediaId: String, status: MediaUploadStatus) {
uploadStatus[mediaId] = status
}
} }
// MARK: - Preview Helper // MARK: - Preview Helper
@ -171,7 +198,6 @@ struct MediaUploadExample: View {
@StateObject private var uploadManager = MediaUploadManager() @StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false @State private var showMediaPicker = false
//
let imageSelectionLimit: Int let imageSelectionLimit: Int
let videoSelectionLimit: Int let videoSelectionLimit: Int
@ -231,7 +257,15 @@ struct MediaUploadExample: View {
.navigationTitle("媒体上传") .navigationTitle("媒体上传")
.sheet(isPresented: $showMediaPicker) { .sheet(isPresented: $showMediaPicker) {
MediaPicker( 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, imageSelectionLimit: imageSelectionLimit,
videoSelectionLimit: videoSelectionLimit, videoSelectionLimit: videoSelectionLimit,
onDismiss: { showMediaPicker = false } onDismiss: { showMediaPicker = false }
@ -253,15 +287,15 @@ struct MediaSelectionView: View {
// //
List { List {
ForEach(0..<uploadManager.selectedMedia.count, id: \.self) { index in ForEach(uploadManager.selectedMedia, id: \.id) { media in
let media = uploadManager.selectedMedia[index] let status = uploadManager.uploadStatus[media.id] ?? .pending
let mediaId = "\(index)"
let status = uploadManager.uploadStatus[mediaId] ?? .pending
HStack { HStack {
// //
MediaThumbnailView(media: media, onDelete: nil) MediaThumbnailView(media: media, onDelete: {
.frame(width: 60, height: 60) uploadManager.removeMedia(id: media.id)
})
.frame(width: 60, height: 60)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(media.isVideo ? "视频" : "图片") Text(media.isVideo ? "视频" : "图片")
@ -283,11 +317,6 @@ struct MediaSelectionView: View {
} }
.padding(.vertical, 8) .padding(.vertical, 8)
} }
.onDelete { indexSet in
indexSet.forEach { index in
uploadManager.removeMedia(at: index)
}
}
} }
.frame(height: 300) .frame(height: 300)
} }
@ -320,18 +349,35 @@ private struct MediaThumbnailView: View {
Image(uiImage: thumbnail) Image(uiImage: thumbnail)
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: 80, height: 80) .frame(width: 60, height: 60)
.cornerRadius(8) .cornerRadius(8)
.clipped() .clipped()
if media.isVideo { if media.isVideo {
Image(systemName: "video.fill") Image(systemName: "video.fill")
.foregroundColor(.white) .foregroundColor(.white)
.font(.system(size: 12))
.padding(4) .padding(4)
.background(Color.black.opacity(0.6)) .background(Color.black.opacity(0.6))
.clipShape(Circle()) .clipShape(Circle())
.padding(4) .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)
} }
} }
} }

View File

@ -24,17 +24,21 @@ struct MediaUploadDemo: View {
.padding(.horizontal) .padding(.horizontal)
.sheet(isPresented: $showMediaPicker) { .sheet(isPresented: $showMediaPicker) {
MediaPicker( MediaPicker(
selectedMedia: $uploadManager.selectedMedia, selectedMedia: Binding(
get: { uploadManager.selectedMedia },
set: { newMedia in
uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
}
),
imageSelectionLimit: 1, imageSelectionLimit: 1,
videoSelectionLimit: 0, videoSelectionLimit: 0,
allowedMediaTypes: .imagesOnly, // This needs to come before selectionMode allowedMediaTypes: .imagesOnly,
selectionMode: .single, // This was moved after allowedMediaTypes selectionMode: .single,
onDismiss: { onDismiss: {
showMediaPicker = false showMediaPicker = false
//
if !uploadManager.selectedMedia.isEmpty { if !uploadManager.selectedMedia.isEmpty {
isUploading = true // Start upload logic here
uploadManager.startUpload()
} }
} }
) )

View File

@ -113,7 +113,13 @@ public struct AvatarPicker: View {
} }
.sheet(isPresented: $showMediaPicker) { .sheet(isPresented: $showMediaPicker) {
MediaPicker( MediaPicker(
selectedMedia: $uploadManager.selectedMedia, selectedMedia: Binding(
get: { uploadManager.selectedMedia },
set: { newMedia in
uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
}
),
imageSelectionLimit: 1, imageSelectionLimit: 1,
videoSelectionLimit: 0, videoSelectionLimit: 0,
allowedMediaTypes: .imagesOnly, allowedMediaTypes: .imagesOnly,
@ -165,7 +171,8 @@ public struct AvatarPicker: View {
.sheet(isPresented: $showImageCapture) { .sheet(isPresented: $showImageCapture) {
CustomCameraView(isPresented: $showImageCapture) { image in CustomCameraView(isPresented: $showImageCapture) { image in
selectedImage = image selectedImage = image
uploadManager.selectedMedia = [.image(image)] uploadManager.clearAllMedia()
uploadManager.addMedia([.image(image)])
withAnimation { withAnimation {
isUploading = true isUploading = true
} }

View File

@ -143,7 +143,8 @@ struct MediaUploadView: View {
if !newItems.isEmpty { if !newItems.isEmpty {
// //
let newMedia = newItems + uploadManager.selectedMedia let newMedia = newItems + uploadManager.selectedMedia
uploadManager.selectedMedia = newMedia uploadManager.clearAllMedia()
uploadManager.addMedia(newMedia)
// //
if selectedIndices.isEmpty { if selectedIndices.isEmpty {
@ -225,7 +226,8 @@ struct MediaUploadView: View {
updatedMedia.append(contentsOf: newItems) updatedMedia.append(contentsOf: newItems)
// //
uploadManager.selectedMedia = updatedMedia uploadManager.clearAllMedia()
uploadManager.addMedia(updatedMedia)
// //
if selectedIndices.isEmpty && !newItems.isEmpty { if selectedIndices.isEmpty && !newItems.isEmpty {
@ -419,8 +421,9 @@ struct MainUploadArea: View {
// //
Button(action: { Button(action: {
// // 使API
uploadManager.selectedMedia.remove(at: index) uploadManager.removeMedia(id: media.id)
// //
if selectedMedia == media { if selectedMedia == media {
selectedMedia = nil selectedMedia = nil