动态 Menu 菜单处理方案
规则
规则
- 对于单个路由规则而言(循环):
- 如果
meta && meta.title && meta.icon
:则显示在menu
菜单中,其中title
为显示的内容,icon
为显示的图标- 如果存在
children
:则以el-sub-menu(子菜单)
展示 - 否则:则以
el-menu-item(菜单项)
展示
- 如果存在
- 否则:不显示在
menu
菜单中
- 如果
创建页面组件
- pages 创建如下页面
- 创建文章:
article-create
- 文章详情:
article-detail
- 文章排名:
article-ranking
- 错误页面:
error-page
404
401
- 导入:
import
- 权限列表:
permission-list
- 个人中心:
profile
- 角色列表:
role-list
- 用户信息:
user-info
- 用户管理:
user-manage
创建路由规则
公有路由表
私有路由表
js
import { createRouter, createWebHistory } from "vue-router";
const LayOutPage = () => import("@/layouts/default.vue");
const LoginPage = () => import("@/pages/login/index");
const LoginLayOut = () => import("@/layouts/login");
const ProfilePage = () => import("@/pages/profile/index");
const Page404 = () => import("@/pages/error-page/404");
const Page401 = () => import("@/pages/error-page/401");
/* 私有路由表 */
const UserManagePage = () => import("@/pages/user-manage/index");
const RolePage = () => import("@/pages/role-list/index");
const PermissionPage = () => import("@/pages/permission-list/index");
const UserInfoPage = () => import("@/pages/user-info/index");
const ImportPage = () => import("@/pages/import/index");
const ArticleRankingPage = () => import("@/pages/article-ranking/index");
const ArticleDetailPage = () => import("@/pages/article-detail/index");
const ArticlePageCreate = () => import("@/pages/article-create/index");
/**
* 公开路由表
*/
const publicRoutes = [
{
path: "/login",
name: "LoginLayOut",
component: LoginLayOut,
children: [
{
path: "",
name: "LoginPage",
component: LoginPage,
},
],
},
{
path: "/",
// 注意:带有路径“/”的记录中的组件“默认”是一个不返回 Promise 的函数
component: layout,
redirect: "/profile",
children: [
{
path: "/profile",
name: "profile",
component: ProfilePage,
meta: {
title: "profile",
icon: "el-icon-user",
},
},
{
path: "/404",
name: "404",
component: Page404,
},
{
path: "/401",
name: "401",
component: Page401,
},
],
},
];
/**
* 私有路由表
*/
const privateRoutes = [
{
path: "/user",
component: layout,
redirect: "/user/manage",
meta: {
title: "user",
icon: "personnel",
},
children: [
{
path: "/user/manage",
component: UserManagePage,
meta: {
title: "userManage",
icon: "personnel-manage",
},
},
{
path: "/user/role",
component: RolePage,
meta: {
title: "roleList",
icon: "role",
},
},
{
path: "/user/permission",
component: PermissionPage,
meta: {
title: "permissionList",
icon: "permission",
},
},
{
path: "/user/info/:id",
name: "userInfo",
component: UserInfoPage,
meta: {
title: "userInfo",
},
},
{
path: "/user/import",
name: "import",
component: ImportPage,
meta: {
title: "excelImport",
},
},
],
},
{
path: "/article",
component: layout,
redirect: "/article/ranking",
meta: {
title: "article",
icon: "article",
},
children: [
{
path: "/article/ranking",
component: ArticleRankingPage,
meta: {
title: "articleRanking",
icon: "article-ranking",
},
},
{
path: "/article/:id",
component: ArticleDetailPage,
meta: {
title: "articleDetail",
},
},
{
path: "/article/create",
component: ArticlePageCreate,
meta: {
title: "articleCreate",
icon: "article-create",
},
},
{
path: "/article/editor/:id",
component: ArticlePageCreate,
meta: {
title: "articleEditor",
},
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes: [...publicRoutes, ...privateRoutes],
});
export default router;
创建筛选文件
获取全部的路由
js
import { useRouter } from "vue-router";
const allRouter = useRouter();
console.log(allRouter.getRoutes());
这样获取到全部的路由 里面有一级也有二级,但这样不是我们需要的所以需要过滤
过滤路由
在 utils/route.js 文件
安装 path-browserify
bash
npm install path-browserify
js
import path from "path";
/* 查找出所有子路由 */
const getChildrenRoutes = (routes) => {
const result = [];
routes.forEach((route) => {
if (route.children && route.children.length > 0) {
result.push(...route.children);
}
});
return result;
};
/**
* 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级
* @param {*} routes router.getRoutes()
* return 筛除掉了所有子路由,保留结构
*/
export const filterRouters = (routes) => {
const childrenRoutes = getChildrenRoutes(routes);
return routes.filter((route) => {
return !childrenRoutes.find((childrenRoute) => {
return childrenRoute.path === route.path;
});
});
};
/**
* 判断数据是否为空值
*/
function isNull(data) {
if (!data) return true;
if (JSON.stringify(data) === "{}") return true;
if (JSON.stringify(data) === "[]") return true;
return false;
}
/**
* 重点函数
* 根据 routes 数据,返回对应 menu 规则数组
*/
export function generateMenus(routes, basePath = "") {
const result = [];
// 遍历路由表
routes.forEach((item) => {
// 不存在 children && 不存在 meta 直接 return
if (isNull(item.meta) && isNull(item.children)) return;
// 存在 children 不存在 meta,进入迭代
if (isNull(item.meta) && !isNull(item.children)) {
result.push(...generateMenus(item.children));
return;
}
// 合并 path 作为跳转路径
const routePath = path.resolve(basePath, item.path);
// 路由分离之后,存在同名父路由的情况,需要单独处理
let route = result.find((item) => item.path === routePath);
if (!route) {
route = {
...item,
path: routePath,
children: [],
};
// icon 与 title 必须全部存在
if (route.meta.icon && route.meta.title) {
// meta 存在生成 route 对象,放入 arr
result.push(route);
}
}
// 存在 children 进入迭代到children
if (item.children) {
route.children.push(...generateMenus(item.children, route.path));
}
});
return result;
}
export default generateMenus;
使用
修改 SideBarMenu/index.vue
vue
<template>
<el-menu
:uniqueOpened="true"
default-active="2"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<SideBarItem
v-for="item in routes"
:key="item.path"
:route="item"
></SideBarItem>
</el-menu>
</template>
<script setup>
import { filterRouters, generateMenus } from "@/utils/route";
const router = useRouter();
const routes = computed(() => {
const filterRoutes = filterRouters(router.getRoutes());
return generateMenus(filterRoutes);
});
console.log(routes.value);
</script>
<style lang="scss" scoped></style>
修改 SideBarItem/index.vue
vue
<template>
<!-- 支持渲染多级 menu 菜单 -->
<el-sub-menu v-if="route.children.length > 0" :index="route.path">
<template #title>
<SvgIcon :icon="route.meta.icon" size="16"></SvgIcon>
<span class="title ml">{{ route.meta.title }}</span>
</template>
<!-- 循环渲染 -->
<el-menu-item
v-for="(item, index) in route.children"
:key="item.path"
:index="item.path"
>
<template #title>
<SvgIcon :icon="item.meta.icon" size="16"></SvgIcon>
<span class="title ml">{{ item.meta.title }}</span>
</template>
</el-menu-item>
</el-sub-menu>
<!-- 渲染 item 项 -->
<el-menu-item v-else :index="route.path">
<SvgIcon :icon="route.meta.icon" size="16"></SvgIcon>
<template #title>
<span :class="[useCollapse().sidebarOpened ? 'title' : 'title ml']">{{
route.meta.title
}}</span>
</template>
</el-menu-item>
</template>
<script setup>
import { useCollapse } from "@/stores/sidebaropen";
// 定义 props
defineProps({
route: {
type: Object,
required: true,
},
});
</script>
<style lang="scss">
.ml {
margin-left: 10px;
}
</style>