import SwiftUI import ComposableArchitecture import Combine struct RecoverPasswordView: View { let store: StoreOf 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 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: {} // ) //}