引言

后台管理系统中,前端往往需要根据用户的权限动态添加路由,以此来展现用户所能访问的页面,基于这个要求,根据网上的资料封装动态的侧边导航栏

前端传的权限参数和后端返回的权限都为数组,里面包含的是字符串

router 文件夹

// index.ts
import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from '@/views/login/index.vue';
import Layout from '@/layout/index.vue';

Vue.use(VueRouter);

/**
 * hidden 表示是否需要在侧边导航栏出现 ,true表示不需要
 * isFirst 表示是否只有一级权限,只出现在只有一个子集,没有其他孙子集
 * 当权限拥有多个子集或者孙子集,一级权限需要加上 meta
 * 二级权限拥有子集,也必须有 meta
 */

// 基础路由
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },
  {
    path: '/',
    redirect: '/dashboard',
    hidden: true
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    hidden: true
  },
  {
    path: '/dashboard',
    component: Layout,
    redirect: '/dashboard/index',
    isFirst: true,
    children: [
      {
        path: 'index',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        meta: {
          title: '首页',
          icon: 'el-icon-location'
        }
      }
    ]
  }
];

// 动态路由
export const asyncRoutes = [
  {
    path: '/form',
    component: Layout,
    redirect: '/form/index',
    isFirst: true,
    children: [
      {
        path: 'index',
        name: 'Form',
        component: () => import('@/views/form/index.vue'),
        meta: {
          title: '表单',
          role: 'form',
          icon: 'el-icon-location'
        }
      }
    ]
  },
  {
    path: '/editor',
    component: Layout,
    redirect: '/editor/index',
    meta: {
      role: 'editors',
      title: '总富文本',
      icon: 'el-icon-location'
    },
    children: [
      {
        path: 'index',
        name: 'Editor',
        component: () => import('@/views/editor/index.vue'),
        meta: {
          title: '富文本',
          role: 'editor',
          icon: 'el-icon-location'
        }
      },
      {
        path: 'two',
        name: 'Two',
        redirect: '/editor/two/three',
        component: () => import('@/views/editor/two.vue'),
        meta: {
          title: '二级导航',
          role: 'two',
          icon: 'el-icon-location'
        },
        children: [
          {
            path: 'three',
            name: 'Three',
            component: () => import('@/views/editor/three.vue'),
            meta: {
              title: '三级导航',
              role: 'three',
              icon: 'el-icon-location'
            }
          },
          {
            path: 'four',
            name: 'Four',
            component: () => import('@/views/editor/four.vue'),
            meta: {
              title: '三级导航2',
              role: 'four',
              icon: 'el-icon-location'
            }
          }
        ]
      }
    ]
  },
  {
    path: '/tree',
    component: Layout,
    redirect: '/tree/index',
    isFirst: true,
    children: [
      {
        path: 'index',
        name: 'Tree',
        component: () => import('@/views/tree/index.vue'),
        meta: {
          title: '树状图',
          role: 'tree',
          icon: 'el-icon-location'
        }
      }
    ]
  },
  {
    path: '/excel',
    component: Layout,
    redirect: '/excel/index',
    isFirst: true,
    children: [
      {
        path: 'index',
        name: 'Excel',
        component: () => import('@/views/excel/index.vue'),
        meta: {
          title: '导入导出',
          role: 'excel',
          icon: 'el-icon-location'
        }
      }
    ]
  }
];

// 出错跳转的路由
export const error = [
  // 404
  {
    path: '/404',
    component: () => import('@/views/error/index.vue'),
    hidden: true
  },
  {
    path: '*',
    redirect: '/404',
    hidden: true
  }
];

const createRouter = () =>
  new VueRouter({
    scrollBehavior: () => ({
      x: 0,
      y: 0
    }),
    routes: constantRoutes
  });

const router = createRouter();

// 刷新路由
export function resetRouter () {
  const newRouter = createRouter();
  (router as any).matcher = (newRouter as any).matcher;
}

export default router;

