【Vue框架】菜单栏权限的使用与显示

前言

【Vue框架】Vue路由配置 中的getters.js里,可以看到有一个应用程序的状态(变量)叫 permission_routes,这个就是管理前端菜单栏的状态。具体代码的介绍,都以注释的形式来说明。

1、modules\permission.js

1.1 代码

import {
    
     asyncRoutes, constantRoutes } from '@/router'

/**
 * Use meta.role to determine if the current user has permission
 * @param roles 用户角色
 * @param route 路由信息
 */
function hasPermission(roles, route) {
    
    
  // 判断route是否存在meta和meta.roles
  if (route.meta && route.meta.roles) {
    
    
    // 判断当前传入的角色,是否存在路由的route中
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    
    
    return true
  }
}

/**
 * Filter asynchronous routing tables by recursion
 * 通过 递归 筛选异步路由表
 * 【将 基础路由(不含角色权限)和 符合用户权限的动态路由(需要角色权限)全部存入res】
 * @param routes asyncRoutes【去看router\index.js中asyncRoutes】
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
    
    
  const res = []
  // routes是在router\index.js定义的路由数组
  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) // 用户角色的动态路由加入到基础路由中
  }
}

// 用于存储一系列 触发改变 应用状态的方法
const actions = {
    
    
  generateRoutes({
     
      commit }, roles) {
    
    
    return new Promise(resolve => {
    
    
      let accessedRoutes
      if (roles.includes('admin')) {
    
     // 如果roles数组中包含'admin'
        accessedRoutes = asyncRoutes || [] // 动态路由全部可以访问,如果asyncRoutes为`undefined`或`null`,则取[]
      } else {
    
    
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) // 递归出符合条件的路由
      }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}
// 默认导出一个对象,包含了模块的相关配置
export default {
    
    
  // 设置模块的命名空间为 `true`,这意味着该模块中的状态、mutations、getters、actions将被封装在命名空间中,以避免和其他模块的冲突
  namespaced: true,
  // 模块的状态
  state,
  // 模块的变化方法
  mutations,
  // 模块的行为方法(调用action,触发mutations,改变state)
  actions
}

该文件除了定义的routes路由状态和addRoutes动态添加的路由(后续用到的时候在提),主要在actions中定义了生成路由的行为方法generateRoutes,根据roles角色和内置传入的{ commit }context.commit)获取router\index.js中定义的一系列路由,简单说,就是在前端页面上被访问的路径

补充防忘记:

  1. new Promise()以及其参数
  2. 【Vue框架】Vue路由配置 看看路由
  3. 【Vue框架】Vuex状态管理

1.2 想法:

modules\permission.js中,根据generateRoutes提前写好的逻辑来判断查回用户的权限是否符合,这样不利于添加新角色的改动。
应该都得从数据库中查询,得到库中的角色和每个动态路由。(个人猜想,后续在尝试)

2、layout\components\Sidebar

2.1 index.vue

遍历展示整体的菜单栏。

<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"
      >
        <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' // 从`vuex`库中导入`mapGetters`函数,用于将getter映射到该组件的计算属性中
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
    
    
  // 在该组件中注册了`SidebarItem`和`Logo`两个组件,以便在模板中使用它们
  components: {
    
     SidebarItem, Logo },
  // `computed`计算属性中,可以定义一些依赖于其他数据的属性。这些属性的值会根据其依赖的数据动态计算得出,并在依赖数据发生变化时自动更新。
  // `computed`计算属性的定义方式可以是一个对象,对象的每个属性都是一个计算属性的定义
  // `...mapGetters([...])`将`mapGetters`返回的映射对象展开并添加到该组件的计算属性中,这样可以直接访问映射的getter
  // mapGetters([...])里的数组中包含想要映射的getter方法的名称
  computed: {
    
    
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),
    // 根据当前路由对象的元信息(`meta`)中的`activeMenu`属性来确定活动的菜单项。
    // 如果未设置`activeMenu`属性,则返回当前路径(`path`)
    activeMenu() {
    
    
      const route = this.$route // `this.$route`是在Vue中访问当前路由的对象(全局属性,只读,不能直接更改路由信息)
      const {
    
     meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
    
    
        return meta.activeMenu // 侧边栏将高亮显示设置的路径
      }
      return path // 如果未设置`activeMenu`属性,则返回当前路由的路径。侧边栏将根据当前路径高亮显示相应的菜单项
    },
    showLogo() {
    
    
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
    
    
      return variables
    },
    isCollapse() {
    
    
      // 如果`opened`属性为`true`,则`isCollapse`为`false`,表示侧边栏未折叠;
      // 如果`opened`属性为`false`,则`isCollapse`为`true`,表示侧边栏已折叠
      return !this.sidebar.opened
    }
  }
}
</script>

2.2 SidebarItem.vue

具体遍历显示每一个具体的菜单项。

<template>
  <div v-if="!item.hidden" class="menu-wrapper">
    <!-- 这的item就是route,onlyOneChild就是当前route下的子菜单 -->
    <!-- 1判断route下是否为01个子菜单 && 2(!当前route下的子菜单是还存在子菜单||子菜单不存在时) &&  3-->
    <!-- 这里的template:当前route下只有1个或0个子菜单,且唯一子菜单下不存在下级菜单时,执行这段代码
      效果:菜单栏中,该route为一级菜单,不显示其子菜单-->
    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
      <!-- 其子菜单中存在meta -->
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <!--  存在多级子菜单的时候  -->
    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <!-- 这里的插槽名为title;作用和上面一样,主要是显示一级菜单,但这里没用app-link让该菜单具有跳转功能 -->
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
      </template>
      <!-- 具体的菜单项,遍历各个子菜单 -->
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>
  </div>
</template>

<script>
import path from 'path'
import {
    
     isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'

export default {
    
    
  name: 'SidebarItem',
  components: {
    
     Item, AppLink },
  // 通过 `mixins` 可以将一些常用的选项(如 `data`、`methods`、`created` 等)或复杂逻辑封装成可复用的模块,并在多个组件中混入使用
  mixins: [FixiOSBug],
  // 用于定义组件的属性。可以是数组、对象或具体的属性定义。通过 props 可以接收组件外部传入的数据,并在组件内部使用
  props: {
    
    
    // route object
    item: {
    
    
      type: Object,
      required: true
    },
    isNest: {
    
    
      type: Boolean,
      default: false
    },
    basePath: {
    
    
      type: String,
      default: ''
    }
  },
  data() {
    
    
    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
    // TODO: refactor with render function
    // 当前route的子菜单
    this.onlyOneChild = null
    return {
    
    }
  },
  methods: {
    
    
    hasOneShowingChild(children = [], parent) {
    
    
      const showingChildren = children.filter(item => {
    
    
        if (item.hidden) {
    
    
          return false
        } else {
    
    
          // Temp set(will be used if only has one showing child)
          this.onlyOneChild = item
          return true
        }
      })

      // 只存在一个子菜单
      // When there is only one child router, the child router is displayed by default
      if (showingChildren.length === 1) {
    
    
        return true
      }

      // 不存在菜单
      // Show parent if there are no child router to display
      if (showingChildren.length === 0) {
    
    
        // 如果没有子菜单显示,将父级菜单的一些属性复制到`onlyOneChild`对象中,同时设置`onlyOneChild`的`path`为空字符串,
        // 并设置`noShowingChildren`属性为`true`,表示没有子菜单显示。最后返回`true`。
        this.onlyOneChild = {
    
     ... parent, path: '', noShowingChildren: true }
        return true
      }
      // 子菜单存在多个的时候
      return false
    },
    resolvePath(routePath) {
    
    
      // `isExternal(routePath)`函数判断`routePath`是否为外部链接,如果是外部链接,则直接返回`routePath`,不做处理
      // 当只有唯一子菜单时,`routePath`是子菜单路径
      if (isExternal(routePath)) {
    
    
        return routePath
      }
      // 当只有唯一子菜单时,`basePath`是父级菜单的路径
      if (isExternal(this.basePath)) {
    
    
        return this.basePath
      }
      // `path.resolve()`函数将`this.basePath`和`routePath`拼接成一个完整的路径,并返回该路径
      // 返回 `/父级菜单的路径/子菜单路径`
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>

2.3 Link.vue

<template>
  <!-- eslint-disable vue/require-component-is -->
  <component v-bind="linkProps(to)">
    <slot />
  </component>
</template>

<script>
import {
    
     isExternal } from '@/utils/validate'

export default {
    
    
  props: {
    
    
    to: {
    
    
      type: String,
      required: true
    }
  },
  methods: {
    
    
    linkProps(url) {
    
    
      if (isExternal(url)) {
    
    
        return {
    
    
          is: 'a', // 标签名
          href: url, // 链接
          target: '_blank', // 用于指定链接在哪个窗口或框架中打开,`_blank`在新的标签页中打开链接
          rel: 'noopener' // 用于指定链接与当前页面之间的关系,当使用`target="_blank"`时,防止新打开的窗口通过`window.opener`属性访问到当前页面的信息,提高安全性。
        }
      }
      return {
    
    
        is: 'router-link', // 标签名
        to: url
      }
    }
  }
}
</script>

猜你喜欢

转载自blog.csdn.net/weixin_42516475/article/details/132016679