feat: 键盘动画
This commit is contained in:
parent
edef20028d
commit
58f560c714
Binary file not shown.
@ -1,17 +1,26 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
public struct AvatarPicker: View {
|
public struct AvatarPicker: View {
|
||||||
@StateObject private var uploadManager = MediaUploadManager()
|
@StateObject private var uploadManager = MediaUploadManager()
|
||||||
@State private var showMediaPicker = false
|
@State private var showMediaPicker = false
|
||||||
@State private var isUploading = false
|
@State private var isUploading = false
|
||||||
|
@State private var uploadedFileId: String? = nil
|
||||||
|
@State private var uploadTask: AnyCancellable?
|
||||||
@Binding var selectedImage: UIImage?
|
@Binding var selectedImage: UIImage?
|
||||||
@Binding var showUsername: Bool
|
@Binding var showUsername: Bool
|
||||||
|
@Binding var isKeyboardVisible: Bool
|
||||||
|
|
||||||
public init(selectedImage: Binding<UIImage?>, showUsername: Binding<Bool>) {
|
// 添加完成回调
|
||||||
|
var onFileUploaded: ((String) -> Void)? = nil
|
||||||
|
|
||||||
|
public init(selectedImage: Binding<UIImage?>, showUsername: Binding<Bool>, isKeyboardVisible: Binding<Bool>, onFileUploaded: ((String) -> Void)? = nil) {
|
||||||
self._selectedImage = selectedImage
|
self._selectedImage = selectedImage
|
||||||
self._showUsername = showUsername
|
self._showUsername = showUsername
|
||||||
|
self._isKeyboardVisible = isKeyboardVisible
|
||||||
|
self.onFileUploaded = onFileUploaded
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Avatar Image
|
// Avatar Image
|
||||||
@ -23,12 +32,12 @@ public struct AvatarPicker: View {
|
|||||||
Image(uiImage: selectedImage)
|
Image(uiImage: selectedImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: 225, height: 225)
|
.frame(width: isKeyboardVisible ? 125 : 225, height: isKeyboardVisible ? 125 : 225)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||||
} else {
|
} else {
|
||||||
// Default SVG avatar
|
// Default SVG avatar
|
||||||
SVGImage(svgName: "Avatar")
|
SVGImage(svgName: "Avatar")
|
||||||
.frame(width: 225, height: 225)
|
.frame(width: isKeyboardVisible ? 125 : 225, height: isKeyboardVisible ? 125 : 225)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,17 +80,31 @@ public struct AvatarPicker: View {
|
|||||||
if !uploadManager.selectedMedia.isEmpty {
|
if !uploadManager.selectedMedia.isEmpty {
|
||||||
isUploading = true
|
isUploading = true
|
||||||
uploadManager.startUpload()
|
uploadManager.startUpload()
|
||||||
|
|
||||||
|
// 使用 Combine 监听上传状态变化
|
||||||
|
uploadTask = uploadManager.$uploadStatus
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
if uploadManager.isAllUploaded {
|
||||||
|
isUploading = false
|
||||||
|
if let firstResult = uploadManager.getUploadResults().values.first {
|
||||||
|
self.uploadedFileId = firstResult
|
||||||
|
self.onFileUploaded?(firstResult)
|
||||||
|
}
|
||||||
|
uploadTask?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onChange(of: uploadManager.uploadStatus) { _ in
|
.onDisappear {
|
||||||
if let firstMedia = uploadManager.selectedMedia.first,
|
uploadTask?.cancel()
|
||||||
case .image(let image) = firstMedia,
|
}
|
||||||
uploadManager.isAllUploaded {
|
.onChange(of: uploadManager.selectedMedia) { newMedia in
|
||||||
|
if let firstMedia = newMedia.first,
|
||||||
|
case .image(let image) = firstMedia {
|
||||||
selectedImage = image
|
selectedImage = image
|
||||||
isUploading = false
|
|
||||||
uploadManager.clearAllMedia()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,82 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - UIApplication Extension for Hiding Keyboard
|
||||||
|
#if canImport(UIKit)
|
||||||
|
extension View {
|
||||||
|
func hideKeyboard() {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
struct UserInfo: View {
|
struct UserInfo: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var keyboardHeight: CGFloat = 0
|
||||||
|
@State private var isKeyboardVisible = false
|
||||||
|
|
||||||
// Sample user data - replace with your actual data model
|
// Sample user data - replace with your actual data model
|
||||||
@State private var userName = ""
|
@State private var userName = ""
|
||||||
@State private var userEmail = "memo@example.com"
|
|
||||||
@State private var notificationsEnabled = true
|
@State private var notificationsEnabled = true
|
||||||
@State private var darkModeEnabled = false
|
@State private var darkModeEnabled = false
|
||||||
@State private var showLogoutAlert = false
|
@State private var showLogoutAlert = false
|
||||||
@State private var avatarImage: UIImage?
|
@State private var avatarImage: UIImage? = nil
|
||||||
@State private var showUsername: Bool = false
|
@State private var uploadedFileId: String? = nil
|
||||||
|
@State private var showUsername = false
|
||||||
|
|
||||||
|
private struct MessageView: View {
|
||||||
|
let isKeyboardVisible: Bool
|
||||||
|
|
||||||
|
private var singleLineText: some View {
|
||||||
|
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
|
||||||
|
.font(Typography.font(for: .caption))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.padding(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var multiLineText: some View {
|
||||||
|
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
|
||||||
|
.font(Typography.font(for: .caption))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var background: some View {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
if isKeyboardVisible {
|
||||||
|
singleLineText
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .opacity.combined(with: .scale(scale: 0.95)),
|
||||||
|
removal: .opacity.combined(with: .scale(scale: 1.05))
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
multiLineText
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .opacity.combined(with: .scale(scale: 1.05)),
|
||||||
|
removal: .opacity.combined(with: .scale(scale: 0.95))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(background)
|
||||||
|
.animation(.spring(response: 0.3, dampingFraction: 1), value: isKeyboardVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack {
|
HStack {
|
||||||
@ -25,26 +90,14 @@ struct UserInfo: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
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))
|
MessageView(isKeyboardVisible: isKeyboardVisible).padding(.bottom, 6)
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
if !isKeyboardVisible {
|
||||||
.padding(.vertical, 10)
|
Spacer()
|
||||||
.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)
|
|
||||||
Spacer()
|
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Title
|
// Title
|
||||||
Text(showUsername ? "Add Your Avatar" : "What‘s Your Name?")
|
Text(showUsername ? "Add Your Avatar" : "What‘s Your Name?")
|
||||||
@ -55,7 +108,13 @@ struct UserInfo: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
AvatarPicker(
|
AvatarPicker(
|
||||||
selectedImage: $avatarImage,
|
selectedImage: $avatarImage,
|
||||||
showUsername: $showUsername
|
showUsername: $showUsername,
|
||||||
|
isKeyboardVisible: $isKeyboardVisible,
|
||||||
|
onFileUploaded: { fileId in
|
||||||
|
// 保存上传后的文件ID
|
||||||
|
print("Uploaded file ID: \(fileId)")
|
||||||
|
uploadedFileId = fileId
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
@ -87,17 +146,34 @@ struct UserInfo: View {
|
|||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(Color.themePrimaryLight)
|
.fill(Color.themePrimaryLight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color(.white))
|
.background(Color(.white))
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
Spacer()
|
|
||||||
|
if !isKeyboardVisible {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showUsername = true
|
if showUsername {
|
||||||
|
print("调接口将头像和username传到后端", uploadedFileId ?? "")
|
||||||
|
// 调接口将头像和username传到后端
|
||||||
|
UserService.shared.updateUsername(userName, userId: uploadedFileId ?? "") { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
print("Username updated: \(response.message ?? "")")
|
||||||
|
case .failure(let error):
|
||||||
|
print("Error updating username: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showUsername = true
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("Continue")
|
Text(showUsername ? "Open" : "Continue")
|
||||||
.font(Typography.font(for: .body))
|
.font(Typography.font(for: .body))
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -109,10 +185,23 @@ struct UserInfo: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
|
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
|
||||||
|
.onTapGesture {
|
||||||
|
hideKeyboard()
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
|
||||||
|
withAnimation {
|
||||||
|
isKeyboardVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
||||||
|
withAnimation {
|
||||||
|
isKeyboardVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
wake/View/Owner/UserInfo/UserService.swift
Normal file
44
wake/View/Owner/UserInfo/UserService.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
// MARK: - Request/Response Models
|
||||||
|
struct UpdateUsernameRequest: Codable {
|
||||||
|
let username: String
|
||||||
|
let userId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdateUsernameResponse: Codable {
|
||||||
|
let success: Bool
|
||||||
|
let message: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UserService
|
||||||
|
class UserService {
|
||||||
|
static let shared = UserService()
|
||||||
|
private let networkService = NetworkService.shared
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "UserService")
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func updateUsername(_ username: String, userId: String, completion: @escaping (Result<UpdateUsernameResponse, NetworkError>) -> Void) {
|
||||||
|
let parameters: [String: Any] = [
|
||||||
|
"username": username,
|
||||||
|
"avatar_file_id": userId
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info("🔄 开始更新用户信息: 用户名=\(username), 头像ID=\(userId)")
|
||||||
|
|
||||||
|
networkService.postWithToken(
|
||||||
|
path: "/iam/user/info",
|
||||||
|
parameters: parameters
|
||||||
|
) { [weak self] (result: Result<UpdateUsernameResponse, NetworkError>) in
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
self?.logger.info("✅ 用户信息更新成功: \(response.message ?? "")")
|
||||||
|
case .failure(let error):
|
||||||
|
self?.logger.error("❌ 用户信息更新失败: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user