import SwiftUI // User profile model struct UserProfile: Codable { let userId: String let nickname: String let avatarUrl: String? let account: String let email: String enum CodingKeys: String, CodingKey { case userId = "user_id" case nickname case avatarUrl = "avatar_file_url" case account case email } } // API Response wrapper struct APIResponse: Codable { let code: Int let data: T } struct UserProfileModal: View { @Binding var showModal: Bool @Binding var showSettings: Bool @State private var userProfile: UserProfile? @State private var isLoading = false @State private var errorMessage: String? @State private var isCopied = false var body: some View { VStack(spacing: 20) { Spacer() .frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0) if isLoading { ProgressView() .padding() } else if let error = errorMessage { Text(error) .foregroundColor(.red) .padding() } HStack(alignment: .center, spacing: 16) { if let avatarUrl = userProfile?.avatarUrl, !avatarUrl.isEmpty, let url = URL(string: avatarUrl) { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) .frame(width: 60, height: 60) .clipShape(Circle()) default: Image(systemName: "person.circle.fill") .resizable() .aspectRatio(contentMode: .fill) .frame(width: 60, height: 60) .foregroundColor(.blue) } } } else { Image(systemName: "person.circle.fill") .resizable() .aspectRatio(contentMode: .fill) .frame(width: 60, height: 60) .foregroundColor(.blue) } VStack(alignment: .leading, spacing: 10) { Text(userProfile?.nickname ?? "Name") .font(Typography.font(for: .body)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) HStack(spacing: 4) { Text("ID: \(userProfile?.userId ?? "")") .font(.system(size: 14)) .foregroundColor(.themeTextMessageMain) .lineLimit(1) .truncationMode(.tail) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: 120) Button(action: { print("Copy ID button tapped") UIPasteboard.general.string = userProfile?.userId print("Copied to clipboard:", userProfile?.userId ?? "nil") withAnimation { isCopied = true // Reset after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2) { withAnimation { isCopied = false print("Reset copy button state") } } } }) { if isCopied { Image(systemName: "checkmark") .foregroundColor(.themePrimary) } else { Image(systemName: "doc.on.doc") } } .font(.system(size: 12)) .foregroundColor(.themeTextMessageMain) .animation(.easeInOut, value: isCopied) .contentShape(Rectangle()) // Make the entire button area tappable .frame(width: 24, height: 24) // Ensure minimum touch target size } } Spacer() } .frame(maxWidth: .infinity) .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(Color.white) ) .padding(.horizontal) Button(action: { Router.shared.navigate(to: .subscribe) }) { ZStack(alignment: .center) { // SVG背景 - 确保铺满整个区域 SVGImage(svgName: "Pioneer") .scaledToFill() .frame(maxWidth: .infinity, maxHeight: .infinity) // 内容区域 VStack(alignment: .leading, spacing: 6) { Text("Pioneer") .font(Typography.font(for: .title3)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) .padding(.top, 6) Text("Expires on :") .font(.system(size: 12)) .foregroundColor(.themeTextMessageMain) .padding(.top, 2) Text("March 15, 2025") .font(.system(size: 12)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 32) // 可以添加内边距使内容不紧贴边缘 .padding(.vertical, 12) } .frame(height: 112) .frame(maxWidth: .infinity) .cornerRadius(16) .clipped() // 确保内容不会超出边界 } VStack(spacing: 12) { // upload Button(action: { Router.shared.navigate(to: .mediaUpload) }) { HStack(spacing: 16) { SVGImage(svgName: "Upload") .foregroundColor(.orange) .frame(width: 20, height: 20) Text("Upload Resources") .font(Typography.font(for: .body)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) Spacer() } .padding() .cornerRadius(10) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) // memories Button(action: { Router.shared.navigate(to: .memories) }) { HStack(spacing: 16) { SVGImage(svgName: "Memory") .foregroundColor(.orange) .frame(width: 20, height: 20) Text("My Memories") .font(Typography.font(for: .body)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) Spacer() } .padding() .cornerRadius(10) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) // Box Button(action: { Router.shared.navigate(to: .mediaUpload) }) { HStack(spacing: 16) { SVGImage(svgName: "Box") .foregroundColor(.orange) .frame(width: 20, height: 20) Text("My Blind Box") .font(Typography.font(for: .body)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) Spacer() } .padding() .cornerRadius(10) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) // setting Button(action: { withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { showSettings = true } }) { HStack(spacing: 16) { SVGImage(svgName: "Set") .foregroundColor(.orange) .frame(width: 20, height: 20) Text("Setting") .font(Typography.font(for: .body)) .fontWeight(.bold) .foregroundColor(.themeTextMessageMain) Spacer() } .padding() .cornerRadius(10) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) } .frame(maxWidth: .infinity) .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(Color.white) ) .padding(.horizontal) Spacer() } .frame(width: UIScreen.main.bounds.width * 0.8) .background(Color.themeTextWhiteSecondary) .edgesIgnoringSafeArea(.all) .onAppear { fetchUserInfo() } } private func fetchUserInfo() { isLoading = true errorMessage = nil NetworkService.shared.get( path: "/iam/user-info", parameters: nil ) { (result: Result, NetworkError>) in DispatchQueue.main.async { isLoading = false switch result { case .success(let response): self.userProfile = response.data print("✅ Successfully fetched user info:", response.data) case .failure(let error): self.errorMessage = error.localizedDescription print("❌ Failed to fetch user info:", error) } } } } } #Preview { UserProfileModal(showModal: .constant(true), showSettings: .constant(false)) }