Vue实现左侧菜单项与tab标签页的同步效果(有源码链接)

1、全部代码如下

// 主页面代码
<template>
  <el-container style="height: 100%; width: 100%; ">
    <!-- 左侧导航 -->
    <div :style="asideStyle" class="asideForm">
      <div class="user-photo">
        <a class="img" title="我的头像"><img src="../assets/timg.jpg" /></a>
        <p>您好!<span>梁先生</span></p>
      </div>
      <el-aside style="height: 671px;">
        <el-menu :router="true" :default-active="menuActive" @select="getSubmenu"
          :unique-opened="true"><!--2023-02-12修改为此-->
          <template v-for="item in $store.state.meunRouter">
            <!-- 遍历vuex维护的生成菜单的时候根据对象是否有subMenu来判断是否有二级菜单,没有就直接生成一级菜单 -->
            <!-- v-if="item.subMenu",判断遍历出来的对象是否有subMeun对象,有的话说明有二级菜单 -->
            <el-submenu v-if="item.subMenu" :index="item.title" :key="item.title">
              <template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
              <el-menu-item-group>
                <el-menu-item v-for="item_group in item.subMenu" :index="item_group.path" :key="item_group.title">
                  <i :class="item_group.icon"></i>{{ item_group.title }}
                </el-menu-item>
              </el-menu-item-group>
            </el-submenu>
            <!-- 没有二级菜单  :key值必须唯一,否则报错-->
            <el-menu-item v-else :index="item.path" :key="item.title + '_first'" class="first-menu"
              style="padding-left: 20px !important;">
              <i :class="item.icon"></i>
              <span slot="title">{{ item.title }}</span>
            </el-menu-item>
          </template>
        </el-menu>
      </el-aside>
    </div>
    <!-- 主内容区域 -->
    <el-main :style="mainStyle">
      <div class="mainUrl_box">
        <!-- tab区 -->
        <el-tabs type="card" v-model="active_name" @tab-click="tabClick" @tab-remove="tabRemove">
          <el-tab-pane :key="item.key" v-for="item in $store.state.routerMeunMessage" :label="item.title"
            :name="item.path" :closable="item.title == '后台首页' ? false : true">
            {{ item.title }}
          </el-tab-pane>
        </el-tabs>
      </div>
      <div class="main_body">
        <router-view />
      </div>
    </el-main>
  </el-container>
</template>
<script>
import store from '@/store';
import Header from "./Index/IndexHeader.vue";

