From abcea6798093d298d5f7ef653f1f1e145592460d Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Fri, 19 Dec 2025 22:36:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=A1=B5=E9=9D=A2):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=9B=B8=E5=85=B3=E9=A1=B5=E9=9D=A2UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现登录页面,支持邮箱验证码登录 - 实现注册页面,支持邮箱验证注册 - 实现重置密码页面 - 统一认证页面布局和样式 --- src/app/(auth)/layout.tsx | 73 ++++++ src/app/(auth)/login/page.tsx | 272 ++++++++++++++++++++++ src/app/(auth)/register/page.tsx | 282 ++++++++++++++++++++++ src/app/(auth)/reset-password/page.tsx | 309 +++++++++++++++++++++++++ 4 files changed, 936 insertions(+) create mode 100644 src/app/(auth)/layout.tsx create mode 100644 src/app/(auth)/login/page.tsx create mode 100644 src/app/(auth)/register/page.tsx create mode 100644 src/app/(auth)/reset-password/page.tsx diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..8efbef9 --- /dev/null +++ b/src/app/(auth)/layout.tsx @@ -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 = { + '/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 ( +
+ {/* 顶部导航栏 */} +
+ + + + LionCode + + + +
+ + {/* 主内容区 */} +
+ {/* 品牌标识和标题 */} +
+ +

+ {config.title} +

+

+ {config.subtitle} +

+
+ + {/* 表单卡片 */} +
+
+ {children} +
+
+
+ + {/* 页脚 */} +
+

Powered by LionCode

+
+
+ ); +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..91c26ca --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -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('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 ( +
+ {/* 登录方式切换 */} +
+ + +
+ + {/* 表单 */} +
+ {/* 邮箱 */} +
+ +
+ + 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" + /> +
+
+ + {/* 密码 */} + {loginType === 'password' && ( +
+ +
+ + 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" + /> + +
+
+ )} + + {/* 验证码 */} + {loginType === 'code' && ( +
+ +
+
+ + 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" + /> +
+ +
+
+ )} + + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 登录按钮 */} + +
+ + {/* 底部链接 */} +
+ + 忘记密码? + +

+ 没有账号? + + 立即注册 + +

+
+
+ ); +} + +function LoginLoading() { + return ( +
+ +
+ ); +} + +export default function LoginPage() { + return ( + }> + + + ); +} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx new file mode 100644 index 0000000..5db4a80 --- /dev/null +++ b/src/app/(auth)/register/page.tsx @@ -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 ( +
+ {/* 表单 */} +
+ {/* 邮箱 */} +
+ +
+ + 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" + /> +
+
+ + {/* 验证码 */} +
+ +
+
+ + 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" + /> +
+ +
+
+ + {/* 昵称 */} +
+ +
+ + 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" + /> +
+
+ + {/* 密码 */} +
+ +
+ + 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" + /> + +
+
+ + {/* 确认密码 */} +
+ +
+ + 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" + /> + +
+
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 注册按钮 */} + +
+ + {/* 底部链接 */} +
+

+ 已有账号? + + 立即登录 + +

+
+
+ ); +} diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..6d35cb3 --- /dev/null +++ b/src/app/(auth)/reset-password/page.tsx @@ -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 ( +
+ {/* 返回按钮 */} + {step === 'reset' && ( + + )} + + {/* 步骤提示 */} +

+ {step === 'email' ? '请输入您的邮箱地址' : '请输入验证码和新密码'} +

+ + {/* 第一步:输入邮箱 */} + {step === 'email' && ( +
+
+ +
+ + 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" + /> +
+
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 发送验证码按钮 */} + +
+ )} + + {/* 第二步:输入验证码和新密码 */} + {step === 'reset' && ( +
+ {/* 验证码 */} +
+ +
+
+ + 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" + /> +
+ +
+
+ + {/* 新密码 */} +
+ +
+ + 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" + /> + +
+
+ + {/* 确认密码 */} +
+ +
+ + 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" + /> + +
+
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 成功提示 */} + {success && ( +
+ {success} +
+ )} + + {/* 重置密码按钮 */} + +
+ )} + + {/* 底部链接 */} +
+

+ 想起密码了? + + 返回登录 + +

+
+
+ ); +}