自制一个简单demo,实现一个包含侧边栏、tab标签栏的后台管理系统的大致框架,在此做一下总结。
先上图
项目地址
功能点
- 可配置的侧边栏;
- 侧边栏点击时跳转至相应的router,若没有该标签则新建,已有则激活这个标签;
- 点击标签栏中的标签实现下方的路由跳转;
- 关闭所有标签栏则自动打开至index。
1、侧边栏
src/components/navMenu/navMenu.vue
<!--本页为左侧下拉菜单-->
<template>
<el-row class="tac">
<el-col :span="24">
<el-menu
default-active=""
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
unique-opened
router
active-text-color="#fff"
>
<el-menu-item index="index" @click="clickMenu('index')">
<i class="el-icon-star-on"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-submenu
v-for="item of menu"
:index="item.id"
:key="item.id"
>
<template slot="title">
<i class="el-icon-location"></i>
<span>{{item.name}}</span>
</template>
<el-menu-item-group class="over-hide">
<el-menu-item
v-for="sub of item.sub"
:index="sub.componentName"
:key="sub.componentName"
@click="clickMenu(sub.componentName)"
>
{{sub.name}}
</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-col>
</el-row>
</template>
<script>
import menu from '@/config/menu-config.js'
export default {
name: 'navMenu',
methods: {
clickMenu (componentName) {
this.openedTab = this.$store.state.openedTab
// tabNum 为当前点击的列表项在openedTab中的index,若不存在则为-1
let tabNum = this.openedTab.indexOf(componentName)
if (tabNum === -1) {
// 该标签当前没有打开
// 将componentName加入到已打开标签页state.openedTab数组中
this.$store.commit('addTab', componentName)
} else {
// 该标签是已经打开过的,需要激活此标签页
this.$store.commit('changeTab', componentName)
}
}
},
data () {
return {
menu: menu,
openedTab: []
}
}
}
</script>
<style scoped>
.over-hide{
overflow-x:hidden;
}
.el-submenu__title{
border-bottom:1px solid #8d98a2
}
</style>
重点:
- .over-hide 属性用于解决element-ui中子选项长度有时会超出边框的问题;
- < el-menu >标签中 : unique-opened属性用于设置只允许一栏处于打开状态,点击某title时,其他title都会折叠起来;router属性用于使其下的标签可以通过:index进行路由跳转(需项目里安装了vue-router);
- clickMenu 点击事件:若当前点击标签不在$store.state.openedTab中,则该标签为未打开过的,则触发addTab事件,在便签栏中新建此标签,否则触发changeTab事件,跳转至此标签;
src/store/index.js 的mutations中:
addTab (state, componentName) {
state.openedTab.push(componentName)
},
changeTab (state, componentName) {
state.activeTab = componentName
}
- import menu from ‘@/config/menu-config.js’
src/config/menu-config.js
module.exports = [{
name: '基础',
id: 'basic',
sub: [{
name: 'BasicLayout',
componentName: 'BasicLayout'
}, {
name: 'BasicContainer',
componentName: 'BasicContainer'
}]
}, {
name: 'Form',
id: 'form',
sub: [{
name: 'BasicRadio',
componentName: 'BasicRadio'
}, {
name: 'BasicCheckbox',
componentName: 'BasicCheckbox'
}]
}]
该config文件用于保存侧边栏各个选项的具体内容,需要修改时去修改该文件即可而不需要修改html。
2、标签栏
src/components/navMain/navMain.vue
<!--本页为tab标签-->
<template>
<el-tabs
v-model="editableTabsValue"
type="card"
closable
@tab-remove="removeTab"
@tab-click="handleClickTab($event.name)"
>
<el-tab-pane
:key="item.name"
v-for="item in editableTabs"
:label="item.title"
:name="item.name"
>
</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
name: 'navMain',
data () {
return {
editableTabsValue: 'index',
editableTabs: [{
title: 'index',
name: 'index'
}],
tabIndex: 1,
openedTab: ['index']
}
},
methods: {
handleClickTab (route) {
this.$store.commit('changeTab', route)
this.$router.push(route)
},
removeTab (targetName) {
let tabs = this.editableTabs
let activeName = this.editableTabsValue
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.$store.commit('deductTab', targetName)
let deductIndex = this.openedTab.indexOf(targetName)
this.openedTab.splice(deductIndex, 1)
this.$router.push(activeName)
this.editableTabsValue = activeName
this.editableTabs = tabs.filter(tab => tab.name !== targetName)
if (this.openedTab.length === 0) {
this.$store.commit('addTab', 'index')
this.$router.push('index')
}
}
},
computed: {
getOpenedTab () {
return this.$store.state.openedTab
},
changeTab () {
return this.$store.state.activeTab
}
},
watch: {
getOpenedTab (val) {
if (val.length > this.openedTab.length) {
// 添加了新的tab页
// 导致$store.state中的openedTab长度
// 大于
// 标签页中的openedTab长度
// 因此需要新增标签页
let newTab = val[val.length - 1] // 新增的肯定在数组最后一个
++this.tabIndex
this.editableTabs.push({
title: newTab,
name: newTab,
content: ''
})
this.editableTabsValue = newTab
this.openedTab.push(newTab)
}
},
changeTab (val) {
// 监听activetab以实现点击左侧栏时激活已存在的标签
if (val !== this.editableTabsValue) {
this.editableTabsValue = val
}
}
},
created () {
// 刷新页面时(F11)
// 因为<router-view>的<keep-alive>,会保留刷新时所在的router
// 但是tab标签页因为刷新而被重构了,tab没有了
// 因此需要将router回到index
this.$router.push('index')
}
}
</script>
<style scoped>
</style>
重点:
- data中:editableTabsValue 用于设置哪个标签处于激活状态;editableTabs 用于保存所有标签;openedTab用于保存打开过的标签;
- handleClickTab 方法:点击标签时,触发changeTab事件,切换至相应路由;
- removeTab 方法:点击标签上的x时触发,关闭该标签,触发deduct方法,跳转至合适的标签并激活,若已经没有标签了,则打开index;
deductTab (state, componentName) {
let index = state.openedTab.indexOf(componentName)
state.openedTab.splice(index, 1)
}
- computed和watch中的两个方法配合使用,实现监控store中openedTab和activeTab的变化并相应的改变标签栏中的标签状态,实现方式注释里已经写明。
一些补充
- 通过keep-alive实现各个标签之间切换路由改变时保留缓存(否则输入了一半的input切换个标签回来就没了),但是现在仍存bug:即使标签关闭了,再打开缓存仍在,再慢慢解决;
- 侧边栏和标签栏之间的联动逻辑较为复杂(对于工作之余刚自学了vue不到一个月的笔者来说),需要考虑的较为周全,包括:新增删除、点击切换、激活标签效果、时时改变openedTab和activeTab等。