先看目标效果图
要做顶部tab栏切换,还需要配合菜单。这里主要讲tab栏的实现方式。
首先为了在样式效果上实现方便,这里决定使用element-ui的el-tabs标签来做。这样只需要改下样式,其他效果例如切换动画都能保存。
当然,除了el-tabs自带的删除等事件,这里还需要添加右键事件,在右键事件里面有关闭全部和关闭其他两个事件选项
示例中主应用和子应用均使用history路由模式
html和css
先来看html部分和css部分
<template>
<div class="navBar" ref="navBar">
<div class="tabsBox">
<el-tabs v-model="$store.state.activeNav" :closable="navBarData.length!==1" type="card" @tab-click="navClick" @tab-remove="delNav" @contextmenu.native="handleContextmenu">
<el-tab-pane v-for="item in navBarData" :key="item.sign" :label="item.label" :name="item.sign" />
</el-tabs>
<div v-if="showContextmenu" :style="{left:contentmenuX+'px',top:contentmenuY+'px'}" class="contentmenu">
<div class="firstItem item font12 gray666" @click="closeOthersTags">关闭其他 </div>
<div class="item font12 gray666" @click="closeAllTags">关闭全部 </div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "~lm-ui-element/lib/lm-ui-element-style/utils/mix";
.navBar{
margin-bottom:10px;
margin-top:5px;
height: 32px;
width:100%;
.tabsBox{
@include positionTopRightSizeIndex($position:fixed,$height:32px,$width:calc(100% - 220px),$top:55px,$z-index:998);
background: #F0F5FA;
.contentmenu{
position: absolute;
z-index:999;
padding:10px 20px;
background:#ffffff;
-webkit-box-shadow: 1px 1px 4px #cccccc;
-moz-box-shadow: 1px 1px 4px #cccccc;
box-shadow: 1px 1px 4px #cccccc;
.firstItem{
margin-bottom:10px;
}
.item{
cursor:pointer;
}
}
}
}
</style>
<style>
.navBar .el-tabs__item{
padding: 0 15px !important;
position: relative;
height: 32px !important;
line-height:32px !important;
border: 1px solid #DCE3EC !important;
border-radius: 3px;
margin-right:5px;
}
.navBar .el-tabs__nav-next,.navBar .el-tabs__nav-prev{
height: 32px !important;
line-height:32px !important;
background:#ffffff;
width:20px !important;
z-index: 999;
text-align: center;
}
.navBar .is-active{
background: #fff!important;
}
.navBar .el-tabs__nav{
border:none !important;
}
.navBar .el-tabs__header{
border:none !important;
}
</style>
这里说下@import "~lm-ui-element/lib/lm-ui-element-style/utils/mix"这行引用,这是lm-ui-element组件库的工具样式。关于lm-ui-element,详情可参考https://blog.csdn.net/qq_41000974/article/details/113759292
如何保存数据和更新数据
接下来是关键点,即tab数据navBarData的更新保存。
首先更新navBarData的地方有以下几个:
- 点击菜单的时候
- 手动刷新浏览器的时候(点击浏览器的刷新按钮或者按F5)
- 点击tab栏删除按钮的时候
- 点击tab栏右键菜单关闭按钮的时候
- 如果项目中使用了微应用,也有可能在微应用中需要更新tab的情况
- 其他特殊情况,比如我之前就遇到这样的需求:想必一个普通的详情路由组件,一般是点击列表的查看详情跳进去,根据携带的id查出内容。这种页面并非菜单页面,我们是不需要将它加到tab上的看。然而我这里接到的需求是,需要将详情页面加到tab上,并且一个详情一个tab。
这里都考虑一下吧,尽量都能适配这些需求。
既然更新tab数据的地方这么多,很明显,完全不在一个页面或组件,甚至不在同一个项目。那么,只有使用vuex最适合了。方便更新,带数据监听。
那好,先把vuex这一套写下来吧。
这里除了更新tab数据,还要更新当前tab数据。也就是actions里面有setNavBarData和setActiveNav两个函数。
建好vuex的模块文件,state.js,mutations.js,actions.js,这里再附加一个mutation-type.js
先在mutation-type.js写上mutations函数名
export const SET_NAVBARDATA='SET_NAVBARDATA' //设置导航栏数据
export const SET_ACTIVENAV='SET_ACTIVENAV' //设置导航栏当前tab
state.js
navBarData:[],//导航栏数据
activeNav:'',//导航栏当前tab
actions.js
import {
SET_NAVBARDATA,
SET_ACTIVENAV
} from './mutation-type'
export default {
//设置导航栏数据
async setNavBarData({commit},navBarData){
//这里将数据存入缓存,方便浏览器刷新时使用
sessionStorage.setItem('navBarData',JSON.stringify(navBarData))
commit(SET_NAVBARDATA,navBarData)
},
//设置导航栏当前tab
async setActiveNav({commit},activeNav){
commit(SET_ACTIVENAV,activeNav)
},
}
mutations.js
import {
SET_NAVBARDATA,
SET_ACTIVENAV
} from './mutation-type'
[SET_NAVBARDATA](state,navBarData){//设置导航栏数据
state.navBarData = navBarData
},
[SET_ACTIVENAV](state,activeNav){//导航栏当前tab
state.activeNav = activeNav
},
操作navbar的函数
如果是直接从菜单点击的,那么更新数据就很好更新。只需要寻找到对应的nabbar数据,如果找到,说明存在,替换即可,如果没有,添加即可
如果是刷新的情况,vuex里面肯定没有了,需要从本地缓存取数据,然后还要知道当前页面是哪个,因为当前页面的tab菜单要高亮
然后上面说的其他情况,这时候就存在同一个路由名有多个tab的情况。一开始是考虑使用routeName来做唯一标志的,但是这样看来不行了。所以我们另外给个变量sign,作为唯一标志,当然通常情况下,sign和routeName相等。
为了方便页面调用,以及微应用调用,我们将该函数挂在vue原型上。函数为setNavBarDataFun
新建vue-global-methos.js
import store from '../store'
export default {
install(Vue) {
Vue.prototype.$globalMethods = {
//处理导航数据
setNavBarDataFun(menu={}){
let {label,path,routeName,isChildApp,params,sign}=menu
let {navBarData}=store.state
navBarData=navBarData.length ? navBarData : sessionStorage.getItem('navBarData')
navBarData=typeof navBarData==='string' ? JSON.parse(navBarData) : (navBarData || [])
if(!sign){
sign=routeName
if(params instanceof Object){
for(let i in params){
sign+='='+ params[i]
}
}
}
let navIndex=navBarData.findIndex(item=>item.sign===sign)
navBarData.map(item=>{
item.switchClass='defaultLi'
return item
})
if(Object.keys(menu).length){
let activeNavData={
label,
path,
routeName,
switchClass:'activeLi',
isChildApp,
params,
sign
}
if(navIndex>-1){
navBarData.splice(navIndex,1,activeNavData)
}else{
// console.log(params)
navBarData.push(activeNavData)
}
}
if(!sign){
let pathnameArr=location.pathname.split('/')
pathnameArr.splice(pathnameArr.length-1)
let navInfo=navBarData.find(item=>new RegExp(pathnameArr.join('/')).test(item.path))
sign=navInfo ? navInfo.routeName : ''
}
store.dispatch('setNavBarData',navBarData)
store.dispatch('setActiveNav',sign)
},
}
}
上面可以看到,有两个sign非空判断,第一个是用于其他情况说的需求的,这个时候将该页面的一些参数,通常为id之类,反正可以唯一区分页面的,放在params里面,并且拼接上当前页面的路由名。通常情况下,子应用菜单数据结构应当和主应用一致,因此这里的非空判断只是以防万一。拼接参数才是目的。
第二个非空判断,是在页面刷新的时候会发生。针对子应用,并且有种情况是,当前页面是菜单的子页面,也就是当前页面的路由名并不是我们想要的标志。既然是菜单子页面,那么就是从菜单点进来的,也就是已经有相应的navData了,那么就只能通过路径来查询是哪个了。当然,这里要能正确查询,path命名必须遵循一定规范,否则查不出来的。
isChildApp表示是否是子应用的菜单
然后我们在main.js里面将函数挂在到 vue原型链
import globalComponents from './utils/global-components'
Vue.use(globalComponents)
菜单点击添加数据
我们先来看菜单里点击添加navdata的数据方法,这里比较简单,因为菜单里的数据是比较完整和规范的。
这里假设使用element-ui的menu菜单标签。我们在select事件和open事件里面添加。
//中菜单
select(cMenu){
// console.log(cMenu)
let {path,isChildApp,routeName}=cMenu
// console.log(isChildApp,routeName)
if(isChildApp){
window.history.pushState(null, path, path)
}else{
this.$router.push({
name:routeName
})
}
this.$globalMethods.setNavBarDataFun(cMenu)
},
navbar组件的js部分
接下来看组件的js部分。首先mouted里面,这里面通常是刷新的时候处理数据,首先对主应用的路由数据进行筛选,筛选出当前路由页面的navdata数据,当然,不一定有。然后就是设置tab数据了
另外,就是文档监听鼠标点击事件,关闭右键菜单。
data(){
return{
contentmenuX:'',//关闭弹窗x
contentmenuY:'',//关闭弹窗Y
showContextmenu:false,//是否显示右键菜单
mouseRightActiveName:'',//鼠标右键点击的当前tab名称
}
},
computed:{
...mapState(['navBarData','activeNav']),
},
created(){
this.$nextTick(()=>{
let {name,params}=this.$route
let wisdomRoutes=sessionStorage.getItem('wisdomRoutes')
wisdomRoutes=JSON.parse(wisdomRoutes)
let navData=[]
this.filterNavData(wisdomRoutes,name,navData)
if(navData.length){
if(parseInt(navData[0].isLeftMenu)===1){
//属于菜单
this.$globalMethods.setNavBarDataFun({...navData[0],params,isChildApp:/^\/work\//.test(location.pathname)})
}else{
this.$globalMethods.setNavBarDataFun()
}
}else{
this.$globalMethods.setNavBarDataFun()
}
//给页面添加点击事件,点击页面关闭导航右键弹窗
document.addEventListener('click',(e)=>{
if(this.showContextmenu){
this.showContextmenu=false
this.contentmenuX=''
this.contentmenuY=''
}
})
})
},
navdata数据的删除和跳转方法
跳转和菜单点击差不多,只是少了不设置navdata数据
删除,清空,关闭其他比较简单,不细说。
然后鼠标右键事件,就是鼠标右键时,判断如果是在有类名tabs__item的标签或者父级是tabs__item的标签,说明鼠标点在我们想要的nav按钮上,这时候获取右键的鼠标x和y坐标,然后x减去左侧菜单宽度,y减去头部高度,就是右键弹出菜单的左上角位置了。
methods:{
//导航栏点击
navClick(navObj){
// console.log(this.navBarData)
let {index,name}=navObj
let nav=this.navBarData[index]
// console.log(nav)
if(nav.switchClass==='activeLi') return
this.$globalMethods.setNavBarDataFun(nav)
let {path,isChildApp,routeName,params={}}=nav
// console.log(params)
if(isChildApp){//子应用
for(let i in params){//将params参数拼接到path
path=path.replace(`/:${i}`,`/${params[i]}`)
}
console.log(path)
window.history.pushState(null, path, path)
}else{
this.$router.push({
name:routeName,
params:{
...params
}
})
}
},
//删除导航栏
delNav(name){
// console.log(name)
let index=this.navBarData.findIndex(item=>item.sign===name)
let navMenu=index>0 ? this.navBarData[index-1] : this.navBarData[index+1]
this.navBarData.splice(index,1)
this.$store.dispatch('setNavBarData',this.navBarData)
// console.log(navMenu)
// console.log(index)
let {routeName}=navMenu
this.navClick({index:parseInt(index)-1,name:routeName})
this.$globalMethods.setNavBarDataFun(navMenu)
},
//鼠标右键事件
handleContextmenu(event) {
// console.log(event)
let target = event.target
// 解决 https://github.com/d2-projects/d2-admin/issues/54
let flag = false
if (target.className.indexOf('el-tabs__item') > -1) flag = true
else if (target.parentNode.className.indexOf('el-tabs__item') > -1) {
target = target.parentNode
flag = true
}
if (flag) {
event.preventDefault()
event.stopPropagation()
this.contentmenuX = event.clientX-200
this.contentmenuY = event.clientY-50
this.mouseRightActiveName = target.getAttribute('aria-controls').slice(5)
this.showContextmenu = true
console.log(this.mouseRightActiveName)
}
},
//关闭其他
closeOthersTags(){
let activeNavArr=this.navBarData.filter(item=>item.sign===this.mouseRightActiveName)
activeNavArr[0].switchClass='activeLi'
sessionStorage.setItem('navBarData',activeNavArr)
this.$store.dispatch('setNavBarData',activeNavArr)
this.$store.dispatch('setActiveNav',activeNavArr[0].sign)
},
//关闭全部
closeAllTags(){
sessionStorage.removeItem('navBarData')
this.$store.dispatch('setNavBarData',[])
this.$store.dispatch('setActiveNav','')
},
//递归过滤导航数据,筛选出当前页面的导航数据
filterNavData(routes,name,navData){
for(let i=0;i<routes.length;i++){
if(name===routes[i].routeName){
navData.push(routes[i])
}
if(routes[i].children && routes[i].children.length){
this.filterNavData(routes[i].children,name,navData)
}
}
}
},
最后,由于页面鼠标点击事件是addEventListener绑定的,vue无法销毁,需要手动销毁。
beforeDestroy(){
document.removeEventListener('click')
}