import SwiftUI struct UserInfo: View { let createFirstBlindBox: Bool @Environment(\.dismiss) private var dismiss @StateObject private var router = Router.shared // Sample user data - replace with your actual data model @State private var userName = "" @State private var userEmail = "memo@example.com" @State private var notificationsEnabled = true @State private var darkModeEnabled = false @State private var showLogoutAlert = false @State private var avatarImage: UIImage? @State private var showUsername: Bool = false @State private var uploadedFileId: String? // Add state for file ID @State private var errorMessage: String = "" @State private var showError: Bool = false // 添加一个状态来跟踪键盘是否显示 @State private var isKeyboardVisible = false @FocusState private var isTextFieldFocused: Bool // 使用静态属性来确保只初始化一次 private static let keyboardPreloader: Void = { let textField = UITextField() textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.spellCheckingType = .no textField.isHidden = true if let window = UIApplication.shared.windows.first { window.addSubview(textField) textField.becomeFirstResponder() textField.resignFirstResponder() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { textField.removeFromSuperview() } } }() private let keyboardPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification) .map { _ in true } .merge(with: NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification) .map { _ in false }) .receive(on: RunLoop.main) init(createFirstBlindBox: Bool) { // 在初始化时预加载键盘 _ = UserInfo.keyboardPreloader self.createFirstBlindBox = createFirstBlindBox } var body: some View { ZStack { // 背景色 Color.themeTextWhiteSecondary .edgesIgnoringSafeArea(.all) VStack(spacing: 0) { // 固定的顶部导航栏 HStack { Button(action: { dismiss() }) { 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) } .padding(.vertical, 12) .background(Color.themeTextWhiteSecondary) .zIndex(1) // 确保导航栏在最上层 // Dynamic text that changes based on keyboard state HStack(spacing: 6) { Image(systemName: "lightbulb") .font(.system(size: 16, weight: .regular)) .foregroundColor(.themeTextMessageMain) .padding(.leading,6) Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.") .font(Typography.font(for: .caption)) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) .padding(3) } .animation(.easeInOut(duration: 0.3), value: isKeyboardVisible) .transition(.opacity) .background( Color.themeTextWhite .cornerRadius(12) ) .padding(10) // 可滚动的内容区域 GeometryReader { geometry in ZStack(alignment: .bottom) { ScrollView { VStack(spacing: 0) { // 顶部Spacer - 只在非键盘状态下生效 if !isKeyboardVisible { Spacer(minLength: 0) .frame(height: geometry.size.height * 0.1) // 10% of available height } // Content VStack VStack(spacing: 20) { // Title Text(showUsername ? "Enter your favorite nickname" : "Add Your Avatar") .font(Typography.font(for: .body, family: .quicksandBold)) .frame(maxWidth: .infinity, alignment: .center) // Avatar AvatarPicker( selectedImage: $avatarImage, showUsername: $showUsername, isKeyboardVisible: $isKeyboardVisible, uploadedFileId: $uploadedFileId ) .padding(.top, isKeyboardVisible ? 0 : 20) if showUsername { TextField("Username", text: $userName) .font(Typography.font(for: .subtitle, family: .inter)) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.themePrimaryLight) ) .focused($isTextFieldFocused) .submitLabel(.done) .onSubmit { isTextFieldFocused = false } } } .padding() .background(Color(.white)) .cornerRadius(20) .padding(.horizontal) // 添加底部间距,为固定按钮留出空间 Spacer(minLength: 40) // 按钮高度 + 底部间距 } .frame(minHeight: geometry.size.height) // 确保内容至少填满可用高度 .padding(.bottom, isKeyboardVisible ? 300 : 0) // 为键盘留出空间 } // Fixed Button at bottom VStack { Spacer() Button(action: { if showUsername { let parameters: [String: Any] = [ "user_name": userName, "avatar_file_id": uploadedFileId ?? "" ] NetworkService.shared.postWithToken( path: "/iam/user/info", parameters: parameters ) { (result: Result) in DispatchQueue.main.async { switch result { case .success(let response): print("✅ 用户信息更新成功") if let userData = response.data { self.userName = userData.username } if createFirstBlindBox { // 上传头像为素材 MaterialUpload.shared.addMaterial( fileId: uploadedFileId ?? "", previewFileId: uploadedFileId ?? "" ) { result in switch result { case .success(let data): print("素材添加成功,返回ID: \(data ?? [])") // 触发盲盒生成 BlindBoxApi.shared.generateBlindBox( boxType: "First", materialIds: data ?? [] ) { result in switch result { case .success(let blindBoxData): print("✅ 盲盒生成成功: \(blindBoxData?.id ?? "0")") // 导航到首页盲盒等待用户开启第一个盲盒 Router.shared.navigate(to: .blindBox(mediaType: .image, blindBoxId: blindBoxData?.id ?? "0")) case .failure(let error): print("❌ 盲盒生成失败: \(error.localizedDescription)") } } case .failure(let error): print("素材添加失败: \(error.localizedDescription)") } } } else { Router.shared.navigate(to: .blindBox(mediaType: .all)) } case .failure(let error): print("❌ 用户信息更新失败: \(error.localizedDescription)") self.errorMessage = "更新失败: \(error.localizedDescription)" self.showError = true } } } } else { withAnimation { showUsername = true } } }) { // Text(showUsername ? "Open" : "Continue") Text("Continue") .font(Typography.font(for: .body)) .fontWeight(.bold) .frame(maxWidth: .infinity) .padding() .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 25) .fill(Color.themePrimary) ) } .padding(.horizontal, 32) .padding(.bottom, isKeyboardVisible ? 20 : 40) .animation(.easeInOut, value: showUsername) } } } .background(Color.themeTextWhiteSecondary) } .navigationBarHidden(true) .navigationBarBackButtonHidden(true) // 键盘出现时的半透明覆盖层 if isKeyboardVisible { Color.black.opacity(0.001) .edgesIgnoringSafeArea(.all) .onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } // 错误提示 if showError { VStack { Text(errorMessage) .font(Typography.font(for: .body)) .foregroundColor(.red) .padding() .background(Color.white) .cornerRadius(10) .shadow(radius: 2) } .frame(maxWidth: .infinity, alignment: .center) .padding() } } .onAppear { // Set up keyboard notifications NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { _ in withAnimation(.easeInOut(duration: 0.3)) { isKeyboardVisible = true } } NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in withAnimation(.easeInOut(duration: 0.3)) { isKeyboardVisible = false // 当键盘隐藏时,确保预加载的TextField失去焦点 } } } .onDisappear { // Clean up observers NotificationCenter.default.removeObserver(self) } .onReceive(keyboardPublisher) { isVisible in withAnimation(.easeInOut(duration: 0.2)) { isKeyboardVisible = isVisible } } .onChange(of: isTextFieldFocused) { _, newValue in withAnimation(.easeInOut(duration: 0.2)) { isKeyboardVisible = newValue } } } } // MARK: - Settings Row View struct SettingsRow: View { let icon: String let title: String let color: Color var body: some View { HStack { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) .padding(6) .background(color.opacity(0.1)) .foregroundColor(color) .cornerRadius(6) Text(title) .padding(.leading, 5) } .padding(.vertical, 4) } } // MARK: - Preview struct UserInfo_Previews: PreviewProvider { static var previews: some View { UserInfo(createFirstBlindBox: false) } }