340 lines
16 KiB
Swift
340 lines
16 KiB
Swift
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<UserInfoResponse, NetworkError>) 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)
|
||
}
|
||
}
|