Browse Source

更新:重构布局组件和认证相关功能

web
hyh 3 months ago
parent
commit
2339a4273c
  1. 39
      src/CellularManagement.WebUI/package-lock.json
  2. 4
      src/CellularManagement.WebUI/package.json
  3. 9
      src/CellularManagement.WebUI/src/App.tsx
  4. 33
      src/CellularManagement.WebUI/src/components/auth/ProtectedRoute.tsx
  5. 23
      src/CellularManagement.WebUI/src/components/layout/DashboardLayout.tsx
  6. 18
      src/CellularManagement.WebUI/src/components/layout/Header.tsx
  7. 42
      src/CellularManagement.WebUI/src/components/layout/Sidebar.tsx
  8. 4
      src/CellularManagement.WebUI/src/components/layout/Tabs.tsx
  9. 43
      src/CellularManagement.WebUI/src/components/layout/menuConfig.ts
  10. 43
      src/CellularManagement.WebUI/src/components/ui/SidebarMenuItem.tsx
  11. 26
      src/CellularManagement.WebUI/src/components/ui/SidebarToggleButton.tsx
  12. 69
      src/CellularManagement.WebUI/src/constants/menuConfig.ts
  13. 53
      src/CellularManagement.WebUI/src/hooks/useAuth.ts
  14. 26
      src/CellularManagement.WebUI/src/hooks/useDrawer.ts
  15. 34
      src/CellularManagement.WebUI/src/hooks/useSidebarToggle.ts
  16. 29
      src/CellularManagement.WebUI/src/pages/auth/ForbiddenPage.tsx
  17. 25
      src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx
  18. 27
      src/CellularManagement.WebUI/src/routes/AppRouter.tsx

39
src/CellularManagement.WebUI/package-lock.json

@ -18,11 +18,11 @@
"lucide-react": "^0.323.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"react-router-dom": "^6.22.0",
"recoil": "^0.7.7",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"zustand": "^4.5.0"
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20.11.16",
@ -4007,6 +4007,14 @@
"react": "^18.3.1"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmmirror.com/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
@ -4953,33 +4961,6 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

4
src/CellularManagement.WebUI/package.json

@ -20,11 +20,11 @@
"lucide-react": "^0.323.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"react-router-dom": "^6.22.0",
"recoil": "^0.7.7",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"zustand": "^4.5.0"
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20.11.16",

9
src/CellularManagement.WebUI/src/App.tsx

@ -1,10 +1,13 @@
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import { AppRouter } from './routes/AppRouter';
export function App() {
return (
<BrowserRouter>
<AppRouter />
</BrowserRouter>
<RecoilRoot>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</RecoilRoot>
);
}

33
src/CellularManagement.WebUI/src/components/auth/ProtectedRoute.tsx

@ -0,0 +1,33 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { hasPermission, Permission } from '@/constants/menuConfig';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredPermission?: Permission;
}
export function ProtectedRoute({ children, requiredPermission }: ProtectedRouteProps) {
const { isAuthenticated, userPermissions, loading } = useAuth();
const location = useLocation();
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (!isAuthenticated) {
// 将用户重定向到登录页面,但保存他们尝试访问的URL
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredPermission && !hasPermission(userPermissions, requiredPermission)) {
// 如果用户没有所需权限,重定向到403页面
return <Navigate to="/403" replace />;
}
return <>{children}</>;
}

23
src/CellularManagement.WebUI/src/components/layout/DashboardLayout.tsx

@ -14,19 +14,16 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const { isCollapsed } = useSidebarToggle();
return (
<div className="relative min-h-screen bg-background">
<Sidebar />
<div
className={cn(
"flex flex-col transition-all duration-300 ease-in-out",
isCollapsed ? "lg:pl-20" : "lg:pl-64"
)}
>
<Header />
<Tabs />
<Content>
{children}
</Content>
<div className="h-screen flex flex-col">
<Header />
<div className="flex flex-1">
<Sidebar />
<div className="flex-1 flex flex-col transition-all duration-300 ease-in-out pl-3">
<Tabs />
<Content>
{children}
</Content>
</div>
</div>
</div>
);

18
src/CellularManagement.WebUI/src/components/layout/Header.tsx

