打造vuecli3+element后台管理系统(四)讲讲让本秃头星人头大的动态菜单、页面权限和角色赋权在后台系统中的实现

许多时候我们的作的后台系统,面向的人群多是五花八门的,后台系统中展现的数据大部分是公司相关的运营数据,因此呢必须严格控制用户的权限。用户是否有权访问这个菜单、用户访问这个菜单以后,是否有权进行增删改查,这都是身为一个合格滴后台系统所要具有的功能(敲黑板)。javascript

1、定义权限接口返回的数据的json结构

权限模块能够说是后台系统的重中之重,它可简单,可复杂,具体看产品大大如何定义。css

平时后台兄弟的接口返回的数据体结构,都是他说了算,他怎么给滴,咱就怎么渲染。可是其实这样是很被动的,为了提升咱们的开发效率,咱们要把精力更多的放在页面上而不是把精力放在绞尽脑汁想怎么把后台给的数据遍历转化为我想要的结构,数据的二次处理有时正是咱们被吐槽开发慢的缘由之一呀!(摔杯前端

因此适当的和后台大兄弟沟通一下返回的数据体的结构,能让后台大兄弟处理的,就让他处理,相信我,其实开口沟通没那么难。vue

扯远了,话又说回来,由于权限模块的特殊性,因此这一块返回的结构是怎么样的,咱们须要给后台大兄弟提供大体的维度结构。java

个人项目里是这样去定义这个结构的:git

这里是简化了的结构,保留了核心字段,在这个项目里菜单是二级结构的,一级是菜单大类,children表示底下的二级页面,二级下面就是页面的路由名称和该用户在这个菜单下面拥有的权限,这里定义了增add、删delete、改edit、查check四个github

[
    {
        name: 'Table',
        children: [
            {
                name: 'TableDemo',
                auth: {
                    add: true,
                    check: true,
                    delete: true,
                    edit: true
                }
            }
        ]
    }
]
复制代码

2、定义须要动态加载的路由,定义mock接口

假设如今有一个路由是须要权限才能访问的,咱们在router/modules下定义一个table.js文件,这下面的demo页是须要后台返回了相关菜单,用户才能有权访问。vuex

// table.js
const table = {
  path: 'table',
  component: () => import('@/layout'),
  redirect: '/table/demo',
  name: 'Table',
  meta: {
    title: 'parentTitle',
    icon: 'table'
  },
  children: [
    {
      path: '/table/demo',
      name: 'TableDemo',
      component: resolve => void require(['@/views/table/demo'], resolve),
      meta: {
        title: 'tableDemo'
      }
    },
    {
      path: '/table/demoTest',
      name: 'DemoTest',
      component: resolve => void require(['@/views/table/demoTest'], resolve),
      meta: {
        title: 'demoTest'
      }
    }
  ]
}

export default table

复制代码

mock接口数据,这里咱们只给用户了第一个子菜单,第二个不给看element-ui

// mock/index.js
const permissionData = () => {
  result.data = [
    {
      name: 'Table',
      children: [
        {
          name: 'TableDemo',
          auth: {
            add: true,
            check: true,
            delete: true,
            edit: true
          }
        }
      ]
    }
  ]
  return result
}

Mock.mock('/apiReplace/permission', 'post', permissionData)
复制代码

接口数据咱们已经mock中定义了,能够着手写如何获取动态路由的逻辑了json

3、定义处理权限相关逻辑的vuex仓库文件。

在store/modules目录下新建permission.js,咱们须要在vuex中定义路由和权限的逻辑,包括初始化动态路由、重置路由等。

// permission.js

/** 这些在上一篇路由模块的定义里有讲到,或者是小伙伴能够去项目里头看看router文件,我这里不贴router文件的代码了~~ * constantRoutes 常规路由,不须要权限便可访问 * asyncRoutes 须要访问权限的路由 * notFoundRoutes 404路由 * resetRouter 重置路由的方法 */
import { asyncRoutes, constantRoutes, notFoundRoutes, resetRouter } from '@/router'
import API from '@/assets/http/apiUrl'
import Request from '@/assets/http'

const permission = {
  state: {
    routes: [],
    addRoutes: [] // 异步加载的路由
  },

  mutations: {
    SET_ROUTES: (state, routes) => {
      state.addRoutes = routes
      state.routes = constantRoutes.concat(routes)
    }
  },

  actions: {
    // 获取动态路由
    GenerateRoutes({ commit }, isSuperAdmin) {
      resetRouter() // 先初始化路由
      return new Promise((resolve, reject) => {
        // 若是是超级管理员,挂载所有路由所有权限
        if (isSuperAdmin) {
          // 重定向404的匹配规则须要在整个完整路由定义的最后面,不然刷新会出错。
          const accessedRoutes = [...asyncRoutes, ...notFoundRoutes]
          accessedRoutes.forEach(item => {
            if (item.children) {
              // 超级管理员赋所有权限
              item.children.forEach(elem => {
                elem.meta = {
                  ...elem.meta,
                  check: true,
                  delete: true,
                  add: true,
                  edit: true
                }
              })
            }
          })
          commit('SET_ROUTES', accessedRoutes)
          resolve(accessedRoutes)
        } else {
          Request.httpRequest({
            method: 'post',
            url: API.GetPermissionData,
            noLoading: true,
            params: {},
            success: (data) => {
              console.log(data)
              let accessedRoutes = []
              // 匹配前端路由和后台返回的菜单
              accessedRoutes = filterAsyncRoutes(asyncRoutes, data)
              // 重定向404的匹配规则须要在整个完整路由定义的最后面,不然刷新会出错。
              accessedRoutes.push(...notFoundRoutes)
              commit('SET_ROUTES', accessedRoutes)
              resolve(accessedRoutes)
            },
            error: res => {
              reject(res)
            }
          })
        }
      })
    }
  }
}

/** * Filter asynchronous routing tables by recursion * 匹配后台返回的菜单信息和前端定义的路由 * @param routes 前端定义好的异步路由 * @param menus 后台返回的菜单 */
export function filterAsyncRoutes(routes = [], menus = []) {
  const res = []

  routes.forEach(route => {
    // 复制一遍路由,这样改变tmp的同时路由不会受影响
    const tmp = {
      ...route
    }

    // 是否匹配到了
    if (hasPermission(menus, tmp)) { // 有符合的匹配项
      // 找出那一条匹配成功的路由项
      const findMenu = menus.find((menu, index, menus) => {
        return menu.name.includes(tmp.name)
      })

      // 赋权
      if (findMenu.hasOwnProperty('auth')) {
        tmp.meta = {
          ...tmp.meta,
          ...findMenu.auth
        }
      }

      // 若是该路由项中含有子路由,子路由也是须要和菜单进行匹配的
      if (findMenu.hasOwnProperty('children') && findMenu.children.length) {
        // 子路由匹配的步骤和父路由同样
        tmp.children = filterAsyncRoutes(tmp.children, findMenu.children)
      } else {
        // 将匹配不到的子路由从路由中删除
        delete tmp.children
      }

      // 最后获得的结果就是和后台返回菜单匹配一致的异步路由值
      res.push(tmp)
    }
  })

  return res
}

/** * Use meta.role to determine if the current user has permission * @param menus 后台返回的菜单 * @param route 前端定义好的异步路由中的项 */
function hasPermission(menus, route) {
  // 进行匹配
  if (route.name) { // 前提是异步路由要存在name
    // 匹配的规则是,name要一致,只要匹配到就返回true,中止继续往下循环
    return menus.some(menu => route.name.includes(menu.name))
  } else {
    return true
  }
}

export default permission

复制代码

4、在项目中生成动态路由

一切都准备就绪了,接下来就剩,咱们应该在哪里调用生成动态路由的方法呢。我更趋向于,每次切换路由时进行判断,若是当前用户是第一次进入项目,则在路由跳转前,来调用生成动态路由的方法,路由生成以后再往下走。因此咱们能够在router.beforeEach的钩子函数中调用生成动态路由的方法。

在src目录下新建permission.js,用来定义router.beforeEach中的逻辑

import router from '@/router'
import store from '@/store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import getPageTitle from '@/assets/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/register', '/resetPsw'] // 不重定向白名单

router.beforeEach(async(to, from, next) => {
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // 有无token判断
  const token = localStorage.getItem('ADMIN_TOKEN')
  if (token) {
    if (whiteList.includes(to.path)) {
      next()
      NProgress.done()
    } else {
      // 判断当前用户是否是进行了刷新操做,防止进入死循环,若是存在就表示正常跳转,若是不存在就表示刷新了,vuex中的状态丢失了,须要从新挂载路由
      const hasUser = store.state.user.token
      if (hasUser) {
        next()
      } else {
        try {
          // 防止进入死循环
          await store.commit('SET_TOKEN', token)
          // 是否是超级管理员
          const isSuperAdmin = store.state.user.roles.some(item => item.id === 1)
          const accessRoutes = await store.dispatch('GenerateRoutes', isSuperAdmin)
          // 异步加载路由
          router.addRoutes(accessRoutes)
          router.options.routes = store.state.permission.routes
          // 设置replace:true,导航不会留下历史记录
          next({ ...to, replace: true })
        } catch (error) {
          // 移除token,重定向到登陆页
          await store.dispatch('ResetToken')
          Message.error(error || '身份验证出错,请从新登陆。')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      // next(`/login?redirect=${to.path}`) // 不然所有重定向到登陆页
      next('/login') // 不然所有重定向到登陆页
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done() // 结束Progress
})

复制代码

而后在入口文件引入,全局注册:

// main.js
import '@/permission'
复制代码

而后运行项目,你会发现用户只能访问第一个子菜单了,是否是不难呢。

5、根据权限来为页面加限制

细心的大兄弟会发现咱们给每一个页面路由的meta下头都定义了增add、删delete、改edit、查check四个权限。咱们在页面中经过$route.meta就能获取增删改查的具体权限哦。这里贴一个栗子,咱们定义一个表格:

<template>
  <div class="table-demo">
    <el-card class="list-content" shadow="hover">
      <template v-if="$route.meta.check">
        <el-table
          v-loading="tableLoading"
          :data="tableData"
          :cell-style="{ whiteSpace: 'nowrap'}"
          :header-row-style="{ background: '#EBEEF5'}"
          style="width: 100%"
          class="table-content"
        >
          <el-table-column
            type="index"
            label="序号"
            align="center"
            sortable
            width="50"
          />
          <el-table-column
            v-for="(item,index) in tableHeader"
            :key="index"
            :prop="index"
            sortable
            :label="item"
            align="center"
          />
          <el-table-column
            label="操做"
            width="230"
            align="center"
            class-name="operation"
          >
            <template slot-scope="scope">
              <a v-if="$route.meta.edit" class="item" @click="test(scope.row)">修改</a>
              <a v-if="$route.meta.delete" class="item" @click="test(scope.row)">删除</a>
            </template>
          </el-table-column>
        </el-table>
      </template>
      <div v-else class="no-data">
        您暂时没有查看的权限
      </div>
    </el-card>
    <!-- 分页 -->
    <el-pagination
      v-if="$route.meta.check"
      :total="total"
      :pager-count="5"
      :page-sizes="[10, 20, 30, 50]"
      :page-size="pageSize"
      :current-page="currentPage"
      background
      layout="total, sizes, prev, pager, next, jumper"
      class="pagination"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>
复制代码

咱们能够根据

$route.meta.add
$route.meta.edit
$route.meta.delete
$route.meta.check
复制代码

来控制相应入口的显示与否

还有不少细节的东西没有详细写出来,我这里贴一下项目地址,有兴趣的能够看一看哦~

效果图:

效果图
相关文章
相关标签/搜索