From 3558d3c102930c19d26a7e2ac5d4eaa50416bcef Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Thu, 19 Mar 2026 16:52:07 +0800 Subject: [PATCH] fix login view --- punchnet/Views/Login/LoginView.swift | 402 +++++++++++++-------------- 1 file changed, 188 insertions(+), 214 deletions(-) diff --git a/punchnet/Views/Login/LoginView.swift b/punchnet/Views/Login/LoginView.swift index a7c1dc5..e16906c 100644 --- a/punchnet/Views/Login/LoginView.swift +++ b/punchnet/Views/Login/LoginView.swift @@ -4,231 +4,132 @@ // // 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 { - case token - case account + enum AuthMethod: String, CaseIterable { + case account = "账户登录" + case token = "密钥认证" } var body: some View { - VStack { - Text("PunchNet") - .font(.system(size: 16, weight: .medium)) - - HStack(alignment: .center, spacing: 30) { - Button { - self.authMethod = .token - } label: { - HStack { - Image("logo") - .resizable() - .clipped() - .frame(width: 25, height: 25) - - Text("密钥登陆") - .foregroundColor(self.authMethod == .token ? .blue : .black) - } - .contentShape(Rectangle()) + 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) } - .buttonStyle(.plain) - Button { - self.authMethod = .account - } label: { - HStack { - Image("logo") - .resizable() - .clipped() - .frame(width: 25, height: 25) - Text("账户登陆") - .foregroundColor(self.authMethod == .account ? .blue : .black) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) + Text("PunchNet") + .font(.system(size: 24, weight: .bold, design: .rounded)) + .tracking(1) } + .padding(.top, 40) + .padding(.bottom, 30) - Group { - switch self.authMethod { - case .token: - LoginTokenView() - case .account: - LoginAccountView() + // 原生分段切换器 + 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: 400, height: 400) - } - -} - -struct LoginTokenView: View { - @Environment(UserContext.self) var userContext: UserContext - @State private var token: String = "" - - @State private var showAlert = false - @State private var errorMessage = "" - - var body: some View { - VStack { - TextField("认证密钥", text: $token) - .multilineTextAlignment(.leading) - .textFieldStyle(PlainTextFieldStyle()) - .frame(width: 200, height: 25) - .background(Color.clear) - .foregroundColor(Color.black) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue) - .padding(.top, 25) - , alignment: .topLeading) - - Rectangle() - .overlay { - Text("登陆") - .font(.system(size: 14, weight: .regular)) - .foregroundColor(.black) - } - .frame(width: 120, height: 35) - .foregroundColor(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) - .cornerRadius(5.0) - .onTapGesture { - Task { - await self.doLogin() - } - } - } - .onAppear { - if let cacheToken = self.userContext.loadCacheToken() { - self.token = cacheToken - } - } - } - - // 执行登陆操作 - private func doLogin() async { - do { - try await self.userContext.loginWithToken(token: self.token) - print(self.userContext.networkSession?.accessToken) - - // 保存信息到KeychainStore - // let store = KeychainStore.shared - // if let tokenData = loginResult.accessToken.data(using: .utf8) { - // try store.save(tokenData, account: self.username) - // } - - } catch let err as SDLAPIError { - await MainActor.run { - self.showAlert = true - self.errorMessage = err.message - } - } catch { - await MainActor.run { - self.showAlert = true - self.errorMessage = "内部错误,请稍后重试" - } - } + .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 - @Environment(UserContext.self) var userContext: UserContext - @Environment(AppContext.self) var appContext: AppContext - - @State private var username: String = "" - @State private var password: String = "" - - @State private var showAlert = false - @State private var errorMessage = "" - - struct LoginResult: Decodable { - var accessToken: String - var networkId: Int - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case networkId = "network_id" - } - } + @State private var username = "" + @State private var password = "" + @State private var isLoading = false var body: some View { - VStack(alignment: .center) { - VStack(alignment: .leading) { - TextField("手机号/邮箱", text: $username) - .multilineTextAlignment(.leading) - .textFieldStyle(PlainTextFieldStyle()) - .frame(width: 200, height: 25) - .background(Color.clear) - .foregroundColor(Color.black) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue) - .padding(.top, 25) - , alignment: .topLeading) + VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 12) { + // 标准圆角输入框 + CustomTextField(title: "手机号/邮箱", text: $username, icon: "person.fill") - SecureField("密码", text: $password) - .multilineTextAlignment(.leading) - .textFieldStyle(PlainTextFieldStyle()) - .frame(width: 200, height: 25) - .background(Color.clear) - .foregroundColor(Color.black) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.blue) - .padding(.top, 25) - , alignment: .topLeading) - - Button { - openWindow(id: "resetPassword") - } label: { - Text("忘记密码?") - .underline(true, color: .blue) - .font(.system(size: 14, weight: .regular)) - .foregroundColor(.blue) - } - .buttonStyle(.plain) - } - - Button { - if self.username.isEmpty { - self.showAlert = true - self.errorMessage = "账号不能为空" - } else if self.password.isEmpty { - self.showAlert = true - self.errorMessage = "密码不能为空" - } else { - Task { - await self.doLogin() + VStack(alignment: .trailing, spacing: 4) { + CustomSecureField(title: "密码", text: $password, icon: "lock.fill") + + Button("忘记密码?") { + openWindow(id: "resetPassword") } + .buttonStyle(.link) + .font(.system(size: 11)) + .foregroundColor(.secondary) } - } label: { - Text("登陆") - .font(.system(size: 14, weight: .regular)) - .foregroundColor(.black) - .frame(width: 120, height: 35) } - .frame(width: 120, height: 35) - .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) - .cornerRadius(5.0) - } - .alert(isPresented: $showAlert) { - Alert(title: Text("错误提示"), message: Text("账号密码为空")) + .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() { @@ -238,33 +139,106 @@ struct LoginAccountView: View { } } - // 执行登陆操作 - private func doLogin() async { - do { - try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password) + 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) - // 保存信息到KeychainStore - // let store = KeychainStore.shared - // if let tokenData = loginResult.accessToken.data(using: .utf8) { - // try store.save(tokenData, account: self.username) - // } - - } catch let err as SDLAPIError { - await MainActor.run { - self.showAlert = true - self.errorMessage = err.message + Button(action: { /* Token login logic */ }) { + Text("验证并连接") + .fontWeight(.medium) + .frame(maxWidth: .infinity) } - } catch { - await MainActor.run { - self.showAlert = true - self.errorMessage = "内部错误,请稍后重试" + .buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(width: 280) + .disabled(token.isEmpty) + } + .onAppear { + if let cacheToken = self.userContext.loadCacheToken() { + self.token = cacheToken } } } } -#Preview { - // ResetPasswordView() - // .environment(UserContext()) +// 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()) }