fix login view
This commit is contained in:
parent
f379d734a8
commit
3558d3c102
@ -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 {
|
||||||
HStack(alignment: .center, spacing: 30) {
|
Circle()
|
||||||
Button {
|
.fill(Color.accentColor.opacity(0.1))
|
||||||
self.authMethod = .token
|
.frame(width: 80, height: 80)
|
||||||
} label: {
|
|
||||||
HStack {
|
Image(systemName: "network") // 建议使用 SF Symbol 保持精致感
|
||||||
Image("logo")
|
.font(.system(size: 38, weight: .semibold))
|
||||||
.resizable()
|
.foregroundColor(.accentColor)
|
||||||
.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
|
||||||
LoginTokenView()
|
Text(method.rawValue).tag(method)
|
||||||
case .account:
|
|
||||||
LoginAccountView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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()
|
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)
|
Button("忘记密码?") {
|
||||||
.background(Color.clear)
|
openWindow(id: "resetPassword")
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
Button(action: { /* Token login logic */ }) {
|
||||||
// let store = KeychainStore.shared
|
Text("验证并连接")
|
||||||
// if let tokenData = loginResult.accessToken.data(using: .utf8) {
|
.fontWeight(.medium)
|
||||||
// try store.save(tokenData, account: self.username)
|
.frame(maxWidth: .infinity)
|
||||||
// }
|
|
||||||
|
|
||||||
} catch let err as SDLAPIError {
|
|
||||||
await MainActor.run {
|
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = err.message
|
|
||||||
}
|
}
|
||||||
} 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())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user