文章目录
简介
在开发后台系统时,通过菜单进行导航是非常重要的一件事情,在前端开发过程中使用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>