创建 layout 文件夹

<!-- index.vue 用于定义页面的基础布局 -->
<template>
  <div class="layout">
    <el-container>
      <!-- 头部 -->
      <el-header>
        <div class="header-content">
          <p class="header-tit">运营后台</p>
          <div class="user-info">
            <el-dropdown placement="bottom" @command="handleCommand">
              <div class="el-dropdown-link">
                <img src="" alt="" />
                <p class="header-username">小红</p>
                <i class="el-icon-arrow-down el-icon--right"></i>
              </div>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item command="info">个人信息</el-dropdown-item>
                <el-dropdown-item command="logout">退出登录</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </div>
        </div>
      </el-header>
      <el-container :class="{ hideSidebar: isCollapse }">
        <!-- 侧边导航栏 -->
        <el-aside class="sidebar-container">
          <el-scrollbar>
            <el-menu
              :collapse="isCollapse"
              :default-active="$router.currentRoute.path"
              :collapse-transition="false"
              background-color="#eee"
              text-color="#666"
              active-text-color="#0099ff"
              @select="handleSelect"
              v-if="permissionRoutes"
            >
              <template v-for="item in permissionRoutes">
                <el-menu-item
                  v-if="
                    !item.hidden && item.children.length === 1 && item.isFirst
                  "
                  :index="item.redirect"
                  :key="item.path"
                >
                  <i :class="item.children[0].meta.icon"></i>
                  <span slot="title">{{ item.children[0].meta.title }}</span>
                </el-menu-item>
                <sub-menu
                  v-if="!item.hidden && !item.isFirst"
                  :item="item"
                  :key="item.path"
                  :basePath="item.path"
                ></sub-menu>
              </template>
            </el-menu>
          </el-scrollbar>
        </el-aside>
        <!-- 主体内容 -->
        <el-main>
          <router-view />
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import { mapGetters } from 'vuex';
import SubMenu from '@/components/SubMenu/index.vue';
import { resetRouter } from '@/router';

export default Vue.extend({
  computed: {
    // 路由
    ...mapGetters(['permissionRoutes'])
  },
  methods: {
    // 页面跳转
    handleSelect (index: string) {
      if (this.$router.currentRoute.path === index) {
        return;
      }
      this.$router.push(index);
    },
    // 下拉框选择
    handleCommand (command: string) {
      if (command === 'logout') {
        localStorage.clear();
        resetRouter();
        this.$router.push({ name: 'Login' });
      }
    }
  },
  components: {
    SubMenu
  }
});
</script>

<style lang="less" scoped>
.layout {
  width: 100%;
  height: 100vh;
  .header-content {
    height: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: #fff;
    .header-tit {
      font-size: 18px;
      font-weight: bold;
    }
    .user-info {
      display: flex;
      align-items: center;
      .el-dropdown-link {
        display: flex;
        align-items: center;
        img {
          width: 35px;
          height: 35px;
          border-radius: 50%;
          margin-right: 10px;
        }
        .header-username {
          font-size: 16px;
          color: #fff;
        }
      }
    }
  }
}

/deep/.el-header {
  background-color: #333;
}
/deep/.el-main {
  background-color: #f2f2f2;
}
/deep/.el-scrollbar {
  height: 100%;
  background-color: #eee;
}

// 折叠展开动画
.sidebar-container {
  transition: width 0.28s;
  width: 200px !important;
  height: 100%;
  overflow: hidden;

  .el-menu {
    border: none;
    height: 100%;
    width: 100% !important;
  }
}
.hideSidebar {
  .sidebar-container {
    width: 60px !important;
  }
}
</style>

components 创建 SubMenu 文件夹封装动态侧边栏

