Files
e-party-iOS/yana/Views/RecoverPasswordView.swift
edwinQQQ fb7ae9e0ad feat: 更新.gitignore,删除需求文档,优化API调试信息
- 在.gitignore中添加忽略项以排除不必要的文件。
- 删除架构分析需求文档以简化项目文档。
- 在APIEndpoints.swift和LoginModels.swift中移除调试信息的异步调用,提升代码简洁性。
- 在EMailLoginFeature.swift和HomeFeature.swift中新增登录流程状态管理,优化用户体验。
- 在多个视图中调整状态管理和导航逻辑,确保一致性和可维护性。
- 更新Xcode项目配置以增强调试信息的输出格式。
2025-07-18 15:57:54 +08:00

351 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import ComposableArchitecture
import Combine
struct RecoverPasswordView: View {
let store: StoreOf<RecoverPasswordFeature>
let onBack: () -> Void
// 使@StateUI
@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 timerCancellable: AnyCancellable?
//
private var isEmailValid: Bool {
!email.isEmpty
}
private var isVerificationCodeValid: Bool {
!verificationCode.isEmpty
}
private var isNewPasswordValid: Bool {
!newPassword.isEmpty
}
private var isStoreNotLoading: Bool {
!store.isResetLoading
}
private var isCodeNotLoading: Bool {
!store.isCodeLoading
}
private var isCountdownFinished: Bool {
countdown == 0
}
//
private var isConfirmButtonEnabled: Bool {
isStoreNotLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid
}
//
private var isGetCodeButtonEnabled: Bool {
isCodeNotLoading && isEmailValid && isCountdownFinished
}
//
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
} else if countdown > 0 {
return "\(countdown)s"
} else {
return NSLocalizedString("recover_password.get_code", comment: "")
}
}
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(NSLocalizedString("recover_password.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
//
emailInputField
//
verificationCodeInputField
//
newPasswordInputField
}
.padding(.horizontal, 32)
Spacer()
.frame(height: 80)
//
confirmButton
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
resetState()
}
.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.isResetSuccess) { isResetSuccess in
if isResetSuccess {
onBack()
}
}
}
// MARK: - UI Components
private var emailInputField: some View {
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(NSLocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
}
}
private var verificationCodeInputField: some View {
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(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
//
Button(action: {
startCountdown()
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)
}
}
private var newPasswordInputField: some View {
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(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
.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)
}
}
private var confirmButton: some View {
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 ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(!isConfirmButtonEnabled)
.opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
}
// MARK: - Private Methods
private func resetState() {
store.send(.resetState)
email = ""
verificationCode = ""
newPassword = ""
isNewPasswordVisible = false
countdown = 0
#if DEBUG
email = "exzero@126.com"
store.send(.emailChanged(email))
#endif
}
private func startCountdown() {
stopCountdown()
countdown = 60
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { _ in
if countdown > 0 {
countdown -= 1
} else {
stopCountdown()
}
}
}
private func stopCountdown() {
timerCancellable?.cancel()
timerCancellable = nil
countdown = 0
}
}
//#Preview {
// RecoverPasswordView(
// store: Store(
// initialState: RecoverPasswordFeature.State()
// ) {
// RecoverPasswordFeature()
// },
// onBack: {}
// )
//}