1 需求
- 管理系统“菜单”由后端接口返回,前端需要根据后端返回的“菜单”数组,构造路由,渲染侧栏菜单
- 有些菜单是子菜单,有对应的路由,但是不在侧栏显示(比如一些详情页面) 注:这里的“菜单”,不是字面意思的菜单,即可以是菜单也可以是按钮,如果是菜单则对应路由
2 分析
一般我们“菜单“保存在数据库时,不会仅仅保存路由相关参数,还会增加一下自定义参数,类似下面的数据结构:
const menu = {id: "1",pid: "-1", // 父idnameCn: "平台管理", //中文名type: "menu", //类别:menu 菜单,button 按钮icon: "platform", //图标routeName: "platform", //路由名routePath: "/platform", //路由地址routeLevel: 1, //路由级别: 一级路由(左侧显示) 二级路由(左侧不显示)componentPath: "", //组件路径sort: 1, //排序children: [],
};
所以需要手动构造路由,渲染侧栏菜单
需要注意的地方:
- 通过设置 routeLevel 字段来判断当前“菜单“是否在左侧菜单显示
- 添加 el-menu 的 router 属性,这样可以点击 menu 自动跳转到 index 绑定的路径,否则需要绑定 click 事件,进行手动跳转(适用于需要跳转外链的情况,需要再递归菜单时进行判断,不给index赋值path)
- 如果当前“菜单“存在 children,但是 children 所有 child 是二级路由并且 path 没有以“/”开头(如下面路由中的硬件管理),那么需要将一个 Empty.vue 组件指向当前“菜单“,并且创建一个 path 为空的 child 来匹配当前“菜单“,否则二级路由无法跳转(无法跳转指的是是当前“菜单“组件路径直接配置目的组件路径)
- 如果当前“菜单“存在 children,但是 children 中有 child 的 path 没有以“/”开头,在构造左侧菜单时,需要拼接祖先 path 后再赋值给 index,否则无法跳转 这里使用 router.getRoutes()返回的所有路由,并且使用路由 name 匹配,这样比自己递归拼接方便,如下面 MenuItem.vue 组件中使用的 processRoutePath 函数
3 实现
效果图:
3.1 目录结构
3.2 代码:
components/SvgIcon.vue
<template><svgaria-hidden="true":class="svgClass":style="{ width: size, height: size }"><use :xlink:href="symbolId" :fill="color" /></svg>
</template><script setup lang="ts">
const props = defineProps({prefix: {type: String,default: "icon",},name: {type: String,required: false,default: "",},color: {type: String,default: "",},size: {type: String,default: "1em",},className: {type: String,default: "",},
});const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const svgClass = computed(() => {if (props.className) {return `svg-icon ${props.className}`;}return "svg-icon";
});
</script><style scoped>
.svg-icon {display: inline-block;width: 1em;height: 1em;overflow: hidden;vertical-align: -0.15em;outline: none;fill: currentcolor;
}
</style>
layout/index.vue
<template><div class="container"><div class="aside"><el-scrollbar height="100%"><el-menu:default-active="activeIndex":ellipsis="false":mode="'vertical'":collapse="false"background-color="#545c64"text-color="#fff"active-text-color="#ffd04b"router><template v-for="item in menus" :key="item.id"><MenuItem :menu="item"></MenuItem></template></el-menu></el-scrollbar></div><div class="content"><RouterView></RouterView></div></div>
</template>
<script setup lang="ts">
import MenuItem from "./components/MenuItem.vue";
import { menus } from "../router/index";const route = useRoute();
const activeIndex = ref<string>(route.path);
</script>
<style lang="scss">
.container {width: 100%;height: 100%;display: flex;.aside {width: 300px;height: 100%;background-color: #545c64;}.content {flex: 1;}
}
</style>
layout/components/MenuItem.vue
<template><!-- 不存在children --><template v-if="!menu.children?.length"><el-menu-itemv-if="menu.type === 'menu' && menu.routeLevel === 1":index="processRoutePath(menu)"><template #title><SvgIcon :name="menu.icon" class="icon"></SvgIcon><span>{{ menu.nameCn }}</span></template></el-menu-item></template><!-- 存在children --><template v-else><el-sub-menuv-if="menu.children.some((c: any) => c.type === 'menu' && c.routeLevel === 1)":index="menu.routePath"><template #title><SvgIcon :name="menu.icon" class="icon"></SvgIcon><span>{{ menu.nameCn }}</span></template><template v-for="item in menu.children" :key="item.id"><MenuItem :menu="item"></MenuItem></template></el-sub-menu><el-menu-itemv-else-if="menu.type === 'menu' && menu.routeLevel === 1":index="processRoutePath(menu)"><template #title><SvgIcon :name="menu.icon" class="icon"></SvgIcon><span>{{ menu.nameCn }}</span></template></el-menu-item></template>
</template><script lang="ts" setup>
import MenuItem from "./MenuItem.vue";
import SvgIcon from "../../components/SvgIcon.vue";const props = defineProps({menu: {type: Object,required: true,},
});const router = useRouter();
const routes = router.getRoutes();
<!-- 用于处理子路由path没有以/开头的情况 -->
const processRoutePath = (item) => {for (let route of routes) {if (route.name === item.routeName) {return route.path;}}return item.routePath;
};
</script>
<style lang="scss">
.icon {margin-right: 10px;
}
</style>
router/index.ts
import { createRouter, createWebHistory } from "vue-router";const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: "/",name: "index",component: () => import("../layout/index.vue"),meta: {title: "index",sort: 1,},},],// 刷新时,滚动条位置还原scrollBehavior: () => ({ left: 0, top: 0 }),
});export const menus = [{id: "1",pid: "-1", // 父idnameCn: "平台管理", //中文名type: "menu", //类别:menu 菜单,button 按钮icon: "platform", //图标routeName: "platform", //路由名routePath: "/platform", //路由地址routeLevel: 1, //路由级别: 一级路由(左侧显示) 二级路由(左侧不显示)componentPath: "", //组件路径sort: 1, //排序children: [{id: "1-1",nameCn: "角色管理",permission: "",type: "menu",pid: "1",icon: "role",iconColor: "",routeName: "role",routePath: "role",routeLevel: 1,componentPath: "/platform/role/index.vue",sort: 1,children: [{id: "1-1-1",nameCn: "新增",permission: "account:add",type: "button",pid: "1-1",icon: "user",iconColor: "",routeName: "",routePath: "",routeLevel: null,componentPath: "",sort: 1,children: [],},],},{id: "1-2",nameCn: "账户管理",permission: "",type: "menu",pid: "1",icon: "user",iconColor: "",routeName: "account",routePath: "account",routeLevel: 1,componentPath: "/platform/account/index.vue",sort: 2,children: [],},],},{id: "2",pid: "-1",nameCn: "资产管理",type: "menu",icon: "property",routeName: "",routePath: "/property",routeLevel: 1,componentPath: "",sort: 2,children: [{id: "2-1",pid: "2",nameCn: "文档管理",permission: "",type: "menu",icon: "document",iconColor: "",routeName: "document",routePath: "document",routeLevel: 1,componentPath: "/property/document/index.vue",sort: 1,children: [],},{id: "2-2",pid: "2",nameCn: "硬件管理",permission: null,type: "menu",icon: "hardware",iconColor: "",routeName: "",routePath: "hardware",routeLevel: 1,componentPath: "/property/layout/Empty.vue",sort: 2,children: [{id: "2-2-1",pid: "2-2",nameCn: "硬件管理",permission: null,type: "menu",icon: "",routeName: "hardware",routePath: "",routeLevel: 2,componentPath: "/property/hardware/index.vue",sort: 1,children: [],},{id: "2-2-2",pid: "2-2",nameCn: "硬件配置",permission: null,type: "menu",icon: "",routeName: "config",routePath: "config",routeLevel: 2,componentPath: "/property/hardware/Config.vue",sort: 2,children: [],},],},],},
];const initRouter = (routerTree: any) => {const routerArr: any = [];routerTree.forEach((item: any) => {if (item.type === "menu") {routerArr.push({meta: { title: item.nameCn },name: item.routeName || "",path: item.routePath || "",component: item.componentPath? () => import(/* @vite-ignore */ "../view" + item.componentPath): "",children: item.children ? initRouter(item.children) : [],});}});return routerArr;
};const routers = initRouter(menus);
routers.forEach((item: any) => {router.addRoute("index", item);
});console.log(router.getRoutes());export default router;
view/platform/account/index.vue
<template>账户管理</template>
view/platform/role/index.vue
<template>角色管理</template>
view/property/layout/Empty.vue
<template><router-view />
</template>
view/property/hardware/index.vue
<template><div>硬件管理</div><el-button type="primary" @click="handleCilck">硬件配置</el-button>
</template>
<script setup lang="ts">
const router = useRouter();
const handleCilck = () => {router.push({name: "config",});
};
</script>
view/property/hardware/Config.vue
<template>硬件配置</template>
view/property/document/index.vue
<template>文档管理</template>