react-menu:React menu 菜单栏,基于 antd 的 menu 组件和 react-router
1. 代码仓库
2. 主线任务
1、目录结构

2、下载项目依赖
tsx
pnpm add antd @ant-design/icons [email protected] -S
2.1. V1版本
先实现基本功能,点击菜单能跳转即可
1、layouts/index.tsx
tsx
import { Outlet } from 'react-router';
import MenuLayout from './v1';
import { Layout, Card } from 'antd';
const { Sider, Content } = Layout;
export default function Layouts() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider width={256} theme='dark'>
<MenuLayout />
</Sider>
<Content>
<Card>
<Outlet />
</Card>
</Content>
</Layout>
);
}
2、layouts/v1.tsx
tsx
import React from 'react';
import {useNavigate} from "react-router"
import { Menu, type MenuProps } from 'antd';
import { AppstoreOutlined } from '@ant-design/icons';
type MenuItem = Required<MenuProps>['items'][number];
const items: MenuItem[] = [
{
key: '1',
label: 'V1版本',
icon: <AppstoreOutlined />,
children: [
{ key: 'home', label: 'home' },
{ key: 'about', label: 'about' },
],
},
{
key: '2',
label: '公开路由',
icon: <AppstoreOutlined />,
children: [
{ key: 'login', label: 'login' },
],
},
];
export const MenuLayout: React.RC = () => {
const navigate = useNavigate()
const onClick: MenuProps['onClick'] = (e) => {
console.log(e);
navigate(e.key)
};
return (
<Menu
onClick={onClick}
style={{ width: 256 }}
defaultSelectedKeys={['1']}
defaultOpenKeys={['1']}
mode='inline'
items={items}
/>
);
};
export default MenuLayout;
2.2. V2版本
完善
routes
中的mate
属性,menu
数据走动态读取
1、layouts/v2.tsx
tsx
import { useNavigate } from 'react-router';
import { Menu, type MenuProps } from 'antd';
import React, { useMemo } from 'react';
import { AppstoreOutlined } from '@ant-design/icons';
import {
getRoutesFromModules,
menuFilter,
useRouteToMenuFn,
} from '@/router/utils';
export const MenuLayout: React.RC = () => {
const routes = getRoutesFromModules();
const routeToMenuFn = useRouteToMenuFn();
const menuList = useMemo(() => {
const menuRoutes = menuFilter(routes);
return routeToMenuFn(menuRoutes);
}, [routeToMenuFn]);
const navigate = useNavigate();
const onClick: MenuProps['onClick'] = (e) => {
console.log(e);
navigate(e.key);
};
return (
<Menu
onClick={onClick}
style={{ width: 256 }}
defaultSelectedKeys={['1']}
defaultOpenKeys={['1']}
mode='inline'
items={menuList}
/>
);
};
export default MenuLayout;
2、router/utils.tsx
tsx
import { useCallback } from 'react';
export const menuFilter = (list) => {
return list.filter((item) => {
const key = item.meta?.key;
if ((key && item.children) || item.children?.length) {
item.children = menuFilter(item.children);
}
return key;
});
};
// 基于 src/router/routes.tsx 文件结构动态生成路由
export function getRoutesFromModules() {
const menuModules = [];
const modules = import.meta.glob('./routes/**/*.tsx', { eager: true });
for (const key in modules) {
const mod = (modules as any)[key].default || {};
const modList = Array.isArray(mod) ? [...mod] : [mod];
menuModules.push(...modList);
}
return menuModules;
}
import * as Icons from '@ant-design/icons';
const getIcon = (iconName) => {
const Icon = Icons[iconName];
return <Icon />;
};
export function useRouteToMenuFn() {
const routeToMenuFn = useCallback((items) => {
return items.map((item) => {
const { meta, children } = item;
// 如果没有meta,直接返回空对象
if (!meta) return {};
const menuItem = {
key: meta.key, // antd menu 需要的 key
disabled: meta.disabled,
// 这里可以写成函数 然后国际化在这个函数中处理
label: meta.label, // antd menu 需要的 label
icon: getIcon(meta.icon),
...(children && { children: routeToMenuFn(children) }),
};
return menuItem;
});
}, []);
return routeToMenuFn;
}
3. 支线任务
3.1. router
1、router/index.tsx
:
tsx
import { RouterProvider } from 'react-router/dom';
import { createBrowserRouter, Navigate } from 'react-router';
import Layouts from '@/layouts';
import { publicConfig } from './routes/public';
import { dashboardConfig } from './routes/dashboard';
const Router = () => {
const routes = [
{
path: '/',
element: <Layouts></Layouts>,
children: [
{ index: true, element: <Navigate to='dashboard/home' replace /> },
...dashboardConfig,
],
},
];
const router = createBrowserRouter([
...routes,
...publicConfig,
{ path: '*', element: <Navigate to='/login' replace /> },
]);
return <RouterProvider router={router} />;
};
export default Router;
2、router/routes/dashboard.tsx
:
tsx
import Home from '@/pages/home';
import About from '@/pages/about';
export const dashboardConfig = [
{
order: 1,
path: 'dashboard',
meta: {
label: 'V2 版本',
key: '/dashboard',
icon: 'AppstoreOutlined',
},
children: [
{
path: 'home',
element: <Home />,
meta: {
label: '首页',
key: '/dashboard/home',
icon: 'AppstoreOutlined',
},
},
{
path: 'about',
element: <About />,
meta: {
label: '关于',
key: '/dashboard/about',
icon: 'AppstoreOutlined',
},
},
],
},
];
export default dashboardConfig;
3、router/routes/public.tsx
:
tsx
import Login from '@/pages/login';
export const publicConfig = [
{
order: 2,
path: 'public',
meta: {
label: '公开路由',
key: '/public',
icon: 'AppstoreOutlined',
},
children: [
{
path: 'login',
element: <Login />,
meta: {
label: '首页',
key: '/public/login',
icon: 'AppstoreOutlined',
},
}
],
},
];
export default publicConfig;
4、pages
目录下页面:
tsx
// home/index.tsx
import React from 'react';
export const Home: React.RC = () => {
return <h1>home</h1>;
};
export default Home;
// about/index.tsx
import React from 'react';
export const About: React.RC = () => {
return <h1>about</h1>;
};
export default About;
// login/index.tsx
import React from 'react';
export const Login: React.RC = () => {
return <h1>登录页面</h1>
};
export default Login;
5、App.tsx
tsx
import Router from './router';
export default function App() {
return <Router />;
}