侧边栏渲染
(一) 修改路由文件
- 这里最重要的就是handle,他会作为判断的依据,hidden为true时,不会在侧边栏中显示
import HomePage from '@/views/home';
import LoginPage from '@/views/login';
import ErrorPage from '@/views/error';
import { Navigate } from 'react-router-dom';
import AuthGuard from './auth';
import LayoutPage from '@/layouts';
// 引入user模块
import UserPage from '@/views/user';
// 引入blog模块
import ArticlePage from '@/views/blog/aritcle';
import TagesPage from '@/views/blog/tags';
const commonRoutes = [
{
// 需要认证的路由:由 AuthGuard 统一拦截
element: <AuthGuard />,
children: [
{
path: '/',
element: <LayoutPage />,
children: [
{
path: 'admin',
element: <HomePage />,
handle: { title: '首页' },
},
{
path: 'user',
element: <UserPage />,
handle: { title: '用户管理' },
},
{
path: 'blog',
handle: { title: '博客管理' },
children: [
{
path: 'article',
element: <ArticlePage />,
handle: { title: '文章管理', hidden: true },
},
{
path: 'tags',
element: <TagesPage />,
handle: { title: '标签管理' },
},
],
},
],
},
],
},
{
// 公开路由:无需 token
path: '/login',
element: <LoginPage />,
},
{
path: '*',
element: <Navigate to="/404" />,
},
{
path: '/404',
element: <ErrorPage />,
},
];
export default commonRoutes;(二) 修改侧边栏组件
- components/SideBar/index.tsx
import React, { useMemo } from "react";
import { PieChartOutlined } from "@ant-design/icons";
import type { MenuProps } from "antd";
import { Menu } from "antd";
import { useLocation, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio";
import { collapseStore } from "@/store/collapse";
import commonRoutes from "@/routes/modules/common";
type MenuItem = Required<MenuProps>["items"][number];
interface RouteHandle {
title?: string;
hidden?: boolean;
}
interface RouteConfig {
path?: string;
handle?: RouteHandle;
children?: RouteConfig[];
}
/** 从路由配置中提取菜单路由(LayoutPage 下的子路由) */
function extractMenuRoutes(routes: RouteConfig[]): RouteConfig[] {
// 直接遍历找到包含 handle.title 的路由层
for (const route of routes) {
// 如果当前层有带 title 的路由,说明是菜单层,直接返回
if (route.handle?.title) {
return routes;
}
// 否则递归查找子路由
if (route.children) {
const result = extractMenuRoutes(route.children);
if (result.length > 0) return result;
}
}
return [];
}
/** 递归生成菜单项 */
function buildMenuItems(routes: RouteConfig[], parentPath = ""): MenuItem[] {
return routes
.filter(
(route) => route.path && route.handle?.title && !route.handle?.hidden,
)
.map((route) => {
// 拼接完整路径
const fullPath = route.path!.startsWith("/")
? route.path!
: `${parentPath}/${route.path}`.replace(/\/+/g, "/");
const icon = <PieChartOutlined />;
const label = route.handle!.title!;
// 过滤子路由中 hidden 的项
const visibleChildren = route.children?.filter(
(child) => !child.handle?.hidden,
);
if (visibleChildren && visibleChildren.length > 0) {
return {
key: fullPath,
icon,
label,
children: buildMenuItems(visibleChildren, fullPath),
};
}
return { key: fullPath, icon, label };
});
}
const SideBar: React.FC = () => {
const { collapsed } = useSnapshot(collapseStore);
const location = useLocation();
const navigate = useNavigate();
// 从路由配置中提取菜单项
const items = useMemo(() => {
const menuRoutes = extractMenuRoutes(commonRoutes as RouteConfig[]);
return buildMenuItems(menuRoutes);
}, []);
// 当前选中的菜单项
const selectedKeys = useMemo(() => {
return [location.pathname];
}, [location.pathname]);
// 菜单点击跳转
const handleClick: MenuProps["onClick"] = ({ key }) => {
navigate(key);
};
return (
<Menu
selectedKeys={selectedKeys}
mode="inline"
theme="dark"
inlineCollapsed={collapsed}
items={items}
onClick={handleClick}
/>
);
};
export default SideBar;详细的解构这个文件
流程
注意
- 先利用 import commonRoutes from '@/routes/modules/common'; 引入路由配置
- 从路由配置中提取菜单路由(LayoutPage 下的子路由)
- 递归生成菜单项
- 处理选中菜单项
- 处理点击跳转
第一步 提取数据
这样能获取到路由文件里面所有的数据
import commonRoutes from "@/routes/modules/common";第二步 提取菜单路由 extractMenuRoutes 这是一个递归函数
/** 从路由配置中提取菜单路由(LayoutPage 下的子路由) */
function extractMenuRoutes(routes: RouteConfig[]): RouteConfig[] {
// 直接遍历找到包含 handle.title 的路由层
for (const route of routes) {
// 如果当前层有带 title 的路由,说明是菜单层,直接返回
if (route.handle?.title) {
return routes;
}
// 否则递归查找子路由
if (route.children) {
const result = extractMenuRoutes(route.children);
if (result.length > 0) return result;
}
}
return [];
}注意
- 第1次调用:extractMenuRoutes(commonRoutes)
routes = 最外层数组,包含 4 个路由:
遍历顺序 route 检查结果 操作 1 { element: <AuthGuard />, children: [...] } 无path ,无handle 有children → 递归调用
- 第2次调用:extractMenuRoutes(AuthGuard.children)
routes = AuthGuard 的 children,包含 1 个路由:
遍历顺序 route 检查结果 操作 1 { path: '/', element: <LayoutPage />, children: [...] } 有path ,但 无handle 有children → 递归调用
- 第3次调用:extractMenuRoutes(LayoutPage.children)
routes = LayoutPage 的 children,包含 4 个路由:
遍历顺序 route 检查结果 操作 1 { path: '/', handle: { title: '首页' } } 有handle.title ! 返回整个routes 数组
- 函数结束后返回
[ { path: '/', handle: { title: '首页' } }, { path: 'admin', handle: { title: '首页' } }, { path: 'user', handle: { title: '用户管理' } }, { path: 'blog', handle: { title: '博客管理' }, children: [...] }, ]
- 关键点总结
第1层 (commonRoutes) → 无 handle,继续向下找
↓
第2层 (AuthGuard.children) → 无 handle,继续向下找
↓
第3层 (LayoutPage.children) → 第1个路由就有 handle.title,找到!返回整层- 核心逻辑
一旦在某一层发现 任意一个 带handle.title 的路由,就认为这一层是菜单层, 立即返回该层的所有路由 (包括没有 title 的,但后续buildMenuItems 会过滤掉)。
第三步 递归生成菜单项 buildMenuItems
- 函数源代码
function buildMenuItems(routes: RouteConfig[], parentPath = ""): MenuItem[] {
return routes
.filter(
(route) => route.path && route.handle?.title && !route.handle?.hidden,
)
.map((route) => {
// 拼接完整路径
const fullPath = route.path!.startsWith("/")
? route.path!
: `${parentPath}/${route.path}`.replace(/\/+/g, "/");
const icon = <PieChartOutlined />;
const label = route.handle!.title!;
// 过滤子路由中 hidden 的项
const visibleChildren = route.children?.filter(
(child) => !child.handle?.hidden,
);
if (visibleChildren && visibleChildren.length > 0) {
return {
key: fullPath,
icon,
label,
children: buildMenuItems(visibleChildren, fullPath),
};
}
return { key: fullPath, icon, label };
});
}- 第一步过滤路由
.filter((route) => route.path && route.handle?.title && !route.handle?.hidden)需要同时满足三个条件才保留:
route.path— 必须有路径route.handle?.title— 必须有菜单标题!route.handle?.hidden— 不能是隐藏菜单
示例 :article 路由有hidden: true ,在这一步被过滤掉。
- 第二步 映射转换
对过滤后的路由进行映射转换,将每个路由转换为一个菜单项。
- 第三步 拼接完整的路径
const fullPath = route.path!.startsWith("/")
? route.path! // 绝对路径:直接使用
: `${parentPath}/${route.path}`.replace(/\/+/g, "/"); // 相对路径:拼接父路径| 场景 | route.path | parentPath | fullPath |
|---|---|---|---|
一级菜单admin | 'admin' | '' | '/admin' |
一级菜单user | 'user' | '' | '/user' |
二级菜单article | 'article' | '/blog' | '/blog/article' |
二级菜单tags | 'tags' | '/blog' | '/blog/tags' |
- 第四步 图标
const icon = <PieChartOutlined />; // 统一图标
const label = route.handle!.title!; // 从 handle 读取菜单名- 第五步 处理子菜单(递归)
const visibleChildren = route.children?.filter(
(child) => !child.handle?.hidden,
);
if (visibleChildren && visibleChildren.length > 0) {
return {
key: fullPath,
icon,
label,
children: buildMenuItems(visibleChildren, fullPath), // 递归
};
}注意
- 先过滤子路由中
hidden: true的项 - 如果有可见子路由 → 生成 带子菜单的父菜单 ,递归调用
buildMenuItems - 递归时
parentPath传当前fullPath,用于子路由拼接路径
- 无子菜单
return { key: fullPath, icon, label };