diff --git a/punchnet/Views/Login/LoginView.swift b/punchnet/Views/Login/LoginView.swift index 15b980a..e4bd44c 100644 --- a/punchnet/Views/Login/LoginView.swift +++ b/punchnet/Views/Login/LoginView.swift @@ -17,16 +17,16 @@ enum AuthScreen { 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() + ResetPasswordRootView() case .register: - ResetPasswordView() + EmptyView() } } .onChange(of: self.authScreen) { @@ -41,7 +41,7 @@ struct LoginView: View { @Environment(UserContext.self) var userContext: UserContext @State private var authMethod: AuthMethod = .account @Binding var authScreen: AuthScreen - + enum AuthMethod { case token case account @@ -68,7 +68,7 @@ struct LoginView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - + Button { self.authMethod = .account } label: { @@ -84,7 +84,7 @@ struct LoginView: View { } .buttonStyle(.plain) } - + Group { switch self.authMethod { case .token: @@ -101,18 +101,103 @@ struct LoginView: View { } -extension LoginView { + +struct LoginTokenView: View { + @Environment(UserContext.self) var userContext: UserContext + @State private var token: String = "" - 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 + + @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 - @State private var showAlert = false - @State private var errorMessage = "" - - var body: some View { - VStack { - TextField("认证密钥", text: $token) + 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) @@ -125,250 +210,90 @@ extension LoginView { .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 - - @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(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) - .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 - } - } - } - - // 执行登陆操作 - 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 = "内部错误,请稍后重试" - } - } - } - - } -} - -// 忘记密码 -struct ResetPasswordView: View { - @Environment(UserContext.self) var userContext: UserContext - - @State private var username: String = "" - @State private var showAlert = false - @State private var errorMessage = "" - - var body: some View { - VStack(spacing: 30) { - - // 标题 - Text("重置密码") - .font(.system(size: 18, weight: .medium)) - - VStack(alignment: .leading, spacing: 16) { - TextField("请输入手机号或邮箱", text: $username) - .textFieldStyle(.plain) - .frame(width: 260, height: 28) + 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), - alignment: .bottom - ) + .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 { - Task { @MainActor in - await self.sendVerifyCode() + 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)) + Text("登陆") + .font(.system(size: 14, weight: .regular)) .foregroundColor(.black) - .frame(width: 160, height: 36) - .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + .frame(width: 120, height: 35) } - .frame(width: 160, height: 36) - punchnet/Core/SDLUtil.swift.cornerRadius(6) - - Spacer() + .frame(width: 120, height: 35) + .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) + .cornerRadius(5.0) } - .padding(.top, 40) - .frame(width: 400, height: 400) .alert(isPresented: $showAlert) { - Alert(title: Text("提示"), message: Text(self.errorMessage)) + Alert(title: Text("错误提示"), message: Text("账号密码为空")) } - } - - private func sendVerifyCode() async { - if username.isEmpty { - self.showAlert = true - self.errorMessage = "手机号/邮箱为空" - return - } - - switch SDLUtil.identifyContact(username) { - case .email, .phone: - do { - let result = try await self.userContext.sendVerifyCode(username: username) - print("send verify code result: \(result)") - } catch { - self.showAlert = true - self.errorMessage = error.localizedDescription + .onAppear { + if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() { + self.username = cacheUsername + self.password = cachePassword } - - case .invalid: - self.showAlert = true - 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 { + self.showAlert = true + self.errorMessage = err.message + } + } catch { + await MainActor.run { + self.showAlert = true + self.errorMessage = "内部错误,请稍后重试" + } + } + } + } + #Preview { -// ResetPasswordView() -// .environment(UserContext()) + // ResetPasswordView() + // .environment(UserContext()) } diff --git a/punchnet/Views/Login/ResetPasswordView.swift b/punchnet/Views/Login/ResetPasswordView.swift new file mode 100644 index 0000000..3a5244e --- /dev/null +++ b/punchnet/Views/Login/ResetPasswordView.swift @@ -0,0 +1,304 @@ +// +// ResetPasswordView.swift +// punchnet +// +// Created by 安礼成 on 2026/3/9. +// +import SwiftUI + +enum ResetPasswordStage { + case getVerifyCode + case submitVerifyCode(username: String) + case resetPassword(username: String) +} + +struct ResetPasswordRootView: View { + @State private var stage: ResetPasswordStage = .getVerifyCode + + var body: some View { + switch stage { + case .getVerifyCode: + GetVerifyCodeView(stage: $stage) + case .submitVerifyCode(let username): + SubmitVerifyCodeView(username: username, stage: $stage) + case .resetPassword(let username): + ResetPasswordView(stage: $stage, username: username) + } + } +} + +// 获取验证码 +struct GetVerifyCodeView: View { + @Environment(UserContext.self) var userContext: UserContext + @Binding var stage: ResetPasswordStage + + @State private var username: String = "" + @State private var showAlert = false + @State private var errorMessage = "" + + var body: some View { + VStack(spacing: 30) { + + // 标题 + Text("重置密码") + .font(.system(size: 18, weight: .medium)) + + VStack(alignment: .leading, spacing: 16) { + TextField("手机号/邮箱", text: $username) + .textFieldStyle(.plain) + .frame(width: 260, height: 28) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.blue), + alignment: .bottom + ) + } + + Button { + Task { @MainActor in + //await self.sendVerifyCode() + withAnimation { + self.stage = .submitVerifyCode(username: self.username) + } + } + } label: { + Text("获取验证码") + .font(.system(size: 14)) + .foregroundColor(.black) + .frame(width: 160, height: 36) + .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + } + .frame(width: 160, height: 36) + .cornerRadius(6) + + Spacer() + } + .padding(.top, 40) + .frame(width: 400, height: 400) + .alert(isPresented: $showAlert) { + Alert(title: Text("提示"), message: Text(self.errorMessage)) + } + } + + private func sendVerifyCode() async { + if username.isEmpty { + self.showAlert = true + self.errorMessage = "手机号/邮箱为空" + return + } + + switch SDLUtil.identifyContact(username) { + case .email, .phone: + do { + let result = try await self.userContext.sendVerifyCode(username: username) + print("send verify code result: \(result)") + } catch { + self.showAlert = true + self.errorMessage = error.localizedDescription + } + + case .invalid: + self.showAlert = true + self.errorMessage = "手机号/邮箱格式错误" + } + } +} + +// 输入验证码 +struct SubmitVerifyCodeView: View { + @Environment(UserContext.self) var userContext: UserContext + + @State var username: String + @Binding var stage: ResetPasswordStage + + @State private var verifiyCode: String = "" + + @State private var showAlert = false + @State private var errorMessage = "" + + var body: some View { + VStack(spacing: 30) { + + // 标题 + Text("重置密码") + .font(.system(size: 18, weight: .medium)) + + VStack(alignment: .leading, spacing: 16) { + TextField("手机号/邮箱", text: $username) + .textFieldStyle(.plain) + .frame(width: 260, height: 28) + .disabled(true) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.blue), + alignment: .bottom + ) + + HStack { + TextField("验证码", text: $verifiyCode) + .textFieldStyle(.plain) + .frame(width: 260, height: 28) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.blue), + alignment: .bottom + ) + Spacer() + + Button { + Task { @MainActor in + //await self.sendVerifyCode() + } + } label: { + Text("再次获取") + .font(.system(size: 14)) + .foregroundColor(.black) + .frame(width: 160, height: 36) + .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + } + .frame(width: 160, height: 36) + .cornerRadius(6) + } + } + + Button { + Task { @MainActor in + await self.submitVerifyCode() + withAnimation { + self.stage = .resetPassword(username: username) + } + } + } label: { + Text("设置密码") + .font(.system(size: 14)) + .foregroundColor(.black) + .frame(width: 160, height: 36) + .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + } + .frame(width: 160, height: 36) + .cornerRadius(6) + + Spacer() + } + .padding(.top, 40) + .frame(width: 400, height: 400) + .alert(isPresented: $showAlert) { + Alert(title: Text("提示"), message: Text(self.errorMessage)) + } + } + + private func submitVerifyCode() async { + if verifiyCode.isEmpty { + self.showAlert = true + self.errorMessage = "请输入验证码" + return + } + + if verifiyCode.count != 4 { + self.showAlert = true + self.errorMessage = "验证码错误" + return + } + + do { + let result = try await self.userContext.submitVerifyCode(username: username, verifyCode: verifiyCode) + print("submit verify code result: \(result)") + } catch { + self.showAlert = true + self.errorMessage = error.localizedDescription + } + } + +} + +// 重置密码 +struct ResetPasswordView: View { + @Environment(UserContext.self) var userContext: UserContext + @Binding var stage: ResetPasswordStage + var username: String + + @State private var password: String = "" + @State private var confirmPassword: String = "" + + @State private var showAlert = false + @State private var errorMessage = "" + + var body: some View { + VStack(spacing: 30) { + + // 标题 + Text("重置密码") + .font(.system(size: 18, weight: .medium)) + + VStack(alignment: .leading, spacing: 16) { + SecureField("新密码", text: $password) + .textFieldStyle(.plain) + .frame(width: 260, height: 28) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.blue), + alignment: .bottom + ) + + SecureField("再次输入密码", text: $confirmPassword) + .textFieldStyle(.plain) + .frame(width: 260, height: 28) + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.blue), + alignment: .bottom + ) + } + + Button { + Task { @MainActor in + await self.resetPassword() + self.stage = .resetPassword(username: username) + } + } label: { + Text("保存") + .font(.system(size: 14)) + .foregroundColor(.black) + .frame(width: 160, height: 36) + .background(Color(red: 74/255, green: 207/255, blue: 154/255)) + } + .frame(width: 160, height: 36) + .cornerRadius(6) + + Spacer() + } + .padding(.top, 40) + .frame(width: 400, height: 400) + .alert(isPresented: $showAlert) { + Alert(title: Text("提示"), message: Text(self.errorMessage)) + } + } + + private func resetPassword() async { + if password.isEmpty { + self.showAlert = true + self.errorMessage = "请输入新密码" + return + } + + if confirmPassword.isEmpty || confirmPassword != password { + self.showAlert = true + self.errorMessage = "两次输入的密码不一致" + return + } + + do { + let result = try await self.userContext.resetPassword(username: username, password: self.password) + print("send verify code result: \(result)") + } catch { + self.showAlert = true + self.errorMessage = error.localizedDescription + } + } + +} diff --git a/punchnet/Views/UserContext.swift b/punchnet/Views/UserContext.swift index 1ade08d..36cd09f 100644 --- a/punchnet/Views/UserContext.swift +++ b/punchnet/Views/UserContext.swift @@ -92,7 +92,6 @@ class UserContext { } } - @MainActor func sendVerifyCode(username: String) async throws -> String { var params: [String: Any] = [ "username": username @@ -102,6 +101,26 @@ class UserContext { return try await SDLAPIClient.doPost(path: "/auth/sendVerifyCode", params: params, as: String.self) } + func submitVerifyCode(username: String, verifyCode: String) async throws -> String { + var params: [String: Any] = [ + "username": username, + "verify_code": verifyCode, + ] + params.merge(baseParams) {$1} + + return try await SDLAPIClient.doPost(path: "/auth/submitVerifyCode", params: params, as: String.self) + } + + func resetPassword(username: String, password: String) async throws -> String { + var params: [String: Any] = [ + "username": username, + "password": password, + ] + params.merge(baseParams) {$1} + + return try await SDLAPIClient.doPost(path: "/auth/resetPassword", params: params, as: String.self) + } + func loadCacheToken() -> String? { if let data = try? KeychainStore.shared.load(account: "token") { return String(data: data, encoding: .utf8)