阅读 2195

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

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

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

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

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

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

扯远了,话又说回来,因为权限模块的特殊性,所以这一块返回的结构是怎么样的,我们需要给后台大兄弟提供大致的维度结构。

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

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

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

二、定义需要动态加载的路由,定义mock接口

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

// 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接口数据,这里我们只给用户了第一个子菜单,第二个不给看

// 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中定义了,可以着手写如何获取动态路由的逻辑了

三、定义处理权限相关逻辑的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

复制代码

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

一切都准备就绪了,接下来就剩,我们应该在哪里调用生成动态路由的方法呢。我更趋向于,每次切换路由时进行判断,如果当前用户是第一次进入项目,则在路由跳转前,来调用生成动态路由的方法,路由生成之后再往下走。所以我们可以在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'
复制代码

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

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

细心的大兄弟会发现我们给每个页面路由的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
复制代码

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

还有很多细节的东西没有详细写出来,我这里贴一下项目地址,有兴趣的可以看一看哦~

效果图:

效果图

关注下面的标签,发现更多相似文章
评论