后端管理系统,需要添加tabs需要,所以,使用vue做了一个动态添加tabs和删除tabs的功能。
最后的效果图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200311161712356.gif
实现功能步骤:
1.明确需求,听过elementUI官方例子生成一个Nav
elementUI官方的左侧导航栏例子
2.点击左侧导航栏,动态在右侧生成一个tabs并显示对应的内容(可删除)
3.首页固定,不可删除,其余动态生成的tabs皆可以删除
4.点击tabs右上角的删除时,如果当前tabs未活跃,则不需要重新处理活跃状态,如果当前tabs正处于活跃状态,则通过elementUI的思路处理下一个需要处于活跃的tabs
5.刷新浏览器的时候,希望当前tabs顺序可以保存,待浏览器刷新完毕,原封不动的重新展示。
1)、以下的全部的代码
<template>
<el-container>
<el-header>
<div style="float: left; cursor:pointer" @click="isOpen">
<i :class="{'el-icon-s-fold': isCollapse == false?true:false, 'el-icon-s-unfold': isCollapse == true?true:false}"></i>
</div>
<el-dropdown trigger="click" style="cursor: pointer; float: right">
<i class="fa fa-ellipsis-v fa-lg"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>关闭全部页面</el-dropdown-item>
<el-dropdown-item>刷新页面</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-header>
<el-container :style="{height: setHeight+'px'}">
<el-aside style="width: auto">
<el-menu :default-active="String(activeNav)" router class="el-menu-vertical-demo" :collapse="isCollapse">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-setting"></i>
<span slot="title">基础设置</span>
</template>
<el-menu-item index="/nav">导航</el-menu-item>
<el-menu-item index="/mineInfo">个人信息</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="fa fa-book"></i>
<span slot="title">文章管理</span>
</template>
<el-menu-item index="/fuillEditor">fuillEditor</el-menu-item>
<el-menu-item index="/category">分类管理</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-main :style="{height: setHeight+'px', padding: 0}">
<el-tabs v-model="activeTab" type="card" @tab-remove="removeTab" @tab-click="tabClick">
<el-tab-pane v-for="(item, index) in tabsItem"
:key="item.name"
:label="item.title"
:name="item.name"
:closable="item.closable"
:ref="item.ref">
<component :is="item.content"></component>
</el-tab-pane>
</el-tabs>
</el-main>
</el-container>
</el-container>
</template>
<script>
import welcome from '../views/Welcome'
import setNav from '../views/SetNav'
import mineInfo from '../views/MineInfo'
import fuillEditor from "../views/FuillEditor";
import category from "../views/Category";
export default {
name: "Home",
data() {
return {
isCollapse: false, //false为展开 true为收缩
activeTab: '1', //默认显示的tab
tabIndex: 1, //tab目前显示数
tabsItem: [
{
title: '首页',
name: '1',
closable: false,
ref: 'tabs',
content: welcome
}
],
tabsPath: [{
name: "1",
path: '/welcome'
}]
}
},
computed: {
setHeight() {
return document.documentElement.clientHeight - 65
},
activeNav() { //当前激活的导航
return this.$route.path
}
},
watch: {
'$route': function (to) { //监听路由的变化,动态生成tabs
let flag = true //判断是否需要新增页面
const path = to.path
if (Object.keys(to.meta).length != 0) {
for (let i = 0; i < this.$refs.tabs.length; i++) {
if (i != 0) { //首页不判断 如果页面已存在,则直接定位当页面,否则新增tab页面
if (this.$refs.tabs[i].label == to.meta.name) {
this.activeTab = this.$refs.tabs[i].name //定位到已打开页面
flag = false
break
}
}
}
//新增页面
if (flag) {
//获得路由元数据的name和组件名
const thisName = to.meta.name
const thisComp = to.meta.comp
//对tabs的当前激活下标和tabs数量进行自加
let newActiveIndex = ++this.tabIndex + ''
//动态双向追加tabs
this.tabsItem.push({
title: thisName,
name: String(newActiveIndex),
closable: true,
ref: 'tabs',
content: thisComp
})
this.activeTab = newActiveIndex
/*
* 当添加tabs的时候,把当前tabs的name作为key,path作为value存入tabsPath数组中
* key:tabs的name
* value:tabs的path
* {
* key: name,
* value: path
* }
* ///后面需要得到当前tabs的时候可以通过当前tabs的name获得path
* */
if (this.tabsPath.indexOf(path) == -1) {
this.tabsPath.push({
name: newActiveIndex,
path: path
})
}
}
}
}
},
methods: {
isOpen() { //判断左侧栏是否展开或收缩
if (this.isCollapse == false) {
this.isCollapse = true
} else {
this.isCollapse = false
}
},
removeTab(targetName) { //删除Tab
let tabs = this.tabsItem; //当前显示的tab数组
let activeName = this.activeTab; //点前活跃的tab
//如果当前tab正活跃 被删除时执行
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
let nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
activeName = nextTab.name;
this.tabClick(nextTab)
}
}
});
}
this.activeTab = activeName;
this.tabsItem = tabs.filter(tab => tab.name !== targetName);
//在tabsPath中删除当前被删除tab的path
this.tabsPath = this.tabsPath.filter(item => item.name !== targetName)
},
tabClick(thisTab) {
/*
* thisTab:当前选中的tabs的实例
* 通过当前选中tabs的实例获得当前实例的path 重新定位路由
* */
let val = this.tabsPath.filter(item => thisTab.name == item.name)
this.$router.push({
path: val[0].path
})
}
},
mounted() {
/*
* 监听页面刷新事件
* 页面刷新前 需要保存当前打开的tabs的位置,刷新后按刷新前的顺序展示
* 使用js的sessionStorage保存刷新页面前的数据
* */
window.addEventListener('beforeunload', e => {
sessionStorage.setItem("tabsItem", JSON.stringify({
currTabsItem: this.tabsItem.filter(item => item.name !== "1"),
currTabsPath: this.tabsPath.filter(item => item.name !== "1"),
currActiveTabs: this.activeTab,
currIndex: this.tabIndex
}))
});
},
created() {
/*
* 使用js的sessionStorage读取刷新前的数据,并按刷新前的tabs顺序重新生成tabs
* */
const sessionTabs = JSON.parse(sessionStorage.getItem("tabsItem"))
if(sessionTabs.currTabsItem.length != 0 && sessionTabs.currTabsPath.length != 0) {
for(let i=0; i<sessionTabs.currTabsItem.length; i++) {
this.tabsItem.push({
title: sessionTabs.currTabsItem[i].title,
name: sessionTabs.currTabsItem[i].name,
closable: true,
ref: sessionTabs.currTabsItem[i].ref,
content: sessionTabs.currTabsItem[i].content
})
}
for(let j=0; j<sessionTabs.currTabsPath.length; j++) {
this.tabsPath.push({
name: sessionTabs.currTabsPath[j].name,
path: sessionTabs.currTabsPath[j].path
})
}
this.activeTab = sessionTabs.currActiveTabs
this.tabIndex = sessionTabs.currIndex
//避免强制修改url 出现浏览器的url输入框的路径和当前tabs选中的路由路径不匹配
const activePath = this.tabsPath.filter(item => item.name == this.activeTab)
this.$router.push({
path: activePath[0].path
})
}
},
components: {
welcome,
setNav,
mineInfo,
fuillEditor,
category
}
}
</script>
<style scoped>
@import "~font-awesome/css/font-awesome.min.css";
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
height: 100%;
}
.el-menu--collapse {
height: 100%;
}
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
.el-submenu [class^=fa] {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
font-size: 16px;
}
</style>
2)、出于需求,静态数据相对来说不太友好,不利于拓展,所以写了一个加载本地的json数据,动态来生成Aside导航功能(主要是递归组件).
说明:动态生成aside导航,只附上核心代码,其余代码和静态的一样,小伙伴们可以细心的上去查看,谢谢。
<!--这个是elementUI的aside导航的最外面一层,所以不用多说,直接按官方的写上-->
<el-menu :class="colorObject"
:default-active="String(activeNav)" router
:collapse="isCollapse">
<!--动态生成sideItem-->
<template v-for="(item, parentIndex) in nav.nav"><!--本地public文件夹下/json的数据-->
<SideNav :item="item" :index="parentIndex"></SideNav>
</template>
</el-menu>
##SideNav子组件代码:
<template>
<div :class="{'menu-wrapper': state}">
<!--有子导航-->
<el-submenu v-if="item.child" :index="String(index+1)">
<template slot="title">
<!--如果item有icon才添加icon图标-->
<i v-if="item.icon" :class="item.icon"></i>
<span slot="title">{{item.title}}</span>
</template>
<!--遍历子导航-->
<!--如果子导航中没有存在下一级的子导航 则是<el-menu-item> 否则为<el-submenu>-->
<template v-for="(cItem, cIndex) in item.child" :index="String(index+1+'-'+cIndex+1)">
<el-menu-item v-if="!cItem.child" :index="cItem.path">{{cItem.title}}</el-menu-item>
<el-submenu v-else :index="String(index+1+'-'+cIndex+1)">
<template slot="title">
<i v-if="item.icon" :class="cItem.icon"></i>
<span slot="title">{{cItem.title}}</span>
</template>
<!--递归自己 遍历子..导航-->
<template v-for="(item, parentIndex) in cItem.child">
<SideNav :item="item" :index="parentIndex"></SideNav>
</template>
</el-submenu>
</template>
</el-submenu>
<!--没有子导航-->
<el-menu-item v-else :index="item.path">
<i v-if="item.icon" :class="item.icon"></i>
<span slot="title">{{item.title}}</span>
</el-menu-item>
</div>
</template>
<script>
export default {
name: "SideNav",
props: {
item: {
type: Object,
required: true
},
index: {
type: Number,
required: true
},
state: { //解决添加SideNav套div后收缩无法隐藏文字的问题,通过收缩的状态isCollapse来处理
type: Boolean,
required: true
}
}
}
</script>
<style scoped>
.el-submenu [class^=fa] {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
font-size: 16px;
}
.menu-wrapper span[slot="title"] {
display: none;
}
</style>
json格式:
{
"LOGO": {<!--可不要-->
"id": "1",
"img": "/imgs/logo.png",
"title": "博客管理系统"
},
"nav": [
{
"icon": "el-icon-setting",
"title": "基础设置",
"child": [
{
"title": "导航",
"path": "/nav"
},
{
"title": "个人信息",
"path": "/mineInfo"
}
]
},
{
"icon": "fa fa-book",
"title": "文章分类",
"child": [
{
"title": "fuillEdito",
"path": "/fuillEditor"
},
{
"title": "分类管理",
"path": "/category"
}
]
}
]
}
注意:SideNav最外层不要套div标签或者其他HTML标签,否则做出来的效果,当你点击折叠左侧导航的时候,你会发现和官方的效果不同,这就是因为套了div或者其他标签所致的。所以,说到这里,就有个问题了,你们发现没有,我这里都是使用props传递数据的,这种思路避免了在子组件套DIV的现象 (这个问题依然是存在的,只是如果处理得当还是没有问题的)
<template v-for="(item, parentIndex) in nav.nav">
<SideNav :item="item" :index="parentIndex"></SideNav>
</template>
<template v-for="(item, parentIndex) in cItem.child">
<SideNav :item="item" :index="parentIndex"></SideNav>
</template>
我测试只测到三层关系。。。。
附上所有代码,是让读者更加清晰思路,但本人还是希望读者,先理清自己的思路,然后参照我的例子,吸其精华,融合到自己的代码中,而不是整个代码块都复制进去,这样来说,相当的不好哦。
如有疑问,请在下方评论处提问,我会及时回复,谢谢…
**
**
三、新版的布局:
tabs虽然能实现点击导航显示对应的页面,但是感觉也太局限发挥了,所以,琢磨了一下,废除了tabs,自己手写代码,做出和tabs一样的功能,不再受tabs限制了自己的代码,新版的功能和旧版的实现一样,区别在于自己和别人的好,代码不再上传了,提供仓库地址给大家:github仓库 如果大家喜欢的话,麻烦点击一下star(项目换在开发过程中…)