- 修复DashboardLayout中的退出登录函数,确保清除所有认证信息 - 恢复_app.tsx中的认证逻辑,确保仪表盘页面需要登录访问 - 完善退出登录流程:清除本地存储 -> 调用登出API -> 重定向到登录页面 - 添加错误边界组件提升用户体验 - 优化React水合错误处理 - 添加JWT令牌验证API - 完善各个仪表盘页面的功能和样式
186 lines
5.2 KiB
TypeScript
186 lines
5.2 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { AppProps } from 'next/app';
|
|
import { Toaster } from 'react-hot-toast';
|
|
import { User } from '@supabase/supabase-js';
|
|
import useClientMount from '../utils/useClientMount';
|
|
import ErrorBoundary from '../components/ErrorBoundary';
|
|
import '../styles/globals.css';
|
|
|
|
// 自定义用户类型,用于 JWT 认证
|
|
interface CustomUser {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
userType: string;
|
|
phone?: string;
|
|
avatarUrl?: string;
|
|
}
|
|
|
|
export default function App({ Component, pageProps }: AppProps) {
|
|
const [user, setUser] = useState<CustomUser | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [navigationInProgress, setNavigationInProgress] = useState(false);
|
|
const isClient = useClientMount();
|
|
const router = useRouter();
|
|
const navigationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// 防抖路由跳转函数
|
|
const navigateWithDebounce = (path: string) => {
|
|
if (navigationInProgress) return;
|
|
|
|
setNavigationInProgress(true);
|
|
|
|
// 清除之前的定时器
|
|
if (navigationTimeoutRef.current) {
|
|
clearTimeout(navigationTimeoutRef.current);
|
|
}
|
|
|
|
// 设置新的定时器
|
|
navigationTimeoutRef.current = setTimeout(() => {
|
|
router.push(path).finally(() => {
|
|
setNavigationInProgress(false);
|
|
});
|
|
}, 100); // 100ms 防抖延迟
|
|
};
|
|
|
|
// 检查 JWT 令牌的有效性
|
|
const checkJWTAuth = async () => {
|
|
try {
|
|
const token = localStorage.getItem('adminToken');
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
|
|
// 验证令牌
|
|
const response = await fetch('/api/auth/verify-token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.user;
|
|
} else {
|
|
// 令牌无效,清除本地存储
|
|
localStorage.removeItem('adminToken');
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Token verification error:', error);
|
|
localStorage.removeItem('adminToken');
|
|
return null;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
// 只在客户端执行
|
|
if (!isClient) return;
|
|
|
|
// 检查是否为演示模式
|
|
const isDemoMode = !process.env.NEXT_PUBLIC_SUPABASE_URL ||
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL === 'https://demo.supabase.co' ||
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL === '';
|
|
|
|
const checkUser = async () => {
|
|
try {
|
|
// 使用自定义 JWT 认证检查
|
|
const jwtUser = await checkJWTAuth();
|
|
|
|
if (jwtUser) {
|
|
setUser(jwtUser);
|
|
// 如果当前在登录页面且已经认证,重定向到仪表板
|
|
if (router.pathname === '/auth/login') {
|
|
navigateWithDebounce('/dashboard');
|
|
}
|
|
} else {
|
|
setUser(null);
|
|
// 如果在需要认证的页面但未登录,重定向到登录页
|
|
const protectedRoutes = ['/dashboard', '/admin', '/settings'];
|
|
const isProtectedRoute = protectedRoutes.some(route =>
|
|
router.pathname.startsWith(route)
|
|
);
|
|
|
|
if (isProtectedRoute) {
|
|
navigateWithDebounce('/auth/login');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth check error:', error);
|
|
setUser(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
checkUser();
|
|
|
|
// 监听路由变化,重新检查认证状态
|
|
const handleRouteChange = () => {
|
|
checkUser();
|
|
};
|
|
|
|
router.events.on('routeChangeComplete', handleRouteChange);
|
|
|
|
return () => {
|
|
router.events.off('routeChangeComplete', handleRouteChange);
|
|
// 清理定时器
|
|
if (navigationTimeoutRef.current) {
|
|
clearTimeout(navigationTimeoutRef.current);
|
|
}
|
|
};
|
|
}, [router, isClient]);
|
|
|
|
// 在客户端挂载之前,显示最小化的 loading 状态以避免水合错误
|
|
if (!isClient) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50" suppressHydrationWarning>
|
|
<div className="loading-spinner"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 显示加载状态
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
<div className="loading-spinner"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ErrorBoundary>
|
|
<div suppressHydrationWarning>
|
|
<Component {...pageProps} user={user} />
|
|
<Toaster
|
|
position="top-right"
|
|
toastOptions={{
|
|
duration: 4000,
|
|
style: {
|
|
background: '#363636',
|
|
color: '#fff',
|
|
},
|
|
success: {
|
|
duration: 3000,
|
|
iconTheme: {
|
|
primary: '#10b981',
|
|
secondary: '#fff',
|
|
},
|
|
},
|
|
error: {
|
|
duration: 5000,
|
|
iconTheme: {
|
|
primary: '#ef4444',
|
|
secondary: '#fff',
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|