From 45b8d211afb8f923fdf2aa8111c798ab1b16115f Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Thu, 28 Aug 2025 16:18:50 +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/Upload/MediaUploadView.swift | 449 ++++++------------------- 1 file changed, 102 insertions(+), 347 deletions(-) diff --git a/wake/View/Upload/MediaUploadView.swift b/wake/View/Upload/MediaUploadView.swift index a63a635..203b399 100644 --- a/wake/View/Upload/MediaUploadView.swift +++ b/wake/View/Upload/MediaUploadView.swift @@ -302,7 +302,7 @@ struct MainUploadArea: View { .frame(height: 50) // 主显示区域 if let mediaToDisplay = selectedMedia ?? uploadManager.selectedMedia.first { - MediaPreview(media: mediaToDisplay, uploadManager: uploadManager) + MediaPreview(media: mediaToDisplay) .id(mediaToDisplay.id) .frame(width: 225, height: 225) .overlay( @@ -374,120 +374,106 @@ struct MainUploadArea: View { /// - Returns: 媒体项视图 private func mediaItemView(for media: MediaType, at index: Int) -> some View { ZStack(alignment: .topTrailing) { - VStack(spacing: 4) { - // 媒体预览 - MediaPreview(media: media, uploadManager: uploadManager) - .frame(width: 58, height: 58) - .cornerRadius(8) - .shadow(radius: 1) - .overlay( - // 左上角序号 - ZStack(alignment: .topLeading) { - // 左上角序号 - ZStack(alignment: .topLeading) { - Path { path in - let radius: CGFloat = 4 - let width: CGFloat = 14 - let height: CGFloat = 10 - - // 从左上角开始(带圆角) - 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(hex: "BEBEBE").opacity(0.6)) - - Text("\(index + 1)") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .frame(width: 14, height: 10) - .offset(y: -1) - } - .frame(width: 14, height: 10, alignment: .topLeading) - .padding([.top, .leading], 2) + // 媒体预览 - 始终使用本地资源 + MediaPreview(media: media) + .frame(width: 58, height: 58) + .cornerRadius(8) + .shadow(radius: 1) + .overlay( + // 左上角序号 + ZStack(alignment: .topLeading) { + Path { path in + let radius: CGFloat = 4 + let width: CGFloat = 14 + let height: CGFloat = 10 - // 右下角视频时长 - if case .video(let url, _) = media, let videoURL = url as? URL { - VStack { + // 从左上角开始(带圆角) + 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(hex: "BEBEBE").opacity(0.6)) + .frame(width: 14, height: 10) + .overlay( + Text("\(index + 1)") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .frame(width: 14, height: 10) + .offset(y: -1), + alignment: .topLeading + ) + .padding([.top, .leading], 2) + + // 右下角视频时长 + if case .video(let url, _) = media, let videoURL = url as? URL { + VStack { + Spacer() + HStack { Spacer() - HStack { - Spacer() - Text(getVideoDuration(url: videoURL)) - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .frame(height: 10) - .background(Color(hex: "BEBEBE").opacity(0.6)) - .cornerRadius(2) - } - .padding([.trailing, .bottom], 0) + Text(getVideoDuration(url: videoURL)) + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .padding(.horizontal, 4) + .frame(height: 10) + .background(Color(hex: "BEBEBE").opacity(0.6)) + .cornerRadius(2) } - } else { - VStack { - Spacer() - HStack { - Spacer() - Text("") - .font(.system(size: 8, weight: .bold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .frame(height: 10) - .background(Color(hex: "BEBEBE").opacity(0.6)) - .cornerRadius(2) - } - .padding([.trailing, .bottom], 0) - } - .opacity(0) + .padding([.trailing, .bottom], 0) } + }else{ + // 占位 + VStack { + Spacer() + HStack { + Spacer() + Text("占位") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.black) + .padding(.horizontal, 4) + .frame(height: 10) + .background(Color(hex: "BEBEBE").opacity(0.6)) + .cornerRadius(2) + } + .padding([.trailing, .bottom], 0) + } + .opacity(0) } - ) - // 在 mediaItemView 函数中,更新 onTapGesture: - // .onTapGesture { - // print("Tapped media at index: \(index)") // 添加日志 - // withAnimation { - // selectedIndices = [index] - // selectedMedia = media - // } - // } - // .contentShape(Rectangle()) // 确保整个区域都可点击 - // 在 mediaItemView 中,更新 onTapGesture: - .onTapGesture { - print("点击了媒体项,索引: \(index)") - withAnimation { - selectedMedia = media // 直接更新选中的媒体 - } + }, + alignment: .topLeading + ) + .onTapGesture { + print("点击了媒体项,索引: \(index)") + withAnimation { + selectedMedia = media } - .contentShape(Rectangle()) // 确保整个区域都可点击 - } + } + .contentShape(Rectangle()) // 右上角关闭按钮 Button(action: { - // 使用公共API移除媒体 uploadManager.removeMedia(id: media.id) - - // 重置选中的媒体 if selectedMedia == media { selectedMedia = nil } }) { Image(systemName: "xmark") - .font(.system(size: 10, weight: .bold)) + .font(.system(size: 8, weight: .bold)) .foregroundColor(.black) .frame(width: 12, height: 12) .background( @@ -496,51 +482,12 @@ struct MainUploadArea: View { .frame(width: 12, height: 12) ) } - .offset(x: 6, y: -6) // 调整位置,确保完全可见 + .offset(x: 6, y: -6) } - .padding(.horizontal,4) + .padding(.horizontal, 4) .contentShape(Rectangle()) } - /// 上传状态视图 - /// - Parameter index: 媒体索引 - /// - Returns: 状态视图 - @ViewBuilder - 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) - .padding(.horizontal, 4) - .padding(.bottom, 2) - } - .frame(width: 60) - - case .completed: - // 上传完成,显示完成图标 - Image(systemName: "checkmark.circle.fill") - .font(.caption) - .foregroundColor(.green) - - case .failed: - // 上传失败,显示错误图标 - Image(systemName: "exclamationmark.triangle.fill") - .font(.caption) - .foregroundColor(.red) - - default: - EmptyView() - } - } - } - /// 添加更多按钮 private var addMoreButton: some View { Button(action: { showMediaPicker = true }) { @@ -599,102 +546,23 @@ struct UploadPromptView: View { // MARK: - 媒体预览视图 /// 媒体预览视图 -/// 显示图片或视频的预览图 +/// 显示图片或视频的预览图,始终使用本地资源 struct MediaPreview: View { // MARK: - 属性 - /// 图片处理队列 - private let imageProcessingQueue = DispatchQueue( - label: "com.yourapp.imageprocessing", - qos: .userInitiated, - attributes: .concurrent - ) - /// 媒体类型 let media: MediaType - /// 上传管理器 - @ObservedObject var uploadManager: MediaUploadManager - - // MARK: - 状态 - - /// 加载的图片 - @State private var image: UIImage? - - /// 加载状态 - enum LoadState { - /// 加载成功 - case success(UIImage) - /// 加载失败 - case failure(Error) - } - - /// 当前加载状态 - @State private var loadState: LoadState? - - // MARK: - 图片缓存 - - /// 图片缓存 - 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: - 计算属性 - /// 上传进度 - private var uploadProgress: Double { - guard let index = uploadManager.selectedMedia.firstIndex(where: { $0.id == media.id }) else { - return 0 + /// 获取要显示的图片 + private var displayImage: UIImage? { + switch media { + case .image(let uiImage): + return uiImage + case .video(_, let thumbnail): + return thumbnail } - - if case .uploading(let progress) = uploadManager.uploadStatus["\(index)"] { - return progress - } else if case .completed = uploadManager.uploadStatus["\(index)"] { - return 1.0 - } - return 0 - } - - /// 是否正在上传 - private var isUploading: Bool { - guard let index = uploadManager.selectedMedia.firstIndex(where: { $0.id == media.id }) else { - return false - } - - if case .uploading = uploadManager.uploadStatus["\(index)"] { - return true - } - return false } // MARK: - 视图主体 @@ -702,33 +570,14 @@ struct MediaPreview: View { var body: some View { ZStack { // 1. 显示图片或视频缩略图 - if let image = image { - ZStack(alignment: .bottomTrailing) { - loadedImageView(image) - } - - // 上传进度条(仅当正在上传且进度小于100%时显示) - if isUploading && uploadProgress < 1.0 { - VStack { - Spacer() - ProgressView(value: uploadProgress, total: 1.0) - .progressViewStyle(LinearProgressViewStyle(tint: .white)) - .frame(height: 2) - .padding(.horizontal, 4) - .padding(.bottom, 2) - } - } + if let image = displayImage { + Image(uiImage: image) + .resizable() + .scaledToFill() + .transition(.opacity.animation(.easeInOut(duration: 0.2))) } else { // 2. 加载中的占位图 - placeholderView - .onAppear { - loadImage() - } - } - - // 3. 显示错误状态(如果有) - if case .failure(let error) = loadState, image == nil { - errorView(error: error) + Color.gray.opacity(0.1) } } .aspectRatio(1, contentMode: .fill) @@ -739,100 +588,6 @@ struct MediaPreview: View { .stroke(Color.themePrimary.opacity(0.3), lineWidth: 1) ) } - - // 加载中的占位图 - private var placeholderView: some View { - Color.gray.opacity(0.1) - } - - // 加载完成的图片视图 - private func loadedImageView(_ image: UIImage) -> some View { - Image(uiImage: image) - .resizable() - .scaledToFill() - .transition(.opacity.animation(.easeInOut(duration: 0.2))) - } - - /// 错误视图 - private func errorView(error: Error) -> some View { - VStack(spacing: 8) { - Image(systemName: "exclamationmark.triangle") - .font(.system(size: 20)) - .foregroundColor(.orange) - - Text("加载失败") - .font(.caption2) - .foregroundColor(.secondary) - - Button(action: { - loadState = nil - loadImage() - }) { - Image(systemName: "arrow.clockwise") - .font(.caption) - .padding(4) - .background(Color.gray.opacity(0.2)) - .clipShape(Circle()) - } - .padding(.top, 4) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - } - - // MARK: - 私有方法 - - /// 加载图片 - private func loadImage() { - let cacheKey = "\(media.id)" as NSString - - // 检查缓存 - if let cachedImage = ImageCache.getImage(forKey: cacheKey as String) { - image = cachedImage - loadState = .success(cachedImage) - return - } - - // 使用专用的图片处理队列 - imageProcessingQueue.async { - do { - let imageToCache: UIImage - - switch media { - case .image(let uiImage): - imageToCache = uiImage - - case .video(_, let thumbnail): - guard let thumbnail = thumbnail else { - throw NSError( - domain: "com.yourapp.media", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "视频缩略图加载失败"] - ) - } - imageToCache = thumbnail - } - - // 缓存图片 - ImageCache.setImage(imageToCache, forKey: cacheKey as String) - - // 更新UI - DispatchQueue.main.async { - withAnimation(.easeInOut(duration: 0.2)) { - image = imageToCache - loadState = .success(imageToCache) - } - } - - } catch { - print("图片加载失败: \(error.localizedDescription)") - DispatchQueue.main.async { - loadState = .failure(error) - } - } - } - } } private func getVideoDuration(url: URL) -> String {