export default {
  name: "IndexHome",
  //局部注册组件
  components: { Header },
  data() {
    return {
      isCollapse: false,
      icon: 'el-icon-s-unfold',
      tabindex: 0,
      //主内容区样式
      mainStyle: {
        position: "absolute",
        top: "55px",
        left: "200px",
        right: "0px",
      },
      asideStyle: {
        position: "fixed",
        backgroundColor: '#393D49',
        width: '200px',
        top: '55px',
        left: '0px',
        bottom: '0px',
        overflow: "hidden",
      },
      //刷新操作icon
      optionIcon: 'el-icon-arrow-down',
      //保存匹配到的菜单项的信息
      routerMeunMessage: [],
      //将监视到的路由赋值给active_name作为tab标签页的v-model绑定值,用来控制选中对应name的选项卡,本人给el-tab-pane的name赋予路由值
      //el-tabs中v-model绑定的值对应el-tab-pane中的name值,以此来指定选中的选项卡,选中的选项卡会被追加class,即样式修改,具体追加的看开发者工具
      active_name: '/home/admin',
      //当前激活菜单的 index,需要设置默认值
      menuActive: '/home/admin',
    }
  },
  //挂载完毕后
  mounted() {
    //刷新vuex共享值
    store.state.routerMeunMessage = [{ title: '后台首页', path: '/home/admin', key: 0 }]
    //刷新时或者重新进入页面时挂载主页与首页内容
    this.$router.push('/home/admin')
  },
  //监听函数
  watch: {
    // 通过监听路由来控制tab项的高亮,实现点击菜单项时对应的tab项也高亮
    $route: {
      handler(nowPath, oldPath) {
        console.log('indexAside监听到的路由:', nowPath.path, oldPath.path)
        //将当前路由赋值给 menuActive,这里用来控制激活菜单项
        this.menuActive = nowPath.path
        //获取点击存储了的tab项信息列表
        const routeMeunForVuex = store.state.routerMeunMessage
        console.log('indexAside监听routeMeunForVuex:', routeMeunForVuex)
        //监听到的当前路由
        const pathNew = nowPath.path
        //遍历判断获取的pathNew在数组routeMeunForVuex中的位置
        const getResultIndex = routeMeunForVuex.findIndex((item) => (item.path == pathNew))
        //若tab项中的name与v-model绑定的值一致,则该tab被选中
        this.active_name = routeMeunForVuex[getResultIndex].path
      },
      // 深度观察监听
      deep: true
    },
  },
  //方法对象
  methods: {
    DaddoCollapse() {
      this.isCollapse = !this.isCollapse;//布尔值可以这样取反,改变isCollapse的值

      if (!this.isCollapse) {//展开
        this.asideStyle.width = '200px';
        this.icon = 'el-icon-s-unfold';
        this.mainStyle.left = '200px'

      } else {
        this.asideStyle.width = '0px';
        this.icon = 'el-icon-s-fold';
        this.mainStyle.left = '0px'
      }
    },
    //aside相关函数-------------------------
    getSubmenu(path) {

    },
    //tab标签的相关函数------------------------

    //点击tab后切换路由,
    tabClick(nowTabMessage) {
      console.log('tab点击获取的数据:', nowTabMessage)
      //获取点击的tab的name值,这里name值为路由值
      const path = nowTabMessage.name
      if (path == this.$route.path) return;
      //这里跳转路由是控制菜单的高亮,通过点击tab来获取tab的点击回调值,从而来改变路由,而菜单通过defult-active来控制选中的菜单项
      this.$router.push(path)



    },
    //删除tab标签
    tabRemove(tabMessage) {
      //获取到的回调值为路由,因为我赋予的tab-pane的name为path,tab的删除回调值为name的值
      console.log('已删除tab', tabMessage);
      //先判断tab列表是否只有一条信息
      let routerMeunMessage = store.state.routerMeunMessage
      if (routerMeunMessage.length == 1) {
        this.active_name = routerMeunMessage[0].path
      } else {
        //获取点击了删除的tab项在store.state.routerMeunMessage(vuex数据共享) 列表中的位置
        let routerMeunMessage = store.state.routerMeunMessage
        const getTabIndex = routerMeunMessage.findIndex((item) => (item.path == tabMessage))
        //routerMeunMessage数据长度,长度在删除前计算,否则会逻辑出错
        const lengthMeunMessage = routerMeunMessage.length
        //删除指定的tab对象信息,在页面的tab中会消失
        routerMeunMessage.splice(getTabIndex, 1)
        //删除的tab存在两种情况,一是后面还有tab标签项,一种是其是最后一个tab项
        //若删除的为最后一个tab
        if (getTabIndex + 1 == lengthMeunMessage) {
          //高亮它的前一个tab项
          this.active_name = routerMeunMessage[getTabIndex - 1].path
          //菜单高亮也要对应,跳转路由使其defult-active获取最新的值来控制对应菜单项高亮,菜单的defult-active通过获取最新的路由来控制高亮
          this.$router.push(routerMeunMessage[getTabIndex - 1].path)
        }
        //若删除的不是最后一个tab项
        else {
          //高亮它的后一个tab项,这里直接使用getTabIndex,因为删除后后一个tab顶替了它原本的位置index
          this.active_name = routerMeunMessage[getTabIndex].path
          //菜单高亮也要对应,跳转路由使其defult-active获取最新的值来控制对应菜单项高亮
          this.$router.push(routerMeunMessage[getTabIndex].path)

        }
      }
    },
    //改变刷新页面图标
    optionIconChange(is_appear) {
      if (is_appear == true) {
        this.optionIcon = "el-icon-arrow-up"
      }
      if (is_appear == false) {
        this.optionIcon = "el-icon-arrow-down"
      }
    },
    //刷新当前页面
    refreshNowRouter() {
      this.$router.push(this.$route.path)
    },
    //刷新全部
    refreshAll() {

      this.$router.push('/home/admin')
      store.state.routerMeunMessage = [{ title: '后台首页', path: '/home/admin', key: 0 }]
    }
  }
};
</script >

<style lang="less">
.bgFirstTab {
  background-color: #009688 !important;
}

.bgColorNone {
  background-color: #fff !important;
}

.user-photo {
  height: 120px;
  width: 100%;
}

.user-photo .img {
  display: block;
  width: 80px;
  height: 80px;
  margin: 0 auto 10px;
  margin-top: 10px;

}

a {
  color: #333;
  text-decoration: none;
}

.user-photo .img img {
  display: block;
  border: none;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
  border: 4px solid #44576b;
  box-sizing: border-box;
}

.user-photo p {
  display: block;
  width: 100%;
  height: 25px;
  color: #ffffff;
  text-align: center;
  font-size: 12px;
  white-space: nowrap;
  line-height: 25px;
  overflow: hidden;
}

.el-header {
  background-color: #23262E;
  color: #333;
  line-height: 60px;
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;

}

//aside样式----------------------------------- 
.el-aside {
  color: #333;
  overflow-x: hidden;
  overflow-y: scroll !important;
  position: absolute;
  left: 0px;
  width: 100% !important;
}

.el-aside::-webkit-scrollbar {
  /* 设置滚动条不显示但仍可以实现滚动效果 */
  display: none !important;
}

.el-aside .el-menu {
  overflow-y: scroll !important;
}

.el-aside .el-menu::-webkit-scrollbar {
  /* 设置滚动条不显示但仍可以实现滚动效果 */
  display: none !important;
}

.el-main {
  /* 绝对定位 */
  position: absolute;
  top: 55px;
  left: 200px;
  bottom: 0;
  right: 0;
  padding: 0 !important;
  border-top: 5px solid #1AA094;
  border-left: 2px solid #1AA094;
  overflow: hidden !important;
}

.el-menu {
  border-right: none !important;
  background-color: #393D49 !important;
  /* 强制覆盖 */
  /* height: 100vh; */
  overflow-x: hidden;

}

.el-submenu__title {
  color: white !important;
}

.el-menu-item-group {
  background-color: rgba(0, 0, 0, 0.3) !important;
}

.el-menu-item-group__title {
  padding: 0 !important;
}

.el-menu-item {
  color: white !important;
  padding-left: 40px !important;
  text-align: left;
  // padding-left: 51px !important;
  height: 40px !important;

}

.el-submenu .el-menu-item {
  line-height: 45px !important;
}

.el-container {
  width: 100%;
}

.el-submenu__title:active {

  border-left: 4px solid #009688;
}

.el-submenu__title:hover {
  /* 伪元素,悬停在元素添加样式*/
  border-left: 4px solid #009688;
  background-color: #fefffe51 !important;
}

.el-menu-item:hover {
  /* 鼠标悬停伪元素,悬停后改变样式 */
  background-color: #5FB878 !important;
}

//.is-opened 设置动态展示,通过状态来启用
.is-opened {
  /* 此class是点击父级导航时才动态加上的,所以配合上伪元素使用是无效的,因为其也是通过事件产生 */
  border-left: 4px solid #009688;
}

.el-submenu__title {
  height: 45px !important;
  padding-left: 20px !important;
  text-align: left;
}

.el-menu-item,
.el-submenu__title {
  line-height: 45px !important;
}

.el-menu-item-group ul {
  overflow: hidden;
}

.first-menu {
  padding-left: 20px !important;
  // background-color: #009688 !important;
}

