import SwiftUI /// 主上传视图 /// 提供媒体选择、预览和上传功能 @MainActor struct MediaUploadView: View { // MARK: - 属性 /// 上传管理器,负责处理上传逻辑 @StateObject private var uploadManager = MediaUploadManager() /// 控制媒体选择器的显示/隐藏 @State private var showMediaPicker = false /// 当前选中的媒体项 @State private var selectedMedia: MediaType? = nil /// 当前选中的媒体索引集合 @State private var selectedIndices: Set = [] // MARK: - 视图主体 var body: some View { VStack(spacing: 0) { // 顶部导航栏 topNavigationBar // 上传提示信息 uploadHintView // 主上传区域 MainUploadArea( uploadManager: uploadManager, showMediaPicker: $showMediaPicker, selectedMedia: $selectedMedia, selectedIndices: $selectedIndices ) .padding() .id("mainUploadArea\(uploadManager.selectedMedia.count)") Spacer() // 继续按钮 continueButton .padding(.bottom, 24) } .background(Color.themeTextWhiteSecondary) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) .sheet(isPresented: $showMediaPicker) { // 媒体选择器 mediaPickerView } .onChange(of: uploadManager.selectedMedia) { [oldMedia = uploadManager.selectedMedia] newMedia in handleMediaChange(newMedia, oldMedia: oldMedia) } } // MARK: - 子视图 /// 顶部导航栏 private var topNavigationBar: some View { HStack { // 返回按钮 Button(action: { Router.shared.pop() }) { Image(systemName: "chevron.left") .font(.system(size: 17, weight: .semibold)) .foregroundColor(.themeTextMessageMain) } .padding(.leading, 16) Spacer() // 标题 Text("Complete Your Profile") .font(Typography.font(for: .title2, family: .quicksandBold)) .foregroundColor(.themeTextMessageMain) Spacer() // 右侧占位视图(保持布局平衡) Color.clear .frame(width: 24, height: 24) .padding(.trailing, 16) } .background(Color.themeTextWhiteSecondary) .padding(.horizontal) .zIndex(1) // 确保导航栏显示在最上层 } /// 上传提示视图 private var uploadHintView: some View { HStack { Text("The upload process will take approximately 2 minutes. Thank you for your patience.") .font(.caption) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) .padding(12) .background( LinearGradient( gradient: Gradient(colors: [ Color(red: 1.0, green: 0.97, blue: 0.87), .white, Color(red: 1.0, green: 0.97, blue: 0.84) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) .cornerRadius(8) ) .padding(.horizontal) } .padding(.vertical, 8) } /// 继续按钮 private var continueButton: some View { Button(action: { // 处理继续操作 // Router.shared.navigate(to: .avatarBox) }) { Text("Continue") .font(.headline) .foregroundColor(uploadManager.selectedMedia.isEmpty ? Color.themeTextMessage : Color.themeTextMessageMain) .frame(maxWidth: .infinity) .frame(height: 56) .background(uploadManager.selectedMedia.isEmpty ? Color.white : Color.themePrimary) .cornerRadius(28) .padding(.horizontal, 24) } .buttonStyle(PlainButtonStyle()) .disabled(uploadManager.selectedMedia.isEmpty) } /// 媒体选择器视图 private var mediaPickerView: some View { MediaPicker( selectedMedia: $uploadManager.selectedMedia, imageSelectionLimit: 20, videoSelectionLimit: 5, onDismiss: handleMediaPickerDismiss, onUploadProgress: { index, progress in print("文件 \(index) 上传进度: \(progress * 100)%") } ) } // MARK: - 私有方法 /// 处理媒体选择器关闭事件 private func handleMediaPickerDismiss() { showMediaPicker = false print("媒体选择器关闭 - 开始处理") // 如果有选中的媒体,开始上传 if !uploadManager.selectedMedia.isEmpty { uploadManager.startUpload() } } /// 处理媒体变化 /// - Parameters: /// - newMedia: 新的媒体数组 /// - oldMedia: 旧的媒体数组 private func handleMediaChange(_ newMedia: [MediaType], oldMedia: [MediaType]) { print("开始处理媒体变化,新数量: \(newMedia.count), 原数量: \(oldMedia.count)") // 如果没有变化,直接返回 guard newMedia != oldMedia else { print("媒体未发生变化,跳过处理") return } // 在后台线程处理媒体变化 DispatchQueue.global(qos: .userInitiated).async { let startTime = Date() // 找出新增的媒体(在newMedia中但不在oldMedia中的项) let newItems = newMedia.filter { newItem in !oldMedia.contains { $0.id == newItem.id } } print("检测到\(newItems.count)个新增媒体项") // 如果有新增媒体 if !newItems.isEmpty { print("准备添加\(newItems.count)个新项...") // 回到主线程更新UI状态 DispatchQueue.main.async { // 如果当前没有选中的媒体,则选中第一个新增的媒体 if self.selectedIndices.isEmpty && !newItems.isEmpty { self.selectedIndices = [self.uploadManager.selectedMedia.count] // 选择第一个新增项的索引 self.selectedMedia = newItems.first } // 开始上传新添加的媒体 self.uploadManager.startUpload() print("媒体添加完成,总数量: \(self.uploadManager.selectedMedia.count)") } } else if newMedia.isEmpty { // 清空选择 DispatchQueue.main.async { self.selectedIndices = [] self.selectedMedia = nil print("媒体已清空,重置选择状态") } } print("媒体变化处理完成,总耗时: \(String(format: "%.3f", Date().timeIntervalSince(startTime)))s") } } /// 检查是否有正在上传的文件 /// - Returns: 是否正在上传 private func isUploading() -> Bool { return uploadManager.uploadStatus.values.contains { status in if case .uploading = status { return true } return false } } } // MARK: - 主上传区域 /// 主上传区域视图 /// 显示上传提示、媒体预览和添加更多按钮 struct MainUploadArea: View { // MARK: - 属性 /// 上传管理器 @ObservedObject var uploadManager: MediaUploadManager /// 控制媒体选择器的显示/隐藏 @Binding var showMediaPicker: Bool /// 当前选中的媒体 @Binding var selectedMedia: MediaType? /// 当前选中的媒体索引 @Binding var selectedIndices: Set // MARK: - 视图主体 var body: some View { VStack(spacing: 16) { // 标题 Text("Click to upload 20 images and 5 videos to generate your next blind box.") .font(Typography.font(for: .title2, family: .quicksandBold)) .fontWeight(.bold) .foregroundColor(.black) .multilineTextAlignment(.center) .padding(.horizontal) // 上传提示视图 UploadPromptView(showMediaPicker: $showMediaPicker) // 媒体预览区域 mediaPreviewSection // 当没有选择媒体时显示添加更多按钮 if !uploadManager.selectedMedia.isEmpty { addMoreButton } } .background(Color.white) .cornerRadius(16) .shadow(radius: 2) } // MARK: - 子视图 /// 媒体预览区域 private var mediaPreviewSection: some View { Group { if !uploadManager.selectedMedia.isEmpty { VStack(spacing: 8) { // 已选择文件数量 Text("已选择 \(uploadManager.selectedMedia.count) 个文件") .font(.subheadline) .foregroundColor(.gray) // 横向滚动的缩略图列表 ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 10) { ForEach(Array(uploadManager.selectedMedia.enumerated()), id: \.element.id) { index, media in mediaItemView(for: media, at: index) } } .padding(.horizontal) } .frame(height: 140) } .padding(.vertical, 8) } } } /// 单个媒体项视图 /// - Parameters: /// - media: 媒体项 /// - 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 } // 上传状态指示器 uploadStatusView(for: index) } .padding(4) } /// 上传状态视图 /// - 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) .tint(Color.themePrimary) } .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 }) { VStack(spacing: 8) { Image(systemName: "plus.circle.fill") .font(.system(size: 30)) .foregroundColor(.themePrimary) Text("Add More") .font(.subheadline) .foregroundColor(.gray) } .frame(width: 80, height: 80) .background(Color.gray.opacity(0.1)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(style: StrokeStyle( lineWidth: 2, dash: [8, 4] )) .foregroundColor(.gray.opacity(0.5)) ) .padding(4) } } } // MARK: - 上传提示视图 /// 上传提示视图 /// 显示上传区域的占位图和提示 struct UploadPromptView: View { /// 控制媒体选择器的显示/隐藏 @Binding var showMediaPicker: Bool var body: some View { Button(action: { showMediaPicker = true }) { // 上传图标 SVGImage(svgName: "IP") .frame(width: 225, height: 225) .contentShape(Rectangle()) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(style: StrokeStyle( lineWidth: 5, lineCap: .round, dash: [12, 8] )) .foregroundColor(Color.themePrimary) ) } } } // 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() } // MARK: - 计算属性 /// 上传进度 private var uploadProgress: Double { guard let index = uploadManager.selectedMedia.firstIndex(where: { $0.id == media.id }) else { return 0 } 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: - 视图主体 var body: some View { ZStack { // 显示图片或错误状态 if let image = image { loadedImageView(image) // 视频播放按钮 if case .video = media { playButton } // 上传进度指示器(仅在上传时显示) if isUploading || uploadProgress > 0 { loadingOverlay } } else if case .failure(let error) = loadState { // 加载失败状态 errorView(error: error) } else { // 初始加载时显示占位图 placeholderView .onAppear { loadImage() } } } .aspectRatio(1, contentMode: .fill) .clipped() .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.themePrimary.opacity(0.3), lineWidth: 1) ) } // MARK: - 子视图 /// 加载中的占位图 private var placeholderView: some View { Color.gray.opacity(0.1) .overlay( ProgressView() .progressViewStyle(CircularProgressViewStyle()) ) } /// 加载完成的图片视图 private func loadedImageView(_ image: UIImage) -> some View { Image(uiImage: image) .resizable() .scaledToFill() .transition(.opacity.animation(.easeInOut(duration: 0.2))) } /// 播放按钮 private var playButton: some View { Image(systemName: "play.circle.fill") .font(.system(size: 24)) .foregroundColor(.white) .shadow(radius: 4) } /// 错误视图 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) } /// 上传进度遮罩 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: - 私有方法 /// 加载图片 private func loadImage() { let cacheKey = "\(media.id)" as NSString // 检查缓存 if let cachedImage = ImageCache.shared.object(forKey: cacheKey) { self.image = cachedImage self.loadState = .success(cachedImage) return } // 使用专用的图片处理队列 imageProcessingQueue.async { do { let imageToCache: UIImage switch self.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.shared.setObject(imageToCache, forKey: cacheKey) // 更新UI DispatchQueue.main.async { withAnimation(.easeInOut(duration: 0.2)) { self.image = imageToCache self.loadState = .success(imageToCache) } } } catch { print("图片加载失败: \(error.localizedDescription)") DispatchQueue.main.async { self.loadState = .failure(error) } } } } } // MARK: - 扩展 /// 扩展 MediaType 以支持 Identifiable 协议 extension MediaType: Identifiable { /// 唯一标识符 public var id: String { switch self { case .image(let uiImage): return "image_\(uiImage.hashValue)" case .video(let url, _): return "video_\(url.absoluteString)" } } } // MARK: - 预览 struct MediaUploadView_Previews: PreviewProvider { static var previews: some View { NavigationView { MediaUploadView() } } }