2026-03-14 18:24:33 +08:00
|
|
|
|
import { createBrowserRouter, Navigate, useLocation } from 'react-router-dom'
|
|
|
|
|
|
import { Suspense, lazy } from 'react'
|
|
|
|
|
|
import { useAuthStore, ROLES } from '@/stores/authStore'
|
|
|
|
|
|
|
|
|
|
|
|
// 路由守卫组件
|
|
|
|
|
|
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
const { isAuthenticated, isLoading } = useAuthStore()
|
|
|
|
|
|
const location = useLocation()
|
|
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-screen items-center justify-center">
|
|
|
|
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
|
return <Navigate to="/login" state={{ from: location }} replace />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <>{children}</>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function RoleGuard({
|
|
|
|
|
|
children,
|
|
|
|
|
|
roles
|
|
|
|
|
|
}: {
|
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
|
roles: string[]
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const { hasAnyRole, isAuthenticated, isLoading } = useAuthStore()
|
|
|
|
|
|
const location = useLocation()
|
|
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-screen items-center justify-center">
|
|
|
|
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
|
return <Navigate to="/login" state={{ from: location }} replace />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasAnyRole(roles)) {
|
|
|
|
|
|
return <Navigate to="/403" replace />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <>{children}</>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function AdminGuard({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<RoleGuard roles={[ROLES.SUPER_ADMIN, ROLES.ADMIN]}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</RoleGuard>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function ModeratorGuard({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<RoleGuard roles={[ROLES.SUPER_ADMIN, ROLES.ADMIN, ROLES.MODERATOR]}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</RoleGuard>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 公开路由守卫(已登录用户重定向到dashboard)
|
|
|
|
|
|
export function PublicGuard({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
const { isAuthenticated, isLoading } = useAuthStore()
|
|
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-screen items-center justify-center">
|
|
|
|
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isAuthenticated) {
|
|
|
|
|
|
return <Navigate to="/dashboard" replace />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <>{children}</>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载指示器组件
|
|
|
|
|
|
function PageLoader() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full items-center justify-center p-8">
|
|
|
|
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 懒加载页面组件
|
|
|
|
|
|
const Login = lazy(() => import('@/pages/Login'))
|
|
|
|
|
|
const Dashboard = lazy(() => import('@/pages/Dashboard'))
|
|
|
|
|
|
const Users = lazy(() => import('@/pages/Users'))
|
|
|
|
|
|
const Roles = lazy(() => import('@/pages/Roles'))
|
|
|
|
|
|
const Posts = lazy(() => import('@/pages/Posts'))
|
|
|
|
|
|
const Comments = lazy(() => import('@/pages/Comments'))
|
|
|
|
|
|
const Groups = lazy(() => import('@/pages/Groups'))
|
2026-03-26 01:27:59 +08:00
|
|
|
|
const Channels = lazy(() => import('@/pages/Channels'))
|
2026-03-29 20:13:32 +08:00
|
|
|
|
const Reports = lazy(() => import('@/pages/Reports'))
|
2026-03-25 20:44:27 +08:00
|
|
|
|
const MaterialSubjects = lazy(() => import('@/pages/MaterialSubjects'))
|
|
|
|
|
|
const Materials = lazy(() => import('@/pages/Materials'))
|
2026-04-05 20:26:57 +08:00
|
|
|
|
const Verifications = lazy(() => import('@/pages/Verifications'))
|
2026-03-14 18:24:33 +08:00
|
|
|
|
const Forbidden = lazy(() => import('@/pages/Forbidden'))
|
|
|
|
|
|
const NotFound = lazy(() => import('@/pages/NotFound'))
|
|
|
|
|
|
|
|
|
|
|
|
// 布局组件
|
|
|
|
|
|
import MainLayout from '@/components/Layout/MainLayout'
|
|
|
|
|
|
|
|
|
|
|
|
// 创建路由
|
|
|
|
|
|
export const router = createBrowserRouter([
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/login',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<PublicGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Login />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</PublicGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AuthGuard>
|
|
|
|
|
|
<MainLayout />
|
|
|
|
|
|
</AuthGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{
|
|
|
|
|
|
index: true,
|
|
|
|
|
|
element: <Navigate to="/dashboard" replace />,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'dashboard',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Dashboard />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'users',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AdminGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Users />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</AdminGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'roles',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AdminGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Roles />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</AdminGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'posts',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<ModeratorGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Posts />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</ModeratorGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'comments',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<ModeratorGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Comments />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</ModeratorGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'groups',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<ModeratorGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Groups />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</ModeratorGuard>
|
2026-03-29 20:13:32 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'reports',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<ModeratorGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Reports />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</ModeratorGuard>
|
2026-03-14 18:24:33 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-03-26 01:27:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
path: 'channels',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AdminGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Channels />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</AdminGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-03-25 20:44:27 +08:00
|
|
|
|
{
|
|
|
|
|
|
path: 'materials/subjects',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AdminGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<MaterialSubjects />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</AdminGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: 'materials',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AdminGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Materials />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</AdminGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-04-05 20:26:57 +08:00
|
|
|
|
{
|
|
|
|
|
|
path: 'verifications',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<AdminGuard>
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Verifications />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</AdminGuard>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-03-14 18:24:33 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '/403',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<Forbidden />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
path: '*',
|
|
|
|
|
|
element: (
|
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
|
<NotFound />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
export default router
|