一个顶部菜单和侧边菜单联动的后台管理系统

1,384 阅读5分钟

前言

根据公司业务需求,需要一个顶部菜单和侧边菜单联动的后台管理系统,在接受这个需求后就在脑子里构思框架应该怎么搭,之后就想到两种方式

方案一:顶部菜单触发后 在子面嵌入左侧侧边栏,但是这个方法会让每个有侧边菜单的页面,都要引入一遍侧边栏组件.

方案二:就是和顶部菜单写在一个页面,通过判断是否有侧边栏,是否展示侧边栏组件,这样的好处就是每次新增菜单时不用多次引入侧边菜单栏

实践永远大于理论 直接动手干. 原本是个以为是个简单的需求,结果随着项目完成的进度,问题也在一点点出现.接下来看看如何实现的以及碰到的问题.

效果演示

本系统是用方案二来实现的,接下来先看下效果:

sy.gif

开发需求

开发准备

首先需要创建个项目,此步骤就不一一赘述了,我们从项目创建完成开始.

需要用到element 如果不清楚如何安装的可以去官网看下很简单的 传送门

页面配置

首先我们需要新建几个菜单文件,文件的存放目录自定,我的目录如下

1667992441502.png

其中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文件

路由进行跳转需要借助两个路由钩子函数beforeEachafterEach这两个钩子函数是路由跳转之前和跳转之后触发的,所以我们要在这两个钩子中进行接下来的操作。

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搭建一个完整的项目,第二个原因就是,可以储备一个后台管理系统的模板,下次在遇到这种项目,可以直接克隆下来使用。

还望各位大佬多多指正,轻点喷,前端小白在此致谢