.first-menu:hover {
  background-color: #009688 !important;
}

//菜单项被点击选中后在其追加的class样式,设置选中菜单项的高亮的样式
.el-menu-item.is-active {
  background-color: #009688 !important;
}

//tab样式-----------------------------------
.mainUrl_box {
  margin-right: 138px;
  text-align: left;
  height: 40px;
  border-bottom: 1px solid #e2e2e2;
}

.main_body {
  top: 41px;
  position: absolute;
  bottom: 0;
  width: 100%;
  padding: 0;
  overflow: auto;

}


.el-dropdown {

  width: 120px;
  right: 10px;
  text-align: center;
  position: absolute !important;

}

.el-tabs--card>.el-tabs__header .el-tabs__item {
  border: none !important;
}

.el-tabs__content {
  display: none; //隐藏卡片标签块
}

// tab被激活时改变的背景颜色,tab项被选中后会被追加.is-active,由v-model="active_name"绑定的值控制选中的tab项
.el-tabs__item.is-top.is-active {
  background-color: #009688;
  color: #fff !important;
}

.el-tabs--card>.el-tabs__header .el-tabs__item {
  color: #000
}

.el-tabs--card>.el-tabs__header {
  border-bottom: none !important;
}
</style>

2、结构细化

2.1 利用vuex共享状态,在store中定义菜单列表结构

ant design vue layout让侧边栏可以拖曳_javascript

// 参考结构
 state: {
    //将后台首页路由改为与主页一样,方便后面设置默认显示该tab项
    routerMeunMessage: [{ title: '后台首页', path: '/home/admin', key: 0, icon: 'el-icon-s-home' }],
    //菜单列表
    meunRouter: [
      {
        title: "后台首页",
        path: "/home/admin",
        icon: "el-icon-s-home"
      },
      {
        title: '门诊管理',
        icon: "el-icon-menu",
        // subMenu就是二级菜单
        subMenu: [
          {
            title: "用户挂号",
            path: "/home/userReport",
            icon: "el-icon-user"
          },
          {
            title: "处方划价",
            path: "/home/prescriptionPricing",
            icon: "el-icon-shopping-cart-2"
          },
          {
            title: "项目划价",
            path: "/home/projectPricing",
            icon: "el-icon-document"
          },
          {
            title: "项目缴费",
            path: "/home/projectPayment",
            icon: "el-icon-money"
          },
          {
            title: "项目检查",
            path: "/home/projectChecks",
            icon: "el-icon-view"
          },
          {
            title: "药品缴费",
            path: "/home/medicinesPayment",
            icon: "el-icon-money"
          },
          {
            title: "门诊患者库",
            path: "/home/outpatientLibrary",
            icon: "el-icon-user-solid"
          },
        ],
      }
    ]
  },

2.2 遍历vuex中的菜单共享数据生成菜单

// 遍历菜单
<el-menu :router="true" :default-active="menuActive" @select="getSubmenu"
          :unique-opened="true"><!--2023-02-12修改为此-->
          <template v-for="item in $store.state.meunRouter">
            <!-- 遍历vuex菜单结构的时候根据对象是否有subMenu来判断是否有二级菜单,没有就直接生成一级菜单 -->
            <!-- v-if="item.subMenu",判断遍历出来的对象是否有subMeun对象,有的话说明有二级菜单 -->
            <el-submenu v-if="item.subMenu" :index="item.title" :key="item.title">
              <template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
              <el-menu-item-group>
                <el-menu-item v-for="item_group in item.subMenu" :index="item_group.path" :key="item_group.title">
                  <i :class="item_group.icon"></i>{{ item_group.title }}
                </el-menu-item>
              </el-menu-item-group>
            </el-submenu>
            <!-- 没有二级菜单  :key值必须唯一,否则报错-->
            <el-menu-item v-else :index="item.path" :key="item.title + '_first'" class="first-menu"
              style="padding-left: 20px !important;">
              <i :class="item.icon"></i>
              <span slot="title">{{ item.title }}</span>
            </el-menu-item>
          </template>
        </el-menu>