@ -4,22 +4,21 @@ import { NotificationDrawer } from './NotificationDrawer';
import { useDrawer } from '@/hooks/useDrawer';
import { useSidebarToggle } from '@/hooks/useSidebarToggle';
import { cn } from '@/lib/utils';
import { SidebarToggleButton } from '../ui/SidebarToggleButton';
export function Header() {
const { isOpen: isNotificationOpen, toggle: toggleNotification } = useDrawer();
const { isCollapsed } = useSidebarToggle();
const { isCollapsed, toggleSidebar } = useSidebarToggle();
return (
<header
className={cn(
"sticky top-0 z-30 flex h-16 items-center gap-4 border-b bg-background px-4 shadow-sm transition-all duration-300 ease-in-out sm:px-6",
isCollapsed ? "lg:pl-20" : "lg:pl-64"
)}
<header
className="sticky top-0 z-30 flex h-16 items-center justify-between border-b bg-background pl-0 pr-4 shadow-sm transition-all duration-300 ease-in-out sm:pl-0 sm:pr-6"
>
<div className="flex flex-1 items-center gap-4">
{/* 这里可以放别的内容,比如 logo */}
<div className="flex items-center h-full">
<SidebarToggleButton onClick={toggleSidebar} isCollapsed={isCollapsed} />
<img src="/logo.svg" alt="Logo" className="h-10 w-10 ml-2" />
<span className="ml-2 text-2xl font-bold tracking-wide"></span>
</div>
<div className="flex items-center gap-4">
<button
onClick={toggleNotification}
@ -47,7 +46,6 @@ export function Header() {
<SearchInput />
<UserAvatarMenu username="用户" email="user@example.com" />
</div>
<NotificationDrawer isOpen={isNotificationOpen} onClose={toggleNotification} />
</header>
);

42
src/CellularManagement.WebUI/src/components/layout/Sidebar.tsx

@ -3,30 +3,42 @@ import { cn } from '@/lib/utils';
import { SidebarMenuItem } from '../ui/SidebarMenuItem';
import { SidebarToggleButton } from '../ui/SidebarToggleButton';
import { useSidebarToggle } from '@/hooks/useSidebarToggle';
import { menuItems } from './menuConfig';
import { menuItems, hasPermission, Permission, MenuItem } from '@/constants/menuConfig';
import { useAuth } from '@/hooks/useAuth'; // 假设你有一个useAuth hook来获取用户权限
export function Sidebar() {
const { isCollapsed, toggleSidebar } = useSidebarToggle();
const { userPermissions } = useAuth(); // 获取用户权限
// 过滤菜单项
const filteredMenuItems = menuItems.filter((item: MenuItem) => {
// 检查主菜单权限
if (!hasPermission(userPermissions, item.permission)) {
return false;
}
// 如果有子菜单,过滤子菜单
if (item.children) {
item.children = item.children.filter(child =>
hasPermission(userPermissions, child.permission)
);
// 如果过滤后没有子菜单,且主菜单没有href,则不显示
return item.children.length > 0 || item.href !== '#';
}
return true;
});
return (
<aside
className={cn(
'fixed left-0 top-0 z-10 h-screen w-64 border-r bg-card shadow-lg',
'transform-gpu will-change-transform',
'transition-transform duration-300 ease-in-out',
isCollapsed && '-translate-x-full lg:translate-x-0 lg:w-20'
'flex flex-col h-full w-64 border-r bg-card shadow-lg',
'transition-all duration-300 ease-in-out',
isCollapsed ? 'w-14' : 'w-64'
)}
>
<div className="flex h-16 items-center justify-between border-b px-4">
<div className={cn('flex items-center', isCollapsed && 'justify-center')}>
<img src="/logo.svg" alt="Logo" className="h-8 w-8" />
{!isCollapsed && <span className="ml-2 text-lg font-semibold"></span>}
</div>
<SidebarToggleButton onClick={toggleSidebar} isCollapsed={isCollapsed} />
</div>
<nav className="mt-4 space-y-1 px-2">
{menuItems.map((item) => (
<nav className="flex-1 mt-4 space-y-1 px-2 overflow-auto">
{filteredMenuItems.map((item) => (
<SidebarMenuItem
key={item.href}
item={item}

4
src/CellularManagement.WebUI/src/components/layout/Tabs.tsx

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { menuItems } from './menuConfig';
import { menuItems } from '@/constants/menuConfig';
import { cn } from '@/lib/utils';
import { ChevronLeft, ChevronRight, XCircle, MinusCircle, RefreshCcw, ChevronDown } from 'lucide-react';
@ -171,7 +171,7 @@ export function Tabs() {
}, [menuOpen]);
return (
<div className="flex items-center bg-background px-4 py-2 border-b relative">
<div className="flex items-center bg-background py-2 border-b relative w-full">
<div className="flex-1 flex items-center overflow-x-auto">
{tabs.map(tab => (
<div

43
src/CellularManagement.WebUI/src/components/layout/menuConfig.ts

@ -1,43 +0,0 @@
import { LucideIcon, LayoutDashboard, Users, Settings } from 'lucide-react';
interface MenuItem {
title: string;
icon: LucideIcon;
href: string;
children?: {
title: string;
href: string;
}[];
}
export const menuItems: MenuItem[] = [
{
title: '仪表盘',
icon: LayoutDashboard,
href: '/dashboard',
},
{
title: '用户管理',
icon: Users,
href: '/dashboard/users',
children: [
{
title: '用户列表',
href: '/dashboard/users/list',
},
{
title: '角色管理',
href: '/dashboard/users/roles',
},
{
title: '权限管理',
href: '/dashboard/users/permissions',
},
],
},
{
title: '系统设置',
icon: Settings,
href: '/dashboard/settings',
},
];

43
src/CellularManagement.WebUI/src/components/ui/SidebarMenuItem.tsx

@ -1,7 +1,7 @@
import { NavLink, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { LucideIcon, ChevronDown } from 'lucide-react';
import { useState, useEffect, memo } from 'react';
import { useState, useEffect, memo, useRef, useLayoutEffect } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar';
import {
DropdownMenu,
@ -12,6 +12,7 @@ import {
DropdownMenuTrigger,
} from '@radix-ui/react-dropdown-menu';
import { User, Lock, LogOut } from 'lucide-react';
import { createPortal } from 'react-dom';
interface MenuItem {
title: string;
@ -31,6 +32,8 @@ interface SidebarMenuItemProps {
export const SidebarMenuItem = memo(function SidebarMenuItem({ item, isCollapsed }: SidebarMenuItemProps) {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
const btnRef = useRef<HTMLButtonElement>(null);
const [menuPos, setMenuPos] = useState({ left: 0, top: 0 });
// 检查是否是子菜单项被选中
const isChildActive = item.children?.some(child => child.href === location.pathname);
@ -45,6 +48,13 @@ export const SidebarMenuItem = memo(function SidebarMenuItem({ item, isCollapsed
}
}, [isChildActive]);
useLayoutEffect(() => {
if (isCollapsed && isOpen && btnRef.current) {
const rect = btnRef.current.getBoundingClientRect();
setMenuPos({ left: rect.right, top: rect.top });
}
}, [isCollapsed, isOpen]);
if (!item.children) {
return (
<NavLink
@ -65,8 +75,13 @@ export const SidebarMenuItem = memo(function SidebarMenuItem({ item, isCollapsed
}
return (
<div className="space-y-1">
<div
className="space-y-1 relative"
onMouseEnter={() => isCollapsed && setIsOpen(true)}
onMouseLeave={() => isCollapsed && setIsOpen(false)}
>
<button
ref={btnRef}
onClick={() => setIsOpen(!isOpen)}
className={cn(
'flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm transition-all hover:bg-accent',
@ -87,6 +102,30 @@ export const SidebarMenuItem = memo(function SidebarMenuItem({ item, isCollapsed
/>
)}
</button>
{/* 折叠时悬浮展开二级菜单,使用Portal */}
{isCollapsed && isOpen && createPortal(
<div
className="z-50 min-w-[140px] bg-white shadow-lg rounded-lg py-2 fixed"
style={{ left: menuPos.left, top: menuPos.top }}
>
{item.children.map((child) => (
<NavLink
key={child.href}
to={child.href}
className={({ isActive }) =>
cn(
'block px-4 py-2 text-sm whitespace-nowrap rounded hover:bg-accent',
isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'
)
}
>
{child.title}
</NavLink>
))}
</div>,
document.body
)}
{/* 非折叠时的二级菜单,保持原有逻辑 */}
{!isCollapsed && (
<div
className={cn(

26
src/CellularManagement.WebUI/src/components/ui/SidebarToggleButton.tsx

@ -1,4 +1,4 @@
import { ChevronLeft } from 'lucide-react';
import { MdSyncAlt } from 'react-icons/md';
import { cn } from '@/lib/utils';
interface SidebarToggleButtonProps {
@ -8,14 +8,20 @@ interface SidebarToggleButtonProps {
export function SidebarToggleButton({ onClick, isCollapsed }: SidebarToggleButtonProps) {
return (
<button
onClick={onClick}
className={cn(
'rounded-full p-1 hover:bg-accent',
isCollapsed && 'rotate-180'
)}
>
<ChevronLeft className="h-5 w-5" />
</button>
<div className="flex flex-col items-center">
<button
onClick={onClick}
className={cn(
'flex items-center justify-center w-10 h-10 rounded-full',
'transition-all duration-200',
'hover:bg-gray-100',
'focus:outline-none',
'focus:bg-transparent active:bg-transparent'
)}
aria-label={isCollapsed ? '展开侧边栏' : '折叠侧边栏'}
>
<MdSyncAlt className="w-5 h-5 text-gray-700" />
</button>
</div>
);
}

69
src/CellularManagement.WebUI/src/constants/menuConfig.ts

@ -0,0 +1,69 @@
import { LucideIcon, LayoutDashboard, Users, Settings } from 'lucide-react';
// 定义权限类型
export type Permission =
| 'dashboard.view'
| 'users.view'
| 'users.manage'
| 'roles.view'
| 'roles.manage'
| 'permissions.view'
| 'permissions.manage'
| 'settings.view'
| 'settings.manage';
export interface MenuItem {
title: string;
icon: LucideIcon;
href: string;
permission?: Permission;
children?: {
title: string;
href: string;
permission?: Permission;
}[];
}
export const menuItems: MenuItem[] = [
{
title: '仪表盘',
icon: LayoutDashboard,
href: '/dashboard',
permission: 'dashboard.view',
},
{
title: '用户管理',
icon: Users,
href: '/dashboard/users',
permission: 'users.view',
children: [
{
title: '用户列表',
href: '/dashboard/users/list',
permission: 'users.view',
},
{
title: '角色管理',
href: '/dashboard/users/roles',
permission: 'roles.view',
},
{
title: '权限管理',
href: '/dashboard/users/permissions',
permission: 'permissions.view',
},
],
},
{
title: '系统设置',
icon: Settings,
href: '/dashboard/settings',
permission: 'settings.view',
},
];
// 导出权限检查工具函数
export const hasPermission = (userPermissions: Permission[], requiredPermission?: Permission): boolean => {
if (!requiredPermission) return true; // 如果没有设置权限要求,则默认允许访问
return userPermissions.includes(requiredPermission);
};

53
src/CellularManagement.WebUI/src/hooks/useAuth.ts

@ -0,0 +1,53 @@
import { useState, useEffect } from 'react';
import type { Permission } from '@/constants/menuConfig';
interface User {
id: string;
name: string;
email: string;
permissions: Permission[];
}
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 这里应该从你的认证系统获取用户信息
// 例如从localStorage、API或状态管理库中获取
const fetchUser = async () => {
try {
// 示例:从localStorage获取用户信息
const userData = localStorage.getItem('user');
if (userData) {
setUser(JSON.parse(userData));
}
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, []);
const login = async (userData: User) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
return {
user,
loading,
userPermissions: user?.permissions || [],
isAuthenticated: !!user,
login,
logout
};
}

26
src/CellularManagement.WebUI/src/hooks/useDrawer.ts

@ -1,11 +1,19 @@
import { create } from 'zustand';
import { atom, useRecoilState } from 'recoil';
interface DrawerState {
isOpen: boolean;
toggle: () => void;
}
const drawerState = atom({
key: 'drawerState',
default: false,
});
export const useDrawer = create<DrawerState>((set) => ({
isOpen: false,
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}));
export const useDrawer = () => {
const [isOpen, setIsOpen] = useRecoilState(drawerState);
const toggle = () => {
setIsOpen((prev) => !prev);
};
return {
isOpen,
toggle,
};
};

34
src/CellularManagement.WebUI/src/hooks/useSidebarToggle.ts

@ -1,19 +1,19 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { atom, useRecoilState } from 'recoil';
interface SidebarState {
isCollapsed: boolean;
toggleSidebar: () => void;
}
const sidebarState = atom({
key: 'sidebarState',
default: false,
});
export const useSidebarToggle = create<SidebarState>()(
persist(
(set) => ({
isCollapsed: false,
toggleSidebar: () => set((state) => ({ isCollapsed: !state.isCollapsed })),
}),
{
name: 'sidebar-state',
}
)
);
export const useSidebarToggle = () => {
const [isCollapsed, setIsCollapsed] = useRecoilState(sidebarState);
const toggleSidebar = () => {
setIsCollapsed((prev) => !prev);
};
return {
isCollapsed,
toggleSidebar,
};
};

29
src/CellularManagement.WebUI/src/pages/auth/ForbiddenPage.tsx

@ -0,0 +1,29 @@
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/Button';
export default function ForbiddenPage() {
const navigate = useNavigate();
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold text-gray-900">403</h1>
<p className="mt-4 text-xl text-gray-600">访</p>
<div className="mt-8">
<Button
onClick={() => navigate(-1)}
variant="outline"
className="mr-4"
>
</Button>
<Button
onClick={() => navigate('/dashboard')}
>
</Button>
</div>
</div>
</div>
);
}

25
src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx

@ -1,17 +1,36 @@
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import { LoginForm } from '../../components/auth/LoginForm';
import { DEFAULT_CREDENTIALS } from '../../constants/auth';
import { useAuth } from '@/hooks/useAuth';
export function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const handleLogin = (username: string, password: string) => {
const handleLogin = async (username: string, password: string) => {
const isValidLogin =
(username === DEFAULT_CREDENTIALS.username || username === DEFAULT_CREDENTIALS.email) &&
password === DEFAULT_CREDENTIALS.password;
if (isValidLogin) {
navigate('/dashboard');
// 模拟用户数据
const userData = {
id: '1',
name: username,
email: `${username}@example.com`,
permissions: ['dashboard.view', 'users.view', 'roles.view', 'permissions.view', 'settings.view']
};
// 保存用户数据到 localStorage
localStorage.setItem('user', JSON.stringify(userData));
// 调用 login 方法更新认证状态
await login(userData);
// 获取重定向地址,如果没有则默认到 dashboard
const from = (location.state as any)?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
} else {
alert('用户名/邮箱或密码错误');
}

27
src/CellularManagement.WebUI/src/routes/AppRouter.tsx

@ -1,10 +1,12 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { Suspense, lazy } from 'react';
import { DashboardLayout } from '../components/layout/DashboardLayout';
import { ProtectedRoute } from '../components/auth/ProtectedRoute';
// 使用 lazy 加载组件
const LoginPage = lazy(() => import('../pages/auth/LoginPage').then(module => ({ default: module.LoginPage })));
const DashboardHome = lazy(() => import('../pages/dashboard/DashboardHome').then(module => ({ default: module.DashboardHome })));
const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage').then(module => ({ default: module.default })));
// 加载中的占位组件
const LoadingFallback = () => (
@ -16,7 +18,7 @@ const LoadingFallback = () => (
export function AppRouter() {
return (
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route
path="/login"
element={
@ -25,17 +27,24 @@ export function AppRouter() {
</Suspense>
}
/>
<Route path="/403" element={
<Suspense fallback={<LoadingFallback />}>
<ForbiddenPage />
</Suspense>
} />
<Route
path="/dashboard/*"
element={
<DashboardLayout>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route index element={<DashboardHome />} />
{/* 添加更多路由 */}
</Routes>
</Suspense>
</DashboardLayout>
<ProtectedRoute requiredPermission="dashboard.view">
<DashboardLayout>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route index element={<DashboardHome />} />
{/* 添加更多路由 */}
</Routes>
</Suspense>
</DashboardLayout>
</ProtectedRoute>
}
/>
</Routes>

Loading…
Cancel
Save