import SwiftUI public struct AvatarPicker: View { @StateObject private var uploadManager = MediaUploadManager() @State private var showMediaPicker = false @State private var showImageCapture = false @State private var isUploading = false @Binding var selectedImage: UIImage? @Binding var showUsername: Bool @Binding var isKeyboardVisible: Bool @Binding var uploadedFileId: String? // Animation state @State private var isAnimating = false public init(selectedImage: Binding, showUsername: Binding, isKeyboardVisible: Binding, uploadedFileId: Binding) { self._selectedImage = selectedImage self._showUsername = showUsername self._isKeyboardVisible = isKeyboardVisible self._uploadedFileId = uploadedFileId } // 添加缩放比例 private var scaleFactor: CGFloat { isKeyboardVisible ? 0.55 : 1.0 } private var borderWidth: CGFloat { isKeyboardVisible ? 3 : 4 } // 添加动画配置 private var animation: Animation { .spring(response: 0.4, dampingFraction: 0.7, blendDuration: 0.3) } public var body: some View { VStack(spacing: 20) { VStack(spacing: 20) { // Avatar Image Button Button(action: { withAnimation(animation) { showMediaPicker = true } }) { ZStack { if let selectedImage = selectedImage { Image(uiImage: selectedImage) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 225, height: 225) .clipShape(RoundedRectangle(cornerRadius: 20)) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(Color.themePrimary, lineWidth: borderWidth) ) .scaleEffect(scaleFactor) } else { // Default SVG avatar with animated dashed border SVGImageHtml(svgName: "IP") .frame(width: 225, height: 225) .scaleEffect(scaleFactor) .contentShape(Rectangle()) .clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor)) .overlay( RoundedRectangle(cornerRadius: 20) .stroke(style: StrokeStyle( lineWidth: borderWidth, lineCap: .round, dash: [12, 8], dashPhase: isAnimating ? 40 : 0 )) .foregroundColor(Color.themePrimary) .scaleEffect(scaleFactor) ) .onAppear { withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) { isAnimating = true } } } // Upload indicator if isUploading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .themePrimary)) .scaleEffect(1.5 * scaleFactor) .frame(width: 225, height: 225) .scaleEffect(scaleFactor) .background(Color.black.opacity(0.3)) .clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor)) .transition(.opacity) } } .frame(width: 225 * scaleFactor, height: 225 * scaleFactor) .animation(animation, value: isKeyboardVisible) } .buttonStyle(ScaleButtonStyle()) // Upload Button (only shown when username is not shown) if !showUsername { Button(action: { withAnimation(animation) { showMediaPicker = true } }) { Text("Upload from Gallery") .font(Typography.font(for: .subtitle, family: .inter)) .fontWeight(.regular) .frame(maxWidth: .infinity) .padding() .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.themePrimaryLight) ) .scaleEffect(scaleFactor) } .frame(maxWidth: .infinity) .transition(.opacity.combined(with: .move(edge: .bottom))) .animation(animation, value: isKeyboardVisible) } } .animation(animation, value: isKeyboardVisible) .sheet(isPresented: $showMediaPicker) { MediaPicker( selectedMedia: Binding( get: { uploadManager.selectedMedia }, set: { newMedia in // Only process if we have new media if !newMedia.isEmpty { uploadManager.clearAllMedia() uploadManager.addMedia(newMedia) // Start upload process withAnimation(animation) { isUploading = true } uploadManager.startUpload() print("🔄 Upload started") } // Dismiss the picker after processing showMediaPicker = false } ), imageSelectionLimit: 1, videoSelectionLimit: 0, allowedMediaTypes: .imagesOnly, selectionMode: .single, onDismiss: { showMediaPicker = false } ) } .onChange(of: uploadManager.uploadStatus) { _, status in print("🔄 Upload status changed: ", status) // 检查是否有待处理的上传 let pendingUploads = uploadManager.selectedMedia.filter { media in guard let status = uploadManager.uploadStatus[media.id] else { return true } return !status.isCompleted && !status.isUploading } if !pendingUploads.isEmpty { print("🔄 Found \(pendingUploads.count) pending uploads, starting upload...") uploadManager.startUpload() } // 检查是否有已完成的上传 for (mediaId, status) in status { if case .completed(let fileId) = status { print("✅ Found completed upload with fileId: ", fileId) // 查找对应的媒体项 if let media = uploadManager.selectedMedia.first(where: { $0.id == mediaId }), case .image(let image) = media { print("🖼️ Updating selected image") DispatchQueue.main.async { withAnimation(animation) { self.selectedImage = image self.uploadedFileId = fileId self.isUploading = false } // 成功更新后清除上传状态 self.uploadManager.clearAllMedia() } return } } } // 检查是否有失败的上传 let hasFailures = status.values.contains { if case .failed = $0 { return true } return false } if hasFailures { print("❌ Some uploads failed") DispatchQueue.main.async { withAnimation(animation) { self.isUploading = false } } } } if !showUsername { Button(action: { withAnimation(animation) { showImageCapture = true } }) { Text("Take a Photo") .font(Typography.font(for: .subtitle, family: .inter)) .fontWeight(.regular) .frame(maxWidth: .infinity) .padding() .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.themePrimaryLight) ) .scaleEffect(scaleFactor) } .frame(maxWidth: .infinity) .animation(animation, value: isKeyboardVisible) .sheet(isPresented: $showImageCapture) { CustomCameraView(isPresented: $showImageCapture) { image in selectedImage = image uploadManager.clearAllMedia() uploadManager.addMedia([.image(image)]) withAnimation(animation) { isUploading = true } uploadManager.startUpload() } } } } } } // Button style for scale effect private struct ScaleButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.95 : 1.0) .animation(.easeInOut(duration: 0.2), value: configuration.isPressed) } } // MARK: - Preview struct AvatarPicker_Previews: PreviewProvider { static var previews: some View { AvatarPicker( selectedImage: .constant(nil), showUsername: .constant(false), isKeyboardVisible: .constant(false), uploadedFileId: .constant(nil) ) .padding() .background(Color.gray.opacity(0.1)) .previewLayout(.sizeThatFits) } }