vue后台管理系统--权限管理模块

1,165 阅读6分钟

这个项目是尚硅谷的vue后台管理项目,没有完全跟着做,最近用到了权限管理,又学了最后两节课,我的源码地址:gitee.com/qiuyan-yin/… 这个项目有一个问题未解决,就是在首页外的任一个页面刷新就会变成空白页。找了很久还未找到解决办法,有空再改吧,如果有小伙伴也遇到了这个问题,希望能一起讨论一下~~~

vue后台权限管理使用的是RBAC的逻辑。

RBAC模型(Role-Based Access Control:基于角色的访问控制),在REAC中,有三个组成:用户、角色、权限。

RBAC通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离(区别于ACL模型),极大地方便了权限的管理 :

  • User(用户):每个用户都有唯一的UID识别,并被授予不同的角色

  • Role(角色):不同角色具有不同的权限

  • Permission(权限):访问权限

  • 用户-角色映射:用户和角色之间的映射关系
    (角色:boss、运维、程序员、运营专员)

  • 角色-权限映射:角色和权限之间的映射
    (权限:超级管理员[boss]--是有权力操作整个项目的所有模板;运营专员--只能看到首页、商品管理页面等一部分菜单数据)

它们之间的关系如下图所示: image.png

权限管理菜单页面如下:

给用户设置角色: image.png

给角色设置权限: image.png

超级管理员可以管理菜单项: 6df14f607a3045e29cfd9310bbfeb864_tplv-k3u1fbpfcp-watermark.png

把项目中的路由进行拆分!

因为注册的路由是“死的”,"活的"路由可以根据不同用户可以展示不同菜单。如何实现菜单的权限?不同的用户所能操作|查看菜单不一样,不同的用户登录的时候会向服务器发请求,服务器会把用户相应的菜单的权限的信息返回给我们,我们可以根据服务器返回的数据,可以动态的设置路由,根据不同的用户展示不同的菜单。

常量路由:不管用户是什么角色,都可以看见的路由。 比如登录、首页、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【按钮的信息:按钮权限用的标记】 image.png

store所存的用户信息: image.png

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.添加一个菜单栏

image.png

这个功能权限值与router中的name相对应: image.png

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>