vue 实现顶部tab栏菜单(顶部tab按钮)切换(添加删除nav数据,适配微前端应用,滑动动画,右键菜单弹窗)
要做顶部tab栏切换,还需要配合菜单。这里主要讲tab栏的实现方式。首先为了在样式效果上实现方便,这里决定使用element-ui的el-tabs标签来做。这样只需要改下样式,其他效果例如切换动画都能保存。当然,除了el-tabs自带的删除等事件,这里还需要添加右键事件,在右键事件里面有关闭全部和关闭其他两个事件选项示例中主应用和子应用均使用history路由模式。
要做顶部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')
}
更多推荐
所有评论(0)