忘记密码界面
This commit is contained in:
301
src/components/auth/ForgotPasswordForm.tsx
Normal file
301
src/components/auth/ForgotPasswordForm.tsx
Normal 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;
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user