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中定义菜单列表结构
// 参考结构
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标签页信息初始化
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