From 9a432f65acfe5359e16010b6cb0c57cef4fc8df5 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Mon, 25 Aug 2025 20:10:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A2=84=E8=A7=88=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wake/View/Upload/MediaUploadView.swift | 294 ++++++++++++++++--------- 1 file changed, 185 insertions(+), 109 deletions(-) diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift index e7d48f4..3f4a11b 100644 --- a/wake/View/Upload/MediaUploadView.swift +++ b/wake/View/Upload/MediaUploadView.swift @@ -14,6 +14,7 @@ struct MediaUploadView: View { @State private var selectedMedia: MediaType? = nil /// 当前选中的媒体索引集合 @State private var selectedIndices: Set = [] + @State private var mediaPickerSelection: [MediaType] = [] // 添加这个状态变量 // MARK: - 视图主体 @@ -48,9 +49,6 @@ struct MediaUploadView: View { // 媒体选择器 mediaPickerView } - .onChange(of: uploadManager.selectedMedia) { [oldMedia = uploadManager.selectedMedia] newMedia in - handleMediaChange(newMedia, oldMedia: oldMedia) - } } // MARK: - 子视图 @@ -132,14 +130,51 @@ struct MediaUploadView: View { /// 媒体选择器视图 private var mediaPickerView: some View { MediaPicker( - selectedMedia: $uploadManager.selectedMedia, - imageSelectionLimit: 20, - videoSelectionLimit: 5, + selectedMedia: Binding( + get: { mediaPickerSelection }, + set: { newSelections in + // 只处理尚未选择的媒体项 + let newItems = newSelections.filter { newItem in + !uploadManager.selectedMedia.contains { $0.id == newItem.id } + } + + if !newItems.isEmpty { + // 将新项添加到现有选择的开头 + let newMedia = newItems + uploadManager.selectedMedia + uploadManager.selectedMedia = newMedia + + // 如果没有选中的媒体,则选中第一个新增的 + if selectedIndices.isEmpty { + selectedIndices = [0] // 选择第一个新增的项 + selectedMedia = newItems.first + } + + // 开始上传新添加的媒体 + uploadManager.startUpload() + } + + // 重置选择 + mediaPickerSelection = [] + } + ), + imageSelectionLimit: max(0, 20 - uploadManager.selectedMedia.filter { + if case .image = $0 { return true } + return false + }.count), + videoSelectionLimit: max(0, 5 - uploadManager.selectedMedia.filter { + if case .video = $0 { return true } + return false + }.count), + selectionMode: .multiple, onDismiss: handleMediaPickerDismiss, onUploadProgress: { index, progress in print("文件 \(index) 上传进度: \(progress * 100)%") } ) + .onAppear { + // 重置选择状态当选择器出现时 + mediaPickerSelection = [] + } } // MARK: - 私有方法 @@ -151,7 +186,7 @@ struct MediaUploadView: View { // 如果有选中的媒体,开始上传 if !uploadManager.selectedMedia.isEmpty { - uploadManager.startUpload() + // 不需要在这里开始上传,因为handleMediaChange会处理 } } @@ -169,9 +204,7 @@ struct MediaUploadView: View { } // 在后台线程处理媒体变化 - DispatchQueue.global(qos: .userInitiated).async { - let startTime = Date() - + DispatchQueue.global(qos: .userInitiated).async { [self] in // 找出新增的媒体(在newMedia中但不在oldMedia中的项) let newItems = newMedia.filter { newItem in !oldMedia.contains { $0.id == newItem.id } @@ -183,28 +216,26 @@ struct MediaUploadView: View { if !newItems.isEmpty { print("准备添加\(newItems.count)个新项...") - // 回到主线程更新UI状态 - DispatchQueue.main.async { + // 在主线程更新UI + DispatchQueue.main.async { [self] in + // 创建新的数组,包含原有媒体和新媒体 + var updatedMedia = uploadManager.selectedMedia + updatedMedia.append(contentsOf: newItems) + + // 更新选中的媒体 + uploadManager.selectedMedia = updatedMedia + // 如果当前没有选中的媒体,则选中第一个新增的媒体 - if self.selectedIndices.isEmpty && !newItems.isEmpty { - self.selectedIndices = [self.uploadManager.selectedMedia.count] // 选择第一个新增项的索引 - self.selectedMedia = newItems.first + if selectedIndices.isEmpty && !newItems.isEmpty { + selectedIndices = [oldMedia.count] // 选择第一个新增项的索引 + selectedMedia = newItems.first } // 开始上传新添加的媒体 - self.uploadManager.startUpload() - print("媒体添加完成,总数量: \(self.uploadManager.selectedMedia.count)") - } - } else if newMedia.isEmpty { - // 清空选择 - DispatchQueue.main.async { - self.selectedIndices = [] - self.selectedMedia = nil - print("媒体已清空,重置选择状态") + uploadManager.startUpload() + print("媒体添加完成,总数量: \(uploadManager.selectedMedia.count)") } } - - print("媒体变化处理完成,总耗时: \(String(format: "%.3f", Date().timeIntervalSince(startTime)))s") } } @@ -264,11 +295,6 @@ struct MainUploadArea: View { Group { if !uploadManager.selectedMedia.isEmpty { VStack(spacing: 8) { - // 已选择文件数量 - Text("已选择 \(uploadManager.selectedMedia.count) 个文件") - .font(.subheadline) - .foregroundColor(.gray) - // 横向滚动的缩略图列表 ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 10) { @@ -295,22 +321,86 @@ struct MainUploadArea: View { /// - index: 索引 /// - Returns: 媒体项视图 private func mediaItemView(for media: MediaType, at index: Int) -> some View { - VStack(spacing: 4) { - // 媒体预览 - MediaPreview(media: media, uploadManager: uploadManager) - .frame(width: 80, height: 80) - .cornerRadius(8) - .shadow(radius: 1) - .onTapGesture { - // 更新选中的媒体 - selectedIndices = [index] - selectedMedia = media - } + ZStack(alignment: .topTrailing) { + VStack(spacing: 4) { + // 媒体预览 + MediaPreview(media: media, uploadManager: uploadManager) + .frame(width: 80, height: 80) + .cornerRadius(8) + .shadow(radius: 1) + .overlay( + // 左上角序号 + ZStack { + Path { path in + let radius: CGFloat = 4 + let width: CGFloat = 28 + let height: CGFloat = 18 + + // 从左上角开始(带圆角) + path.move(to: CGPoint(x: 0, y: radius)) + path.addQuadCurve(to: CGPoint(x: radius, y: 0), + control: CGPoint(x: 0, y: 0)) + + // 上边缘(右上角保持直角) + path.addLine(to: CGPoint(x: width, y: 0)) + + // 右边缘(右下角保持直角) + path.addLine(to: CGPoint(x: width, y: height - radius)) + + // 右下角圆角 + path.addQuadCurve(to: CGPoint(x: width - radius, y: height), + control: CGPoint(x: width, y: height)) + + // 下边缘(左下角保持直角) + path.addLine(to: CGPoint(x: 0, y: height)) + + // 闭合路径 + path.closeSubpath() + } + .fill(Color.white.opacity(0.5)) + + // Text + Text("\(index + 1)") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(.black) + .frame(width: 28, height: 18) + .offset(x: 0, y: -1) + } + .frame(width: 28, height: 18) + .offset(x: 0, y: 0) + .padding([.top, .leading], 0), + alignment: .topLeading + ) + .onTapGesture { + // 更新选中的媒体 + selectedIndices = [index] + selectedMedia = media + } + } - // 上传状态指示器 - uploadStatusView(for: index) + // 右上角关闭按钮 + Button(action: { + // 移除选中的媒体项 + uploadManager.selectedMedia.remove(at: index) + // 更新选中状态 + if selectedIndices.contains(index) { + selectedIndices = [] + selectedMedia = nil + } + }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 20)) + .foregroundColor(.white.opacity(0.5)) + .background( + Circle() + .fill(Color.black.opacity(0.5)) + .frame(width: 20, height: 20) + ) + } + .offset(x: 6, y: -6) // 调整位置,确保完全可见 } .padding(4) + .contentShape(Rectangle()) } /// 上传状态视图 @@ -320,18 +410,18 @@ struct MainUploadArea: View { private func uploadStatusView(for index: Int) -> some View { if let status = uploadManager.uploadStatus["\(index)"] { switch status { - // case .uploading(let progress): - // // 上传中,显示进度条 - // VStack(alignment: .center, spacing: 2) { - // Text("\(Int(progress * 100))%") - // .font(.caption2) - // .foregroundColor(.gray) - // ProgressView(value: progress, total: 1.0) - // .progressViewStyle(LinearProgressViewStyle()) - // .frame(height: 3) - // .tint(Color.themePrimary) - // } - // .frame(width: 60) + case .uploading(let progress): + // 上传中,显示进度条 + VStack(alignment: .center, spacing: 2) { + Text("\(Int(progress * 100))%") + .font(.caption2) + .foregroundColor(.gray) + ProgressView(value: progress, total: 1.0) + .progressViewStyle(LinearProgressViewStyle()) + .frame(height: 3) + .tint(Color.themePrimary) + } + .frame(width: 60) case .completed: // 上传完成,显示完成图标 @@ -439,6 +529,37 @@ struct MediaPreview: View { /// 图片缓存 private struct ImageCache { static let shared = NSCache() + private static var cacheKeys = Set() + + static func setImage(_ image: UIImage, forKey key: String) { + shared.setObject(image, forKey: key as NSString) + cacheKeys.insert(key) + } + + static func getImage(forKey key: String) -> UIImage? { + return shared.object(forKey: key as NSString) + } + + static func clearCache() { + cacheKeys.forEach { key in + shared.removeObject(forKey: key as NSString) + } + cacheKeys.removeAll() + } + } + + // MARK: - 初始化 + + init(media: MediaType, uploadManager: MediaUploadManager) { + self.media = media + self.uploadManager = uploadManager + + // 尝试从缓存加载图片 + let cacheKey = "\(media.id)" as NSString + if let cachedImage = ImageCache.shared.object(forKey: cacheKey) { + self._image = State(initialValue: cachedImage) + self._loadState = State(initialValue: .success(cachedImage)) + } } // MARK: - 计算属性 @@ -481,11 +602,6 @@ struct MediaPreview: View { if case .video = media { playButton } - - // // 上传进度指示器(仅在上传时显示) - // if isUploading || uploadProgress > 0 { - // loadingOverlay - // } } else if case .failure(let error) = loadState { // 加载失败状态 errorView(error: error) @@ -561,46 +677,6 @@ struct MediaPreview: View { .cornerRadius(8) } - /// 上传进度遮罩 - private var loadingOverlay: some View { - ZStack { - // 半透明黑色背景 - Color.black.opacity(0.3) - - // 进度条 - VStack { - Spacer() - - // 圆形进度指示器 - ZStack { - Circle() - .stroke( - Color.white.opacity(0.3), - lineWidth: 4 - ) - - Circle() - .trim(from: 0.0, to: uploadProgress) - .stroke( - Color.white, - style: StrokeStyle( - lineWidth: 4, - lineCap: .round - ) - ) - .rotationEffect(.degrees(-90)) - - Text("\(Int(uploadProgress * 100))%") - .font(.system(size: 12, weight: .bold)) - .foregroundColor(.white) - } - .frame(width: 40, height: 40) - .padding(.bottom, 8) - } - } - .cornerRadius(8) - } - // MARK: - 私有方法 /// 加载图片 @@ -608,9 +684,9 @@ struct MediaPreview: View { let cacheKey = "\(media.id)" as NSString // 检查缓存 - if let cachedImage = ImageCache.shared.object(forKey: cacheKey) { - self.image = cachedImage - self.loadState = .success(cachedImage) + if let cachedImage = ImageCache.getImage(forKey: cacheKey as String) { + image = cachedImage + loadState = .success(cachedImage) return } @@ -619,7 +695,7 @@ struct MediaPreview: View { do { let imageToCache: UIImage - switch self.media { + switch media { case .image(let uiImage): imageToCache = uiImage @@ -635,20 +711,20 @@ struct MediaPreview: View { } // 缓存图片 - ImageCache.shared.setObject(imageToCache, forKey: cacheKey) + ImageCache.setImage(imageToCache, forKey: cacheKey as String) // 更新UI DispatchQueue.main.async { withAnimation(.easeInOut(duration: 0.2)) { - self.image = imageToCache - self.loadState = .success(imageToCache) + image = imageToCache + loadState = .success(imageToCache) } } } catch { print("图片加载失败: \(error.localizedDescription)") DispatchQueue.main.async { - self.loadState = .failure(error) + loadState = .failure(error) } } }