忘记密码界面

This commit is contained in:
chenZB666
2025-10-10 18:10:55 +08:00
parent 960442b1fd
commit 01fbffa1eb
5 changed files with 468 additions and 1 deletions

View File

@@ -0,0 +1,301 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Input } from '@/components/ui/input';
import { getVerificationCode, verifyCode as verifyCodeApi, resetPassword } from '../../lib/api/actions';
import { Button } from '@/components/ui/button';
import { Label } from '@radix-ui/react-label';
interface ForgotPasswordFormProps {
onBack?: () => void;
}
const ForgotPasswordForm: React.FC<ForgotPasswordFormProps> = ({ onBack }) => {
// 表单状态
const [step, setStep] = useState<1 | 2>(1);
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
const [verificationCode, setVerificationCode] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
// UI状态
const [isLoading, setIsLoading] = useState(false);
const [isSendingCode, setIsSendingCode] = useState(false);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [countdown, setCountdown] = useState(0);
// 处理验证码倒计时
useEffect(() => {
let timer: NodeJS.Timeout;
if (countdown > 0) {
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [countdown]);
// 处理获取验证码
const handleSendCode = async () => {
if (!email?.trim()) {
setError('请输入邮箱');
return;
}
if (!username?.trim()) {
setError('请输入用户名');
return;
}
// 邮箱格式检查
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError('请输入有效的邮箱地址');
return;
}
setIsSendingCode(true);
setError('');
try {
const result = await getVerificationCode(email);
if (result.success) {
setSuccessMessage('验证码已发送到您的邮箱');
setCountdown(60); // 60秒倒计时
// 清除成功消息
setTimeout(() => setSuccessMessage(''), 3000);
} else {
setError(result.error || '发送验证码失败');
}
} catch (err) {
setError('发送验证码失败,请稍后再试');
} finally {
setIsSendingCode(false);
}
};
// 处理验证验证码
const handleVerifyCode = async () => {
if (!email?.trim() || !username?.trim() || !verificationCode?.trim()) {
setError('请填写所有必填项');
return;
}
setIsLoading(true);
setError('');
try {
const result = await verifyCodeApi(email, verificationCode);
if (result.success) {
// 验证成功,进入第二步
setStep(2);
setError('');
} else {
setError(result.error || '验证码错误');
}
} catch (err) {
setError('验证失败,请稍后再试');
} finally {
setIsLoading(false);
}
};
// 处理重置密码
const handleResetPassword = async () => {
if (!newPassword?.trim() || !confirmPassword?.trim()) {
setError('请填写新密码和确认密码');
return;
}
if (newPassword !== confirmPassword) {
setError('两次输入的密码不一致');
return;
}
if (newPassword.length < 6) {
setError('密码长度至少为6位');
return;
}
setIsLoading(true);
setError('');
try {
const result = await resetPassword(email, username, newPassword, verificationCode);
if (result.success) {
setSuccessMessage('密码重置成功,即将跳转到登录页面...');
// 3秒后跳转到登录页面
setTimeout(() => {
window.location.href = '/login';
}, 3000);
} else {
setError(result.error || '重置密码失败');
}
} catch (err) {
setError('重置密码失败,请稍后再试');
} finally {
setIsLoading(false);
}
};
// 返回上一步
const handleBack = () => {
if (step === 2) {
setStep(1);
setError('');
} else if (onBack) {
onBack();
} else {
window.history.back();
}
};
// 处理表单提交
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (step === 1) {
handleVerifyCode();
} else {
handleResetPassword();
}
};
return (
<div className="w-full max-w-md">
<h2 className="text-2xl font-bold mb-6 text-center"></h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
{successMessage && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{successMessage}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{step === 1 ? (
<>
{/* 邮箱输入 */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="请输入您的邮箱"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
disabled={isLoading}
/>
</div>
{/* 用户名输入 */}
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入您的用户名"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
disabled={isLoading}
/>
</div>
{/* 验证码输入 */}
<div className="space-y-2">
<label htmlFor="verificationCode" className="block text-sm font-medium text-gray-700 mb-1"></label>
<div className="flex space-x-2">
<Input
id="verificationCode"
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="请输入验证码"
className="flex-1"
/>
<Button
type="button"
onClick={handleSendCode}
disabled={isSendingCode || countdown > 0 || !email}
className="whitespace-nowrap"
>
{isSendingCode ? '发送中...' :
countdown > 0 ? `${countdown}s后重新发送` :
'获取验证码'}
</Button>
</div>
</div>
</>
) : (
<>
{/* 新密码输入 */}
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="password"
id="newPassword"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="请输入新密码"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
disabled={isLoading}
/>
<p className="text-xs text-gray-500 mt-1">6</p>
</div>
{/* 确认新密码输入 */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入新密码"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
disabled={isLoading}
/>
</div>
</>
)}
{/* 按钮区域 */}
<div className="pt-4">
<div className="flex items-center justify-between">
<Button
type="button"
onClick={handleBack}
variant="ghost"
className="text-emerald-600 hover:text-emerald-500 dark:text-emerald-400 dark:hover:text-emerald-300"
>
</Button>
<Button
type="submit"
disabled={isLoading}
variant="default"
className="w-auto"
>
{isLoading ? (step === 1 ? '验证中...' : '重置中...') : (step === 1 ? '验证并继续' : '重置密码')}
</Button>
</div>
</div>
</form>
</div>
);
};
export default ForgotPasswordForm;

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 disabled:bg-gray-300 disabled:text-gray-500 dark:disabled:bg-gray-700 dark:disabled:text-gray-400 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {