项目背景:登录的时候,后端在返回token的同时还一并返回用户的登录权限,且我司返回的是一串数组,里面的内容对应每个要显示的路由,没有admin之类的权限。
实现流程(具体看代码,超级详细):
实现过程遇到的bug:
修改路由结构:
把需要动态加载的菜单放在asyncRoutes里面,并给每个路由加个角色(父路由也需要的哦),放在meta里哦,【分为constantRoutes(静态路由) 和 asyncRoutes(动态路由)】
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
/* Layout */
import Layout from "@/layout";
export const constantRoutes = [
{
path: "/login",
component: () => import("@/views/login/index"),
hidden: true
},
{
path: "/",
component: Layout,
redirect: "/dashboard",
children: [
{
path: "dashboard",
name: "主页",
component: () => import("@/views/dashboard/index"),
meta: { title: "主页", icon: "dashboard" }
}
]
},
{
path: "/external-link",
component: Layout,
children: [
{
path: "/",
meta: { title: "关于我们", icon: "link" }
}
]
},
{
path: "/404",
component: () => import("@/views/404"),
hidden: true
},
{ path: "*", redirect: "/404", hidden: true },
];
export const asyncRoutes = [
{
path: "/example",
component: Layout,
redirect: "/example/table",
name: "Example",
meta: { title: "个人管理", icon: "el-icon-user-solid", roles: ['Personal'] },
children: [
{
path: "table",
name: "Table3",
component: () => import("@/views/table/index"),
meta: { title: "单", roles: ['Personal'] }
},
{
path: "tree",
name: "Tree3",
component: () => import("@/views/table/rate"),
// meta: { title: "进度", icon: "tree", roles: ['Personal'] }
}
]
},
{
path: "/proxy",
component: Layout,
redirect: "proxy/manage",
meta: { title: "服务商管理", icon: "el-icon-office-building", roles: ['maintainCompanyPolicy', 'maintainCompanyEmployee'] },
children: [
{
path: "manage",
name: "Proxy",
component: () => import("@/views/manage/index"),
meta: { title: "单", roles: ['maintainCompanyPolicy'] }
},
{
path: "employee",
name: "Employee",
component: () => import("@/views/manage/employee"),
// meta: { title: "管理", roles: ['maintainCompanyEmployee'] }
}
]
},
{
path: "/company",
component: Layout,
redirect: "company/maintain/table",
name: "Admin",
meta: { title: "后台管理", icon: "el-icon-s-help", roles: ['maintainAdminCompany'] },
children: [
{
path: "maintain",
component: () => import("@/views/company/index"), // Parent router-view
name: "Maintain",
redirect: "maintain/table",
meta: { title: "公司", roles: ['maintainAdminCompany'] },
children: [
{
path: "table",
name: "Table2",
component: () => import("@/views/company/table"),
meta: { title: "公司", roles: ['maintainAdminCompany'] }
}
]
},
{
path: "manage",
component: () => import("@/views/company/maintain/index"),
name: "Manage",
meta: {
title: "维保单", roles: ['maintainAdminCompany']
}
},
{
path: "recycle",
component: () => import("@/views/company/recycle/index"),
name: "Unusual",
meta: { title: "回收站", roles: ['maintainAdminCompany'] }
}
]
},
]
const createRouter = () =>
new Router({
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
});
const router = createRouter();
export function resetRouter () {
const newRouter = createRouter();
router.matcher = newRouter.matcher; // reset router
}
export default router;
把后端传来的角色存储起来:
import { login, logout, getInfo } from "@/api/user";
import { getToken, setToken, removeToken, setUser, removeUser } from "@/utils/auth";
import { resetRouter } from "@/router";
const getDefaultState = () => {
return {
token: getToken(), //token
role: [], //角色列表
init: false //这个是解决后面‘切换用户,左侧路由菜单不变,要刷新一下才可以’的问题,如果在退出时刷新了,就不用写这个
};
};
const state = getDefaultState();
const mutations = {
RESET_STATE: state => {
Object.assign(state, getDefaultState());
},
SET_TOKEN: (state, token) => {
state.token = token;
}
SET_ROLE: (state, role) => {
state.role = role;
}
// 判断是否初次登陆,在src/permission.js用到init
SET_INIT: (state, data) => {
state.init = data
}
};
const actions = {
//初次登陆赋予init为true
changeInit ({ commit }) {
commit("SET_INIT", true);
},
// 账号密码登录
login ({ commit }, userInfo) {
const menulist = []
const { username, password } = userInfo;
const formdata = new FormData();
formdata.append('username', username.trim())
formdata.append('password', password)
return new Promise((resolve, reject) => {
login(formdata)
.then(response => {
const data = response.data;
// 权限
const role = data.userAuth.userAuth.map(roles => roles.moduleCode) //将角色列表格式化
localStorage.setItem('role', JSON.stringify(role)) // 将角色存储在本地
commit("SET_ROLE", role);
setToken(data.token.value);
resolve();
})
.catch(error => {
reject(error);
});
});
}
// 退出登录
logout ({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
logout(state.token)
.then(() => {
// location.reload() //退出 刷新页面,如果写了这个就不用写init了,选用init 只是因为个人感觉比较优雅
localStorage.removeItem('role')
commit('SET_INIT', false)
commit('SET_TOKEN', '');
commit("SET_ROLE", '');
removeToken(); // must remove token first
removeUser();
commit("RESET_STATE");
commit('SET_MENULIST', '');
dispatch('tagsView/delAllViews', null, { root: true });
resetRouter();
resolve();
})
.catch(error => {
reject(error);
});
});
},
// remove token
resetToken ({ commit }) {
return new Promise(resolve => {
removeToken(); // must remove token first
removeUser();
commit("RESET_STATE");
resolve();
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
这个别漏啦
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
account: state => state.user.account,
name: state => state.user.name,
role: state => state.user.role, //加上这个
permission_routes: state => state.permission.routes, //别漏了这个哦
init: state => state.user.init //加上这个
}
export default getters
动态添加路由:
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // 是否有转圈效果
const whiteList = ['/login'] // 没有重定向的白名单
router.beforeEach(async (to, from, next) => {
// 开始进度条
NProgress.start()
// 设置页面标题
document.title = getPageTitle(to.meta.title)
// 确定页面是否已登录
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 如果已登录,则重定向到主页
next({ path: '/' })
NProgress.done()
} else {
// 获取到的静态路由 + 动态动态,如果选择的方案是不使用init,则不要注释此条,把下面的a注释掉
// const hasGetPermissionRoutes = store.getters.permission_routes && store.getters.permission_routes.length > 0
// 判断是否第一次登陆
const a = store.getters.init
if (a) {
next()
} else {
try {
// const roles = store.state.user.role,原文档写法,但是这样的话 会导致刷新后,数据丢失
const roles = JSON.parse(localStorage.getItem('role'))
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
await store.dispatch('user/changeInit')
router.addRoutes(accessRoutes)
// 在这里动态添加最后的通配路由,确保先有动态路由,再有通配路由,解决动态路由刷新会跳转到404问题
let lastRou = [{ path: '*', redirect: '/404' }]
router.addRoutes(lastRou)
next({ ...to, replace: true })
} catch (error) {
// 删除令牌并进入登录页面重新登录
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
筛选应该被添加的路由:
import { asyncRoutes, constantRoutes } from '@/router'
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission (roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes (routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
// 权限路由放在最后
// state.routes = constantRoutes.concat(routes)
// 把权限路由放在中间,在constontRoutes
let rou = constantRoutes
rou.splice(2, 0, ...routes) //把第二个放在最后
state.routes = rou
}
}
const actions = {
generateRoutes ({ commit }, roles) {
return new Promise(resolve => {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
渲染路由导航:
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
//这里一定要注意修改为 permission_routes
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes', //这里引入permission_routes
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
想看‘解决登录,退出后,显示的路由指向路径不变’的问题,可直接拉到后面的代码,修改一行代码就行啦
<template>
<div class="navbar">
<hamburger
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb class="breadcrumb-container" />
<div class="right-menu">
<el-dropdown
class="avatar-container"
trigger="click"
>
<div class="avatar-wrapper">
<img
src="./favicon.png"
class="user-avatar"
>
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu
slot="dropdown"
class="user-dropdown"
>
<router-link to="/">
<el-dropdown-item v-if="user!=null">
<!-- 用户名:{{user}} -->
用户:{{ user.nickname }}
</el-dropdown-item>
</router-link>
<a
target="_blank"
href="#"
>
<!-- <el-dropdown-item v-if="user!=null">账号:{{ user.userId }}</el-dropdown-item> -->
</a>
<a href="#">
<el-dropdown-item>状态:在线</el-dropdown-item>
</a>
<el-dropdown-item
divided
@click.native="logout"
>
<span style="display:block;">退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import { getUser } from "@/utils/auth";
export default {
components: {
Breadcrumb,
Hamburger
},
computed: {
...mapGetters(['sidebar', 'avatar', 'name', 'account'])
},
data () {
return {
user: {}
}
},
created () {
this.getData()
},
methods: {
toggleSideBar () {
this.$store.dispatch('app/toggleSideBar')
},
async logout () {
await this.$store.dispatch('user/logout')
// this.$router.push(`/login?redirect=${this.$route.fullPath}`)
//退出的时候 直接到登录界面,解决再次登录别的账号出现进去即404
this.$router.push(`/login`)
},
getData () {
this.user = JSON.parse(getUser());
}
}
}
</script>
在router/index.js中,动态加载路由模块中的最后添加一下这行代码,相对的前面的一样的代码就删除了,还有方案一的代码也不必要存在了
export const asyncRoutes = [
{ path: "*", redirect: "/404", hidden: true } //把通配404页面放在动态路由的最底部,就不用在promission中设置了
]