<!-- index.vue-->
<template functional>
  <el-submenu :index="props.item.path" popper-append-to-body>
    <template slot="title">
      <i :class="props.item.meta.icon"></i>
      <span>{{ props.item.meta.title }}</span>
    </template>
    <template v-for="item in props.item.children">
      <el-menu-item
        :index="props.basePath + '/' + item.path"
        :key="item.path"
        v-if="!item.children"
      >
        <template slot="title">
          <i :class="item.meta.icon"></i>
          <span>{{ item.meta.title }}</span>
        </template>
      </el-menu-item>
      <sub-menu
        v-else
        :item="item"
        :key="item.path"
        :basePath="props.basePath + '/' + item.path"
      ></sub-menu>
    </template>
  </el-submenu>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'Submenu',
  props: {
    item: {
      type: Object,
      required: true
    },
    basePath: {
      type: String,
      required: true
    }
  }
});
</script>

<style lang="less" scoped>
</style>

vuex 中根据权限动态获取路由

// modules/permission.ts
import { getUserPermission } from '@/utils/localStorage';
import { constantRoutes, asyncRoutes, error } from '@/router';

// 判断是否有此权限路由
function hasPermissionRouter (roles: any, route: any) {
  if (route.children && route.children.length === 1 && !route.meta) {
    return roles.some((role: any) => route.children[0].meta.role === role);
  } else if (route.meta && route.meta.role) {
    return roles.some((role: any) => route.meta.role === role);
  } else {
    return false;
  }
}

// 获取用户路由
export function filterAsyncRoutes (router: any[], roles: any[]) {
  const resArr: any[] = [];

  router.forEach((route) => {
    if (hasPermissionRouter(roles, route)) {
      if (route.children && (route.children.length > 1 || route.meta)) {
        route.children = filterAsyncRoutes(route.children, roles);
      }
      resArr.push(route);
    }
  });
  return resArr;
}

const state = {
  // 所有路由(不包含 error 路由)
  routes: [],
  // 动态添加的路由
  addRoutes: []
};

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

const actions = {
  generateRoutes ({ commit }: { commit: any }) {
    return new Promise((resolve) => {
      const accessedRoutes = filterAsyncRoutes(
        asyncRoutes,
        JSON.parse(getUserPermission())
      );
      commit('SET_ROUTES', accessedRoutes);
      // error 路由需要放在最后,不然会出现所有路径都跳转到 404 页面的情况
      resolve(accessedRoutes.concat(error));
    });
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};
// getters.ts
const getters = {
  // 用户路由
  permissionRoutes: (state: any) => state.permission.routes
};
export default getters;
// index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters';

Vue.use(Vuex);

const modulesFiles = require.context('./modules', true, /\.ts$/);

const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  // set './permission.ts' => 'permission'
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1');
  const value = modulesFiles(modulePath);
  (modules as any)[moduleName] = value.default;
  return modules;
}, {});

export default new Vuex.Store({
  modules,
  getters
});

获取路由的时机

import router, { resetRouter } from './router';
import store from './store';
import { getUserPermission, setUserPermission } from '@/utils/localStorage';

// 判断是否初次或者刷新页面 0表示初次
let isRequest = 0;

router.beforeEach(async (to, from, next) => {
  async function init () {
    // 调用方法获取路由
    const accessRoutes = await store.dispatch('permission/generateRoutes');
    accessRoutes.forEach((route: any) => {
      router.addRoute(route);
    });
    isRequest = 1;
  }

  // const hasToken = getToken();
  const userPermission = JSON.parse(getUserPermission());

  // 判断条件根据项目更改
  if (userPermission.length) {
    if (to.path === '/login') {
      next({ path: '/' });
    }
    if (isRequest) {
      next();
    } else {
      // 刷新页面,在这里需要重新获取并设置权限
      const userPermission = [
        'form',
        'editor',
        'editors',
        'two',
        'three',
        'tree',
        'four'
      ];
      setUserPermission(JSON.stringify(userPermission));
      await init();
      next({ ...(to as any), replace: true });
    }
  } else {
    localStorage.clear();
    isRequest = 0;
    if (to.path === '/login') {
      next();
    } else {
      resetRouter();
      next({
        path: '/login'
      });
    }
  }
});

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