127 lines
5.1 KiB
TypeScript
127 lines
5.1 KiB
TypeScript
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||
import { useAuthStore } from '@/store/auth'
|
||
import { useAppStore } from '@/store/app'
|
||
import AdminLayout from '@/layouts/AdminLayout'
|
||
import LoginPage from '@/pages/LoginPage'
|
||
import ErrorBoundary from '@/components/ErrorBoundary'
|
||
import { Suspense, useMemo, lazy, useEffect } from 'react'
|
||
import { Loader2, Shield } from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
import type { SystemMenu } from '@/api/system'
|
||
|
||
const pages = import.meta.glob('./pages/**/*.tsx')
|
||
|
||
const dynamicComponentMap: Record<string, React.LazyExoticComponent<any>> = {}
|
||
for (const path in pages) {
|
||
let routePath = path.replace(/^\.\/pages/, '').replace(/\.tsx$/, '').replace(/\/index$/, '').toLowerCase()
|
||
if (routePath === '/loginpage') continue
|
||
dynamicComponentMap[routePath] = lazy(pages[path] as any)
|
||
}
|
||
|
||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||
const isAuthenticated = useAuthStore(s => s.isAuthenticated)
|
||
if (!isAuthenticated) return <Navigate to="/login" replace />
|
||
return <>{children}</>
|
||
}
|
||
|
||
function PublicRoute({ children }: { children: React.ReactNode }) {
|
||
const isAuthenticated = useAuthStore(s => s.isAuthenticated)
|
||
if (isAuthenticated) return <Navigate to="/" replace />
|
||
return <>{children}</>
|
||
}
|
||
|
||
function NoPermission() {
|
||
const logout = useAuthStore(s => s.logout)
|
||
return (
|
||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center w-full">
|
||
<Shield className="h-16 w-16 text-muted-foreground/30 mb-4" />
|
||
<h2 className="text-2xl font-bold mb-2 text-foreground">访问受限</h2>
|
||
<p className="text-muted-foreground mb-6 max-w-md">抱歉,您当前暂无任何系统权限,请联系管理员为您分配相关菜单与角色。</p>
|
||
<Button onClick={logout} variant="default" className="w-32">退出登录</Button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function AppRoutes() {
|
||
const menus = useAuthStore(s => s.menus)
|
||
const isAuthenticated = useAuthStore(s => s.isAuthenticated)
|
||
const refreshMenus = useAuthStore(s => s.refreshMenus)
|
||
const hasFetchedMenus = useAuthStore(s => s.hasFetchedMenus)
|
||
|
||
useEffect(() => {
|
||
if (isAuthenticated && menus.length === 0) refreshMenus()
|
||
}, [isAuthenticated, menus.length, refreshMenus])
|
||
|
||
const dynamicRoutes = useMemo(() => {
|
||
const routes: { path: string; Component: React.ComponentType }[] = []
|
||
const traverse = (items: SystemMenu[]) => {
|
||
items.forEach(item => {
|
||
if (item.children?.length) traverse(item.children)
|
||
const routeKey = item.path || item.code
|
||
if (routeKey && dynamicComponentMap[routeKey]) {
|
||
routes.push({ path: routeKey, Component: dynamicComponentMap[routeKey] })
|
||
}
|
||
})
|
||
}
|
||
if (menus) traverse(menus)
|
||
return routes
|
||
}, [menus])
|
||
|
||
const Loading = <div className="flex justify-center p-8"><Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /></div>
|
||
|
||
return (
|
||
<Routes>
|
||
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
|
||
<Route path="/" element={<ProtectedRoute><AdminLayout /></ProtectedRoute>}>
|
||
<Route index element={
|
||
hasFetchedMenus ? (
|
||
dynamicRoutes.length > 0 ? <Navigate to={dynamicRoutes[0].path} replace /> : <Navigate to="/403" replace />
|
||
) : Loading
|
||
} />
|
||
{dynamicRoutes.map(({ path, Component }) => (
|
||
<Route key={path} path={path.startsWith('/') ? path.substring(1) : path}
|
||
element={<ErrorBoundary><Suspense fallback={Loading}><Component /></Suspense></ErrorBoundary>} />
|
||
))}
|
||
{hasFetchedMenus && dynamicRoutes.length === 0 && (
|
||
<Route path="*" element={<NoPermission />} />
|
||
)}
|
||
{hasFetchedMenus && dynamicRoutes.length > 0 && (
|
||
<Route path="*" element={
|
||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center w-full">
|
||
<h2 className="text-2xl font-bold mb-2 text-foreground">页面不存在或开发中</h2>
|
||
<p className="text-muted-foreground">该菜单没有对应的页面组件,或者路径未匹配。</p>
|
||
</div>
|
||
} />
|
||
)}
|
||
</Route>
|
||
<Route path="*" element={<Navigate to="/" replace />} />
|
||
</Routes>
|
||
)
|
||
}
|
||
|
||
export default function App() {
|
||
const themeHue = useAppStore(s => s.themeHue)
|
||
|
||
useEffect(() => {
|
||
document.documentElement.style.setProperty('--theme-hue', themeHue)
|
||
if (themeHue === '45') {
|
||
document.documentElement.style.setProperty('--theme-l', '0.65')
|
||
document.documentElement.style.setProperty('--theme-c', '0.18')
|
||
document.documentElement.style.setProperty('--theme-l-dark', '0.70')
|
||
document.documentElement.style.setProperty('--theme-c-dark', '0.16')
|
||
} else {
|
||
document.documentElement.style.removeProperty('--theme-l')
|
||
document.documentElement.style.removeProperty('--theme-c')
|
||
document.documentElement.style.removeProperty('--theme-l-dark')
|
||
document.documentElement.style.removeProperty('--theme-c-dark')
|
||
}
|
||
}, [themeHue])
|
||
|
||
return (
|
||
<BrowserRouter>
|
||
<AppRoutes />
|
||
</BrowserRouter>
|
||
)
|
||
}
|
||
|