vue + elementui implement dynamic side navigation bar

introduction

In the background management system, the front-end often needs to dynamically add routes according to the user's permissions to display the pages that the user can access. Based on this requirement, a dynamic side navigation bar is encapsulated according to the online information.

The permission parameters passed by the front end and the permissions returned by the back end are arrays, which contain strings.

router folder

// 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;

Create layout folder

<!-- 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 creates a SubMenu folder to encapsulate the dynamic sidebar

<!-- 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>

Dynamically obtain routes based on permissions in 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
});

Time to get route

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'
      });
    }
  }
});

Guess you like

Origin blog.csdn.net/m0_64344940/article/details/122542250