当前位置: 首页 > news >正文

Vue2集成ElementUI实现左侧菜单导航

文章目录

  • 简介
  • 静态导航
    • 安装element-ui,vue-router,vuex
    • 编写router/index.js
    • main.js中引入elementui,router
    • 编写左侧导航
    • 返回的菜单数据
  • 动态导航
    • 编写router/index.js
    • 左侧菜单
      • 通过for循环生成
      • 通过for循环+递归生成
    • store/index.js
    • main.js中引入store
    • 登录页面代码
    • 菜单返回数据
  • 总结
    • 动态菜单的递归调用
    • 动态路由
  • 记录菜单处理的另外一种方式

简介

在开发后台系统时,通过菜单进行导航是非常重要的一件事情,在前端开发过程中使用vue2+elementui可以快速搭建菜单导航,本文主要记录两个菜单的生成方式,通过在前端router/index.js中直接进行配置,后端返回菜单数据进行对应,可以通过后端返回的菜单数据控制权限;另一种是部门静态导航,然后再拼接动态导航,生成完成页面导航。

静态导航

安装element-ui,vue-router,vuex

npm install elementui --Snpm install vue-router@3 --Snpm 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 = falseVue.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-itemv-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-submenuv-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-itemv-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. 添加嵌套路由到HomeasyncRoutes.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-submenuv-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-constructorisCollapse: 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: {//用户iduserId: {},//用户tokentoken: "",//用户角色role: "",//用户权限permission: "",//用户菜单menus: [],//用户路由},getters: {//获取用户idgetUserId(state) {return state.userId;},//获取用户tokengetToken(state) {return state.token;},//获取用户角色getRole(state) {return state.role;},//获取用户权限getPermission(state) {return state.permission;},//获取用户菜单getMenus(state) {return state.menus;},},mutations: {//设置用户idsetUserId(state, userId) {state.userId = userId;localStorage.setItem("userId", userId);},//设置用户tokensetToken(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 = falsenew 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-submenuv-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>

相关文章:

  • 电子电器架构 ---软件定义汽车的电子/电气(E/E)架构
  • HarmonyOS-ArkUI: animateTo 显式动画
  • mapbox进阶,实现掩膜效果,并控制掩膜透明度
  • OpenCV---图像预处理(四)
  • 使用 Flutter 遇坑小计
  • Uniapp:pages.json页面路由
  • ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(显示输出类外设之IS31FL3216)
  • 【无人机】无人机的电调校准,ESC Calibration,PX4使用手册电调校准详细步骤
  • 超详细实现单链表的基础增删改查——基于C语言实现
  • 基于 FFmpeg 的音视频处理基础原理与实验探究
  • 运维概述(linux 系统)
  • 《解锁增强型上下文学习,打造你的专属智能助手》
  • 徐州服务器租用:虚拟主机的应用场景
  • Spring AI MCP
  • Linux之信号
  • Linux——系统安全及应用
  • 2025年pta团队设计天梯赛题解
  • 【软件工程】 适配器模式
  • C#接口开发异常:System.Web.HttpRequestValidationException
  • 怎么建立自然语言领域的评价标准
  • 经济日报:锚定重点领域和关键环节,上海浦东谋划高水平对外开放
  • 尹锡悦涉嫌发动内乱案第二次庭审举行
  • AI换脸侵权案入选最高法典型案例:明晰人工智能使用边界
  • 徐之凯评《突如其来的勇气》|早熟的抵抗
  • 泽连斯基:俄军违反停火承诺,20日10时起前线俄炮击增加
  • 俄官员称乌克兰未遵守停火,乌方暂无回应