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

先看目标效果图


1623852408(1).png

要做顶部tab栏切换,还需要配合菜单。这里主要讲tab栏的实现方式。

首先为了在样式效果上实现方便,这里决定使用element-ui的el-tabs标签来做。这样只需要改下样式,其他效果例如切换动画都能保存。

当然,除了el-tabs自带的删除等事件,这里还需要添加右键事件,在右键事件里面有关闭全部和关闭其他两个事件选项

示例中主应用和子应用均使用history路由模式

html和css

先来看html部分和css部分




这里说下@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

最后,由于页面鼠标点击事件是addEventListener绑定的,vue无法销毁,需要手动销毁。

   beforeDestroy(){
            document.removeEventListener('click')
        }

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