通常来说,用户在登录成功后会在首页左侧或者上方显示一个用户菜单。而这个菜单数据是根据用户的角色动态加载的,即不同身份的用户登录成功后看到的菜单是不一样的。下面通过样例演示如何实现菜单的加载与展示。
一、后端部分
(1)后端接口实现比较容易,先根据登录用户的 id 查询该用户具有的角色,在根据角色信息查看对应的 Menu,最后将 Menu 返回到前端。
(2)这里假设后端接口为 /sysmenu,返回的数据格式如下:
(1)这里菜单一共有两级,结构上采用嵌套的形式,关于如何查询树形结构的数据,可以参考我之前写的文章:
name:菜单名称
component:菜单对应的 vue 模块名(客户端会根据这个名字记载实际的 component 组件)
path:菜单对应的 vue 模块路径
[
{
"id":2,
"path":"/home",
"component":"Home",
"name":"人员管理",
"iconCls":"fa fa-user-circle-o",
"children":[
{
"id":null,
"path":"/emp/basic",
"component":"EmpBasic",
"name":"基本资料",
"iconCls":null,
"children":[
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
}
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
},
{
"id":5,
"path":"/home",
"component":"Home",
"name":"统计管理",
"iconCls":"fa fa-bar-chart",
"children":[
{
"id":null,
"path":"/sta/all",
"component":"StaAll",
"name":"综合信息统计",
"iconCls":null,
"children":[
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
},
{
"id":null,
"path":"/sta/pers",
"component":"StaPers",
"name":"人事信息统计",
"iconCls":null,
"children":[
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
}
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
},
{
"id":6,
"path":"/home",
"component":"Home",
"name":"系统管理",
"iconCls":"fa fa-windows",
"children":[
{
"id":null,
"path":"/sys/basic",
"component":"SysBasic",
"name":"基础设置",
"iconCls":null,
"children":[
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
},
{
"id":null,
"path":"/sys/log",
"component":"SysLog",
"name":"日志管理",
"iconCls":null,
"children":[
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
}
],
"meta":{
"keepAlive":false,
"requireAuth":true
}
}
]
二、前端部分
1,初始路由设置(router/index.js)
系统初始路由只有一个 /home (即进入首页面)。等后面菜单完毕后,会根据菜单项自动添加对应的路由以及模块。
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
Vue.use(Router)
export default new Router({
routes: [
/* {
path: '/',
name: 'Login',
component: Login,
hidden: true
},*/
{
path: '/home',
name: 'Home',
component: Home
}
]
})
2,创建 store 用来保存菜单数据(store/index.js)
首先在 store 中创建一个 routes 数组,这个是一个空数组,后面我们将会把服务端返回的 JSON 格式的菜单数据保存在 store 中,然后各个 Vue 页面根据 store 中的数据来渲染菜单。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
routes: []
},
mutations: {
initMenu(state, menus){
state.routes = menus;
}
}
});
3,菜单初始化工具类(utils/menuUtils.js)
(1)该工具类主要用于初始化菜单,其中一个重要的工作是将服务器返回的 JSON 格式的数据转成 router 需要的格式。因为服务端返回的 component 是一个字符串,而 router 中需要的却是一个组件,我们要在这里动态加载相应的组件:
import axios from 'axios';
// 请求菜单数据并初始化
export const initMenu = (router, store)=> {
// 首先判断 store 中数据是否存在,如果存在,则说明这次跳转是正常的跳转
// 而不是用户按F5键或者直接在地址栏输入某个地址进入的,这时直接返回,不必执行菜单初始化
if (store.state.routes.length > 0) {
return;
}
// 若 store 中不存在菜单数据,则需要初始化数据
axios.get("/sysmenu").then(resp=> {
if (resp && resp.status == 200) {
// 将服务器返回的 JSON 格式的数据转成 router 需要的格式
var fmtRoutes = formatRoutes(resp.data);
// 将准备好的数据动态添加到路由中
router.addRoutes(fmtRoutes);
// 同时也将数据存到 store 中
store.commit('initMenu', fmtRoutes);
}
})
}
// 将服务器返回的 JSON 转为 router 需要的格式
export const formatRoutes = (routes)=> {
let fmRoutes = [];
routes.forEach(router=> {
let {
path,
component,
name,
meta,
iconCls,
children
} = router;
if (children && children instanceof Array) {
// 如果有子节点则递归转换
children = formatRoutes(children);
}
let fmRouter = {
path: path,
// 根据服务器返回的 component 动态加载需要的组件
component(resolve){
if (component.startsWith("Home")) {
require(['../components/' + component + '.vue'], resolve)
} else if (component.startsWith("Emp")) {
require(['../components/emp/' + component + '.vue'], resolve)
} else if (component.startsWith("Sta")) {
require(['../components/statistics/' + component + '.vue'], resolve)
} else if (component.startsWith("Sys")) {
require(['../components/system/' + component + '.vue'], resolve)
}
},
name: name,
iconCls: iconCls,
meta: meta,
children: children
};
fmRoutes.push(fmRouter);
})
return fmRoutes;
}
(2)红框部分为项目里菜单对应的各个组件位置:
4,项目主入口代码(main.js)main.js中除了将前面定义的 route、store 引入外,还需要开启一个路由全局守卫,在每次访问某个页面前都去加载一次菜单数据:
1,为什么每次访问页面前都需要加载一次菜单数据?
通常情况下,我们只在登录成功之后请求一次菜单资源,然后将 JSON 数据保存在 store 中,以便下一次使用。但是这样会有一个问题:
假如用户登录成功之后,单击 Home 页的某一个按钮,进入某一个子页面中,然后按一下 F5键进行刷新,这个时候就会出现空白页面,因为按 F5 键刷新之后 store 中的数据就没了。
2,解决F5刷新后Store数据丢失问题?
要解决这个问题有如下两种方案:
方案一,不要将菜单资源保存到 store 中,而是保存到 localStorage 中,这样即使按 F5 键刷新之后数据还在。由于菜单资源是非常敏感的,因此不建议将其保存到本地,故舍弃。
方案二,直接在每一个页面的 mounted 方法中都加载一次菜单资源。但这种做法工作量有点大,而且也不易维护,这里可以使用路由中的导航守卫来简化这个方案的工作量。
import Vue from 'vue';
import App from './App';
import router from './router';
import store from './store';
import {initMenu} from './utils/menuUtils'
Vue.config.productionTip = false;
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
// 配置一个全局前置守卫
router.beforeEach((to, from, next)=> {
// 首先判断目标页面是不是Login,若是Login页面,则直接通过,因为登录页不需要菜单数据
if (to.name == 'Login') {
next();
return;
}
// 判断当前用户是否已经登录,否则跳回登录页
// ........
// 先初始化菜单数据
initMenu(router, store);
// 再进入下一个页面
next();
}
)
new Vue({
el: '#app',
router,
store,
components: { App },
template: ''
})
5,主视图代码(Home.vue)
菜单渲染操作在 Home.vue组件中完成,并且菜单点击后里面的子路由视图会进行切换:
动态菜单 DEMO
{{item.name}}
style="padding-left: 30px;width: 170px;text-align: left"
v-for="child in item.children"
:index="child.path"
:key="child.path">{{child.name}}
首页
export default{
methods: {
},
data(){
return {
}
},
computed: {
// 在计算属性中返回 routes 数据
routes(){
return this.$store.state.routes
}
}
}
.home-container {
height: 100%;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
}
.home-header {
background-color: #20a0ff;
color: #333;
text-align: center;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: content-box;
padding: 0px;
}
.home-aside {
background-color: #ECECEC;
}
.home_title {
color: #fff;
font-size: 22px;
display: inline;
margin-left: 8px;
}
.el-submenu .el-menu-item {
width: 180px;
min-width: 175px;
}
6,运行效果
(1)默认访问 /home首页显示效果如下:
(2)点击左侧菜单栏自动切换右侧组件,并且无论当前在那个页面,按下 F5 刷新后菜单都不会丢失。