feat: 增强邮箱登录功能和密码恢复流程

- 更新邮箱登录相关功能,新增邮箱验证码获取和登录API端点。
- 添加AccountModel以管理用户认证信息,支持会话票据的存储和更新。
- 实现密码恢复功能,支持通过邮箱获取验证码和重置密码。
- 增加本地化支持,更新相关字符串以适应新功能。
- 引入ValidationHelper以验证邮箱和密码格式,确保用户输入的有效性。
- 更新视图以支持邮箱登录和密码恢复的用户交互。
This commit is contained in:
edwinQQQ
2025-07-10 14:00:58 +08:00
parent c470dba79c
commit e45ad3bad5
23 changed files with 2054 additions and 164 deletions

View File

@@ -10,32 +10,29 @@ struct EMailLoginFeature {
var isLoading: Bool = false
var isCodeLoading: Bool = false
var errorMessage: String? = nil
var codeCountdown: Int = 0
var isCodeButtonEnabled: Bool = true
var isCodeSent: Bool = false
// Debug
#if DEBUG
init() {
self.email = "85494536@gmail.com"
self.email = "exzero@126.com"
self.verificationCode = ""
}
#endif
}
enum Action: Equatable {
enum Action {
case emailChanged(String)
case verificationCodeChanged(String)
case getVerificationCodeTapped
case getCodeResponse(Result<EmailGetCodeResponse, Error>)
case loginButtonTapped(email: String, verificationCode: String)
case loginResponse(Result<AccountModel, Error>)
case forgotPasswordTapped
case codeCountdownTick
case setLoading(Bool)
case setCodeLoading(Bool)
case setError(String?)
case startCodeCountdown
case resetCodeCountdown
case resetState
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
@@ -55,28 +52,57 @@ struct EMailLoginFeature {
return .none
}
guard isValidEmail(state.email) else {
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = "email_login.invalid_email".localized
return .none
}
state.isCodeLoading = true
state.isCodeSent = false //
state.errorMessage = nil
return .run { send in
// API
try await Task.sleep(nanoseconds: 1_000_000_000) // 1
await send(.setCodeLoading(false))
await send(.startCodeCountdown)
return .run { [email = state.email] send in
do {
guard let request = LoginHelper.createEmailGetCodeRequest(email: email) else {
await send(.getCodeResponse(.failure(APIError.encryptionFailed)))
return
}
let response = try await apiService.request(request)
await send(.getCodeResponse(.success(response)))
} catch {
await send(.getCodeResponse(.failure(error)))
}
}
case .getCodeResponse(.success(let response)):
state.isCodeLoading = false
if response.isSuccess {
state.isCodeSent = true
return .none
} else {
state.errorMessage = response.errorMessage
return .none
}
case .getCodeResponse(.failure(let error)):
state.isCodeLoading = false
if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription
} else {
state.errorMessage = "验证码发送失败,请检查网络连接"
}
return .none
case .loginButtonTapped(let email, let verificationCode):
guard !email.isEmpty && !verificationCode.isEmpty else {
state.errorMessage = "email_login.fields_required".localized
return .none
}
guard isValidEmail(email) else {
guard ValidationHelper.isValidEmail(email) else {
state.errorMessage = "email_login.invalid_email".localized
return .none
}
@@ -85,61 +111,78 @@ struct EMailLoginFeature {
state.errorMessage = nil
return .run { send in
// API
try await Task.sleep(nanoseconds: 2_000_000_000) // 2
await send(.setLoading(false))
//
print("🔐 邮箱登录尝试: \(email), 验证码: \(verificationCode)")
do {
guard let request = LoginHelper.createEmailLoginRequest(email: email, code: verificationCode) else {
await send(.loginResponse(.failure(APIError.encryptionFailed)))
return
}
let response = try await apiService.request(request)
if response.isSuccess, let loginData = response.data {
guard let accountModel = AccountModel.from(loginData: loginData) else {
await send(.loginResponse(.failure(APIError.invalidResponse)))
return
}
// Ticket
let ticketRequest = TicketHelper.createTicketRequest(
accessToken: accountModel.accessToken ?? "",
uid: accountModel.uid.flatMap { Int($0) }
)
let ticketResponse = try await apiService.request(ticketRequest)
if ticketResponse.isSuccess, let ticket = ticketResponse.ticket {
let completeAccount = accountModel.withTicket(ticket)
await send(.loginResponse(.success(completeAccount)))
} else {
await send(.loginResponse(.failure(APIError.ticketFailed)))
}
} else {
await send(.loginResponse(.failure(APIError.custom(response.errorMessage))))
}
} catch {
await send(.loginResponse(.failure(error)))
}
}
case .loginResponse(.success(let accountModel)):
state.isLoading = false
// AccountModel
UserInfoManager.saveAccountModel(accountModel)
//
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
return .none
case .loginResponse(.failure(let error)):
state.isLoading = false
if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription
} else {
state.errorMessage = "登录失败,请重试"
}
return .none
case .forgotPasswordTapped:
//
print("📧 忘记密码点击")
return .none
case .codeCountdownTick:
if state.codeCountdown > 0 {
state.codeCountdown -= 1
state.isCodeButtonEnabled = false
return .run { send in
try await Task.sleep(nanoseconds: 1_000_000_000) // 1
await send(.codeCountdownTick)
}
} else {
state.isCodeButtonEnabled = true
return .none
}
case .setLoading(let isLoading):
state.isLoading = isLoading
return .none
case .setCodeLoading(let isLoading):
state.isCodeLoading = isLoading
return .none
case .setError(let error):
state.errorMessage = error
return .none
case .startCodeCountdown:
state.codeCountdown = 60
state.isCodeButtonEnabled = false
return .send(.codeCountdownTick)
case .resetCodeCountdown:
state.codeCountdown = 0
state.isCodeButtonEnabled = true
case .resetState:
state.email = ""
state.verificationCode = ""
state.isLoading = false
state.isCodeLoading = false
state.errorMessage = nil
state.isCodeSent = false
return .none
}
}
}
// MARK: - Helper Methods
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}
}