feat(AuthProvider): 添加认证状态管理和加载界面

重构认证逻辑为独立函数并添加状态管理,在认证过程中显示加载界面。同时添加主题切换按钮到加载界面。
This commit is contained in:
SongPro
2025-07-01 14:18:21 +08:00
parent b880e5dce5
commit 4fac5e6409

View File

@@ -1,7 +1,11 @@
'use client'; 'use client';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { ThemeToggle } from '@/components/ThemeToggle';
import { useSite } from './SiteProvider';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@@ -10,37 +14,73 @@ interface Props {
export default function AuthProvider({ children }: Props) { export default function AuthProvider({ children }: Props) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const { siteName } = useSite();
const authenticate = useCallback(async () => {
// 登录页
if (pathname.startsWith('/login') || pathname.startsWith('/api/login')) {
setIsAuthenticated(true);
return;
}
useEffect(() => { // 从localStorage获取密码
// 登录页或 API 路径不做校验,避免死循环
if (pathname.startsWith('/login')) return;
const password = localStorage.getItem('password'); const password = localStorage.getItem('password');
const fullPath = const fullPath =
typeof window !== 'undefined' typeof window !== 'undefined'
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: pathname; : pathname;
// 有密码时验证 // 有密码直接跳转
(async () => { if (!password) {
try { router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`);
const res = await fetch('/api/login', { setIsAuthenticated(false);
method: 'POST', return;
headers: { 'Content-Type': 'application/json' }, }
body: JSON.stringify({ password }),
});
if (!res.ok) { // 尝试认证
// 校验未通过,清理并跳转登录 try {
localStorage.removeItem('password'); const res = await fetch('/api/login', {
router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); method: 'POST',
} headers: { 'Content-Type': 'application/json' },
} catch (error) { body: JSON.stringify({ password }),
// 网络错误等也认为未登录 });
router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`);
} if (!res.ok) throw new Error('认证失败');
})();
setIsAuthenticated(true);
} catch (error) {
// 认证失败,清理并跳转登录
setIsAuthenticated(false);
localStorage.removeItem('password');
router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`);
}
}, [pathname, router]); }, [pathname, router]);
return <>{children}</>; useEffect(() => {
authenticate();
}, [pathname, authenticate]);
// 认证状态未知时显示加载状态
if (!isAuthenticated) {
return (
<div className='relative min-h-screen flex items-center justify-center px-4 overflow-hidden'>
<div className='absolute top-4 right-4'>
<ThemeToggle />
</div>
<div className='relative z-10 w-full max-w-md rounded-3xl bg-gradient-to-b from-white/90 via-white/70 to-white/40 dark:from-zinc-900/90 dark:via-zinc-900/70 dark:to-zinc-900/40 backdrop-blur-xl shadow-2xl p-10 dark:border dark:border-zinc-800'>
<h1 className='text-green-600 tracking-tight text-center text-3xl font-extrabold mb-8 bg-clip-text drop-shadow-sm'>
{siteName}
</h1>
<div className='flex justify-center my-10'>
<div className='animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-green-500' />
</div>
<p className='text-gray-700 dark:text-gray-300 font-medium text-lg text-center'>
...
</p>
</div>
</div>
);
} else {
return <>{children}</>;
}
} }