一个普通的vue权限管理方案-菜单权限控制

发布于:2025-03-21 ⋅ 阅读:(18) ⋅ 点赞:(0)

渲染左侧菜单

<template>
  <div class="sidebar">
    <el-menu
      ref="sideMenu"
      class="sidebar_menu"
      :default-active="activeNav"
      unique-opened
    >
      <div
        class="sidebar_item"
        v-for="sidebar in sidebarList"
        :key="sidebar.id"
      >
        <!-- 有子级 -->
        <el-submenu
          :index="sidebar.path"
          v-if="
            sidebar.children &&
            sidebar.children.length > 0 &&
            sidebar.name !== sidebar.children[0].name
          "
        >
          <template slot="title">
            <span>{{ sidebar.meta.title }}</span>
          </template>

          <div
            v-for="child1 in sidebar.children"
            :key="child1.name"
            class="sidebar_child"
          >
            <el-submenu
              :index="child1.path"
              v-if="child1.children && child1.children.length > 0"
            >
              <template slot="title">
                <span>{{ child1.meta.title }}</span>
              </template>

              <div
                v-for="child in child1.children"
                :key="child.name"
                class="sidebar_child"
              >
                <el-menu-item
                  :index="child.path"
                  :disabled="!child.path"
                  @click="handleClickMenu(child1.path, child.path)"
                >
                  <span slot="title">{{ child.name }}</span>
                </el-menu-item>
              </div>
            </el-submenu>
            <!-- 无子级 -->
            <el-menu-item
              v-else
              :index="child1.path"
              :disabled="!child1.path"
              @click="handleClickMenu(sidebar.path, child1.path)"
            >
              <span slot="title">{{ child1.name }}</span>
            </el-menu-item>
          </div>
        </el-submenu>
        <!-- 无子级 -->
        <el-menu-item
          v-else
          :index="sidebar.path + '/index'"
          :disabled="!sidebar.path"
          @click="handleClickMenu(sidebar.path)"
        >
          <span slot="title">{{ sidebar.name }}</span>
        </el-menu-item>
      </div>
    </el-menu>
  </div>
</template>

<script>
import { getUserFuncPerm } from '@/api/user'
export default {
  name: 'SideNavigation',
  props: {
    activePath: {
      type: String,
      default: '',
    },
  },
  data() {
    return {
      defaultActive: '/',
    }
  },

  computed: {
	sidebarList(){
	  	return this.$store.getter.sidebarList
	},
    activeNav() {
      return this.activePath ? this.activePath : this.defaultActive
    },
  },

  mounted() {
    this.getMenuList()
  },

  methods: {
    // 菜单
    getMenuList() {
      //
    },

    handleClickMenu(path, subPath) {
      const p = subPath ? subPath : `${path}/index`
      this.$router.push(p)
      this.defaultActive = p
    },

    // 跳转刷新
    handleClickMenuAndRefresh(path, subPath) {
      const p = subPath ? subPath : `${path}/index`
      // 先跳转至空白页,再跳转至目标页,实现刷新目标页
      this.$router.push({
        path: '/redirect',
        query: {
          path: p,
        },
      })
      this.defaultActive = p
    },
  },
}
</script>

首先需要渲染左侧菜单
vuex内部会存贮一个变量sidebarList, 这个变量的结构就是类似于权限接口的返回值,是一个层级结构。
如何存储sidebarList呢,
路由跳转之前会经过 router.beforeEach方法

当进入一个路由之前需要判断是否有能力进入

数据准备sidebarList

有一个获取左侧菜单权限的接口,得到数据

{
  "data": {
    "funcPerm": [
      {
        "children": [
          {
            "id": "10010",
            "name": "日程管理",
            "path": "/schedule/index", // 路由地址
            "component": "meeting/meetingRoom/index", // 页面地址
          }
        ],
        "component": "Layout",
        "id": "10000",
        "meta": {
          "title": "会议日程"
        },
        "name": "会议日程",
        "path": "/index",
      }
    ]
  },
  "errors": [],
  "msg": "操作成功",
  "status": "0"
}

