This commit is contained in:
anlicheng 2026-03-23 22:09:28 +08:00
parent aca4bf1ec2
commit 883f9d7f64
8 changed files with 139 additions and 72 deletions

View File

@ -15,6 +15,17 @@ class AppContext {
// "/connect" // "/connect"
var networkContext: NetworkContext? var networkContext: NetworkContext?
//
enum AppScene {
case login
case logined
case register
case resetPassword
}
// app
var appScene: AppScene = .login
init(noticePort: Int) { init(noticePort: Int) {
self.noticePort = noticePort self.noticePort = noticePort
} }

View File

@ -86,12 +86,16 @@ struct LoginView: View {
// MARK: - // MARK: -
struct LoginAccountView: View { struct LoginAccountView: View {
@Environment(UserContext.self) var userContext: UserContext @Environment(UserContext.self) var userContext: UserContext
@Environment(\.openWindow) private var openWindow @Environment(AppContext.self) var appContext: AppContext
@State private var username = "" @State private var username = ""
@State private var password = "" @State private var password = ""
@State private var isLoading = false @State private var isLoading = false
//
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""
var body: some View { var body: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@ -103,14 +107,18 @@ struct LoginAccountView: View {
HStack { HStack {
Button("注册") { Button("注册") {
openWindow(id: "register") withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.appContext.appScene = .register
}
} }
.buttonStyle(.link) .buttonStyle(.link)
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(.secondary) .foregroundColor(.secondary)
Button("忘记密码?") { Button("忘记密码?") {
openWindow(id: "resetPassword") withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.appContext.appScene = .resetPassword
}
} }
.buttonStyle(.link) .buttonStyle(.link)
.font(.system(size: 11)) .font(.system(size: 11))
@ -121,7 +129,11 @@ struct LoginAccountView: View {
.frame(width: 280) .frame(width: 280)
// //
Button(action: { self.login() }) { Button(action: {
Task { @MainActor in
await self.login()
}
}) {
HStack { HStack {
if isLoading { if isLoading {
ProgressView() ProgressView()
@ -140,6 +152,9 @@ struct LoginAccountView: View {
.keyboardShortcut(.defaultAction) // .keyboardShortcut(.defaultAction) //
.disabled(username.isEmpty || password.isEmpty || isLoading) .disabled(username.isEmpty || password.isEmpty || isLoading)
} }
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
.onAppear { .onAppear {
if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() { if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() {
self.username = cacheUsername self.username = cacheUsername
@ -148,11 +163,20 @@ struct LoginAccountView: View {
} }
} }
private func login() { private func login() async {
isLoading = true self.isLoading = true
Task { defer {
try? await userContext.loginWithAccountAndPassword(username: username, password: password) self.isLoading = false
isLoading = false }
do {
_ = try await userContext.loginWithAccountAndPassword(username: username, password: password)
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.appContext.appScene = .logined
}
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
} }
} }
} }
@ -160,16 +184,24 @@ struct LoginAccountView: View {
// MARK: - // MARK: -
struct LoginTokenView: View { struct LoginTokenView: View {
@Environment(UserContext.self) var userContext: UserContext @Environment(UserContext.self) var userContext: UserContext
@Environment(AppContext.self) var appContext: AppContext
@State private var token = "" @State private var token = ""
@State private var isLoading = false @State private var isLoading = false
//
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""
var body: some View { var body: some View {
VStack(spacing: 20) { VStack(spacing: 20) {
CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill") CustomTextField(title: "请输入认证密钥 (Token)", text: $token, icon: "key.fill")
.frame(width: 280) .frame(width: 280)
Button(action: { Button(action: {
self.login() Task { @MainActor in
await self.login()
}
}) { }) {
Text("验证并连接") Text("验证并连接")
.fontWeight(.medium) .fontWeight(.medium)
@ -180,6 +212,9 @@ struct LoginTokenView: View {
.frame(width: 280) .frame(width: 280)
.disabled(token.isEmpty || isLoading) .disabled(token.isEmpty || isLoading)
} }
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
.onAppear { .onAppear {
if let cacheToken = self.userContext.loadCacheToken() { if let cacheToken = self.userContext.loadCacheToken() {
self.token = cacheToken self.token = cacheToken
@ -187,11 +222,20 @@ struct LoginTokenView: View {
} }
} }
private func login() { private func login() async {
isLoading = true self.isLoading = true
Task { defer {
try? await userContext.loginWithToken(token: token) self.isLoading = false
isLoading = false }
do {
_ = try await userContext.loginWithToken(token: token)
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.appContext.appScene = .logined
}
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
} }
} }

View File

@ -35,6 +35,7 @@ struct RegisterRootView: View {
)) ))
} }
.environment(registerModel) .environment(registerModel)
.frame(width: 500, height: 400)
} }
} }

View File

@ -35,6 +35,7 @@ struct ResetPasswordRootView: View {
)) ))
} }
.environment(resetPasswordModel) .environment(resetPasswordModel)
.frame(width: 500, height: 400)
} }
} }

