先上效果图
达成这样的效果,其实根本在于,通过开启el-menu的route属性,点击menu菜单后会进行路径的跳转,但是你跳转的路由地址必须写为根组件的子路由地址,否则会直接进行整个页面的跳转,失去了这样的效果。
然后看一下el-tabs,这里放一下elementUI的官方文档
子元素都是可以通过for遍历循环出来的,所以到了这里,基本的思路应该就是通过watch来监听$route,根据跳转的路径,在遍历tabs的数组里面进行判断,来动态的增加或者进行路由跳转。
同时,这里面有多个组件之间的通信,我们可以使用vuex,在这里做个最基本的应用。
下面开始上代码。
首先,我的页面结构布局是 左侧的菜单,右侧的头部和主体内容,直接使用element的Container布局容器
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
先来看看左侧的Menu的代码:
<template>
<div class="menu">
<el-menu
:default-active="String(activeNav)"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="getToggle"
:collapse-transition="false"
unique-opened
router
>
<div class="menu_logo">
<img :src="logoImg" alt="" />
<span v-show="!getToggle">{{ menuTitle }}</span>
</div>
<fragment v-for="item in menuList" :key="item.key">
<!-- 一级菜单 -->
<el-submenu
v-if="item.children && item.children.length"
:index="item.path"
:key="item.key"
>
<template slot="title"
><i class="icon iconfont" v-html="item.icon"></i
><span>{{ item.title }}</span></template
>
<!-- 二级菜单 -->
<template v-for="itemChild in item.children">
<el-submenu
v-if="
itemChild.children && itemChild.children.length
"
:index="itemChild.path"
:key="itemChild.key"
>
<template slot="title"
><i
class="icon iconfont"
v-html="itemChild.icon"
></i
><span>{{ itemChild.title }}</span></template
>
<!-- 三级菜单 -->
<el-menu-item
v-for="itemChild_Child in itemChild.children"
:index="itemChild_Child.path"
:key="itemChild_Child.key"
>
<i
class="icon iconfont"
v-html="itemChild_Child.icon"
></i
><span slot="title">{{
itemChild_Child.title
}}</span></el-menu-item
>
</el-submenu>
<el-menu-item
v-else
:index="itemChild.path"
:key="itemChild.key"
><i
class="icon iconfont"
v-html="itemChild.icon"
></i
><span slot="title">{{
itemChild.title
}}</span></el-menu-item
>
</template>
</el-submenu>
<el-menu-item v-else :index="item.path" :key="item.key"
><i class="icon iconfont" v-html="item.icon"></i
><span slot="title">{{ item.title }}</span></el-menu-item
>
</fragment>
</el-menu>
</div>
</template>
<script>
import sunLogo from '@/assets/image/sun-logo.png';
import { mapGetters, mapState } from 'vuex';
export default {
name: 'Menu',
props: {
menuTitle: {
type: String,
default: () => {
return '';
}
}
},
data() {
return {
logoImg: sunLogo
};
},
created() {},
mounted() {},
computed: {
activeNav() {
// 获取当前激活的导航
return this.$route.path;
},
...mapGetters(['getToggle']),
...mapState(['menuList'])
},
methods: {
handleOpen(key, keyPath) {},
handleClose(key, keyPath) {}
}
};
</script>
<style lang="less" scoped>
@import '~@/assets/css/common.less';
.menu {
::v-deep .el-menu {
border-right: none;
background-color: transparent;
.menu_logo {
text-align: center;
padding-top: 1.5rem;
margin-bottom: 1.5rem;
img {
width: 2.83rem;
height: 2.83rem;
vertical-align: middle;
margin-right: 0.5rem;
}
span {
font-size: 1.5rem;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
color: #ffffff;
height: 2.08rem;
line-height: 2.08rem;
}
}
.el-submenu__title,
.el-menu-item {
font-size: 1.17rem;
height: 4rem;
line-height: 4rem;
color: @theme-white;
transition: none;
.icon {
margin-right: 0.33rem;
color: @theme-white;
}
}
.el-submenu__title i {
color: @theme-white;
}
.el-submenu .el-menu-item {
height: 40px;
line-height: 40px;
}
.el-menu-item.is-active {
background-color: @theme-white;
color: @theme-text-color-primary;
border-top-left-radius: 25px;
border-bottom-left-radius: 25px;
position: relative;
.icon {
color: @theme-text-color-primary !important;
}
&::before {
content: '';
width: 0;
height: 0;
border: 4px solid transparent;
border-bottom-color: @theme-white;
border-right-color: @theme-white;
position: absolute;
top: -8px;
right: 0;
}
&::after {
content: '';
width: 0;
height: 0;
border: 4px solid transparent;
border-top-color: @theme-white;
border-right-color: @theme-white;
position: absolute;
bottom: -8px;
right: 0;
}
}
.el-menu--inline {
padding-left: 1.27rem;
}
.el-menu-item:focus,
.el-menu-item:hover,
.el-submenu__title:hover {
background-color: @theme-white;
border-top-left-radius: 25px;
border-bottom-left-radius: 25px;
color: @theme-text-color-primary;
position: relative;
.icon {
color: @theme-text-color-primary !important;
}
i {
color: @theme-text-color-primary !important;
}
&::before {
content: '';
width: 0;
height: 0;
border: 4px solid transparent;
border-bottom-color: @theme-white;
border-right-color: @theme-white;
position: absolute;
top: -8px;
right: 0;
}
&::after {
content: '';
width: 0;
height: 0;
border: 4px solid transparent;
border-top-color: @theme-white;
border-right-color: @theme-white;
position: absolute;
bottom: -8px;
right: 0;
}
}
}
}
</style>
其中,fragment标签是用来替代div使折叠菜单生效的,可以使用 npm i vue-fragment -D 下载包后,在main.js里面 import Fragment from 'vue-fragment' 来使用。
可以看到,是启用了route属性的,而 :default-active="String(activeNav)" 则是来根据跳转的菜单来进行当前选中的激活。
这个 getToggle 则是用来收缩菜单的, 也是放在了 vuex 里面,可以先暂时不考虑这个。
这个 menuList 我则是放在了vuex里面,里面的数据为:
menuList: [
{
key: 0,
title: '首页',
icon: '',
path: '/index'
},
{
key: 1,
title: '人员管理',
icon: '',
path: '/person_manage'
},
{
key: 2,
title: '事物管理',
icon: '',
path: 'drug_case_manage',
children: [
{
key: '2-1',
title: '物件管理',
icon: '',
path: '/case_manage'
},
{
key: '2-2',
title: '物件研判',
icon: '',
path: '/person_involved'
}
]
},
{
key: 3,
title: '一键搜',
icon: '',
path: '/search'
}
]
其中的icon是我本地使用的,这个可以不用管,接下来看下router.js里面的路由配置。
这是我router文件夹下面的路由配置
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '@/views/Home'),
redirect: '/guide',
children: [
{
// 引导页
path: '/guide',
name: 'Guide',
meta: {
name: 'guide',
title: '欢迎使用',
comp: 'guide'
},
component: () => import('@/views/Guide/guide')
},
{
// 首页
path: '/index',
name: 'Index',
meta: {
name: 'index',
title: '首页',
comp: 'Index'
},
component: () => import('@/views/index/Index')
},
{
path: '/search',
name: 'Search',
component: () => import('@/views/OneClickSearch/GoSearch.vue')
},
{
// 人员管理
path: '/person_manage',
name: 'PersonManage',
meta: {
name: 'PersonManage',
title: '人员管理',
comp: 'manage'
},
component: () => import('@/views/PersonManage/manage')
},
{
path: '/case_manage',
name: 'CaseManage',
meta: {
name: 'CaseManage',
title: '物品管理',
comp: 'CaseManage'
},
component: () => import('@/views/CaseManage/manage')
},
{
path: '/person_involved',
name: 'involved',
meta: {
name: 'involved',
title: '物件研判',
comp: 'involved'
},
component: () => import('@/views/CaseManage/involved')
},
{
path: '/search',
name: '一键搜',
meta: {
name: '一键搜',
title: '一键搜',
comp: 'Search'
},
component: () => import('@/views/OneClickSearch/GoSearch')
}
]
}
其中meta是自定义的属性值,可以在监听 $route 时,监听到定义的属性值。
将这些页面设为home根路径下面的子路径,并将home页面重定向到欢迎页,这样打开时直接显示的就是欢迎页面。
接下来就是tabs组件了,主要的逻辑功能都在这一部分。
先上代码:
<template>
<div>
<el-tabs
v-model="activeIndex"
type="card"
@tab-remove="removeTab"
@tab-click="clickTab"
>
<el-tab-pane
:key="item.name"
v-for="item in openTab"
:label="item.title"
:name="item.name"
:closable="item.closable"
>
<router-view></router-view>
</el-tab-pane>
</el-tabs>
</div>
</template>
其中的 activeIndex 就是tabs当前选中的,我们默认为欢迎页面,然后通过循环 openTab 这个数组来进行动态的添加或删除,而这个 closable 属性则是可以手动设置为是否可以关掉tabs的选项卡,我们在里面放一个 router-view 来进行子页面的跳转。
data 里面的则是:
data() {
return {
activeIndex: '/guide',
openTab: [
{
title: '引导页',
name: '/guide',
closable: false
}
]
};
}
这是初始数据,默认设置欢迎页,然后重点就是通过watch来监听 $route ,代码如下:
watch: {
$route(to, form) {
var flag = false;
// 当前页面菜单已打开
for (let i = 0; i < this.openTab.length; i++) {
if (to.path == this.openTab[i].name) {
this.activeIndex = this.openTab[i].name;
flag = true;
break;
}
}
// 打开新的页面
if (!flag) {
let obj = {
title: to.meta.title,
name: to.path,
closable: true
};
this.activeIndex = to.path;
this.openTab.push(obj);
}
}
}
我们加个自定义的 flag 属性来进行往 openTab 这个数组里面push数据的判断依据,同时,通过methods里面的 clickTab 方法来进行tabs页面的切换
clickTab(tab) {
this.activeIndex = tab.paneName;
this.$router.push({ path: this.activeIndex });
}
接下来是 removeTab 方法,关闭打开的tabs标签页
removeTab(target) {
// 删除的是当前选中的页面
if (this.activeIndex === target) {
this.openTab.forEach((item, index) => {
if (item.name == target) {
let nextTab = item[index + 1] || item[index - 1];
if (nextTab) {
this.activeIndex = nextTab.name;
}
}
});
}
var i = 0;
this.openTab.forEach((item, index) => {
if (item.name == target) {
// eslint-disable-next-line no-return-assign
return (i = index);
}
});
this.openTab.splice(i, 1);
// 更新路由
this.$router.push({ path: this.openTab[this.openTab.length - 1].name });
}
到这里,基本的页面跳转以及切换就差不多了,但是我们还需要再补充一下,当我们打开几个页面时,进行刷新,就会发现重置了,这时需要在 mounted 里面添加如下代码:
mounted() {
// 监听页面加载前
window.addEventListener('beforeunload', e => {
sessionStorage.setItem(
'openTab',
JSON.stringify({
openTab: this.openTab.filter(
item => item.name != '/guide'
),
openTabPath: this.openTab.filter(
item => item.name !== '/guide'
),
currActiveTabs: this.activeIndex
})
);
});
}
同时,在created里面进行读取本地 sessionStorage 来进行判断:
created() {
const sessionTab = JSON.parse(window.sessionStorage.getItem('openTab')) || '';
if (sessionTab) {
if (sessionTab.openTab.length != 0 && sessionTab.openTabPath.length != 0) {
for (let i = 0; i < sessionTab.openTab.length; i++) {
this.openTab.push({
title: sessionTab.openTab[i].title,
name: sessionTab.openTab[i].name,
closable: true
});
}
this.activeIndex = sessionTab.currActiveTabs;
this.$router.push({ path: this.activeIndex });
}
}
}
这样,在打开几个页面后,刷新页面之前,保留已打开的页面,刷新之后,还是显示之前打开过的页面,至此,基本的流程已经结束。
我把这些功能都单独拿出去了,所以就贴上了基本的代码,主要的逻辑流程就是这些,其中可能会有些问题,我们还是需要自己来一一解决。
框架完整代码可以在码云上面去看
https://gitee.com/chenkai777/vue_demo