vue无限下级动态路由菜单
前言
通过vue、elementplus、route实现无限下级动态菜单树
应用场景:实现多级菜单列表,使功能分类更加清晰易懂。
实现原理:通过递归动态注册route路由和elementplus的el-menu组件实现树形菜单。
提示:以下是本篇文章正文内容,下面案例可供参考
一、项目结构
1.菜单结构
2.组件结构
二、vuex动态注册路由
1.vuex动态注册路由
代码如下(示例):
import { defineStore } from "pinia";
import { RouteRecordRaw } from "vue-router";
import { ref } from "vue"
import router, { Layout, Directory } from "@/router";
import { listRoutes } from "@/api/menu";
const modules = import.meta.glob("../../views/**/**.vue");
export const useRouterStore = defineStore("router", () => {
const routes: ref<RouteRecordRaw[]> = [];
const hasRole = ref(false);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = newRoutes;
hasRole.value = true;
}
// 生成动态路由
function generateRoutes() {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
listRoutes().then((res) => {
const routes = recurRoutes(res.data);
routes.forEach(route => {
router.addRoute(route)
})
setRoutes(routes);
resolve(routes);
}).catch((err) => {
console.log('err',err)
})
})
}
// 递归引入路由组件
const recurRoutes = (routes: RouteRecordRaw[]) => {
const newRoute: RouteRecordRaw[] = [];
routes.forEach((route) => {
if (route.component?.toString() == "Layout") {
// 一级目录
route.component = Layout;
} else if (route.component?.toString() == "Directory") {
// 其他目录
route.component = Directory;
} else {
const component = modules[`../../views/${route.component}.vue`];
route.component = component;
}
if (route.children) {
route.children = recurRoutes(route.children);
}
newRoute.push(route);
})
return newRoute;
}
return { routes, setRoutes, generateRoutes };
})
2.动态路由注册
路由静态注册时使用的一下方法,无法在运行时动态注册路由。
// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/',
name: '/',
component: Layout,
children: [
{
path: '/shopForm',
name: 'shopForm',
component: () => import('@/views/shop/shopForm/index.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue')
}
];
可以使用vite的 glob 方法实现动态注册
// 引入vite的
const modules = import.meta.glob("../../views/**/**.vue");
const component = modules[`../../views/${route.component}.vue`];
如果框架搭建异常导致无法使用vite的 glob 方法,可使用以下方法代替。
大致原理为:通过后端返回路由数据中的组件路径,配合前端写上的静态路径拼接,以此达到文件引入的效果。
const component = import('../../views/' + route.component.valueOf() + '.vue');
3.配置路由守卫
在router的index.ts中,配置路由守卫,将vuex中的动态路由数据,与router配置的静态路由合并,形成完整的菜单路由。
/**
* 路由守卫
*/
router.beforeEach(async (to, from, next) => {
let token = localStorage.getItem('token');
if (to.path === '/login') {
// 前往等登录页
next();
} else if (!token) {
// 尚未登录
next({path: "/login"})
} else if (to.path === '/' || to.path === '') {
// 前往首页,添加路由
const routerStore = useRouterStore();
await routerStore.generateRoutes();
next()
} else if (router.getRoutes().length == 3) { // 3为router中配置的静态路由数量,因为动态获取路由列表数量的方法出行问题,所以暂时写成固定的
// 没有添加动态路由
const routerStore = useRouterStore();
await routerStore.generateRoutes();
console.log('router', router.getRoutes())
next({path: to.path})
} else {
// 正常放行
next()
}
})
4.页面刷新后动态路由失效问题
每次页面刷新时,已注册的动态路由和vuex中的动态路由,都会被浏览器清空。
所以路由守卫会判断当前路由列表中的路由数量,是包含了vuex中的动态路由,还是只有router配置的静态路由。
如果路由列表中没有vuex中的动态路由,则调用vuex的方法重新请求后端接口,注册动态路由。
三、树形菜单
1.封装树形菜单组件
实现原理:利用递归的逻辑方法判断当前遍历的数据,是否含有子菜单。
如果是含有子菜单的,那当前遍历的数据属于目录,反之则为菜单。
<script lang="ts" setup>
import MenuTree from "@/components/MenuTree/index.vue";
const props = defineProps({
routes: {
type: Object,
required: true
}
})
</script>
<template>
<!-- 循环遍历菜单 -->
<template v-for="(route, index) in props.routes" :index="index">
<!-- 判断菜单是否还有下级 -->
<el-sub-menu :index="route.path" v-if="route.children">
<!-- 有下级渲染为目录 -->
<template #title>
<svg-icon :icon-class="route.meta.icon" />
<span>{{ route.meta.title }}</span>
</template>
<!-- 递归子集 -->
<menu-tree :routes="route.children"></menu-tree>
</el-sub-menu>
<!-- 没有下级渲染为菜单 -->
<el-menu-item :index="route.path" v-else>
<svg-icon :icon-class="route.meta.icon" />
<span>{{ route.meta.title }}</span>
</el-menu-item>
</template>
</template>
<style lang="scss" scoped>
.el-header {
height: 60px;
background-color: #545c64;
.logo {
height: 60px;
}
h2,.logout {
text-align: center;
height: 80px;
line-height: 60px;
color: #e3e2e2;
}
}
.el-container {
// 侧边栏、头部固定,只滚动内容
height: calc(100vh - 60px);
}
.el-aside {
width: 200px;
.el-menu {
height: calc(100vh - 60px);
width: 200px;
}
}
.svg-icon {
padding: 10px;
}
</style>
2.页面嵌套的原因和解决方案
目录路由地址封装成组件引入,一级目录路由组件为Layout,其他下级目录路由组件为Directory。
注:无限下级路由的关键之一在于下级目录的路由组件页面,该页面中需要再次嵌套router-view,否则路由跳转时下级菜单时会出现页面嵌套现象。
下级目录路由组件跳转文件
<template>
<router-view></router-view>
</template>
<script>
export default {
name: "index"
}
</script>
<style scoped>
</style>
四、数据库表结构
总结
下面将其中的几个关键技术点以及解决方案梳理一下:
- 运行时动态注册路由。可以使用vite的 glob 方法实现,如果没有集成 vite,可以配合后端存储的组件路径,和前端的文件路径拼接达到动态注册路由的效果。
- 页面刷新动态路由失效。vuex存在刷新会失效的情况,所以只能在路由守卫中判断,看当前路由列表是否有注册动态路由,没有的话调用vuex重新将路由注册,以此解决刷新失效问题。
- 页面样式嵌套。一级目录的组件路径封装为Layout,及首页路径;其余的二级、三级等目录的组件,都统一封装为一个另外使用了router-view的页面,通过路由嵌套解决页面嵌套的问题。
- 递归。在实现无限下级动态菜单这个功能中,递归这个方法被多次使用,不管是路由的动态注册,还是el-menu的菜单渲染,甚至后端处理菜单列表时,都用到了递归的逻辑。初次接触递归时会觉得很复杂、难以理解,最简单的方法就是将循环多些几层,通过总结其中的规律来封装方法递归的逻辑。
以上就是今天要讲的内容,本文仅仅简单介绍了使用递归逻辑以及动态路由实现无限下级菜单的方法,可以将后台管理系统的目录更加清晰的划分层级,让用户拥有更好的体验。