2026-03-19 18:43:42 +08:00

256 lines
7.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 = ""
@State private var isLoading = false
var body: some View {
VStack(spacing: 20) {
CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill")
.frame(width: 280)
Button(action: {
self.login()
}) {
Text("验证并连接")
.fontWeight(.medium)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(width: 280)
.disabled(token.isEmpty || isLoading)
}
.onAppear {
if let cacheToken = self.userContext.loadCacheToken() {
self.token = cacheToken
}
}
}
private func login() {
isLoading = true
Task {
try? await userContext.loginWithToken(token: token)
isLoading = false
}
}
}
// 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())
}