忘记密码

This commit is contained in:
anlicheng 2026-03-09 18:48:14 +08:00
parent 4032cbd512
commit 253492b481
4 changed files with 272 additions and 123 deletions

View File

@ -15,4 +15,5 @@ class AppContext {
init(noticePort: Int) { init(noticePort: Int) {
self.noticePort = noticePort self.noticePort = noticePort
} }
} }

View File

@ -8,10 +8,45 @@
import SwiftUI import SwiftUI
import Observation 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 { 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
@Binding var authScreen: AuthScreen
enum AuthMethod { enum AuthMethod {
case token case token
@ -61,7 +96,7 @@ struct LoginView: View {
case .token: case .token:
LoginTokenView() LoginTokenView()
case .account: case .account:
LoginAccountView() LoginAccountView(authScreen: $authScreen)
} }
} }
@ -72,98 +107,213 @@ struct LoginView: View {
} }
struct LoginTokenView: View { extension LoginView {
@Environment(UserContext.self) var userContext: UserContext
@State private var token: String = ""
@State private var showAlert = false struct LoginTokenView: View {
@State private var errorMessage = "" @Environment(UserContext.self) var userContext: UserContext
@State private var token: String = ""
var body: some View { @State private var showAlert = false
VStack { @State private var errorMessage = ""
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() var body: some View {
.overlay { 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
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("登陆") Text("登陆")
.font(.system(size: 14, weight: .regular)) .font(.system(size: 14, weight: .regular))
.foregroundColor(.black) .foregroundColor(.black)
.frame(width: 120, height: 35)
} }
.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) .cornerRadius(5.0)
.onTapGesture { }
Task { .alert(isPresented: $showAlert) {
await self.doLogin() 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 @Environment(UserContext.self) var userContext: UserContext
@State private var username: String = "" @State private var username: String = ""
@State private var password: String = ""
@State private var showAlert = false @State private var showAlert = false
@State private var errorMessage = "" @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 { VStack {
Text("重置密码")
TextField("手机号/邮箱", text: $username) TextField("手机号/邮箱", text: $username)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.textFieldStyle(PlainTextFieldStyle()) .textFieldStyle(PlainTextFieldStyle())
@ -177,78 +327,66 @@ struct LoginAccountView: View {
.padding(.top, 25) .padding(.top, 25)
, alignment: .topLeading) , 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 { Button {
if self.username.isEmpty { Task { @MainActor in
self.showAlert = true await self.sendVerifyCode()
self.errorMessage = "账号不能为空"
} else if self.password.isEmpty {
self.showAlert = true
self.errorMessage = "密码不能为空"
} else {
Task {
await self.doLogin()
}
} }
} label: { } label: {
Text("登陆") Text("获取验证码")
.font(.system(size: 14, weight: .regular)) .font(.system(size: 14, weight: .regular))
.foregroundColor(.black) .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)) .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255))
.cornerRadius(5.0) .cornerRadius(5.0)
} }
.alert(isPresented: $showAlert) { .alert(isPresented: $showAlert) {
Alert(title: Text("错误提示"), message: Text("账号密码为空")) Alert(title: Text("提示"), message: Text(self.errorMessage))
}
.onAppear {
if let (cacheUsername, cachePassword) = self.userContext.loadCacheUsernameAndPassword() {
self.username = cacheUsername
self.password = cachePassword
}
} }
} }
// private func sendVerifyCode() async {
private func doLogin() async { if username.isEmpty {
do { self.showAlert = true
try await self.userContext.loginWithAccountAndPassword(username: self.username, password: self.password) self.errorMessage = "手机号/邮箱为空"
return
}
// KeychainStore switch identifyContact(username) {
// let store = KeychainStore.shared case .email, .phone:
// if let tokenData = loginResult.accessToken.data(using: .utf8) { do {
// try store.save(tokenData, account: self.username) let result = try await self.userContext.sendVerifyCode(username: username)
// } print("send verify code result: \(result)")
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
}
case .invalid:
self.showAlert = true
self.errorMessage = "手机号/邮箱格式错误"
}
}
} catch let err as SDLAPIError { private func identifyContact(_ input: String) -> ContactType {
await MainActor.run { let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
self.showAlert = true // 1 11
self.errorMessage = err.message let phoneRegex = /^1[3-9][0-9]{9}$/
} //
} catch { let emailRegex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$/
await MainActor.run { if trimmed.wholeMatch(of: phoneRegex) != nil {
self.showAlert = true return .phone
self.errorMessage = "内部错误,请稍后重试" } else if trimmed.wholeMatch(of: emailRegex) != nil {
} return .email
} else {
return .invalid
} }
} }
} }
#Preview { #Preview {
LoginView() // ResetPasswordView()
// .environment(UserContext())
} }

View File

@ -15,7 +15,7 @@ struct RootView: View {
if userContext.isLogined { if userContext.isLogined {
NetworkView() NetworkView()
} else { } else {
LoginView() LoginRootView()
} }
} }
} }

View File

@ -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? { func loadCacheToken() -> String? {
if let data = try? KeychainStore.shared.load(account: "token") { if let data = try? KeychainStore.shared.load(account: "token") {
return String(data: data, encoding: .utf8) return String(data: data, encoding: .utf8)