Vue2集成ElementUI实现左侧菜单导航功能
作者:冉成未来
简介
在开发后台系统时,通过菜单进行导航是非常重要的一件事情,在前端开发过程中使用vue2+elementui可以快速搭建菜单导航,本文主要记录两个菜单的生成方式,通过在前端router/index.js中直接进行配置,后端返回菜单数据进行对应,可以通过后端返回的菜单数据控制权限;另一种是部门静态导航,然后再拼接动态导航,生成完成页面导航。
静态导航
安装element-ui,vue-router,vuex
npm install elementui --S npm install vue-router@3 --S npm install vuex --S
编写router/index.js
router/index.js
import Vue from 'vue'; import Router from 'vue-router'; Vue.use(Router); const router = new Router({ routes: [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), children: [ { path: '/index', name: 'Index', component: () => import('@/views/Index.vue'), }, { path: '/documents/note', name: 'NoteManagement', component: () => import('@/views/NoteManagement.vue'), }, { path: '/documents/file', name: 'FileManagement', component: () => import('@/views/FileManagement.vue'), }, { path: '/documents/newMarkdown', name: 'NewDocument', component: () => import('@/components/RichTextEditor.vue'), // 新增路由指向RichTextEditor.vue }, { path: '/documents/newWord', name: 'NewWord', component: () => import('@/components/WordEditor.vue'), }, { path: '/documents/newExcel', name: 'NewExcel', component: () => import('@/components/ExcelEditor.vue'), }, { path: '/system/user', name: 'UserManagement', component: () => import('@/views/UserManagement.vue'), }, { path: '/system/menu', name: 'MenuManagement', component: () => import('@/views/MenuManagement.vue'), }, { path: '/system/role', name: 'RoleManagement', component: () => import('@/views/RoleManagement.vue'), }, { path: 'system/company', name: 'CompanyManagement', component: () => import('@/views/CompanyManagement.vue'), }, { path: '/system/dept', name: 'DeptManagement', component: () => import('@/views/DeptManagement.vue'), }, { path: '/target', name: 'TargetManagement', component: () => import('@/views/TargetManage.vue'), }, { path: '/targetTask', name: 'TargetTask', component: () => import('@/views/TargetTask.vue'), // 新增路由指向MonthTask.vue }, { path: '/dayTask', name: 'DayTask', component: () => import('@/views/DayTask.vue'), // 新增路由指向DayTask.vue } ] }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), }, { path: '/register', name: 'Register', component: () => import('@/views/Register.vue'), // 更新注册路由 }, ], }); // 导航守卫 // 使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆 router.beforeEach((to, from, next) => { if (to.path === '/login') { next(); } else { let token = localStorage.getItem('Authorization'); if (token === null || token === '') { next('/login'); } else { next(); } } }); export default router;
main.js中引入elementui,router
main.js
import Vue from 'vue' import App from './App.vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import store from './store' import router from './router/index' // import mavonEditor from 'mavon-editor' // import 'mavon-editor/dist/css/index.css'; // import mermaidItMarkdown from 'mermaid-it-markdown' // mavonEditor.mavonEditor.getMarkdownIt().use(mermaidItMarkdown) // Vue.use(mavonEditor) Vue.config.productionTip = false Vue.use(ElementUI) new Vue({ store, router, render: h => h(App), }).$mount('#app')
编写左侧导航
<!-- 第二部分:导航栏和内容显示区域 --> <div class="main-content"> <el-menu :default-active="activeMenu" @select="handleMenuSelect" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" :collapse="isCollapse" > <el-menu-item v-for="item in filteredMenuItems" :key="item.menuPath" :index="item.menuPath" > <i :class="item.menuIcon"></i> <span slot="title">{{ item.menuName }}</span> </el-menu-item> <el-submenu v-for="item in menuItemsWithChildren" :key="item.menuPath" :index="item.menuPath" > <template #title> <i :class="item.menuIcon"></i> <span slot="title">{{ item.menuName }}</span> </template> <el-menu-item v-for="child in item.children" :key="child.menuPath" :index="child.menuPath" > <i :class="child.menuIcon"></i> <span slot="title">{{ child.menuName }}</span> </el-menu-item> </el-submenu> </el-menu> //路由出口 <div class="content"> <router-view></router-view> </div> </div>
返回的菜单数据
菜单数据时根据用户id请求后端菜单权限后返回的菜单数据
动态导航
安装vue-router、elementui步骤与静态导航相同
编写router/index.js
import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); const constantRoutes = [ { path: "/login", name: "Login", component: () => import("@/views/Login.vue") }, { path: "/home", name: "Home", component: () => import("@/views/Home.vue"), children: [] }, // { // path: "*", // name: "NotFound", // component: () => import("@/views/NotFound.vue") // }, ] const createRouter = () => new VueRouter({ mode: "hash", routes: constantRoutes }) const router = createRouter(); //路由重置方法 export function resetRouter() { const newRouter = createRouter(); router.matcher = newRouter.matcher; // 重置路由 } //动态加载路由方法 export const addDynamicRoutes = (menus) => { debugger; const routes = []; //1.转换菜单为路由配置 const asyncRoutes = coverMenusToRoutes1(routes,menus); //2. 添加嵌套路由到Home asyncRoutes.forEach(route => { // debugger; // if (route.name !== '') { // router.addRoute("Home", route); // } router.addRoute("Home", route); }); } //菜单转换路由方法 const coverMenusToRoutes = (menus) => { if (!menus) return []; const routes = []; menus.forEach(menu => { const route = { path: menu.path, name: menu.path.slice(1), meta: {title: menu.name,icon: menu.icon}, component: resolveComponent(menu.component), }; if (menu.children && menu.children.length > 0) { route.children = coverMenusToRoutes(menu.children); } routes.push(route); }) return routes; } const coverMenusToRoutes1 = (routes,menus) => { if (!menus) return []; // const routes = []; menus.forEach(menu => { if (menu.component.length > 0){ const route = { path: menu.path, name: menu.path.slice(1), meta: {title: menu.name,icon: menu.icon}, component: resolveComponent(menu.component), }; routes.push(route); } if (menu.children && menu.children.length > 0) { coverMenusToRoutes1(routes,menu.children); } }) return routes; } //动态解析组件路由 function resolveComponent(component) { if (!component) return undefined; return () => import(`@/views/${component}`); } // 导航守卫 // 使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆 router.beforeEach((to, from, next) => { if (to.path === '/login') { next(); } else { let token = localStorage.getItem('token'); if (token === null || token === '') { next('/login'); } else { next(); } } }); export default router;
动态导航需要特别注意路径问题,如果路径不正确会导致菜单无法正常显示,因为在项目中返回的菜单数据时树形结构,在处理菜单数据时如果按树形结构嵌套再添加到Home路由的children列表中,菜单无法正常的显示,后面修改了处理逻辑,把有组件的菜单添加到Home路由的children列表后,菜单可以正常显示,需要特别注意
左侧菜单
通过for循环生成
<template> <el-menu :default-active="defaultActive" class="el-menu" @open="handleOpen" @close="handleClose" @select="handleSelect" :collapse="isCollapse" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" > <span><i :class="collapseClass" @click="changeMenu"></i></span> <template v-for="item in menuList"> <el-submenu v-if="item.children && item.children.length" :index="item.path" :key="item.id" > <template slot="title"> <i :class="item.icon"></i> <span slot="title">{{ item.name }}</span> </template> <template v-for="child in item.children"> <el-submenu v-if="child.children && child.children.length" :index="child.path" :key="child.id"> <template slot="title"> <i :class="child.icon"></i> <span slot="title">{{child.name}}</span> </template> <el-menu-item v-for="ch in child.children" :index="ch.path" :key="ch.id"> <i :class="ch.icon"></i> <span slot="title">{{ch.name}}</span> </el-menu-item> </el-submenu> <!-- v-for="child in item.children" --> <el-menu-item v-else :index="child.path" :key="child.id" > <i :class="child.icon"></i> <span slot="title">{{ child.name }}</span> </el-menu-item> </template> </el-submenu> <el-menu-item v-else :index="item.path" :key="item.id"> <i :class="item.icon"></i> <span slot="title">{{ item.name }}</span> </el-menu-item> </template> </el-menu> </template> <script> export default { data() { return { collapseClass: "el-icon-s-fold", isCollapse: false, defaultActive: "1-4-1", menuList: [], }; }, mounted() { //获取动态菜单 this.createMenuList(); }, methods: { createMenuList() { console.log(this.$store.state.menus); this.menuList = this.$store.state.menus; }, handleOpen(key, keyPath) { console.log(key, keyPath); }, handleClose(key, keyPath) { console.log(key, keyPath); }, changeMenu() { this.isCollapse = !this.isCollapse; if (this.isCollapse) { this.collapseClass = "el-icon-s-unfold"; } else { this.collapseClass = "el-icon-s-fold"; } }, handleSelect(index) { this.activeMenu = index; if (this.$route.path !== index) { // 检查当前路径是否与目标路径相同 this.$router.push(index); } }, }, }; </script> <style> .el-menu:not(.el-menu--collapse) { width: 220px; /* height: 100vh; */ overflow: hidden; } .el-menu { width: 60px; /* height: 100vh; */ overflow: hidden; } </style>
通过for循环+递归生成
菜单生成子组件
<template> <el-menu :default-active="defaultActive" class="el-menu" @open="handleOpen" @close="handleClose" @select="handleSelect" :collapse="isCollapse" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" > <template v-for="item in menuList"> <el-submenu v-if="item.children && item.children.length" :index="item.path" :key="item.id"> <template slot="title"> <i :class="item.icon"></i> <span slot="title">{{item.name}}</span> </template> <!-- 递归调用 --> <AsideMenu :menuList="item.children"></AsideMenu> </el-submenu> <el-menu-item v-else :index="item.path" :key="item.id"> <i :class="item.icon"></i> <span slot="title">{{item.name}}</span> </el-menu-item> </template> </el-menu> </template> <script> export default { name: "AsideMenu", //name必须要有,要和递归调用的名称保持一致 components: { }, props: { menuList: [], // eslint-disable-next-line vue/require-prop-type-constructor isCollapse: false, }, data() { return { // collapseClass: "el-icon-s-fold", // isCollapse: false, defaultActive: "1-4-1", }; }, methods: { handleOpen(key, keyPath) { console.log(key, keyPath); }, handleClose(key, keyPath) { console.log(key, keyPath); }, handleSelect(index) { debugger; this.activeMenu = index; console.log(index); console.log(this.$route); console.log(this.$router.getRoutes()); if (this.$route.path !== index) { // 检查当前路径是否与目标路径相同 this.$router.push(index); } }, }, }; </script> <style> .el-menu:not(.el-menu--collapse) { width: 220px; /* height: 100vh; */ overflow: hidden; border-right: none; /* 隐藏右侧的边框 */ } .el-menu { width: 60px; /* height: 100vh; */ overflow: hidden; border-right: none; } </style>
子组件名称是必须要有的,递归调用时按照名称进行递归调用。在这个项目中子组件名称为:AsideMenu,递归调用时使用 AsideMenu来引用自身
调用菜单生成的父组件
<template> <div class="sidebar"> <span><i :class="collapseClass" @click="changeMenu"></i></span> <!-- 调用菜单生成组件 --> <aside-menu :menuList="menuList" :isCollapse="isCollapse" /> </div> </template> <script> import AsideMenu from '@/components/AsideMenu.vue'; export default { components: { AsideMenu }, data() { return { collapseClass: "el-icon-s-fold", isCollapse: false, menuList: [] }; }, mounted() { //获取动态菜单 this.createMenuList(); }, methods: { createMenuList() { console.log(this.$store.state.menus); this.menuList = this.$store.state.menus; }, changeMenu() { this.isCollapse = !this.isCollapse; if (this.isCollapse) { this.collapseClass = "el-icon-s-unfold"; } else { this.collapseClass = "el-icon-s-fold"; } }, } }; </script> <style scoped> .sidebar { /* border: 1px solid red; */ background-color:#545c64; } </style>
store/index.js
使用vuex保存用户id,token,菜单列表,权限信息,角色信息
import Vue from "vue"; import vuex from "vuex"; Vue.use(vuex); const store = new vuex.Store({ state: { //用户id userId: {}, //用户token token: "", //用户角色 role: "", //用户权限 permission: "", //用户菜单 menus: [], //用户路由 }, getters: { //获取用户id getUserId(state) { return state.userId; }, //获取用户token getToken(state) { return state.token; }, //获取用户角色 getRole(state) { return state.role; }, //获取用户权限 getPermission(state) { return state.permission; }, //获取用户菜单 getMenus(state) { return state.menus; }, }, mutations: { //设置用户id setUserId(state, userId) { state.userId = userId; localStorage.setItem("userId", userId); }, //设置用户token setToken(state, token) { state.token = token; localStorage.setItem("token", token); }, //设置用户角色 setRole(state, role) { state.role = role; }, //设置用户权限 setPermission(state, permission) { state.permission = permission; }, //设置用户菜单 setMenus(state, menus) { state.menus = menus; localStorage.setItem("menus", menus); }, } }) export default store;
main.js中引入store
import Vue from 'vue' import App from './App.vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import router from './router' import store from './store' Vue.use(ElementUI) Vue.config.productionTip = false new Vue({ store, router, render: h => h(App), }).$mount('#app')
登录页面代码
登录页面存储用户信息、菜单信息、并动态加载路由
<!-- eslint-disable vue/multi-word-component-names --> <template> <el-row type="flex" justify="center" align="middle" style="height: 100vh;"> <el-col :xs="24" :sm="12" :md="8" :lg="6"> <el-card class="box-card"> <div class="clearfix"> <img src="@/assets/logo.png" class="logo" /> <h2>欢迎登录</h2> </div> <el-form :model="loginForm" ref="loginForm" :rules="loginRules" label-width="100px"> <el-form-item label="用户名" prop="username"> <el-input v-model="loginForm.username" prefix-icon="el-icon-user" autocomplete="off"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input type="password" v-model="loginForm.password" prefix-icon="el-icon-lock" show-password autocomplete="off"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">登录</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> </el-card> </el-col> </el-row> </template> <script> import { Message } from 'element-ui'; import http from '../request/http' import { resetRouter,addDynamicRoutes } from '@/router/index' export default { data() { return { loginForm: { username: '', password: '' }, loginRules: { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }] } }; }, methods: { submitForm() { this.$refs.loginForm.validate((valid) => { if (valid) { // 这里可以添加提交到服务器的逻辑 http.post('/user/login',this.loginForm,{ headers: { "Content-Type": "application/json;charset=UTF-8", }, }).then((res) => { if (res.data.code === 200) { // 登录成功,可以进行后续操作,如跳转到主页 //保存用户id、token、菜单列表 // console.log(res.data.data); const userId = res.data.data.user.id; const token = res.data.data.user.token; this.$store.commit('setUserId', userId); this.$store.commit('setToken', token); this.$store.commit('setMenus', res.data.data.menus); //创建动态路由 //1.重置路由 resetRouter(); //2.添加动态路由 const menus = res.data.data.menus; addDynamicRoutes(menus); this.$router.push('/home'); } else { // 登录失败,可以提示错误信息 Message.error(res.msg); } }); } else { console.log('表单验证失败!'); return false; } }); }, resetForm() { this.$refs.loginForm.resetFields(); } } }; </script> <style scoped> .box-card { /* border: 1px solid red; */ width: 100%; /* 或者具体宽度 */ border-radius: 10px; /* 圆角 */ box-shadow: 0 0 10px rgba(0,0,0,0.1); /* 阴影 */ background-color:aliceblue; } .el-row { background-image: url('../assets/pic01.jpg'); background-size: cover; background-position: center; background-repeat: no-repeat; } .logo { width: 80px; height: 80px; } </style>
菜单返回数据
{ "code": 200, "message": "请求成功", "data": { "menus": [ { "id": "1913479834787434497", "parentId": null, "name": "系统管理", "path": "", "component": "", "perms": null, "type": 1, "icon": "el-icon-s-tools", "orderNum": 0, "visible": false, "createTime": null, "updateTime": null, "rf1": null, "rf2": null, "rf3": null, "rf4": null, "rf5": null, "children": [ { "id": "1913484019050274818", "parentId": "1913479834787434497", "name": "菜单管理", "path": "/menu", "component": "MenuManage.vue", "perms": null, "type": 1, "icon": "el-icon-menu", "orderNum": 0, "visible": false, "createTime": null, "updateTime": null, "rf1": "系统管理", "rf2": null, "rf3": null, "rf4": null, "rf5": null, "children": null }, { "id": "1913488214084083714", "parentId": "1913479834787434497", "name": "二级菜单", "path": "", "component": "", "perms": null, "type": 1, "icon": "el-icon-location", "orderNum": 1, "visible": false, "createTime": null, "updateTime": null, "rf1": "系统管理", "rf2": null, "rf3": null, "rf4": null, "rf5": null, "children": [ { "id": "1913488799369846786", "parentId": "1913488214084083714", "name": "三级菜单", "path": "/san", "component": "SanManage.vue", "perms": null, "type": 1, "icon": "el-icon-star-on", "orderNum": 0, "visible": false, "createTime": null, "updateTime": null, "rf1": "二级菜单", "rf2": null, "rf3": null, "rf4": null, "rf5": null, "children": null } ] } ] } ], "user": { "id": "123456", "username": "admin", "password": "$2a$10$0uPlhwgy.OlkV20pRJ/9Wu8OJ61OfbcMqMXf60qI4qsahlxJD4iUq", "nickname": "wangcheng", "avatar": null, "email": null, "mobile": null, "status": 1, "deptId": null, "createTime": null, "updateTime": null, "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIzMWMwOTgzNzQ2MTQ0ZGNiYmJhZTgwZmJhYzNkNWFjMSIsImlhdCI6MTc0NTE5NjAxNCwic3ViIjoiMTIzNDU2IiwiZXhwIjoxNzQ1ODAwODE0fQ.OyZaKaHRfu0PfMSubrnU1qLDOQHfisdQkTvByCMeIes", "roles": null } } }
总结
项目开发过程中动态菜单生成、动态路由的正确配置是困难点。
动态菜单的递归调用
递归调用最主要的是对自身的调用,要保持名称和调用自身组件的一致性。
动态路由
动态路由要注意路径问题,不要因为菜单返回树形结构,后期处理的路由也是树形结构,造成子路由里面多层嵌套,无法正常渲染菜单。
记录菜单处理的另外一种方式
MenuTreeOne.vue
<template> <div class="custom-menu-tree"> <template v-for="item in menuList"> <el-submenu v-if="item.children && item.children.length" :index="item.path" :key="item.id" > <template slot="title"> <i :class="item.icon"></i> <span slot="title">{{ item.name }}</span> </template> <!-- 递归调用 --> <AsideMenuOne :menuList="item.children"></AsideMenuOne> </el-submenu> <el-menu-item v-else :index="item.path" :key="item.id"> <i :class="item.icon"></i> <span slot="title">{{ item.name }}</span> </el-menu-item> </template> </div> </template> <script> export default { name: "AsideMenuOne", props: { menuList: [], }, data() { return {}; }, mounted() {}, methods: {}, }; </script> <style scoped> .custom-menu-tree { height: 100%; display: flex; flex-direction: column; } </style>
AsideMenuOne.vue
<template> <el-menu :default-active="defaultActive" class="el-menu" @open="handleOpen" @close="handleClose" @select="handleSelect" :collapse="isCollapse" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" > <span><i :class="collapseClass" @click="changeMenu"></i></span> <menu-tree-one :menuList="menuList"></menu-tree-one> </el-menu> </template> <script> import MenuTreeOne from '@/components/MenuTreeOne.vue'; export default { components: { MenuTreeOne }, data() { return { collapseClass: "el-icon-s-fold", isCollapse: false, defaultActive: "1-4-1", menuList: [], }; }, mounted() { //获取动态菜单 this.createMenuList(); }, methods: { createMenuList() { console.log(this.$store.state.menus); this.menuList = this.$store.state.menus; }, handleOpen(key, keyPath) { console.log(key, keyPath); }, handleClose(key, keyPath) { console.log(key, keyPath); }, changeMenu() { this.isCollapse = !this.isCollapse; if (this.isCollapse) { this.collapseClass = "el-icon-s-unfold"; } else { this.collapseClass = "el-icon-s-fold"; } }, handleSelect(index) { this.activeMenu = index; if (this.$route.path !== index) { // 检查当前路径是否与目标路径相同 this.$router.push(index); } }, }, }; </script> <style> .el-menu:not(.el-menu--collapse) { width: 220px; /* height: 100vh; */ overflow: hidden; } .el-menu { width: 60px; /* height: 100vh; */ overflow: hidden; } </style>
到此这篇关于Vue2集成ElementUI实现左侧菜单导航功能的文章就介绍到这了,更多相关Vue ElementUI左侧菜单导航内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!