View File

@ -9,18 +9,29 @@ import SwiftUI
struct RootView: View { struct RootView: View {
@Environment(UserContext.self) var userContext @Environment(UserContext.self) var userContext
@Environment(AppContext.self) var appContext: AppContext
@State private var updateManager = AppUpdateManager.shared @State private var updateManager = AppUpdateManager.shared
var body: some View { var body: some View {
ZStack { ZStack {
// //
Group { Group {
if userContext.isLogined { switch appContext.appScene {
NetworkView() case .login:
} else {
LoginView() LoginView()
case .logined:
NetworkView()
case .register:
RegisterRootView()
case .resetPassword:
ResetPasswordRootView()
} }
} }
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .trailing).combined(with: .opacity)
))
// //
if updateManager.showUpdateOverlay, let info = updateManager.updateInfo { if updateManager.showUpdateOverlay, let info = updateManager.updateInfo {

View File

@ -18,7 +18,7 @@ struct SettingsAccountView: View {
sectionHeader(title: "账户安全", icon: "shield.lefthalf.filled") sectionHeader(title: "账户安全", icon: "shield.lefthalf.filled")
VStack(spacing: 0) { VStack(spacing: 0) {
if userContext.isLogined, let loginCredit = userContext.loginCredit { if let loginCredit = userContext.loginCredit {
switch loginCredit { switch loginCredit {
case .token(let token): case .token(let token):
TokenCreditView(token: token) TokenCreditView(token: token)

View File

@ -10,7 +10,6 @@ import Observation
@Observable @Observable
class UserContext { class UserContext {
var isLogined: Bool = false
var loginCredit: Credit? var loginCredit: Credit?
var networkSession: NetworkSession? var networkSession: NetworkSession?
@ -63,7 +62,7 @@ class UserContext {
] ]
@MainActor @MainActor
func loginWithAccountAndPassword(username: String, password: String) async throws { func loginWithAccountAndPassword(username: String, password: String) async throws -> Bool {
var params: [String: Any] = [ var params: [String: Any] = [
"username": username, "username": username,
"password": password, "password": password,
@ -74,16 +73,17 @@ class UserContext {
self.networkSession = try await SDLAPIClient.doPost(path: "/auth/login", params: params, as: NetworkSession.self) self.networkSession = try await SDLAPIClient.doPost(path: "/auth/login", params: params, as: NetworkSession.self)
self.loginCredit = .accountAndPasword(account: username, password: password) self.loginCredit = .accountAndPasword(account: username, password: password)
self.isLogined = true
// keychain // keychain
if let data = "\(username):\(password)".data(using: .utf8) { if let data = "\(username):\(password)".data(using: .utf8) {
try KeychainStore.shared.save(data, account: "accountAndPasword") try KeychainStore.shared.save(data, account: "accountAndPasword")
} }
return true
} }
@MainActor @MainActor
func loginWithToken(token: String) async throws { func loginWithToken(token: String) async throws -> Bool {
var params: [String: Any] = [ var params: [String: Any] = [
"token": token, "token": token,
"system": SystemConfig.systemInfo, "system": SystemConfig.systemInfo,
@ -93,42 +93,43 @@ class UserContext {
self.networkSession = try await SDLAPIClient.doPost(path: "/auth/token", params: params, as: NetworkSession.self) self.networkSession = try await SDLAPIClient.doPost(path: "/auth/token", params: params, as: NetworkSession.self)
self.loginCredit = .token(token: token) self.loginCredit = .token(token: token)
self.isLogined = true
// keychain // keychain
if let data = token.data(using: .utf8) { if let data = token.data(using: .utf8) {
try KeychainStore.shared.save(data, account: "token") try KeychainStore.shared.save(data, account: "token")
} }
return true
} }
func sendVerifyCode(username: String) async throws -> String { // func sendVerifyCode(username: String) async throws -> String {
var params: [String: Any] = [ // var params: [String: Any] = [
"username": username // "username": username
] // ]
params.merge(baseParams) {$1} // params.merge(baseParams) {$1}
//
return try await SDLAPIClient.doPost(path: "/auth/sendVerifyCode", params: params, as: String.self) // return try await SDLAPIClient.doPost(path: "/auth/sendVerifyCode", params: params, as: String.self)
} // }
//
func submitVerifyCode(username: String, verifyCode: String) async throws -> String { // func submitVerifyCode(username: String, verifyCode: String) async throws -> String {
var params: [String: Any] = [ // var params: [String: Any] = [
"username": username, // "username": username,
"verify_code": verifyCode, // "verify_code": verifyCode,
] // ]
params.merge(baseParams) {$1} // params.merge(baseParams) {$1}
//
return try await SDLAPIClient.doPost(path: "/auth/submitVerifyCode", params: params, as: String.self) // return try await SDLAPIClient.doPost(path: "/auth/submitVerifyCode", params: params, as: String.self)
} // }
//
func resetPassword(username: String, password: String) async throws -> String { // func resetPassword(username: String, password: String) async throws -> String {
var params: [String: Any] = [ // var params: [String: Any] = [
"username": username, // "username": username,
"password": password, // "password": password,
] // ]
params.merge(baseParams) {$1} // params.merge(baseParams) {$1}
//
return try await SDLAPIClient.doPost(path: "/auth/resetPassword", params: params, as: String.self) // return try await SDLAPIClient.doPost(path: "/auth/resetPassword", params: params, as: String.self)
} // }
func loadCacheToken() -> String? { func loadCacheToken() -> String? {
if let data = try? KeychainStore.shared.load(account: "token") { if let data = try? KeychainStore.shared.load(account: "token") {
@ -151,7 +152,6 @@ class UserContext {
// 退 // 退
func logout() async throws { func logout() async throws {
try await VPNManager.shared.disableVpn() try await VPNManager.shared.disableVpn()
self.isLogined = false
} }
} }

View File

@ -45,8 +45,7 @@ struct punchnetApp: App {
var body: some Scene { var body: some Scene {
WindowGroup(id: "main") { WindowGroup(id: "main") {
// RootView() RootView()
SettingsView()
.navigationTitle("") .navigationTitle("")
.environment(self.appContext) .environment(self.appContext)
.environment(self.userContext) .environment(self.userContext)
@ -80,23 +79,23 @@ struct punchnetApp: App {
.windowResizability(.contentSize) .windowResizability(.contentSize)
.defaultPosition(.center) .defaultPosition(.center)
Window("重置密码", id: "resetPassword") { // Window("", id: "resetPassword") {
ResetPasswordRootView() // ResetPasswordRootView()
.environment(self.userContext) // .environment(self.userContext)
.environment(self.appContext) // .environment(self.appContext)
.frame(width: 500, height: 400) // .frame(width: 500, height: 400)
} // }
.windowResizability(.contentSize) // .windowResizability(.contentSize)
.defaultPosition(.center) // .defaultPosition(.center)
//
Window("注册", id: "register") { // Window("", id: "register") {
RegisterRootView() // RegisterRootView()
.environment(self.userContext) // .environment(self.userContext)
.environment(self.appContext) // .environment(self.appContext)
.frame(width: 500, height: 400) // .frame(width: 500, height: 400)
} // }
.windowResizability(.contentSize) // .windowResizability(.contentSize)
.defaultPosition(.center) // .defaultPosition(.center)
// //
// MenuBarExtra("punchnet", image: "logo_32") { // MenuBarExtra("punchnet", image: "logo_32") {
// VStack { // VStack {