2.3 将点击了的菜单项信息保存到vuex中维护,并遍历生成tab标签页

2.3.1 tab标签页信息初始化

ant design vue layout让侧边栏可以拖曳_ico_02

2.3.2 利用导航守卫,获取点击菜单项对应的路由信息保存到上图中的列表中

全局前置守卫 router.beforeEach((to,from,next)=>{})有三个参数
to:表示要去哪里
from:表示从哪里来
next:表示通不通过

// 在main.js中使用
router.beforeEach((to, from, next) => {
  //导航守卫,可以获取router中的对应路由的信息
  let stateTitle = []//保存遍历的已保存的导航title
  let dict = {}
  const title = to.meta.title
  const path = to.path
  const key = to.meta.title
  //判断是否有title
  if (title) {
    dict.title = title
    dict.path = path
    dict.key = key
    let stateRouterMeunMessage = store.state.routerMeunMessage
    //遍历保存到vuex中的state的导航数据的title值,并添加到临时列表保存用于判断tab是否已经存在
    for (let item of stateRouterMeunMessage) {
      stateTitle.push(item.title)
    }
    let isExist = stateTitle.indexOf(title)
    // === -1 则不存在
    if (isExist == -1) {
      //保存到vuex中暴露的公共资源store的state中
      stateRouterMeunMessage.push(dict)
      console.log('store', store.state.routerMeunMessage)
    }
  }
  next();
})

2.3.3 部分路由参考

