前言
根据公司业务需求,需要一个顶部菜单和侧边菜单联动的后台管理系统,在接受这个需求后就在脑子里构思框架应该怎么搭,之后就想到两种方式
方案一:顶部菜单触发后 在子面嵌入左侧侧边栏,但是这个方法会让每个有侧边菜单的页面,都要引入一遍侧边栏组件.
方案二:就是和顶部菜单写在一个页面,通过判断是否有侧边栏,是否展示侧边栏组件,这样的好处就是每次新增菜单时不用多次引入侧边菜单栏
实践永远大于理论 直接动手干. 原本是个以为是个简单的需求,结果随着项目完成的进度,问题也在一点点出现.接下来看看如何实现的以及碰到的问题.
效果演示
本系统是用方案二来实现的,接下来先看下效果:
开发需求
开发准备
首先需要创建个项目,此步骤就不一一赘述了,我们从项目创建完成开始.
需要用到element 如果不清楚如何安装的可以去官网看下很简单的 传送门
页面配置
首先我们需要新建几个菜单文件,文件的存放目录自定,我的目录如下
其中home页面是我们整个项目除App页面外的根页面,所有的页面都会通过这个页面进行呈现,菜单跳转也在这个页面进行开发。
路由配置
首先我们在router文件夹下的index文件中把home页面引入 作为根路由
import Vue from "vue";
import VueRouter from "vue-router";
import allRoutes from "./allRoutes";
//解决路由导航到统一路径重复报错的问题
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err);
};
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "home",
redirect: "/overview",
component: () => import(/* webpackChunkName: "home" */ "../views/home/index"),
children: [...allRoutes],
},
];
const router = new VueRouter({
routes,
});
export default router;
再在router文件夹下新建allRoutes.js文件 ,此页面将是我们把所有页面的路由配在此页面,具体格式如下:
const allRoutes = [
{
path: "system",
name: "system",
menuId: 4,
redirect: "/system/menu",
component: () => import(/*webpackChunkName:"overview"*/ "../views/system"),
meta: {
//储存一些属性
fullPath: "/system",
// icon: "iconfont icon-shujudaping",
title: "系统设置",
showType: true, //是否展示
oneMenuId: 0,
},
children: [
{
path: "menu",
naem: "menu",
menuId: 433,
component: () =>
import(/*webpackChunkName:"overview"*/ "../views/system/menu"),
meta: {
//储存一些属性
fullPath: "/system/menu",
icon: "el-icon-menu",
title: "菜单管理",
showType: true, //是否展示
oneMenuId: 4,//展示侧边栏的重要标识 一定是父路由的menuId
},
},
],
},
];
export default allRoutes;
allRoutes文件会多次用到,这个文件也是这个项目的核心之一。
home页面实现
home页的整体布局如下:
<template>
<el-container class="page-home">
<el-header class="panel-header" height="78">
<el-row type="flex" style="width: 100%">
<el-col :span="6">
<div class="header-left">
<h1 class="header-h1 one-txt-cut">书语演示系统</h1>
</div>
</el-col>
<el-col :span="14">
<el-menu
:default-active="navIndex"
class="el-menu-nav"
mode="horizontal"
background-color="transparent"
router
>
<template v-for="(menu, index) in navMenus">
<el-menu-item
:key="index"
:index="menu.meta.fullPath"
@click="setLeft(menu, true)"
v-if="menu.meta.showType"
>
<i :class="menu.meta.icon"></i>
<span>{{ menu.meta.title }}</span>
</el-menu-item>
</template>
</el-menu>
</el-col>
<el-col :span="4">
<div class="header-right"></div>
</el-col>
</el-row>
</el-header>
<el-container class="plan-main">
<el-aside
:width="isCollapse ? '78px' : '240px'"
v-if="leftMenus.length > 0"
class="func-side"
>
<div
class="menuBtn"
:class="isCollapse ? 'menuBtn-shouqi' : ''"
>
<i
:class="[
isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold',
]"
@click="isCollapse = !isCollapse"
></i>
</div>
<el-menu
:default-active="leftNev"
active-text-color="#00D1FF"
:collapse="isCollapse"
:collapse-transition="false"
router
ref="menuA"
>
<SubMenu :sideMenu="leftMenus"></SubMenu>
</el-menu>
</el-aside>
<el-main :class="{ pd24: leftMenus.length > 0 }">
<div class="panel-crumbs" v-if="leftMenus.length > 0">
<span><i></i>当前位置:</span>
<el-breadcrumb separator="/">
<el-breadcrumb-item
:to="{ path: bread.meta.fullPath }"
v-for="bread in breadCrumb"
:key="bread.name"
>
{{ bread.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<router-view v-if="isChangeArea"></router-view>
</el-main>
</el-container>
</el-container>
</template>
js代码其实很简单就一个方法,但是会出现一些问题,如来回切换菜单的时候,页面已经进行了跳转,但是菜单没有被激活样式,这种现象还会在来回切换带有子菜单的时候 子菜单也会出现此现象,所以我在这个页面加了个监听方法,监听路由跳转,菜单是否被激活问题。
methods钩子函数中的方法:
// 左侧侧边栏
setLeft(arr, type = false) {
// 判断是否有左边菜单栏
if (arr.children && arr.children.length > 0) {
//顶部菜单栏激活
this.navIndex = "/" + arr.path;
this.leftNev = this.$route.meta.fullPath;
// 赋值左边菜单栏
this.leftMenus = arr.children;
} else {
// 顶部菜单栏激活
this.navIndex = arr.meta.fullPath;
// 如果没有左边菜单栏 则把左侧菜单栏清空
this.leftMenus = [];
}
},
watch监听中的方法:
此处还监听了leftMenu 这个字段是从vuex中取的 是为了解决切换菜单 修改了vuex里面的值,home页面的数据不更新的问题。
leftMenu: {
handler() {
this.setLeft(this.leftMenu);
},
immediate: true,
},
// 监听路由 解决顶部菜单从一个带有左侧菜单跳转到另一个带有左侧菜单 菜单没有激活样式问题
$route: {
handler: function (val) {
this.leftNev = val.path;
setTimeout(() => {
if (this.$refs.menuA) {
this.$refs.menuA.activeIndex = val.path;
}
}, 60);
},
// 深度观察监听
deep: true,
},
computed内容:
leftMenu字段是用来存放当前顶部菜单是否有子菜单的字段
computed: {
// vuex state定义的变量
...mapState([
"breadCrumb", //面包屑列表
"leftMenu",//左侧侧边栏列表
]),
},
此外在这个项目中还需要引入侧边菜单栏的组件 <SubMenu><SubMenu/>
这个组件我放在了全局的components里面了
具体内容如下:
<template>
<div>
<div v-for="(menu, index) in sideMenu" :key="index" class="side-menu1">
<el-submenu
v-if="menu.children && menu.children.length > 0 && menu.meta.showType"
:key="index"
:index="menu.meta.fullPath"
>
<template slot="title">
<i class="menuIcon" :class="menu.meta.icon"></i>
<span slot="title">{{ menu.meta.title }}</span>
</template>
<sub-menu
:sideMenu="menu.children"
:parentPath="'/' + menu.path"
></sub-menu>
</el-submenu>
<el-menu-item
v-else-if="menu.meta.showType"
:key="index"
:index="menu.meta.fullPath"
ref="selectMenu"
>
<i class="menuIcon" :class="menu.meta.icon"></i
><span slot="title">{{ menu.meta.title }}</span>
</el-menu-item>
</div>
</div>
</template>
<script>
export default {
name: "sub-menu", // 给组件取名字之后 可以递归
props: {
parentPath: {
type: String,
default: "",
},
sideMenu: {
type: Array,
default: () => [], // 定义默认数据
},
},
data() {
return {
};
},
methods: {},
mounted() {},
};
</script>
到此home页面告一段落了,接下来是让路由进行跳转的关键,需要在main.js文件中进行开发。
main.js文件
路由进行跳转需要借助两个路由钩子函数beforeEach,afterEach这两个钩子函数是路由跳转之前和跳转之后触发的,所以我们要在这两个钩子中进行接下来的操作。
allRoutes文件需要在main.js文件中引入 需要遍历路由表 来判断当前要跳转的菜单是否有子菜单。
router.beforeEach((to, from, next) => {
// 定义一个空的路由对象
let routerObj = {};
// 循环路由列表
allRoutes.forEach((item) => {
// 判断当前菜单有没有子集菜单 如果有子集菜单 则把当前菜单保存到vuex里否则直接进行跳转
if (
to.meta.oneMenuId == item.menuId ||
(JSON.stringify(to.meta) != "{}" &&
to.meta.fullPath.includes(item.meta.fullPath))
) {
// 如果是二级菜单把一级菜单对象存到路由对象中;
routerObj = item;
}
});
// 判断是否已经存了路由
if (JSON.stringify(routerObj) == "{}") {
// 没有存 代表是一级菜单 需要把当前菜单存进去
store.commit("SET_MENULIST", to);
} else {
// 否则代表二级菜单
store.commit("SET_MENULIST", routerObj);
}
//to 即将进入的路由
//from 即将离开的路由
//next 放行
next();
});
afterEach这个钩子方法我主要是用来设置面包屑了
router.afterEach((to) => {
//全局后置钩子
let matched = [...to.matched];
matched.splice(0, 1);
//调用commit触发mutation 更改vuex中存储的breadCrumb
store.commit("SET_BREADCRUMB", matched);
});
最后
到此这个项目的大致框架已经出来了,但是对于我司这个项目这个框架还是远远不够的,在此基础上又新增了顶级菜单,菜单的显示隐藏,以及菜单跳转外链等需求。
此后续如果有需要的话,在进行补充。以上是这个项目框架的主要代码呈现。
为什么要写这一篇文章,一是为了巩固自己的知识,让自己做一个从0搭建一个完整的项目,第二个原因就是,可以储备一个后台管理系统的模板,下次在遇到这种项目,可以直接克隆下来使用。
还望各位大佬多多指正,轻点喷,前端小白在此致谢