vue 实现顶部tab栏菜单(顶部tab按钮)切换(添加删除nav数据,适配微前端应用,滑动动画,右键菜单弹窗)

要做顶部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')
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

土豆片片

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值