mars 1ba859196a 修复退出登录重定向问题和相关功能优化
- 修复DashboardLayout中的退出登录函数,确保清除所有认证信息
- 恢复_app.tsx中的认证逻辑,确保仪表盘页面需要登录访问
- 完善退出登录流程:清除本地存储 -> 调用登出API -> 重定向到登录页面
- 添加错误边界组件提升用户体验
- 优化React水合错误处理
- 添加JWT令牌验证API
- 完善各个仪表盘页面的功能和样式
2025-07-03 20:56:17 +08:00

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>
);
}