From 253492b4813482357ad8d05c72a3369700f951aa Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Mon, 9 Mar 2026 18:48:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=98=E8=AE=B0=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- punchnet/Views/AppContext.swift | 1 + punchnet/Views/Login/LoginView.swift | 382 ++++++++++++++++++--------- punchnet/Views/RootView.swift | 2 +- punchnet/Views/UserContext.swift | 10 + 4 files changed, 272 insertions(+), 123 deletions(-) diff --git a/punchnet/Views/AppContext.swift b/punchnet/Views/AppContext.swift index 22b7715..5f7cd1e 100644 --- a/punchnet/Views/AppContext.swift +++ b/punchnet/Views/AppContext.swift @@ -15,4 +15,5 @@ class AppContext { init(noticePort: Int) { self.noticePort = noticePort } + } diff --git a/punchnet/Views/Login/LoginView.swift b/punchnet/Views/Login/LoginView.swift index 24b943a..48d6fa9 100644 --- a/punchnet/Views/Login/LoginView.swift +++ b/punchnet/Views/Login/LoginView.swift @@ -8,10 +8,45 @@ import SwiftUI import Observation +enum ContactType { + case phone + case email + case invalid +} + +enum AuthScreen { + case login + case resetPassword + case register +} + +struct LoginRootView: View { + @Environment(AppContext.self) var appContext: AppContext + @State private var authScreen = AuthScreen.login + + var body: some View { + Group { + switch self.authScreen { + case .login: + LoginView(authScreen: $authScreen) + case .resetPassword: + ResetPasswordView() + case .register: + ResetPasswordView() + } + } + .onChange(of: self.authScreen) { + print("app auth screen changed") + } + } + +} + // 登陆页面 struct LoginView: View { @Environment(UserContext.self) var userContext: UserContext @State private var authMethod: AuthMethod = .account + @Binding var authScreen: AuthScreen enum AuthMethod { case token @@ -61,7 +96,7 @@ struct LoginView: View { case .token: LoginTokenView() case .account: - LoginAccountView() + LoginAccountView(authScreen: $authScreen) } } @@ -72,98 +107,213 @@ struct LoginView: View { } -struct LoginTokenView: View { - @Environment(UserContext.self) var userContext: UserContext - @State private var token: String = "" +extension LoginView { - @State private var showAlert = false - @State private var errorMessage = "" + 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 = "内部错误,请稍后重试" + } + } + } + + } + + struct LoginAccountView: View { + @Environment(UserContext.self) var userContext: UserContext + @Environment(AppContext.self) var appContext: AppContext + @Binding var authScreen: AuthScreen - 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) + @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 - Rectangle() - .overlay { + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case networkId = "network_id" + } + } + + 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) + + 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 { + self.authScreen = .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() + } + } + } label: { Text("登陆") .font(.system(size: 14, weight: .regular)) .foregroundColor(.black) + .frame(width: 120, height: 35) } .frame(width: 120, height: 35) - .foregroundColor(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) + .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) .cornerRadius(5.0) - .onTapGesture { - Task { - await self.doLogin() - } + } + .alert(isPresented: $showAlert) { + Alert(title: Text("错误提示"), message: Text("账号密码为空")) + } + .onAppear { + if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() { + self.username = cacheUsername + self.password = cachePassword } - } - .onAppear { - if let cacheToken = self.userContext.loadCacheToken() { - self.token = cacheToken } } + + // 执行登陆操作 + private func doLogin() async { + do { + try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password) + + // 保存信息到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 = "内部错误,请稍后重试" + } + } + } + } - - // 执行登陆操作 - 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 = "内部错误,请稍后重试" - } - } - } - } -struct LoginAccountView: View { +// 忘记密码 +struct ResetPasswordView: View { @Environment(UserContext.self) var userContext: UserContext - + @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 { VStack { + Text("重置密码") + TextField("手机号/邮箱", text: $username) .multilineTextAlignment(.leading) .textFieldStyle(PlainTextFieldStyle()) @@ -177,78 +327,66 @@ struct LoginAccountView: View { .padding(.top, 25) , alignment: .topLeading) - 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 { - if self.username.isEmpty { - self.showAlert = true - self.errorMessage = "账号不能为空" - } else if self.password.isEmpty { - self.showAlert = true - self.errorMessage = "密码不能为空" - } else { - Task { - await self.doLogin() - } + Task { @MainActor in + await self.sendVerifyCode() } } label: { - Text("登陆") + Text("获取验证码") .font(.system(size: 14, weight: .regular)) .foregroundColor(.black) - .frame(width: 120, height: 35) + .frame(width: 200, height: 35) } - .frame(width: 120, height: 35) + .frame(width: 200, height: 35) .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) .cornerRadius(5.0) } .alert(isPresented: $showAlert) { - Alert(title: Text("错误提示"), message: Text("账号密码为空")) - } - .onAppear { - if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() { - self.username = cacheUsername - self.password = cachePassword - } + Alert(title: Text("提示"), message: Text(self.errorMessage)) } } - // 执行登陆操作 - private func doLogin() async { - do { - try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password) - - // 保存信息到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 { + private func sendVerifyCode() async { + if username.isEmpty { + self.showAlert = true + self.errorMessage = "手机号/邮箱为空" + return + } + + switch identifyContact(username) { + case .email, .phone: + do { + let result = try await self.userContext.sendVerifyCode(username: username) + print("send verify code result: \(result)") + } catch let err { self.showAlert = true - self.errorMessage = err.message - } - } catch { - await MainActor.run { - self.showAlert = true - self.errorMessage = "内部错误,请稍后重试" + self.errorMessage = err.localizedDescription } + case .invalid: + self.showAlert = true + self.errorMessage = "手机号/邮箱格式错误" + } + } + + private func identifyContact(_ input: String) -> ContactType { + let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) + // 手机号正则(中国手机号为例,以 1 开头,11 位数字) + let phoneRegex = /^1[3-9][0-9]{9}$/ + // 邮箱正则 + let emailRegex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/ + if trimmed.wholeMatch(of: phoneRegex) != nil { + return .phone + } else if trimmed.wholeMatch(of: emailRegex) != nil { + return .email + } else { + return .invalid } } } #Preview { - LoginView() +// ResetPasswordView() +// .environment(UserContext()) } diff --git a/punchnet/Views/RootView.swift b/punchnet/Views/RootView.swift index 882448f..ed3a4bc 100644 --- a/punchnet/Views/RootView.swift +++ b/punchnet/Views/RootView.swift @@ -15,7 +15,7 @@ struct RootView: View { if userContext.isLogined { NetworkView() } else { - LoginView() + LoginRootView() } } } diff --git a/punchnet/Views/UserContext.swift b/punchnet/Views/UserContext.swift index dcf49e6..1ade08d 100644 --- a/punchnet/Views/UserContext.swift +++ b/punchnet/Views/UserContext.swift @@ -92,6 +92,16 @@ class UserContext { } } + @MainActor + func sendVerifyCode(username: String) async throws -> String { + var params: [String: Any] = [ + "username": username + ] + params.merge(baseParams) {$1} + + return try await SDLAPIClient.doPost(path: "/auth/sendVerifyCode", params: params, as: String.self) + } + func loadCacheToken() -> String? { if let data = try? KeychainStore.shared.load(account: "token") { return String(data: data, encoding: .utf8)