feat(页面): 实现认证相关页面UI

- 实现登录页面,支持邮箱验证码登录
- 实现注册页面,支持邮箱验证注册
- 实现重置密码页面
- 统一认证页面布局和样式
This commit is contained in:
gaoziman 2025-12-19 22:36:32 +08:00
parent b0b912274f
commit abcea67980
4 changed files with 936 additions and 0 deletions

73
src/app/(auth)/layout.tsx Normal file
View File

@ -0,0 +1,73 @@
'use client';
import type { ReactNode } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { BrandIcon } from '@/components/ui/BrandIcon';
import { ThemeToggle } from '@/components/ui/ThemeToggle';
interface AuthLayoutProps {
children: ReactNode;
}
// 页面配置
const pageConfig: Record<string, { title: string; subtitle: string }> = {
'/login': {
title: '登录账号',
subtitle: '欢迎回来',
},
'/register': {
title: '创建账号',
subtitle: '注册开始使用 LionCode',
},
'/reset-password': {
title: '重置密码',
subtitle: '找回您的账号',
},
};
export default function AuthLayout({ children }: AuthLayoutProps) {
const pathname = usePathname();
const config = pageConfig[pathname] || pageConfig['/login'];
return (
<div className="min-h-screen bg-[var(--color-bg-secondary)] flex flex-col">
{/* 顶部导航栏 */}
<header className="h-16 px-6 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2 group">
<BrandIcon size="sm" />
<span className="text-lg font-semibold text-[var(--color-text-primary)] group-hover:text-[var(--color-primary)] transition-colors">
LionCode
</span>
</Link>
<ThemeToggle />
</header>
{/* 主内容区 */}
<main className="flex-1 flex flex-col items-center justify-center px-4 py-8">
{/* 品牌标识和标题 */}
<div className="text-center mb-8">
<BrandIcon size="xl" className="mx-auto mb-4" />
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">
{config.title}
</h1>
<p className="mt-2 text-sm text-[var(--color-text-tertiary)]">
{config.subtitle}
</p>
</div>
{/* 表单卡片 */}
<div className="w-full max-w-[480px]">
<div className="bg-[var(--color-bg-primary)] rounded-2xl shadow-lg border border-[var(--color-border-light)] p-8">
{children}
</div>
</div>
</main>
{/* 页脚 */}
<footer className="py-6 text-center text-xs text-[var(--color-text-tertiary)]">
<p>Powered by LionCode</p>
</footer>
</div>
);
}

View File

@ -0,0 +1,272 @@
'use client';
import { useState, useEffect, useCallback, Suspense } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { Mail, Lock, Eye, EyeOff, Loader2, KeyRound } from 'lucide-react';
import { useAuth } from '@/providers/AuthProvider';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui/Toast';
type LoginType = 'password' | 'code';
function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const { login, sendCode } = useAuth();
const [loginType, setLoginType] = useState<LoginType>('password');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [code, setCode] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [countdown, setCountdown] = useState(0);
const [error, setError] = useState('');
const redirect = searchParams.get('redirect') || '/';
// 倒计时
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// 发送验证码
const handleSendCode = useCallback(async () => {
if (!email) {
setError('请输入邮箱');
return;
}
setSendingCode(true);
setError('');
const result = await sendCode(email, 'login');
if (result.success) {
setCountdown(60);
toast.success('验证码已发送', {
description: `验证码已发送到 ${email},请查收`,
});
} else {
setError(result.error || '发送验证码失败');
toast.error('发送失败', {
description: result.error || '发送验证码失败',
});
}
setSendingCode(false);
}, [email, sendCode]);
// 登录
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!email) {
setError('请输入邮箱');
return;
}
if (loginType === 'password' && !password) {
setError('请输入密码');
return;
}
if (loginType === 'code' && !code) {
setError('请输入验证码');
return;
}
setLoading(true);
const result = await login(
email,
loginType === 'password' ? password : undefined,
loginType === 'code' ? code : undefined,
loginType
);
if (!result.success) {
setError(result.error || '登录失败');
setLoading(false);
} else {
router.push(redirect);
}
};
return (
<div>
{/* 登录方式切换 */}
<div className="flex gap-2 p-1 bg-[var(--color-bg-secondary)] rounded-lg mb-6">
<button
type="button"
onClick={() => setLoginType('password')}
className={cn(
'flex-1 py-2 text-sm font-medium rounded-md transition-colors',
loginType === 'password'
? 'bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] shadow-sm'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
)}
>
</button>
<button
type="button"
onClick={() => setLoginType('code')}
className={cn(
'flex-1 py-2 text-sm font-medium rounded-md transition-colors',
loginType === 'code'
? 'bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] shadow-sm'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
)}
>
</button>
</div>
{/* 表单 */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* 邮箱 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="请输入邮箱"
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
</div>
{/* 密码 */}
{loginType === 'password' && (
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
className="w-full h-12 pl-10 pr-12 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
)}
{/* 验证码 */}
{loginType === 'code' && (
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="请输入6位验证码"
maxLength={6}
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
<button
type="button"
onClick={handleSendCode}
disabled={sendingCode || countdown > 0 || !email}
className={cn(
'px-4 h-12 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
sendingCode || countdown > 0 || !email
? 'bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)] cursor-not-allowed'
: 'bg-[var(--color-primary)] text-white hover:opacity-90'
)}
>
{sendingCode ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : countdown > 0 ? (
`${countdown}s`
) : (
'获取验证码'
)}
</button>
</div>
</div>
)}
{/* 错误提示 */}
{error && (
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm">
{error}
</div>
)}
{/* 登录按钮 */}
<button
type="submit"
disabled={loading}
className="w-full h-12 rounded-lg bg-[var(--color-primary)] text-white font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? '登录中...' : '登录'}
</button>
</form>
{/* 底部链接 */}
<div className="mt-6 text-center space-y-2">
<Link
href="/reset-password"
className="text-sm text-[var(--color-text-tertiary)] hover:text-[var(--color-primary)] transition-colors"
>
</Link>
<p className="text-sm text-[var(--color-text-tertiary)]">
<Link
href="/register"
className="ml-1 text-[var(--color-primary)] hover:underline font-medium"
>
</Link>
</p>
</div>
</div>
);
}
function LoginLoading() {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={<LoginLoading />}>
<LoginForm />
</Suspense>
);
}

