
- 更新邮箱登录相关功能,新增邮箱验证码获取和登录API端点。 - 添加AccountModel以管理用户认证信息,支持会话票据的存储和更新。 - 实现密码恢复功能,支持通过邮箱获取验证码和重置密码。 - 增加本地化支持,更新相关字符串以适应新功能。 - 引入ValidationHelper以验证邮箱和密码格式,确保用户输入的有效性。 - 更新视图以支持邮箱登录和密码恢复的用户交互。
303 lines
13 KiB
Swift
303 lines
13 KiB
Swift
import SwiftUI
|
||
import ComposableArchitecture
|
||
|
||
struct RecoverPasswordView: View {
|
||
let store: StoreOf<RecoverPasswordFeature>
|
||
let onBack: () -> Void
|
||
|
||
// 使用本地@State管理UI状态
|
||
@State private var email: String = ""
|
||
@State private var verificationCode: String = ""
|
||
@State private var newPassword: String = ""
|
||
@State private var isNewPasswordVisible: Bool = false
|
||
|
||
// 验证码倒计时状态
|
||
@State private var countdown: Int = 0
|
||
@State private var countdownTimer: Timer?
|
||
|
||
// 计算确认按钮是否可用
|
||
private var isConfirmButtonEnabled: Bool {
|
||
return !store.isResetLoading && !email.isEmpty && !verificationCode.isEmpty && !newPassword.isEmpty
|
||
}
|
||
|
||
// 计算获取验证码按钮是否可用
|
||
private var isGetCodeButtonEnabled: Bool {
|
||
return !store.isCodeLoading && !email.isEmpty && countdown == 0
|
||
}
|
||
|
||
// 计算获取验证码按钮文本
|
||
private var getCodeButtonText: String {
|
||
if store.isCodeLoading {
|
||
return ""
|
||
} else if countdown > 0 {
|
||
return "\(countdown)s"
|
||
} else {
|
||
return "recover_password.get_code".localized
|
||
}
|
||
}
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
ZStack {
|
||
// 背景图片
|
||
Image("bg")
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
.ignoresSafeArea(.all)
|
||
|
||
VStack(spacing: 0) {
|
||
// 顶部导航栏
|
||
HStack {
|
||
Button(action: {
|
||
onBack()
|
||
}) {
|
||
Image(systemName: "chevron.left")
|
||
.font(.system(size: 24, weight: .medium))
|
||
.foregroundColor(.white)
|
||
.frame(width: 44, height: 44)
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, 8)
|
||
|
||
Spacer()
|
||
.frame(height: 60)
|
||
|
||
// 标题
|
||
Text("recover_password.title".localized)
|
||
.font(.system(size: 28, weight: .medium))
|
||
.foregroundColor(.white)
|
||
.padding(.bottom, 80)
|
||
|
||
// 输入框区域
|
||
VStack(spacing: 24) {
|
||
// 邮箱输入框
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.fill(Color.white.opacity(0.1))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
||
)
|
||
.frame(height: 56)
|
||
|
||
TextField("", text: $email)
|
||
.placeholder(when: email.isEmpty) {
|
||
Text("recover_password.placeholder_email".localized)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
.padding(.horizontal, 24)
|
||
.keyboardType(.emailAddress)
|
||
.autocapitalization(.none)
|
||
}
|
||
|
||
// 验证码输入框(带获取按钮)
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.fill(Color.white.opacity(0.1))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
||
)
|
||
.frame(height: 56)
|
||
|
||
HStack {
|
||
TextField("", text: $verificationCode)
|
||
.placeholder(when: verificationCode.isEmpty) {
|
||
Text("recover_password.placeholder_verification_code".localized)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
.keyboardType(.numberPad)
|
||
|
||
// 获取验证码按钮
|
||
Button(action: {
|
||
// 立即开始倒计时
|
||
startCountdown()
|
||
// 发送API请求
|
||
store.send(.getVerificationCodeTapped)
|
||
}) {
|
||
ZStack {
|
||
if store.isCodeLoading {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.scaleEffect(0.7)
|
||
} else {
|
||
Text(getCodeButtonText)
|
||
.font(.system(size: 14, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
.frame(width: 60, height: 36)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 15)
|
||
.fill(Color.white.opacity(isGetCodeButtonEnabled ? 0.2 : 0.1))
|
||
)
|
||
}
|
||
.disabled(!isGetCodeButtonEnabled || store.isCodeLoading)
|
||
}
|
||
.padding(.horizontal, 24)
|
||
}
|
||
|
||
// 新密码输入框
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.fill(Color.white.opacity(0.1))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 25)
|
||
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
||
)
|
||
.frame(height: 56)
|
||
|
||
HStack {
|
||
if isNewPasswordVisible {
|
||
TextField("", text: $newPassword)
|
||
.placeholder(when: newPassword.isEmpty) {
|
||
Text("recover_password.placeholder_new_password".localized)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
} else {
|
||
SecureField("", text: $newPassword)
|
||
.placeholder(when: newPassword.isEmpty) {
|
||
Text("recover_password.placeholder_new_password".localized)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
}
|
||
|
||
Button(action: {
|
||
isNewPasswordVisible.toggle()
|
||
}) {
|
||
Image(systemName: isNewPasswordVisible ? "eye.slash" : "eye")
|
||
.foregroundColor(.white.opacity(0.7))
|
||
.font(.system(size: 18))
|
||
}
|
||
}
|
||
.padding(.horizontal, 24)
|
||
}
|
||
}
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
.frame(height: 80)
|
||
|
||
// 确认按钮
|
||
Button(action: {
|
||
store.send(.resetPasswordTapped)
|
||
}) {
|
||
ZStack {
|
||
// 渐变背景
|
||
LinearGradient(
|
||
colors: [
|
||
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
|
||
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
|
||
],
|
||
startPoint: .leading,
|
||
endPoint: .trailing
|
||
)
|
||
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||
|
||
HStack {
|
||
if store.isResetLoading {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
.scaleEffect(0.8)
|
||
}
|
||
Text(store.isResetLoading ? "recover_password.resetting".localized : "recover_password.confirm_button".localized)
|
||
.font(.system(size: 18, weight: .semibold))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
.frame(height: 56)
|
||
}
|
||
.disabled(!isConfirmButtonEnabled)
|
||
.opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
|
||
.padding(.horizontal, 32)
|
||
|
||
// 错误信息
|
||
if let errorMessage = store.errorMessage {
|
||
Text(errorMessage)
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.red)
|
||
.padding(.top, 16)
|
||
.padding(.horizontal, 32)
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
.onAppear {
|
||
// 每次进入页面都重置状态
|
||
store.send(.resetState)
|
||
|
||
email = ""
|
||
verificationCode = ""
|
||
newPassword = ""
|
||
isNewPasswordVisible = false
|
||
countdown = 0
|
||
stopCountdown()
|
||
|
||
#if DEBUG
|
||
email = "exzero@126.com"
|
||
store.send(.emailChanged(email))
|
||
#endif
|
||
}
|
||
.onDisappear {
|
||
stopCountdown()
|
||
}
|
||
.onChange(of: email) { newEmail in
|
||
store.send(.emailChanged(newEmail))
|
||
}
|
||
.onChange(of: verificationCode) { newCode in
|
||
store.send(.verificationCodeChanged(newCode))
|
||
}
|
||
.onChange(of: newPassword) { newPassword in
|
||
store.send(.newPasswordChanged(newPassword))
|
||
}
|
||
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
||
// 当API请求完成且成功时,自动将焦点切换到验证码输入框
|
||
if !isCodeLoading && store.errorMessage == nil {
|
||
// 可以在这里添加焦点切换逻辑
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Private Methods
|
||
|
||
private func startCountdown() {
|
||
countdown = 60
|
||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||
if countdown > 0 {
|
||
countdown -= 1
|
||
} else {
|
||
stopCountdown()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func stopCountdown() {
|
||
countdownTimer?.invalidate()
|
||
countdownTimer = nil
|
||
countdown = 0
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
RecoverPasswordView(
|
||
store: Store(
|
||
initialState: RecoverPasswordFeature.State()
|
||
) {
|
||
RecoverPasswordFeature()
|
||
},
|
||
onBack: {}
|
||
)
|
||
} |