一、需求
需要实现效果:左侧菜单栏与右侧内容部分联动,当点击左侧的菜单,右侧会展示对应的tab,没有点击时,不展示(如刚进入页面没有点击菜单,则没有tab);点击后没有关闭tab再打开其他菜单(如测试项目2),则测试项目2的tab高亮为选中状态。
实现效果:
二、整体实现思路
1.环境:vue、element-ui
2.首先,在el-tab-pane中,是展示的tab(如上图的测试项目1、测试项目2)。因此我们创建了一个数组activeTabs来储存tab的信息。
- :label="getTabTitle(route.path)" 对应的则是tab展示的标题内容,我用了一个方法getTabTitle获取路由对应的标题。
- :name="route.meta.title" 则是与el-tabs下的 v-model="activeName"相对应,如果name的值与v-model的值一样,则表示当前选中的tab。
- 代码中,
v-model="activeName"
用于控制el-tabs
组件的当前活动标签,而:name="route.meta.title"
用于为每个标签指定一个唯一的名称,这样就可以通过这个名称来确定当前选中的标签。通过这两个指令影响到同一个数据(例如activeName
和route.meta.title
),以达到实现标签切换的效果。 - 当前选中的状态由
@tab-click="selectTab"
事件处理器决定。当点击一个标签时(tab-click
事件),会触发selectTab
方法。在selectTab
方法中,this.activeName
属性根据点击的标签的meta.title
进行更新。v-model="activeName"
会自动反映这个变化,使得el-tabs
组件根据activeName
的值来确定哪个标签是当前选中的,从而产生高亮效果。 - @edit="handleTabsEdit"是为了对tab做一些操作,点击 tabs 的新增按钮或 tab 被关闭后触发。
值得注意的是v-model="activeName"、selectTab、 :name="route.meta.title"几个的对应关系,以及其数据结构。
<el-tabs
v-model="activeName"
editable
@edit="handleTabsEdit"
@tab-click="selectTab"
>
<el-tab-pane
v-for="(route, index) in activeTabs"
:key="index"
:label="getTabTitle(route.path)"
:name="route.meta.title"
>
</el-tab-pane>
</el-tabs>
三、我遇到的问题
1.数据结构问题,一开始拿到的只是name或label、path,这样是不行的,最好还是拿到当前对应菜单栏的完整router,方便我们操作。
2.点击菜单栏对应的tab没有高亮但内容显示了
3.点击关闭tab,tab关闭了但下面的内容没有关闭,没有跳转到下一个剩余tab的内容。
4.点击tab之间相互切换,功能是正常的页面内容切换,但存在问题tab没有高亮,有时候要点击两次才会高亮,判段问题是出在没有更新调用selectTab。
四、具体代码
<template>
<section class="app-main">
<el-tabs
v-model="activeName"
editable
@edit="handleTabsEdit"
@tab-click="selectTab"
>
<el-tab-pane
v-for="(route, index) in activeTabs"
:key="index"
:label="getTabTitle(route.path)"
:name="route.meta.title"
>
</el-tab-pane>
</el-tabs>
<transition name="fade-transform" mode="out-in"> //展示具体页面内容
<content //自定义组件
>
<div class="router-inner-container">
<router-view v-show="activeName" :key="key" /> //v-show="activeName"非常重要,注意不要用v-if,用来与当前tab对应,即关闭tab也关闭当前内容。
</div>
</content>
</transition>
</section>
</template>
<script>
import { mapGetters } from 'vuex'; //引入vuex,来拿到我的所有菜单路由
export default {
name: 'AppMain',
props: {
noTag: {
type: Boolean,
default: false
},
noMap: {
type: Boolean,
default: false
}
},
data() {
return {
activeName: null, // 默认选中第一个 tab
activeTabs: [] // 存储当前显示的 tab
};
},
computed: {
...mapGetters(['sidebar', 'sidebarRouters']), //菜单所有路由sidebarRouters
key() {
return this.$route.path;
},
isStatisticsView() {
if (this.$route.path == '/home/index') {
return true;
} else {
return false;
}
}
},
methods: {
selectTab(tab, event) {
if (Array.isArray(tab) && tab.length > 0) { //由于数据结构问题做的判段,拿到activeName
this.activeName = tab[0].meta.title;
} else if (typeof tab === 'object' && tab.meta) {
this.activeName = tab.meta.title;
} else if (Array.isArray(tab) && !tab.length > 0) {
this.activeName = ''; //当所有tab都关闭时,关闭所有内容,否则页面会tab都关闭了,但还有最后一个关闭的tab的页面内容。
}
},
handleTabsEdit(targetName, action) {
// 处理标签页编辑事件
if (action === 'remove') {
// 删除标签页
this.removeTab(targetName);
}
},
removeTab(targetName) {
const tabs = this.activeTabs;
const index = tabs.findIndex((tab) => tab.meta.title === targetName);
if (index !== -1) {
tabs.splice(index, 1);
this.selectTab(tabs, event); //很重要,更新activeName,否则删除后不会切换下一个对应tab也不会切换页面内容
}
},
updateActiveTabs() {
const currentPath = this.$route;
// 判断对象是否在 activeTabs 中存在
const existsInTabs = this.activeTabs.some(
(tab) => tab.hasOwnProperty('path') && tab.path === currentPath.path
);
if (!existsInTabs) {
// 如果当前路由不在 activeTabs 中,将其添加进去
this.activeTabs.push(currentPath);
}
this.selectTab(currentPath, event); //很重要,更新activeName,否则切换菜单栏时对应的tab不会高亮不会切换
},
findMatchingRoute(routes, targetPath) { //为了处理数组的
for (const route of routes) {
if (route.path === targetPath) {
return route;
}
if (route.children && route.children.length > 0) {
const matchingChild = this.findMatchingRoute(
route.children,
targetPath
);
if (matchingChild) {
return matchingChild;
}
}
}
return null;
},
getTabTitle(route) {
// 根据路由信息获取对应的标题
const matchingRoute = this.findMatchingRoute(this.sidebarRouters, route);
return matchingRoute ? matchingRoute.meta.title : '';
},
findMatchingMenu(routes, targetTitle) {
// 递归查找匹配的菜单项
for (const route of routes) {
if (route.meta) {
if (route.meta.title === targetTitle) {
return route;
}
if (route.children && route.children.length > 0) {
const matchingChild = this.findMatchingMenu(
route.children,
targetTitle
);
if (matchingChild) {
return matchingChild;
}
}
}
}
return null;
}
},
mounted() {},
watch: {
$route(to, from) {
if (to && to.meta && !to.meta.noMap) {
this.$nextTick(() => {
this.$refs.jmap.resizeMap();
});
}
// 更新 activeTabs
this.updateActiveTabs();
},
activeName: {
immediate: true,
deep: true,
handler(val, oldVal) {
if (val && val !== oldVal) {
const matchingMenu = this.findMatchingMenu(this.sidebarRouters, val);
if (matchingMenu) {
this.$router.push({ path: matchingMenu.path }); //切换tab时,切换路由来对应想要页面内容
}
}
}
}
}
};
</script>
<style lang="scss">
.app-main {
/*50 = navbar */
height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f3f3f3;
&.no-header {
height: 100vh;
}
}
.fixed-header + .app-main {
padding-top: 60px;
}
.single-layout {
.app-main {
padding: 0px;
}
}
.map-style + .router-inner-container {
position: absolute;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
.no-header {
.float-panel {
height: 100vh;
}
}
</style>