fix login view

This commit is contained in:
anlicheng 2026-03-19 16:52:07 +08:00
parent f379d734a8
commit 3558d3c102

View File

@ -4,231 +4,132 @@
// //
// Created by on 2026/1/15. // Created by on 2026/1/15.
// //
import SwiftUI import SwiftUI
import Observation import Observation
// // MARK: -
struct LoginView: View { struct LoginView: View {
@Environment(UserContext.self) var userContext: UserContext @Environment(UserContext.self) var userContext: UserContext
@State private var authMethod: AuthMethod = .account @State private var authMethod: AuthMethod = .account
enum AuthMethod { enum AuthMethod: String, CaseIterable {
case token case account = "账户登录"
case account case token = "密钥认证"
} }
var body: some View { var body: some View {
VStack { VStack(spacing: 0) {
Text("PunchNet") // Logo
.font(.system(size: 16, weight: .medium)) VStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.accentColor.opacity(0.1))
.frame(width: 80, height: 80)
HStack(alignment: .center, spacing: 30) { Image(systemName: "network") // 使 SF Symbol
Button { .font(.system(size: 38, weight: .semibold))
self.authMethod = .token .foregroundColor(.accentColor)
} label: {
HStack {
Image("logo")
.resizable()
.clipped()
.frame(width: 25, height: 25)
Text("密钥登陆")
.foregroundColor(self.authMethod == .token ? .blue : .black)
}
.contentShape(Rectangle())
} }
.buttonStyle(.plain)
Button { Text("PunchNet")
self.authMethod = .account .font(.system(size: 24, weight: .bold, design: .rounded))
} label: { .tracking(1)
HStack {
Image("logo")
.resizable()
.clipped()
.frame(width: 25, height: 25)
Text("账户登陆")
.foregroundColor(self.authMethod == .account ? .blue : .black)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
} }
.padding(.top, 40)
.padding(.bottom, 30)
Group { //
switch self.authMethod { Picker("", selection: $authMethod) {
case .token: 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() LoginTokenView()
case .account: .transition(.move(edge: .trailing).combined(with: .opacity))
} else {
LoginAccountView() LoginAccountView()
.transition(.move(edge: .leading).combined(with: .opacity))
} }
} }
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: authMethod)
.frame(height: 180)
Spacer() 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) .frame(width: 380, height: 520)
} // macOS
.background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow))
} .ignoresSafeArea()
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 = "内部错误,请稍后重试"
}
}
} }
} }
// MARK: -
struct LoginAccountView: View { struct LoginAccountView: View {
@Environment(UserContext.self) var userContext: UserContext
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@Environment(UserContext.self) var userContext: UserContext @State private var username = ""
@Environment(AppContext.self) var appContext: AppContext @State private var password = ""
@State private var isLoading = false
@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"
}
}
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(spacing: 16) {
VStack(alignment: .leading) { VStack(alignment: .leading, spacing: 12) {
TextField("手机号/邮箱", text: $username) //
.multilineTextAlignment(.leading) CustomTextField(title: "手机号/邮箱", text: $username, icon: "person.fill")
.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)
SecureField("密码", text: $password) VStack(alignment: .trailing, spacing: 4) {
.multilineTextAlignment(.leading) CustomSecureField(title: "密码", text: $password, icon: "lock.fill")
.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 { Button("忘记密码?") {
openWindow(id: "resetPassword") 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()
} }
.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) .frame(width: 280)
.background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0) //
} Button(action: { self.login() }) {
.alert(isPresented: $showAlert) { HStack {
Alert(title: Text("错误提示"), message: Text("账号密码为空")) 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 { .onAppear {
if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() { if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() {
@ -238,33 +139,106 @@ struct LoginAccountView: View {
} }
} }
// private func login() {
private func doLogin() async { isLoading = true
do { Task {
try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password) try? await userContext.loginWithAccountAndPassword(username: username, password: password)
isLoading = false
}
}
}
// KeychainStore // MARK: -
// let store = KeychainStore.shared struct LoginTokenView: View {
// if let tokenData = loginResult.accessToken.data(using: .utf8) { @Environment(UserContext.self) var userContext: UserContext
// try store.save(tokenData, account: self.username) @State private var token = ""
// }
} catch let err as SDLAPIError { var body: some View {
await MainActor.run { VStack(spacing: 20) {
self.showAlert = true CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill")
self.errorMessage = err.message .frame(width: 280)
Button(action: { /* Token login logic */ }) {
Text("验证并连接")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
} }
} catch { .buttonStyle(.borderedProminent)
await MainActor.run { .controlSize(.large)
self.showAlert = true .frame(width: 280)
self.errorMessage = "内部错误,请稍后重试" .disabled(token.isEmpty)
}
.onAppear {
if let cacheToken = self.userContext.loadCacheToken() {
self.token = cacheToken
} }
} }
} }
} }
#Preview { // MARK: - UI
// ResetPasswordView() struct CustomTextField: View {
// .environment(UserContext()) 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())
} }