上图是一个基本的权限接口的返回值

控制权限展示

import router from "./router";
import store from "./store";
import { Message } from "element-ui";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({ showSpinner: false });

const whiteList = ['/login'];
router.beforeEach(async (to, from, next) => {
  NProgress.start();
  let userInfo = {};
  const storedUserInfo = localStorage.getItem("userInfo");
  if (storedUserInfo) {
    userInfo = JSON.parse(storedUserInfo);
  }
  const userId = userInfo ? userInfo.userId : "";

  // 如果拉取到了登录人信息
  if (userId) {
    if (whiteList.includes(to.path)) {
      next();
      NProgress.done();
    } else {
      // 如果本地已经拉取过路由信息了
      const routerLength = store.getters.router_length;
      if (routerLength) {
        // 这里其实应该需要判断下目标路由是否在权限列表里面
        next();
        NProgress.done();
      } else {
        try {
          let accessRoutes = "";
          const params = {
            userId: userId,
          };
          const res = await store.dispatch("user/getUserFuncPerm", params);
          accessRoutes = await store.dispatch(
            "permission/generateRouter",
            res.funcPerm
          );
          router.addRoutes(accessRoutes);
          next({ ...to, replace: true });
        } catch (error) {
          Message.error(error || "Has Error");
          next(`/uim-view/#/login`);
          NProgress.done();
        }
      }
    }
  } else {
    if (whiteList.includes(to.path)) {
      next();
      NProgress.done();
    }
    // 这里应该还有登出操作
  }
});

router.afterEach(() => {
  NProgress.done();
});

上面代码中有个whiteList常量,这里是一个白名单,当路由进入白名单里面的的时候,直接渲染页面,保证登录页面一直保持着能登录的权限
当进入非白名单时,判断用户是否登录userId,没有登录进入登录页
如果登录过,则判断是否获取过用户的菜单权限,这里的菜单权限使用的时vuex保存的,router_length代表这用户菜单的长度,如果有值则表示获取过权限,直接进入对应页面。(其实,这里需要做一个权限判断,如果用户手动输入路由,且不在路由权限里,这里有问题)

动态路由

如果我们不写一个全静态的路由,全由后端的权限给的路径拼上路由列表

accessRoutes = await store.dispatch(
 "permission/generateRouter",
   res.funcPerm
 );

上面就是实现此功能的代码

  generateRouter({ commit }, data = []) {
    return new Promise(resolve => {
      let serverRouterMap = []
      serverRouterMap = data
      if (Array.isArray(data) && data.length === 0) {
        Message({
          message: '无权限, 请配置权限',
          type: 'warning',
          duration: 3 * 1000,
        })
        return
      }
      try {
        constantRoutes[0].redirect = data[0].children[0].path
      } catch (err) {
        console.log(err)
      }
      const createRouter = () =>
        new Router({
          scrollBehavior: () => ({
            y: 0,
          }),
          routes: constantRoutes,
        })
      router.matcher = createRouter().matcher
      serverRouterMap.push({ path: '*', redirect: '/404', hidden: true })
      const asyncRouterMap = filterAsyncRouter(serverRouterMap)
      commit('SET_ROUTES', asyncRouterMap)
      resolve(asyncRouterMap)
    })
  },
function filterAsyncRouter(asyncRouterMap) {
  // 遍历后台传来的路由字符串,转换为组件对象
  const accessedRouters = asyncRouterMap.filter(route => {
    if (route.component) {
      if (route.component === 'Layout') {
        // Layout组件特殊处理
        route.component = Layout
      } else {
        route.component = _import(route.component)
      }
    }
    if (route.children && route.children.length > 0) {
      route.children = filterAsyncRouter(route.children)
    } else {
      delete route.children
    }
    return true
  })
  return accessedRouters
}