这个项目是尚硅谷的vue后台管理项目,没有完全跟着做,最近用到了权限管理,又学了最后两节课,我的源码地址:gitee.com/qiuyan-yin/… 这个项目有一个问题未解决,就是在首页外的任一个页面刷新就会变成空白页。找了很久还未找到解决办法,有空再改吧,如果有小伙伴也遇到了这个问题,希望能一起讨论一下~~~
vue后台权限管理使用的是RBAC的逻辑。
RBAC模型(Role-Based Access Control:基于角色的访问控制),在REAC中,有三个组成:用户、角色、权限。
RBAC通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离(区别于ACL模型),极大地方便了权限的管理 :
-
User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
-
Role(角色):不同角色具有不同的权限
-
Permission(权限):访问权限
-
用户-角色映射:用户和角色之间的映射关系
(角色:boss、运维、程序员、运营专员) -
角色-权限映射:角色和权限之间的映射
(权限:超级管理员[boss]--是有权力操作整个项目的所有模板;运营专员--只能看到首页、商品管理页面等一部分菜单数据)
它们之间的关系如下图所示:
权限管理菜单页面如下:
给用户设置角色:
给角色设置权限:
超级管理员可以管理菜单项:
把项目中的路由进行拆分!
因为注册的路由是“死的”,"活的"路由可以根据不同用户可以展示不同菜单。如何实现菜单的权限?不同的用户所能操作|查看菜单不一样,不同的用户登录的时候会向服务器发请求,服务器会把用户相应的菜单的权限的信息返回给我们,我们可以根据服务器返回的数据,可以动态的设置路由,根据不同的用户展示不同的菜单。
常量路由:不管用户是什么角色,都可以看见的路由。 比如登录、首页、404.
异步路由:不同的用户,需要过滤筛选出的路由,称为异步路由。
任意路由:当路径出现错误的时候重定向404。
1.router文件夹下的index.js文件代码如下:
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
import Layout from "@/layout";
//常量路由:不管用户是什么角色,都可以看见的路由。比如登录、首页、404.
export const constantRoutes = [
{
path: "/login",
component: () => import("@/views/login/index"),
hidden: true,
},
{
path: "/404",
component: () => import("@/views/404"),
hidden: true,
},
{
path: "/",
component: Layout,
redirect: "/dashboard",
children: [
{
path: "dashboard",
name: "Dashboard",
component: () => import("@/views/dashboard/index"),
meta: { title: "首页", icon: "dashboard" },
},
],
},
];
//异步路由:不同的用户,需要过滤筛选出的路由,称为异步路由。
export const asyncRoutes = [
{
path: "/product",
component: Layout,
name: "Product",
meta: { title: "商品管理", icon: "el-icon-goods" },
children: [
{
path: "trademark",
name: "TradeMark",
component: () => import("@/views/product/trademark"),
meta: { title: "品牌管理" },
},
{
path: "attr",
name: "Attr",
component: () => import("@/views/product/attr"),
meta: { title: "平台属性管理" },
},
……… ……… ……
],
},
{
path: "/acl",
component: Layout,
name: "Acl",
meta: { title: "权限管理", icon: "el-icon-goods" },
children: [
{
path: "users/list",
name: "User",
component: () => import("@/views/acl/user/list"),
meta: { title: "用户管理" },
},
{
path: "role/list",
name: "Role",
component: () => import("@/views/acl/role/list"),
meta: { title: "角色管理" },
},
{
path: "role/auth/:id",
name: "RoleAuth",
component: () => import("@/views/acl/role/roleAuth"),
meta: { title: "角色授权" },
hidden: true,
},
{
path: "permission/list",
name: "Permission",
component: () => import("@/views/acl/permission/list"),
meta: { title: "菜单管理" },
},
],
},
];
//任意路由:当路径出现错误的时候重定向404
export const anyRoutes = [{ path: "*", redirect: "/404", hidden: true }];
const createRouter = () =>
new Router({
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes,
});
const router = createRouter();
export default router;
// 路由模块中重置路由的方法
export function resetRouter() {
const newRouter = createRouter();
router.matcher = newRouter.matcher; // reset router
}
2.更改store文件夹下的user.js文件(只写添加或更改的地方):
// 路由模块中重置路由的方法,异步路由,任意路由,常量路由
import { resetRouter,asyncRoutes,anyRoutes,constantRoutes } from "@/router";
import router from '@/router';
import cloneDeep from "lodash/cloneDeep";
const getDefaultState = () => {
return {
// 获取token
token: getToken(),
// 存储用户名
name: "",
// 存储用户头像
avatar: "",
//服务器返回的菜单信息【根据不同的角色:返回的标记信息,数组里面的元素是字符串】
routes: [],
//角色信息
roles: [],
//按钮权限的信息
buttons: [],
// 对比之后【项目中已有的异步路由,与服务器返回的标记信息进行对比最终需要展示的路由】
resultAsyncRoutes: [],
//用户最终需要展示全部路由
resultAllRputes: [],
};
};
const state = getDefaultState();
//唯一修改state的地方
const mutations = {
// 重置state
RESET_STATE: (state) => {
Object.assign(state, getDefaultState());
},
SET_TOKEN: (state, token) => {
state.token = token;
},
//存储用户信息
SET_USERINFO: (state, userInfo) => {
//用户名
state.name = userInfo.name;
//用户头像
state.avatar = userInfo.avatar;
//菜单权限标记
state.routes = userInfo.routes;
//按钮权限标记
state.buttons = userInfo.buttons;
//角色
state.roles = userInfo.roles;
},
//最终计算出的异步路由
SET_RESULTASYNCROUTES: (state, asyncRoutes) => {
//vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量、异步、任意路由
state.resultAsyncRoutes = asyncRoutes;
//计算出当前用户需要展示的所有路由【异步路由+任意路由】
state.resultAllRputes = constantRoutes.concat(
state.resultAsyncRoutes,
anyRoutes
);
//给路由器添加新的路由
router.addRoutes(state.resultAllRputes);
},
};
const actions = {
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token)
.then((response) => {
const { data } = response;
// vuex 存储用户全部的信息
commit("SET_USERINFO", data); // 返回data信息如下图所示
// asyncRoutes---从router文件夹下的index.js文件引入的
commit("SET_RESULTASYNCROUTES",computedAsyncRoutes(cloneDeep(asyncRoutes), data.routes)
);
resolve(data);
})
.catch((error) => {
reject(error);
});
});
},
// 用户退出的时候用到重置路由的方法
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token)
.then(() => {
removeToken(); // must remove token first
// 重置路由
resetRouter();
commit("RESET_STATE");
resolve();
})
.catch((error) => {
reject(error);
});
});
},
// remove token,这个方法在permission.js(router.beforeEach中用到的)和request.js文件中用到了
resetToken({ commit }) {
return new Promise((resolve) => {
removeToken(); // must remove token first
commit("RESET_STATE");
resolve();
});
},
}
//定义一个函数:两个数组进行对比,对比出当前用户到底显示哪些异步路由
const computedAsyncRoutes = (asyncRoutes, routes) => {
//过滤出当前用户【超级管理|普通员工】需要展示的异步路由
return asyncRoutes.filter((item) => {
//数组当中没有这个元素返回索引值-1,如果有这个元素返回的索引值一定不是-1
if (routes.indexOf(item.name) != -1) {
//递归:别忘记还有2、3、4、5、6级路由
if (item.children && item.children.length) {
item.children = computedAsyncRoutes(item.children, routes);
}
return true;
}
});
};
export default {
namespaced: true,
state,
mutations,
actions,
};
返回的用户信息包含:用户名name、用户头像avatar、routes[返回的标志:不同的用户应该展示哪些菜单的标记]、roles(用户角色信息)、buttons【按钮的信息:按钮权限用的标记】
store所存的用户信息:
3.更改layout/components/siderbar/index.vue文件
<!-- 遍历菜单栏的时候遍历的都是常量路由 -->
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path"/>
<script>
export default {
computed: {
// 应该替换为仓库中已经计算好的需要展示的全部路由
routes() {
// sliderbar 需要遍历的是仓库计算完毕的全部路由
return this.$store.state.user.resultAllRputes;
},
}
}
</script>
菜单管理下的按钮设置
1. 更改element-ui源码(这段话在acl/role/roleAuth.vue页面中的save方法中出现的)
/*
vue elementUI tree树形控件获取父节点ID的实例
修改源码:
情况1: element-ui没有实现按需引入打包
node_modules\element-ui\lib\element-ui.common.js 25382行修改源码 去掉 'includeHalfChecked &&'
// if ((child.checked || includeHalfChecked && child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
if ((child.checked || child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
情况2: element-ui实现了按需引入打包
node_modules\element-ui\lib\tree.js 1051行修改源码 去掉 'includeHalfChecked &&'
// if ((child.checked || includeHalfChecked && child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
if ((child.checked || child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
*/
2.添加一个菜单栏
这个功能权限值与router中的name相对应:
3.写 .vue文件
4.在rouer/index.js文件添加到 异步路由中
export const asyncRoutes = [
{
path: "/device",
component: Layout,
name: "Device",
meta: { title: "设备管理", icon: "el-icon-goods" },
children: [
{
path: "log",
name: "Log",
component: () => import("@/views/device/printlog"),
meta: { title: "打印日志" },
},
{
path: "printer",
name: "Printer",
component: () => import("@/views/device/printer"),
meta: { title: "打印机管理" },
},
],
},
]
5.判断按钮是否展示---根据store中存储的buttons判断
例如:device/printlog/index.vue文件
<div>
<el-button type="primary" v-show="$store.state.user.buttons.indexOf('btn.Search') != -1">搜索</el-button>
<el-button type="primary" v-show="$store.state.user.buttons.indexOf('btn.Delete') != -1">删除</el-button>
</div>