vue + elementui 实现动态侧边导航栏
引言
后台管理系统中,前端往往需要根据用户的权限动态添加路由,以此来展现用户所能访问的页面,基于这个要求,根据网上的资料封装动态的侧边导航栏
前端传的权限参数和后端返回的权限都为数组,里面包含的是字符串
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'
});
}
}
});