// // LoginView.swift // punchnet // // Created by 安礼成 on 2026/1/15. // import SwiftUI import Observation // MARK: - 主容器视图 struct LoginView: View { @Environment(UserContext.self) var userContext: UserContext @State private var authMethod: AuthMethod = .account enum AuthMethod: String, CaseIterable { case account = "账户登录" case token = "密钥认证" } var body: some View { VStack(spacing: 0) { // 顶部 Logo 区域 VStack(spacing: 12) { ZStack { Circle() .fill(Color.accentColor.opacity(0.1)) .frame(width: 80, height: 80) Image(systemName: "network") // 建议使用 SF Symbol 保持精致感 .font(.system(size: 38, weight: .semibold)) .foregroundColor(.accentColor) } Text("PunchNet") .font(.system(size: 24, weight: .bold, design: .rounded)) .tracking(1) } .padding(.top, 40) .padding(.bottom, 30) // 原生分段切换器 Picker("", selection: $authMethod) { ForEach(AuthMethod.allCases, id: \.self) { method in Text(method.rawValue).tag(method) } } .pickerStyle(.segmented) .frame(width: 220) .padding(.bottom, 30) // 动态内容区 ZStack { if authMethod == .token { LoginTokenView() .transition(.move(edge: .trailing).combined(with: .opacity)) } else { LoginAccountView() .transition(.move(edge: .leading).combined(with: .opacity)) } } .animation(.spring(response: 0.3, dampingFraction: 0.8), value: authMethod) .frame(height: 180) Spacer() // 底部页脚 HStack(spacing: 4) { Circle() .fill(Color.green) .frame(width: 8, height: 8) Text("服务状态正常") .font(.system(size: 11)) .foregroundColor(.secondary) } .padding(.bottom, 20) } .frame(width: 380, height: 520) // 关键:macOS 标准毛玻璃背景 .background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) .ignoresSafeArea() } } // MARK: - 账户登录组件 struct LoginAccountView: View { @Environment(UserContext.self) var userContext: UserContext @Environment(\.openWindow) private var openWindow @State private var username = "" @State private var password = "" @State private var isLoading = false var body: some View { VStack(spacing: 16) { VStack(alignment: .leading, spacing: 12) { // 标准圆角输入框 CustomTextField(title: "手机号/邮箱", text: $username, icon: "person.fill") VStack(alignment: .trailing, spacing: 4) { CustomSecureField(title: "密码", text: $password, icon: "lock.fill") Button("忘记密码?") { openWindow(id: "resetPassword") } .buttonStyle(.link) .font(.system(size: 11)) .foregroundColor(.secondary) } } .frame(width: 280) // 蓝色主按钮 Button(action: { self.login() }) { HStack { if isLoading { ProgressView() .controlSize(.small) .padding(.trailing, 4) } Text("登录网络") .fontWeight(.medium) } .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .frame(width: 280) .keyboardShortcut(.defaultAction) // 绑定回车键 .disabled(username.isEmpty || password.isEmpty || isLoading) } .onAppear { if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() { self.username = cacheUsername self.password = cachePassword } } } private func login() { isLoading = true Task { try? await userContext.loginWithAccountAndPassword(username: username, password: password) isLoading = false } } } // MARK: - 密钥登录组件 struct LoginTokenView: View { @Environment(UserContext.self) var userContext: UserContext @State private var token = "" var body: some View { VStack(spacing: 20) { CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill") .frame(width: 280) Button(action: { /* Token login logic */ }) { Text("验证并连接") .fontWeight(.medium) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .frame(width: 280) .disabled(token.isEmpty) } .onAppear { if let cacheToken = self.userContext.loadCacheToken() { self.token = cacheToken } } } } // MARK: - 辅助 UI 组件 struct CustomTextField: View { let title: String @Binding var text: String let icon: String var body: some View { HStack { Image(systemName: icon) .foregroundColor(.secondary) .frame(width: 20) TextField(title, text: $text) .textFieldStyle(.plain) } .padding(8) .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) .cornerRadius(6) .overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2), lineWidth: 1)) } } struct CustomSecureField: View { let title: String @Binding var text: String let icon: String var body: some View { HStack { Image(systemName: icon) .foregroundColor(.secondary) .frame(width: 20) SecureField(title, text: $text) .textFieldStyle(.plain) } .padding(8) .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) .cornerRadius(6) .overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2), lineWidth: 1)) } } // MARK: - 1. 基础 UI 组件 (已修正 Material 枚举) struct VisualEffectView: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.material = material view.blendingMode = blendingMode view.state = .active return view } func updateNSView(_ nsView: NSVisualEffectView, context: Context) { nsView.material = material nsView.blendingMode = blendingMode } } #Preview { LoginView() .environment(UserContext()) }