动态 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-page404401
- 导入:
import - 权限列表:
permission-list - 个人中心:
profile - 角色列表:
role-list - 用户信息:
user-info - 用户管理:
user-manage
创建路由规则
- 公有路由表 public.ts
ts
const LoginLayOut = () => import("@/layouts/loginLayout.vue");
const LoginPage = () => import("@/pages/login.vue");
const ErrorPage = () => import("@/pages/error.vue");
const commonRoutes = [
{
path: "/login",
name: "loginPage",
component: LoginLayOut,
children: [
{
path: "",
name: "loginContent",
component: LoginPage,
},
],
},
{
path: "/404",
name: "errorPage",
component: ErrorPage,
},
{
path: "/:pathMatch(.*)*",
redirect: "/404",
},
];
export default commonRoutes;- 私有路由表 private.ts
ts
const AdminLayout = () => import("@/layouts/default.vue");
const AdminHomePage = () => import("@/pages/index.vue");
const ArticleCreatePage = () => import("@/pages/articleCreate.vue");
const ArticleDetailPage = () => import("@/pages/articleDetail.vue");
const ArticleRankingPage = () => import("@/pages/articleRanking.vue");
const ArticleImportPage = () => import("@/pages/articleimport.vue");
const PermissionListPage = () => import("@/pages/permissionlist.vue");
const ProfilePage = () => import("@/pages/profile.vue");
const RoleListPage = () => import("@/pages/rolelist.vue");
const UserInfoPage = () => import("@/pages/userinfo.vue");
const UserManagePage = () => import("@/pages/usermanage.vue");
const adminRoutes = [
{
path: "/admin",
name: "adminLayout",
component: AdminLayout,
meta: {
title: "admin",
icon: "setting",
},
children: [
{
path: "",
name: "adminHomePage",
component: AdminHomePage,
meta: {
title: "首页",
icon: "home",
},
},
{
path: "/admin/article",
name: "articlePage",
meta: {
title: "文章管理",
icon: "article",
},
children: [
{
path: "/admin/article/create",
name: "articleCreatePage",
component: ArticleCreatePage,
meta: {
title: "文章创建",
icon: "article",
},
},
{
path: "/admin/article/detail",
name: "articleDetailPage",
component: ArticleDetailPage,
meta: {
title: "文章详情",
},
},
{
path: "/admin/article/ranking",
name: "articleRankingPage",
component: ArticleRankingPage,
meta: {
title: "文章排名",
},
},
{
path: "/admin/article/import",
name: "articleImportPage",
component: ArticleImportPage,
meta: {
title: "文章导入",
},
},
],
},
{
path: "/admin/permissionlist",
name: "permissionListPage",
component: PermissionListPage,
meta: {
title: "权限列表",
},
},
{
path: "/admin/profile",
name: "profilePage",
component: ProfilePage,
meta: {
title: "个人中心",
},
},
{
path: "/admin/rolelist",
name: "roleListPage",
component: RoleListPage,
meta: {
title: "角色列表",
},
},
{
path: "/admin/userinfo",
name: "userInfoPage",
component: UserInfoPage,
meta: {
title: "用户信息",
},
},
{
path: "/admin/usermanage",
name: "userManagePage",
component: UserManagePage,
meta: {
title: "用户管理",
},
},
],
},
];
export default adminRoutes;创建筛选文件
获取全部的路由
js
import { useRouter } from "vue-router";
const allRouter = useRouter();
console.log(allRouter.getRoutes());这样获取到全部的路由 里面有一级也有二级,但这样不是我们需要的所以需要过滤
过滤路由
在 utils/route.js 文件
安装 path-browserify
bash
npm install path-browserify
npm install @types/path-browserify --save-devjs
import path from 'path-browserify'
/* 查找出所有子路由 */
function getChildrenRoutes(routes: any) {
const result: any = []
routes.forEach((route: any) => {
if (route.children && route.children.length > 0) {
result.push(...route.children)
}
})
return result
}
/**
* 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级
* @param {*} routes router.getRoutes()
* return 筛除掉了所有子路由,保留结构
*/
export function filterRouters(routes: any) {
const childrenRoutes: any = getChildrenRoutes(routes)
return routes.filter((route: any) => {
return !childrenRoutes.some((childrenRoute: any) => {
return childrenRoute.path === route.path
})
})
}
/**
* 判断数据是否为空值
*/
function isNull(data: any) {
if (!data) {
return true
}
if (JSON.stringify(data) === '{}') {
return true
}
if (JSON.stringify(data) === '[]') {
return true
}
return false
}
/**
* 重点函数
* 根据 routes 数据,返回对应 menu 规则数组
*/
export function generateMenus(routes: any, basePath = '') {
const result: any = []
// 遍历路由表
routes.forEach((item: any) => {
// 不存在 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, basePath))
return
}
// 合并 path 作为跳转路径
const routePath = path.resolve(basePath, item.path)
// 路由分离之后,存在同名父路由的情况,需要单独处理
let route = result.find((item: any) => 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) {
const childMenus = generateMenus(item.children, route.path)
if (result.includes(route)) {
// 父级已在菜单中,子菜单挂到父级下
route.children.push(...childMenus)
} else {
// 父级缺少 icon/title 未入菜单,子菜单提升一层,避免丢失
result.push(...childMenus)
}
}
})
return result
}
export default generateMenus使用
修改 SideBarMenu/index.vue
vue
<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>
<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>
<style lang="scss" scoped></style>修改 SideBarItem/index.vue
vue
<script setup>
import { useCollapse } from "@/stores/sidebaropen";
// 递归引用自身,支持多级菜单
import SideBarItem from "./index.vue";
// 定义 props
defineProps({
route: {
type: Object,
required: true,
},
});
</script>
<template>
<!-- 支持渲染多级 menu 菜单 -->
<el-sub-menu
v-if="route.children && 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>
<!-- 递归渲染子项,有几层渲染几层 -->
<SideBarItem
v-for="item in route.children"
:key="item.path"
:route="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>
<style lang="scss">
.ml {
margin-left: 10px;
}
</style>修改 stores/sidebaropen.js
ts
import { defineStore } from "pinia";
import { ref } from "vue";
export const useCollapse = defineStore("sidebar", () => {
// 侧边栏是否展开
const sidebarOpened = ref(true);
// 切换侧边栏展开/收起
const toggleSidebar = () => {
sidebarOpened.value = !sidebarOpened.value;
};
return {
sidebarOpened,
toggleSidebar,
};
});