GitHub Demo 地址
在线预览
更新:对应的vue3版的demo如下:
GitHub Demo 地址
在线预览
前言
1、demo中的项目已经添加了
TagsView
功能和本地权限控制
关于TagsView
功能的添加可以看:Vue - vue-admin-template模板项目改造:增加TagsView功能
关于本地权限控制相关代码参考vue-admin-template
的 permission-control分支
2、并且demo中的项目在1的基础上
增加TopHeader(顶栏)
功能,在顶栏中显示项目标题和用户信息,即可以支持原有方式展示,又可以通过setting配置显示顶栏
关于增加TopHeader(顶栏)
功能的添加可以看:Vue - vue-admin-template模板项目改造:增加TopHeader(顶栏)
所以项目代码可能和原版的
vue-admin-template
有点差别,vue-admin-template 代码地址
本地权限控制,具体是通过查询用户信息获取用户角色,在路由守卫中通过角色过滤本地配置的路由,把符合角色权限的路由生成一个路由数组
动态获取菜单路由其实思路是一样的,只不过路由数组变成从服务器获取,通过查询某个角色的菜单列表,然后在路由守卫中把获取到的菜单数组转成路由数组
动态路由实现是参考vue-element-admin的issues写的,相关issues:
vue-element-admin/issues/167
vue-element-admin/issues/293
vue-element-admin/issues/3326#issuecomment-832852647
关键点
主要在接口菜单列表中把父
component
的Layout
改为字符串 'Layout',
children
的component: () => import('@/views/table/index'), 改成 字符串'table/index',然后在获取到数据后再转回来
!!!!!!!!!!!! 接口格式可以根据项目需要自定义,不一定非得按照这里的来
本地路由格式:
{
path: '/example',
component: Layout,
redirect: '/example/table',
name: 'Example',
meta: { title: 'Example', icon: 'el-icon-s-help', roles: ['admin'] },
children: [
{
path: 'table',
name: 'Table',
component: () => import('@/views/table/index'),
meta: { title: 'Table', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: () => import('@/views/tree/index'),
meta: { title: 'Tree', icon: 'tree' }
}
]
},
接口路由格式:
{
path: '/example',
component: 'Layout',
redirect: '/example/table',
name: 'Example',
meta: { title: '动态Example', icon: 'el-icon-s-help', roles: ['admin'] },
children: [
{
path: 'table',
name: 'Table',
component: 'table/index',
meta: { title: '动态Table', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: 'tree/index',
meta: { title: '动态Tree', icon: 'tree' }
}
]
},
具体实现
1、接口
因为是通过mock模拟的网络请求,所以需要添加一个获取用户菜单列表的接口(可以把vue-element-admin中的mock文件夹下的role文件夹直接copy到项目中,然后改造)
mock目录下建role文件夹、index.js文件
const Mock = require('mockjs')
const { asyncRoutes } = require('./routes.js')
const routes = asyncRoutes
module.exports = [
// mock get all routes form server
{
url: '/vue-element-admin/routes',
type: 'get',
response: _ => {
return {
code: 20000,
data: routes
}
}
}
mock目录下建role文件夹、routes.js文件
主要用的是asyncRoutes
// Just a mock data
const constantRoutes = [
{
path: '/redirect',
component: 'Layout',
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: 'redirect/index'
}
]
},
{
path: '/login',
component: 'login/index',
hidden: true
},
{
path: '/404',
component: '404',
hidden: true
},
{
path: '/',
component: 'Layout',
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: 'dashboard/index',
meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
}]
}
]
/**
* asyncRoutes
* the routes that need to be dynamically loaded based on user roles
*/
const asyncRoutes = [
{
path: '/example',
component: 'Layout',
redirect: '/example/table',
name: 'Example',
meta: { title: '动态Example', icon: 'el-icon-s-help', roles: ['admin'] },
children: [
{
path: 'table',
name: 'Table',
component: 'table/index',
meta: { title: '动态Table', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: 'tree/index',
meta: { title: '动态Tree', icon: 'tree' }
}
]
},
{
path: '/form',
component: 'Layout',
meta: { roles: ['admin'] },
children: [
{
path: 'index',
name: 'Form',
component: 'form/index',
meta: { title: '动态Form', icon: 'form' }
}
]
},
{
path: '/nested',
component: 'Layout',
redirect: '/nested/menu1',
name: 'Nested',
meta: { title: '动态Nested', icon: 'nested', roles: ['admin'] },
children: [
{
path: 'menu1',
component: 'nested/menu1/index', // Parent router-view
name: 'Menu1',
meta: { title: '动态Menu1' },
children: [
{
path: 'menu1-1',
component: 'nested/menu1/menu1-1',
name: 'Menu1-1',
meta: { title: '动态Menu1-1' }
},
{
path: 'menu1-2',
component: 'nested/menu1/menu1-2',
name: 'Menu1-2',
meta: { title: '动态Menu1-2' },
children: [
{
path: 'menu1-2-1',
component: 'nested/menu1/menu1-2/menu1-2-1',
name: 'Menu1-2-1',
meta: { title: '动态Menu1-2-1' }
},
{
path: 'menu1-2-2',
component: 'nested/menu1/menu1-2/menu1-2-2',
name: 'Menu1-2-2',
meta: { title: '动态Menu1-2-2' }
}
]
},
{
path: 'menu1-3',
component: 'nested/menu1/menu1-3',
name: 'Menu1-3',
meta: { title: '动态Menu1-3' }
}
]
},
{
path: 'menu2',
component: 'nested/menu2/index',
meta: { title: '动态menu2' }
}
]
},
{
path: 'external-link',
component: 'Layout',
children: [
{
path: 'https://panjiachen.github.io/vue-element-admin-site/#/',
meta: { title: '动态External Link', icon: 'link' }
}
]
},
/** when your routing map is too long, you can split it into small modules **/
// componentsRouter,
// chartsRouter,
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
module.exports = {
constantRoutes,
asyncRoutes
}
然后在mock/index.js,把新加的role引用一下
const user = require('./user')
const role = require('./role/index') // 新加的
const table = require('./table')
const dict = require('./demos/dict')
const tables = require('./demos/tables')
const mocks = [
...user,
...role, // 新加的
...table,
...dict,
...tables
]
2、前端调用菜单接口并生成路由数组
先在src / api / roles.js(新建roles.j,或者放到user里) 把mock接口实现一下
import request from '@/utils/request'
export function getUserMenus() {
return request({
url: '/vue-element-admin/routes',
method: 'get'
})
}
然后修改 src/store/modules/permission.js,把处理菜单数组转成路由数组的方法实现一下
const { deepClone } = require('@/utils')
// 加载路由
export const loadView = (view) => {
// 路由懒加载
return (resolve) => require([`@/views/${view}`], resolve)
// return (resolve) => require([`@${view}`], resolve)
}
/**
* 通过递归格式化菜单路由 (配置项规则:https://panjiachen.github.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html#配置项)
* @param routes
* @param roles
*/
export function filterAsyncRoutes2(routes) {
const res = []
routes.forEach((route) => {
const tmp = deepClone(route)
if (route.component === 'Layout') {
tmp.component = Layout
} else if (route.component) {
tmp.component = loadView(route.component)
}
if (route.children && route.children.length > 0) {
tmp.children = filterAsyncRoutes2(route.children)
}
res.push(tmp)
})
return res
}
在actions中新加一个函数
generateDynamicRoutes({ commit }, menus) {
return new Promise(resolve => {
const accessedRoutes = filterAsyncRoutes2(menus)
commit('SET_ROUTES', accessedRoutes) // Todo: 内部拼接constantRoutes,所以查出来的菜单不用包含constantRoutes
resolve(accessedRoutes)
})
}
然后修改src\store\modules\user.js
添加获取用户菜单的方法
const state = {
// ```
menus: [] //这个是我新增的
}
const mutations = {
// ```
SET_MENUS: (state, menus) => { //这里是新增的
state.menus = menus
}
}
getUserMenus({ commit, state }) {
return new Promise((resolve, reject) => {
getUserMenus(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const menus = data
// roles must be a non-empty array
if (!menus || menus.length <= 0) {
reject('getMenus: menus must be a non-null array!')
}
commit('SET_MENUS', menus)
resolve(menus)
}).catch(error => {
reject(error)
})
})
},
然后在修改src\permission.js的路由守卫
要在 store.dispatch('permission/generateRoutes') 代码附近要改一下,原先从本地配置+roles获得用户路由,现在改从服务端获得之后再 addRoutes
我这里在mock中加了个角色
editor2
,当editor2
登录使用的从服务器获取动态路由,其他角色从本地获取路由
const { roles } = await store.dispatch('user/getInfo')
// console.log('roles', JSON.stringify(roles))
var accessRoutes = []
if (roles.includes('editor2')) {
// 根据服务器获取的用户菜单生成路由
const menus = await store.dispatch('user/getUserMenus')
const asyncRoutes = await store.dispatch('permission/generateDynamicRoutes', menus)
accessRoutes = asyncRoutes
} else {
// generate accessible routes map based on roles
accessRoutes = await store.dispatch('permission/generateRoutes', roles)
}
// dynamically add accessible routes
router.addRoutes(accessRoutes)
至此结束。