// 
{
    path: "/home",
    name: "home",
    component: IndexVue,
    children: [
      //注:后面登录成功后直接跳到该路由,默认挂载首页
      {
        path: "admin",//匹配到/home/admin后,子路由的组件会被渲染到父路由组件的<router-view/>中
        name: "backoffice",
        component: HomePage,
        meta: { title: '后台首页' },
      },
      //门诊管理
      {
        path: "userReport",
        name: "userReport",
        meta: { title: '用户挂号' },

        component: () =>
          import(/* webpackChunkName: "userReport" */ "../components/UserRegister.vue"),
      },
      {
        path: "prescriptionPricing",
        name: "prescriptionPricing",
        meta: { title: '处方划价' },
        component: () =>
          import(/* webpackChunkName: "userReport" */ "../components/PrescriptionPricing.vue"),
      },
      {
        path: "projectPricing",
        name: "projectPricing",
        meta: { title: '项目划价' },

        component: () =>
          import(/* webpackChunkName: "userReport" */ "../components/ProjectPricing.vue"),
      },
      {
        path: "projectPayment",
        name: "projectPayment",
        meta: { title: '项目缴费' },

        component: () =>
          import(/* webpackChunkName: "userReport" */ "../components/ProjectPayment.vue"),
      }
 }

2.3.4 遍历生成tab标签页

<el-tabs type="card" v-model="active_name" @tab-click="tabClick" @tab-remove="tabRemove">
   <el-tab-pane :key="item.key" v-for="item in $store.state.routerMeunMessage" :label="item.title"
     :name="item.path" :closable="item.title == '后台首页' ? false : true">

     <!-- 通过判断标签名字来判断是否可以关闭  :closable="item.name=='首页的名字'?true:false"-->
     {{ item.title }}
   </el-tab-pane>
 </el-tabs>

3、tab标签页与菜单项的同步逻辑方法

3.1 先初始化tab标签页与菜单项的默认激活值

// 变量初始化
   active_name: '/home/admin',
   //当前激活菜单的 index,需要设置默认值
   menuActive: '/home/admin',

3.2 当点击菜单项时,通过路由监听来控制tab标签项的同步高亮

//监听函数
  watch: {
    // 通过监听路由来控制tab项的高亮,实现点击菜单项时对应的tab项也高亮
    $route: {
      handler(nowPath, oldPath) {
        console.log('indexAside监听到的路由:', nowPath.path, oldPath.path)
        //将当前路由赋值给 menuActive,这里用来控制激活菜单项
        this.menuActive = nowPath.path
        //获取点击存储了的tab项信息列表
        const routeMeunForVuex = store.state.routerMeunMessage
        console.log('indexAside监听routeMeunForVuex:', routeMeunForVuex)
        //监听到的当前路由
        const pathNew = nowPath.path
        //遍历判断获取的pathNew在数组routeMeunForVuex中的位置
        const getResultIndex = routeMeunForVuex.findIndex((item) => (item.path == pathNew))
        //将匹配到的路由信息的路由赋值给tab的v-model进行绑定,以此来控制要选择的tab项,v-model="active_name"
        this.active_name = routeMeunForVuex[getResultIndex].path
      },
      // 深度观察监听
      deep: true
    },
  },

3.3 点击tab选项时实现菜单项同步高亮

分析:这里切换了tab的同时也切换了路由,而菜单中的:default-active="menuActive"绑定的值是通过路由监听获取的。

// 通过tab标签页点击事件的触发
   tabClick(nowTabMessage) {
      console.log('tab点击获取的数据:', nowTabMessage)
      //获取点击的tab的name值,这里name值为路由值
      const path = nowTabMessage.name
      if (path == this.$route.path) return;
      //这里跳转路由是控制菜单的高亮,通过点击tab来获取tab的点击回调值,从而来改变路由,而菜单通过defult-active来控制选中的菜单项
      this.$router.push(path)
    }

3.4 删除tab标签

//通过tab的删除事件触发
tabRemove(tabMessage) {
      //获取到的回调值为路由,因为我赋予的tab-pane的name为path,tab的删除回调值为name的值
      console.log('已删除tab', tabMessage);
      //先判断tab列表是否只有一条信息
      let routerMeunMessage = store.state.routerMeunMessage
      if (routerMeunMessage.length == 1) {
        this.active_name = routerMeunMessage[0].path
      } else {
        //获取点击了删除的tab项在store.state.routerMeunMessage(vuex数据共享) 列表中的位置
        let routerMeunMessage = store.state.routerMeunMessage
        const getTabIndex = routerMeunMessage.findIndex((item) => (item.path == tabMessage))
        //routerMeunMessage数据长度,长度在删除前计算,否则会逻辑出错
        const lengthMeunMessage = routerMeunMessage.length
        //删除指定的tab对象信息,在页面的tab中会消失
        routerMeunMessage.splice(getTabIndex, 1)
        //删除的tab存在两种情况,一是后面还有tab标签项,一种是其是最后一个tab项
        //若删除的为最后一个tab
        if (getTabIndex + 1 == lengthMeunMessage) {
          //高亮它的前一个tab项,这一步赋值可以省略,因为后面直接切换了路由,而active_name可以通过路由监听获取最新的值
          this.active_name = routerMeunMessage[getTabIndex - 1].path
          //菜单高亮也要对应,跳转路由使其defult-active获取最新的值来控制对应菜单项高亮,菜单的defult-active通过获取最新的路由来控制高亮,
          //因为其defult-active绑定的值是通过路由监听获取的
          this.$router.push(routerMeunMessage[getTabIndex - 1].path)
        }
        //若删除的不是最后一个tab项
        else {
          //高亮它的后一个tab项,这里直接使用getTabIndex,因为删除后后一个tab顶替了它原本的位置index
          //这一步赋值可以省略,因为后面直接切换了路由,而active_name可以通过路由监听获取最新的值
          this.active_name = routerMeunMessage[getTabIndex].path
          //菜单高亮也要对应,跳转路由使其defult-active获取最新的值来控制对应菜单项高亮,因为其defult-active绑定的值是通过路由监听获取的
          this.$router.push(routerMeunMessage[getTabIndex].path)
        }
      }
    }

4、效果


202302150108


系统网址:http://47.96.109.194:8088/login 账号test 123
效果源码:
链接:https://pan.baidu.com/s/1ibU7rGPYFTD1br16VNZyYQ
提取码:yygl