忘记密码界面

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

@@ -68,6 +68,13 @@ export default function LoginPage() {
<CardContent className="space-y-6 px-6 pb-8">
{/* 使用统一的认证表单组件 */}
<AuthForm type="login" />
{/* 忘记密码链接 */}
<div className="text-right mt-2">
<Link href="/forgot-password" className="text-sm text-emerald-600 hover:text-emerald-500 dark:text-emerald-400 dark:hover:text-emerald-300 transition-colors">
</Link>
</div>
{/* 分割线 */}
<div className="relative my-6">

View File

@@ -0,0 +1,96 @@
'use client';
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import ForgotPasswordForm from '@/components/auth/ForgotPasswordForm';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
const ForgotPasswordPage: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800 relative overflow-hidden pb-8">
{/* 背景装饰元素 - 渐变模糊效果 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-80 h-80 bg-emerald-400/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/4 w-64 h-64 bg-emerald-300/10 rounded-full blur-3xl"></div>
</div>
{/* 主要内容区域 */}
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 py-12">
<div className="w-full max-w-md">
{/* 装饰性头部 */}
<div className="text-center mb-8 sm:mb-10">
<div className="flex justify-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 bg-emerald-600 rounded-2xl transform -rotate-6 shadow-lg transition-all duration-500 hover:rotate-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-white rounded-xl transform rotate-6 flex items-center justify-center transition-all duration-500 hover:rotate-0">
<Image
src="/images/mc-favicon.ico"
alt="Logo"
width={40}
height={40}
className="rounded-xl w-8 h-8"
/>
</div>
</div>
</div>
<h1 className="text-3xl sm:text-4xl font-bold text-gray-800 dark:text-white mb-3 tracking-tight">
<span className="text-emerald-600 dark:text-emerald-400"> HITWH</span>
</h1>
<p className="text-gray-600 dark:text-gray-400 text-sm sm:text-base leading-relaxed max-w-sm mx-auto">
Minecraft皮肤创作之旅
</p>
</div>
{/* 找回密码卡片 */}
<div className="relative group">
{/* 卡片阴影装饰 */}
<div className="absolute inset-0 bg-emerald-500 rounded-2xl transform rotate-2 opacity-20 transition-all duration-500 group-hover:rotate-1 group-hover:scale-102"></div>
<div className="absolute inset-0 bg-teal-400 rounded-2xl transform -rotate-1 opacity-15 transition-all duration-500 group-hover:-rotate-0.5 group-hover:scale-101"></div>
<Card className="relative bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm border border-emerald-200 dark:border-emerald-900/50 shadow-xl rounded-2xl overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-teal-500"></div>
<CardHeader className="text-center pb-6 pt-8">
<CardTitle className="text-xl sm:text-2xl font-bold text-gray-800 dark:text-white">
🔑
</CardTitle>
<div className="w-16 h-1 bg-gradient-to-r from-emerald-500 to-teal-500 mx-auto mt-2 rounded-full"></div>
</CardHeader>
<CardContent className="space-y-6 px-6 pb-8">
{/* 使用统一的忘记密码表单组件 */}
<ForgotPasswordForm />
</CardContent>
</Card>
</div>
{/* 底部装饰 */}
<div className="text-center mt-8 sm:mt-10">
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs sm:text-sm text-gray-500 dark:text-gray-400">
<span>🔐 </span>
<span></span>
<span>📧 </span>
<span></span>
<span> </span>
<span></span>
<span>🔄 </span>
</div>
</div>
</div>
</div>
{/* 浮动装饰元素 */}
<div className="absolute bottom-8 left-8 text-emerald-600 opacity-20 dark:opacity-10">
<div className="text-4xl">🔓</div>
</div>
<div className="absolute top-8 right-8 text-teal-500 opacity-20 dark:opacity-10">
<div className="text-4xl">📧</div>
</div>
</div>
);
};
export default ForgotPasswordPage;

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: {

View File

@@ -188,6 +188,14 @@ export const getVerificationCode = async (email: string) => {
// 导出验证验证码函数
export const verifyCode = async (email: string, code: string) => {
// 对于测试环境,可以直接验证测试账号的验证码
const TEST_EMAIL = process.env.TEST_EMAIL || 'test@test.com';
const TEST_VERIFICATION_CODE = process.env.TEST_VERIFICATION_CODE || '123456';
// 测试环境下的特殊处理
if (process.env.NODE_ENV !== 'production' && email === TEST_EMAIL && code === TEST_VERIFICATION_CODE) {
return { success: true };
}
try {
// 验证输入
if (!email?.trim()) {
@@ -214,6 +222,61 @@ export const verifyCode = async (email: string, code: string) => {
}
};
// 导出重置密码函数
export const resetPassword = async (email: string, username: string, newPassword: string, verificationCode: string) => {
try {
// 基本验证
if (!email?.trim()) {
return { success: false, error: '邮箱不能为空' };
}
if (!username?.trim()) {
return { success: false, error: '用户名不能为空' };
}
if (!newPassword?.trim()) {
return { success: false, error: '新密码不能为空' };
}
if (!verificationCode?.trim()) {
return { success: false, error: '验证码不能为空' };
}
// 邮箱格式检查
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return { success: false, error: '请输入有效的邮箱地址' };
}
// 密码强度检查
if (newPassword.length < 6) {
return { success: false, error: '密码长度至少为6位' };
}
// 验证码验证
const verifyResult = await verifyCode(email, verificationCode);
if (!verifyResult.success) {
return verifyResult;
}
// 调用API重置密码
const response = await apiClient.post('/auth/reset-password', {
email,
username,
newPassword
});
return { success: true, ...response.data };
} catch (error) {
console.error('重置密码失败:', error);
if (error instanceof axios.AxiosError) {
if (error.response?.status === 400) {
return { success: false, error: error.response.data?.error || '重置密码失败,请检查信息' };
}
if (error.request) {
return { success: false, error: '网络连接失败,请检查您的网络设置' };
}
}
return { success: false, error: '重置密码失败,请稍后再试' };
}
};
// 导出注册函数
export const register = async (userData: {
username: string;