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

先看目标效果图


1623852408(1).png

要做顶部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')
        }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容