diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 754d41e..42d4c1f 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/Models/AuthModels.swift b/wake/Models/AuthModels.swift index 0f552d0..21ff48a 100644 --- a/wake/Models/AuthModels.swift +++ b/wake/Models/AuthModels.swift @@ -41,3 +41,19 @@ struct LoginResponseData: Codable { /// 认证响应模型 typealias AuthResponse = BaseResponse + +/// 用户信息响应数据 +struct UserInfoData: Codable { + let userId: String + let username: String + let avatarFileId: String? + + enum CodingKeys: String, CodingKey { + case userId = "user_id" + case username + case avatarFileId = "avatar_file_id" + } +} + +/// 用户信息响应模型 +typealias UserInfoResponse = BaseResponse diff --git a/wake/View/Owner/UserInfo/AvatarPicker.swift b/wake/View/Owner/UserInfo/AvatarPicker.swift index 83270c5..e4fec29 100644 --- a/wake/View/Owner/UserInfo/AvatarPicker.swift +++ b/wake/View/Owner/UserInfo/AvatarPicker.swift @@ -7,72 +7,104 @@ public struct AvatarPicker: View { @Binding var selectedImage: UIImage? @Binding var showUsername: Bool @Binding var isKeyboardVisible: Bool + @Binding var uploadedFileId: String? - public init(selectedImage: Binding, showUsername: Binding, isKeyboardVisible: Binding) { + // 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 avatarSize: CGFloat { + isKeyboardVisible ? 125 : 225 + } + + private var borderWidth: CGFloat { + isKeyboardVisible ? 3 : 4 } public var body: some View { VStack(spacing: 20) { - // Avatar Image + // Avatar Image Button Button(action: { - showMediaPicker = true + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + showMediaPicker = true + } }) { - ZStack { + ZStack { if let selectedImage = selectedImage { Image(uiImage: selectedImage) .resizable() .scaledToFill() - .frame(width: isKeyboardVisible ? 125 : 225, - height: isKeyboardVisible ? 125 : 225) + .frame(width: avatarSize, height: avatarSize) .clipShape(RoundedRectangle(cornerRadius: 20)) .overlay( RoundedRectangle(cornerRadius: 20) - .stroke(Color.themePrimary, lineWidth: 4) + .stroke(Color.themePrimary, lineWidth: borderWidth) ) - .animation(.spring(response: 0.4, dampingFraction: 1), value: isKeyboardVisible) } else { - // Default SVG avatar + // Default SVG avatar with animated dashed border SVGImage(svgName: "IP") - .frame(width: isKeyboardVisible ? 125 : 225, - height: isKeyboardVisible ? 125 : 225) + .frame(width: avatarSize, height: avatarSize) .contentShape(Rectangle()) .overlay( RoundedRectangle(cornerRadius: 20) - .stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, dash: [12, 8])) + .stroke(style: StrokeStyle( + lineWidth: borderWidth, + lineCap: .round, + dash: [12, 8], + dashPhase: isAnimating ? 40 : 0 + )) .foregroundColor(Color.themePrimary) ) - .animation(.spring(response: 0.4, dampingFraction: 1), value: isKeyboardVisible) + .onAppear { + withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) { + isAnimating = true + } + } } + // Upload indicator if isUploading { ProgressView() - .progressViewStyle(CircularProgressViewStyle()) + .progressViewStyle(CircularProgressViewStyle(tint: .themePrimary)) .scaleEffect(1.5) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black.opacity(0.3)) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .transition(.opacity) } } + .frame(width: avatarSize, height: avatarSize) + .animation(.spring(response: 0.4, dampingFraction: 0.7), value: isKeyboardVisible) } + .buttonStyle(ScaleButtonStyle()) + + // Upload Button (only shown when username is not shown) if !showUsername { - // Upload button Button(action: { - showMediaPicker = true + withAnimation { + showMediaPicker = true + } }) { Text("Upload from Gallery") .font(Typography.font(for: .subtitle, family: .inter)) .fontWeight(.regular) .frame(maxWidth: .infinity) .padding() - .foregroundColor(.black) + .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.themePrimaryLight) - ) + ) } .frame(maxWidth: .infinity) + .transition(.opacity.combined(with: .move(edge: .bottom))) } - } .sheet(isPresented: $showMediaPicker) { MediaPicker( @@ -84,7 +116,9 @@ public struct AvatarPicker: View { onDismiss: { showMediaPicker = false if !uploadManager.selectedMedia.isEmpty { - isUploading = true + withAnimation { + isUploading = true + } uploadManager.startUpload() } } @@ -94,10 +128,40 @@ public struct AvatarPicker: View { if let firstMedia = uploadManager.selectedMedia.first, case .image(let image) = firstMedia, uploadManager.isAllUploaded { - selectedImage = image - isUploading = false - uploadManager.clearAllMedia() + withAnimation(.spring()) { + selectedImage = image + isUploading = false + if let status = uploadManager.uploadStatus["0"], + case .completed(let fileId) = status { + uploadedFileId = fileId + } + uploadManager.clearAllMedia() + } } } } +} + +// 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) + } } \ No newline at end of file diff --git a/wake/View/Owner/UserInfo/UserInfo.swift b/wake/View/Owner/UserInfo/UserInfo.swift index 319aafc..37521df 100644 --- a/wake/View/Owner/UserInfo/UserInfo.swift +++ b/wake/View/Owner/UserInfo/UserInfo.swift @@ -11,9 +11,19 @@ struct UserInfo: View { @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 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) var body: some View { ZStack { @@ -49,132 +59,166 @@ struct UserInfo: View { .padding(.vertical, 12) .background(Color.themeTextWhiteSecondary) .zIndex(1) // 确保导航栏在最上层 + // Dynamic text that changes based on keyboard state + HStack(spacing: 20) { + 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) + .lineLimit(isKeyboardVisible ? 1 : 2) + .padding(6) + .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 + ) + ) + } + .padding(10) + .animation(.easeInOut(duration: 0.3), value: isKeyboardVisible) + .transition(.opacity) // 可滚动的内容区域 - ScrollView { - VStack(spacing: 0) { - // 单行文本(键盘显示时) - if isKeyboardVisible { - HStack(spacing: 20) { - 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) - .lineLimit(1) - .padding(6) - .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 - ) - ) + GeometryReader { geometry in + ScrollView { + VStack(spacing: 0) { + // 顶部Spacer - 只在非键盘状态下生效 + if !isKeyboardVisible { + Spacer(minLength: 0) + .frame(height: geometry.size.height * 0.1) // 10% of available height } - .padding(10) - .transition(AnyTransition.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.4), value: isKeyboardVisible) - } - // 两行文本(键盘隐藏时) - else { - HStack(spacing: 20) { - 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) - .lineLimit(2) - .padding(6) - .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 - ) - ) - } - .padding(10) - .transition(AnyTransition.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.25), value: isKeyboardVisible) - } - - if !isKeyboardVisible { Spacer() } - - VStack(spacing: 20) { - // Title - Text(showUsername ? "Add Your Avatar" : "What‘s Your Name?") - .font(Typography.font(for: .body, family: .quicksandBold)) - .frame(maxWidth: .infinity, alignment: .center) - // Avatar - ZStack { + // Content VStack + VStack(spacing: 20) { + // Title + Text(showUsername ? "Add Your Avatar" : "What's Your Name?") + .font(Typography.font(for: .body, family: .quicksandBold)) + .frame(maxWidth: .infinity, alignment: .center) + + // Avatar AvatarPicker( selectedImage: $avatarImage, showUsername: $showUsername, - isKeyboardVisible: $isKeyboardVisible + isKeyboardVisible: $isKeyboardVisible, + uploadedFileId: $uploadedFileId ) - } - .padding(.top, isKeyboardVisible ? 0 : 20) - - if !showUsername { - Button(action: { - // Action for second button - }) { - Text("Take a Photo") + .padding(.top, isKeyboardVisible ? 0 : 20) + + if !showUsername { + Button(action: { + // Action for second button + }) { + 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) + ) + } + } + + if showUsername { + TextField("Username", text: $userName) .font(Typography.font(for: .subtitle, family: .inter)) - .fontWeight(.regular) + .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.themePrimaryLight) - ) + ) + .focused($isTextFieldFocused) + .submitLabel(.done) + .onSubmit { + isTextFieldFocused = false + } } } - - if showUsername { - TextField("Username", text: $userName) - .font(Typography.font(for: .subtitle, family: .inter)) - .multilineTextAlignment(.center) + .padding() + .background(Color(.white)) + .cornerRadius(20) + .padding(.horizontal) + + // 底部Spacer - 只在非键盘状态下生效 + if !isKeyboardVisible { + Spacer(minLength: 0) + .frame(height: geometry.size.height * 0.1) // 10% of available height + } + + // Continue Button + Button(action: { + if showUsername { + let parameters: [String: Any] = [ + "username": 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("✅ 用户信息更新成功") + // Update local state with the new user info + if let userData = response.data { + self.userName = userData.username + // You can update other user data here if needed + } + // Show success message or navigate back + self.dismiss() + + case .failure(let error): + print("❌ 用户信息更新失败: \(error.localizedDescription)") + // Show error message to user + // You can use an @State variable to show an alert or toast + self.errorMessage = "更新失败: \(error.localizedDescription)" + self.showError = true + } + } + } + } else { + withAnimation { + showUsername = true + } + } + }) { + Text(showUsername ? "Open" : "Continue") + .font(Typography.font(for: .body)) + .fontWeight(.bold) .frame(maxWidth: .infinity) .padding() - .foregroundColor(.black) + .foregroundColor(.black) .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color.themePrimaryLight) - ) + RoundedRectangle(cornerRadius: 25) + .fill(Color.themePrimary) + ) + } + .padding(.horizontal) + .padding(.bottom, isKeyboardVisible ? 20 : 40) + .disabled(showUsername && userName.trimmingCharacters(in: .whitespaces).isEmpty) + .opacity((showUsername && userName.trimmingCharacters(in: .whitespaces).isEmpty) ? 0.6 : 1.0) + .animation(.easeInOut, value: showUsername) + .frame(maxWidth: .infinity) + + // 底部安全区域占位 + if isKeyboardVisible { + Spacer(minLength: 0) + .frame(height: 20) // 添加一些底部间距当键盘显示时 } } - .padding() - .background(Color(.white)) - .cornerRadius(20) - .padding(.horizontal) - - if !isKeyboardVisible { Spacer() } - - Button(action: { - showUsername = true - }) { - Text("Continue") - .font(Typography.font(for: .body)) - .fontWeight(.bold) - .frame(maxWidth: .infinity) - .padding(16) - .foregroundColor(.black) - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.themePrimary) - ) - } - .padding() - .padding(.bottom, isKeyboardVisible ? 20 : 40) + .frame(minHeight: geometry.size.height) // 确保内容至少填满可用高度 } } .background(Color.themeTextWhiteSecondary) @@ -190,19 +234,49 @@ struct UserInfo: View { 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 - isKeyboardVisible = true + withAnimation(.easeInOut(duration: 0.3)) { + isKeyboardVisible = true + } } NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in - isKeyboardVisible = false + withAnimation(.easeInOut(duration: 0.3)) { + isKeyboardVisible = false + } } } .onDisappear { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + // 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 + } } } }