View File

@ -0,0 +1,282 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Mail, Lock, Eye, EyeOff, Loader2, KeyRound, User } from 'lucide-react';
import { useAuth } from '@/providers/AuthProvider';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui/Toast';
export default function RegisterPage() {
const router = useRouter();
const { register, sendCode } = useAuth();
const [email, setEmail] = useState('');
const [nickname, setNickname] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [code, setCode] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [countdown, setCountdown] = useState(0);
const [error, setError] = useState('');
// 倒计时
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// 发送验证码
const handleSendCode = useCallback(async () => {
if (!email) {
setError('请输入邮箱');
return;
}
setSendingCode(true);
setError('');
const result = await sendCode(email, 'register');
if (result.success) {
setCountdown(60);
toast.success('验证码已发送', {
description: `验证码已发送到 ${email},请查收`,
});
} else {
setError(result.error || '发送验证码失败');
toast.error('发送失败', {
description: result.error || '发送验证码失败',
});
}
setSendingCode(false);
}, [email, sendCode]);
// 注册
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!email) {
setError('请输入邮箱');
return;
}
if (!nickname) {
setError('请输入昵称');
return;
}
if (nickname.length < 2 || nickname.length > 20) {
setError('昵称长度需要在2-20个字符之间');
return;
}
if (!code) {
setError('请输入验证码');
return;
}
if (!password) {
setError('请输入密码');
return;
}
if (password.length < 8) {
setError('密码长度不能少于8位');
return;
}
if (!/[a-zA-Z]/.test(password)) {
setError('密码必须包含字母');
return;
}
if (!/\d/.test(password)) {
setError('密码必须包含数字');
return;
}
if (password !== confirmPassword) {
setError('两次输入的密码不一致');
return;
}
setLoading(true);
const result = await register(email, password, confirmPassword, nickname, code);
if (!result.success) {
setError(result.error || '注册失败');
setLoading(false);
} else {
router.push('/');
}
};
return (
<div>
{/* 表单 */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* 邮箱 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="请输入邮箱"
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
</div>
{/* 验证码 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="请输入6位验证码"
maxLength={6}
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
<button
type="button"
onClick={handleSendCode}
disabled={sendingCode || countdown > 0 || !email}
className={cn(
'px-4 h-12 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
sendingCode || countdown > 0 || !email
? 'bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)] cursor-not-allowed'
: 'bg-[var(--color-primary)] text-white hover:opacity-90'
)}
>
{sendingCode ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : countdown > 0 ? (
`${countdown}s`
) : (
'获取验证码'
)}
</button>
</div>
</div>
{/* 昵称 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="请输入昵称2-20个字符"
maxLength={20}
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
</div>
{/* 密码 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="8位以上包含字母和数字"
className="w-full h-12 pl-10 pr-12 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{/* 确认密码 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入密码"
className="w-full h-12 pl-10 pr-12 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
>
{showConfirmPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{/* 错误提示 */}
{error && (
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm">
{error}
</div>
)}
{/* 注册按钮 */}
<button
type="submit"
disabled={loading}
className="w-full h-12 rounded-lg bg-[var(--color-primary)] text-white font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? '注册中...' : '注册'}
</button>
</form>
{/* 底部链接 */}
<div className="mt-6 text-center">
<p className="text-sm text-[var(--color-text-tertiary)]">
<Link
href="/login"
className="ml-1 text-[var(--color-primary)] hover:underline font-medium"
>
</Link>
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,309 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Mail, Lock, Eye, EyeOff, Loader2, KeyRound, ArrowLeft } from 'lucide-react';
import { useAuth } from '@/providers/AuthProvider';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui/Toast';
export default function ResetPasswordPage() {
const router = useRouter();
const { sendCode, resetPassword } = useAuth();
const [step, setStep] = useState<'email' | 'reset'>('email');
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [countdown, setCountdown] = useState(0);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
// 倒计时
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// 发送验证码
const handleSendCode = useCallback(async () => {
if (!email) {
setError('请输入邮箱');
return;
}
setSendingCode(true);
setError('');
const result = await sendCode(email, 'reset');
if (result.success) {
setCountdown(60);
setStep('reset');
toast.success('验证码已发送', {
description: `验证码已发送到 ${email},请查收`,
});
} else {
setError(result.error || '发送验证码失败');
toast.error('发送失败', {
description: result.error || '发送验证码失败',
});
}
setSendingCode(false);
}, [email, sendCode]);
// 重置密码
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
if (!code) {
setError('请输入验证码');
return;
}
if (!newPassword) {
setError('请输入新密码');
return;
}
if (newPassword.length < 8) {
setError('密码长度不能少于8位');
return;
}
if (!/[a-zA-Z]/.test(newPassword)) {
setError('密码必须包含字母');
return;
}
if (!/\d/.test(newPassword)) {
setError('密码必须包含数字');
return;
}
if (newPassword !== confirmPassword) {
setError('两次输入的密码不一致');
return;
}
setLoading(true);
const result = await resetPassword(email, code, newPassword, confirmPassword);
if (result.success) {
toast.success('密码重置成功', {
description: '即将跳转到登录页...',
});
setTimeout(() => {
router.push('/login');
}, 2000);
} else {
setError(result.error || '重置密码失败');
toast.error('重置失败', {
description: result.error || '重置密码失败',
});
}
setLoading(false);
};
return (
<div>
{/* 返回按钮 */}
{step === 'reset' && (
<button
type="button"
onClick={() => setStep('email')}
className="flex items-center gap-1 text-sm text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] mb-4"
>
<ArrowLeft className="w-4 h-4" />
</button>
)}
{/* 步骤提示 */}
<p className="text-sm text-[var(--color-text-tertiary)] text-center mb-6">
{step === 'email' ? '请输入您的邮箱地址' : '请输入验证码和新密码'}
</p>
{/* 第一步:输入邮箱 */}
{step === 'email' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="请输入注册时的邮箱"
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
</div>
{/* 错误提示 */}
{error && (
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm">
{error}
</div>
)}
{/* 发送验证码按钮 */}
<button
type="button"
onClick={handleSendCode}
disabled={sendingCode || !email}
className="w-full h-12 rounded-lg bg-[var(--color-primary)] text-white font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{sendingCode && <Loader2 className="w-5 h-5 animate-spin" />}
{sendingCode ? '发送中...' : '发送验证码'}
</button>
</div>
)}
{/* 第二步:输入验证码和新密码 */}
{step === 'reset' && (
<form onSubmit={handleSubmit} className="space-y-4">
{/* 验证码 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="flex gap-3">
<div className="relative flex-1">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="请输入6位验证码"
maxLength={6}
className="w-full h-12 pl-10 pr-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
</div>
<button
type="button"
onClick={handleSendCode}
disabled={sendingCode || countdown > 0}
className={cn(
'px-4 h-12 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
sendingCode || countdown > 0
? 'bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)] cursor-not-allowed'
: 'bg-[var(--color-primary)] text-white hover:opacity-90'
)}
>
{sendingCode ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : countdown > 0 ? (
`${countdown}s`
) : (
'重新发送'
)}
</button>
</div>
</div>
{/* 新密码 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type={showPassword ? 'text' : 'password'}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="8位以上包含字母和数字"
className="w-full h-12 pl-10 pr-12 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{/* 确认密码 */}
<div>
<label className="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--color-text-tertiary)]" />
<input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入新密码"
className="w-full h-12 pl-10 pr-12 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]"
>
{showConfirmPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{/* 错误提示 */}
{error && (
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm">
{error}
</div>
)}
{/* 成功提示 */}
{success && (
<div className="p-3 rounded-lg bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 text-sm">
{success}
</div>
)}
{/* 重置密码按钮 */}
<button
type="submit"
disabled={loading}
className="w-full h-12 rounded-lg bg-[var(--color-primary)] text-white font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? '重置中...' : '重置密码'}
</button>
</form>
)}
{/* 底部链接 */}
<div className="mt-6 text-center">
<p className="text-sm text-[var(--color-text-tertiary)]">
<Link
href="/login"
className="ml-1 text-[var(--color-primary)] hover:underline font-medium"
>
</Link>
</p>
</div>
</div>
);
}