import SwiftUI import Combine // MARK: - RecoverPassword ViewModel @MainActor class RecoverPasswordViewModel: ObservableObject { // MARK: - Published Properties @Published var email: String = "" @Published var verificationCode: String = "" @Published var newPassword: String = "" @Published var isNewPasswordVisible: Bool = false @Published var countdown: Int = 0 @Published var isResetLoading: Bool = false @Published var isCodeLoading: Bool = false @Published var errorMessage: String? @Published var isResetSuccess: Bool = false // MARK: - Callbacks var onBack: (() -> Void)? // MARK: - Private Properties private var timerCancellable: AnyCancellable? // MARK: - Computed Properties var isEmailValid: Bool { !email.isEmpty } var isVerificationCodeValid: Bool { !verificationCode.isEmpty } var isNewPasswordValid: Bool { !newPassword.isEmpty } var isConfirmButtonEnabled: Bool { !isResetLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid } var isGetCodeButtonEnabled: Bool { !isCodeLoading && isEmailValid && countdown == 0 } var getCodeButtonText: String { if isCodeLoading { return "" } else if countdown > 0 { return "\(countdown)s" } else { return LocalizedString("recover_password.get_code", comment: "") } } // MARK: - Public Methods func onBackTapped() { onBack?() } func onEmailChanged(_ newEmail: String) { email = newEmail } func onVerificationCodeChanged(_ newCode: String) { verificationCode = newCode } func onNewPasswordChanged(_ newPassword: String) { self.newPassword = newPassword } func onGetVerificationCodeTapped() { guard isGetCodeButtonEnabled else { return } isCodeLoading = true errorMessage = nil Task { do { let result = try await requestVerificationCode() await MainActor.run { self.handleCodeRequestResult(result) } } catch { await MainActor.run { self.handleCodeRequestError(error) } } } } func onResetPasswordTapped() { guard isConfirmButtonEnabled else { return } isResetLoading = true errorMessage = nil Task { do { let result = try await resetPassword() await MainActor.run { self.handleResetResult(result) } } catch { await MainActor.run { self.handleResetError(error) } } } } func resetState() { email = "" verificationCode = "" newPassword = "" isNewPasswordVisible = false countdown = 0 isResetLoading = false isCodeLoading = false errorMessage = nil isResetSuccess = false stopCountdown() #if DEBUG email = "exzero@126.com" #endif } // MARK: - Private Methods private func requestVerificationCode() async throws -> Bool { return false // let request = EmailVerificationCodeRequest(email: email) // let apiService = LiveAPIService() // let response: EmailVerificationCodeResponse = try await apiService.request(request) // // if response.code == 200 { // return true // } else { // throw APIError.serverError(response.message ?? "Failed to send verification code") // } } private func resetPassword() async throws -> Bool { return false // let request = ResetPasswordRequest( // email: email, // verificationCode: verificationCode, // newPassword: newPassword // ) // // let apiService = LiveAPIService() // let response: ResetPasswordResponse = try await apiService.request(request) // // if response.code == 200 { // return true // } else { // throw APIError.serverError(response.message ?? "Failed to reset password") // } } private func handleCodeRequestResult(_ success: Bool) { isCodeLoading = false if success { startCountdown() } } private func handleCodeRequestError(_ error: Error) { isCodeLoading = false errorMessage = error.localizedDescription } private func handleResetResult(_ success: Bool) { isResetLoading = false if success { isResetSuccess = true onBack?() } } private func handleResetError(_ error: Error) { isResetLoading = false errorMessage = error.localizedDescription } private func startCountdown() { stopCountdown() countdown = 60 timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { _ in if self.countdown > 0 { self.countdown -= 1 } else { self.stopCountdown() } } } private func stopCountdown() { timerCancellable?.cancel() timerCancellable = nil countdown = 0 } } // MARK: - RecoverPassword View struct RecoverPasswordPage: View { @StateObject private var viewModel = RecoverPasswordViewModel() let onBack: () -> Void var body: some View { GeometryReader { geometry in ZStack { // 背景图片 LoginBackgroundView() VStack(spacing: 0) { // 顶部导航栏 LoginHeaderView(onBack: { viewModel.onBackTapped() }) Spacer() .frame(height: 60) // 标题 Text(LocalizedString("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 = viewModel.errorMessage { Text(errorMessage) .font(.system(size: 14)) .foregroundColor(.red) .padding(.top, 16) .padding(.horizontal, 32) } Spacer() } } } .onAppear { viewModel.onBack = onBack viewModel.resetState() } .onDisappear { // viewModel.stopCountdown() } .onChange(of: viewModel.email) { _, newEmail in viewModel.onEmailChanged(newEmail) } .onChange(of: viewModel.verificationCode) { _, newCode in viewModel.onVerificationCodeChanged(newCode) } .onChange(of: viewModel.newPassword) { _, newPassword in viewModel.onNewPasswordChanged(newPassword) } } // 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: $viewModel.email) .placeholder(when: viewModel.email.isEmpty) { Text(LocalizedString("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: $viewModel.verificationCode) .placeholder(when: viewModel.verificationCode.isEmpty) { Text(LocalizedString("recover_password.placeholder_verification_code", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) .font(.system(size: 16)) .keyboardType(.numberPad) // 获取验证码按钮 Button(action: { viewModel.onGetVerificationCodeTapped() }) { ZStack { if viewModel.isCodeLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.7) } else { Text(viewModel.getCodeButtonText) .font(.system(size: 14, weight: .medium)) .foregroundColor(.white) } } .frame(width: 60, height: 36) .background( RoundedRectangle(cornerRadius: 15) .fill(Color.white.opacity(viewModel.isGetCodeButtonEnabled ? 0.2 : 0.1)) ) } .disabled(!viewModel.isGetCodeButtonEnabled || viewModel.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 viewModel.isNewPasswordVisible { TextField("", text: $viewModel.newPassword) .placeholder(when: viewModel.newPassword.isEmpty) { Text(LocalizedString("recover_password.placeholder_new_password", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) .font(.system(size: 16)) } else { SecureField("", text: $viewModel.newPassword) .placeholder(when: viewModel.newPassword.isEmpty) { Text(LocalizedString("recover_password.placeholder_new_password", comment: "")) .foregroundColor(.white.opacity(0.6)) } .foregroundColor(.white) .font(.system(size: 16)) } Button(action: { viewModel.isNewPasswordVisible.toggle() }) { Image(systemName: viewModel.isNewPasswordVisible ? "eye.slash" : "eye") .foregroundColor(.white.opacity(0.7)) .font(.system(size: 18)) } } .padding(.horizontal, 24) } } private var confirmButton: some View { Button(action: { viewModel.onResetPasswordTapped() }) { 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 viewModel.isResetLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) } Text(viewModel.isResetLoading ? LocalizedString("recover_password.resetting", comment: "") : LocalizedString("recover_password.confirm_button", comment: "")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(.white) } } .frame(height: 56) } .disabled(!viewModel.isConfirmButtonEnabled) .opacity(viewModel.isConfirmButtonEnabled ? 1.0 : 0.5) .padding(.horizontal, 32) } } #Preview { RecoverPasswordPage(onBack: {}) }