vue2.0 + elementUi 实现无限tab式页面

使用elementUi的tabs组件 + vuex 实现点击菜单或者点击按钮新增一个tab标签页

一、思路

由于之前一直做的单页面项目都是使用路由控制实现的路由改变销毁原有组件加载新组件的方式,是view区域始终只有一个路由的方式实现单页面的管理系统,后面接到一个新的项目后发现原来的逻辑对于新需求不太适应,首先新的需求要求,打开的页面或者类页面的tab标签页数量上可能是无限的,这样对于使用路由逻辑上就不太好实现,比如我现在需要打开N个订单详情,路由控制的方式就比较麻烦了,虽然可以用keep-alive 标签来控制和保存参数,但是各个页面之间的交互以及菜单与tab方面的切换,还有国际化做起来就比较麻烦。后面就想到使用elementUi的tabs组件和vuex来做,因为tabs组件效果上看上去和浏览器的新标签打开页面效果比较相似,这样可以减少很多交互效果的css和js书写,不过这个组件也有很多坑(具体后面再说)

1.产品想要的效果

image

首先点击一个菜单项生成一个tab页面,其他的页面或者组件也能通过点击生成tabs 相同的组件但是除菜单的不可以重复外其他入口的是可以无限打开的,比如订单详情

image

点击x的时候关闭当前页面,点击刷新标识的时候刷新当前页面的数据。

2 菜单效果

image

菜单实现分为三级菜单、一、二级 为展开式菜单,三级菜单为弹出式菜单(这个菜单踩了不少坑)因为这个项目是一开始就确定使用elementUi来做的,这次是属于重构所以优先考虑的是使用elementUi自带的组件来实现这个效果,但是这个效果显然 没有现成的 elementUi的API上只有全展开或者全弹出式的菜单没有这种混合的,后面是做其他业务模块的时候使用到了Popover弹出 组件 于是来了灵感使用 于是使用 NavMenu + Popover 的方式实现了这种混合菜单。

上干货:


el-menu

    :unique-opened="true"

    class="el-menu_nav"

    collapse-transition

    :collapse="isCollapse" 

    :default-active="activeTabName"

    @select="addTab">

      :index="item.index"

      :key="index"

      v-if="!item.children"

      v-for="(item,index) in navList"

      >

        {{item.title}}

      background-color="#fff"

      :index="item.index"

      :key="index"

      v-if="item.children"

      v-for="(item,index) in navList"

      >

          {{item.title}}

        v-if="!it.children||it.children.length==0"     

        :index="it.index"

        :key="indexChild"

        v-for="(it,indexChild) in item.children"> 

          {{it.title}}

          class="sencond_menu_children"

          v-if="it.children && it.children.length>=1"     

          :index="it.index"

          :key="indexChild"

          v-for="(it,indexChild) in item.children"> 

                placement="right-start"

                title=""

                width=""

                trigger="hover"

                v-if="!isCollapse"

                v-model="it.visible"

                >

                    @click="addTab(it1.index),it.visible = false"         

                    :index="it1.index"

                    :key="indexChild1"

                    v-for="(it1,indexChild1) in it.children"> 

                      {{it1.title}}           

                  {{it.title}}

                {{it.title}}

            v-if="isCollapse"         

            :index="it1.index"

            :key="indexChild1"

            v-for="(it1,indexChild1) in it.children"> 
              <template slot="title">
              <i class="nav_icon" :class="it1.icon"></i>
              <span slot="title">{{it1.title}}</span>
            </template>             
            </el-menu-item>             
          </el-submenu>          
      </el-submenu>
    </el-menu>

通过v-if 选择三级级菜单的显示方式 丢弃原组件自带的三级菜单给二级菜单绑定新的hover事件显示弹出三级菜单 这里要注意一个点就是三级菜单的切换时 el-menu 这个组件自带的切换效果是上下式的 我们是不希望这样的 所以要重新定义这个样式 使用 下面的写法禁用掉二级菜单的动画

  // 重置二级副菜单的样式
  .el-submenu.is-opened.sencond_menu_children > .el-submenu__title .el-submenu__icon-arrow{
    transform:none !important;
  }

下面是store的写法

 state : {
        // 管理tabs标签
        activeTabName: "home",
        tabList: [
            {
                label: '主页',
                name: 'home',
                param:{},
                disabled: false,
                closable: false,
                component: appDashboard
            }
        ],
        navBaseInfo:navBaseInfo,
        searchList:searchList,//这个是用来实现页面多个enter事件的定位的配置表
    },
//实现组件懒加载
const componentsOne = resolve => require(['@/components/one'], resolve)
//这个对象只是用来新增tab是取组件方便的
let components = {
componentsOne:componentsOne 
}
/**
         * 新增菜单类型的tab 
         *@param state  {Object} 当前的状态对象
         *@param name  {String} 必传信息 当前需要打开的tab的关键字
        */
        addTab(state, index) {         
            let isRefresh = false
            JSON.parse(sessionStorage.getItem('osMenuArr')).filter(f => {               
                if (f.menu_index == index) {                   
                    isRefresh = f.is_refresh == 1? true : false
                }               
            })
            //第一个版本
            //let const componentsOne = resolve => require(['@/components/one'], resolve)
            if (state.tabList.filter(f => f.name == index) == 0) {
                state.tabList.push({
                    label: '',
                    name: index,
                    param:{},
                    disabled: false,
                    refresh:isRefresh || false,
                    closable: true,                    
                    //component: componentsOne, 这是第一个版本的写法 这样写前面的components 对象和 组件懒加载可以不用定义,但是这个方法有一个巨坑 就是最后打包上线的时候文件会变的巨大,所以最后优化的时候舍弃了
                    component: components[index]
                })
            }
            state.activeTabName = index
        },
/**
         * 新增非菜单类型的tab
         *@param tab  {Object} 当前需要新打开的tab的标签信息
         *@param tab.title  {String} 必传信息 当前需要打开的tab的标题
         *@param tab.index  {String} 必传信息 当前需要打开的tab的关键字
         *@param tab.param  {Object} 必传信息 当前tab页面需要的参数
         *@param tab.beforeCloseam  {Function} 关闭前的函数
         *@param tab.afterClose  {Function} 关闭窗口后的函数
        */
        addNewNotMenuTab(state, tab) {   
            let title =  tab.title || "New Page",
                index = tab.index,
                random = parseInt(new Date().getTime())
                if(!tab.index){
                    alert("Jump param is error, Please check !")
                    return false
                }
            let name = tab.isNoRandom?index : index + random
            if (state.tabList.filter(f => f.name == name) == 0) {
                state.tabList.push({
                    label: title,
                    not_menu:true,
                    name:name,
                    disabled: false,
                    closable: true,
                    refresh:tab.refresh || false,
                    param:tab.param||{},
                    component: components[index],
                    beforeClose:tab.beforeClose,
                    afterClose:tab.afterClose,
                })
            }         
            state.activeTabName =  tab.isNoRandom?index : index + random
        },
async closeTab(state, name,callback) {
            if(typeof name == "function"){
                console.log("close param is Erorr")
                return false
            }           
            let tab = state.tabList.filter(f => f.name == name)[0]
            if(!tab){
                return false
            }
            let index = state.tabList.indexOf(tab)
            if(state.tabList[index].beforeClose){
                try{
                    await new Promise((resolve, reject) => state.tabList[index].beforeClose(resolve, reject))
                } catch (e) {
                    return false
                }
            }
            if (index != state.tabList.length - 1) {
                state.activeTabName = state.tabList[index + 1].name
            } else {
                state.activeTabName = state.tabList[index - 1].name
            }
            state.tabList[index].afterClose && state.tabList[index].afterClose()
            state.tabList = state.tabList.filter(f => f.name != name)
            callback && callback()          
        },

tabs页面结构

        <el-tabs class="tabs" @dragstart.native="dragstart(activeTabName)" v-model="activeTabName" @tab-remove="closeTab" type="border-card">
           <el-tab-pane :index="item.name" v-for="(item,index) in tabList" :key="index" :name="item.name" :label="item.label" :closable="item.closable">
                <span slot="label"><i v-if="item.refresh" class="el-icon-refresh" @click.stop="refresh(item.name)"></i> {{item.not_menu?item.label:$t('message.menu.'+item.name)}}</span>
                <component  :is="item.component" :ref="item.name" :linkParam="item.param" :before-close.sync="item.beforeClose" :after-close.sync="item.afterClose" :tabIndex="item.name"></component>
            </el-tab-pane>
        </el-tabs>

定义完这几个方法基本上需要实现的就差不多了可以了
最后为了方便就是把当前的这些方法挂载到全局上,毕竟都要用到的 不用每个页面都去引用一次vuex 那样很麻烦

import { mapMutations } from 'vuex'

export default {
    install(Vue, options) {
        //特殊发送请求
        Vue.prototype.$restful = restful;

        // 公用发送请求
        Vue.prototype.$sendReq = sendReq;

        // 关闭tab
        Vue.prototype.$closeTab = mapMutations('navTabs', ['closeTab']).closeTab
        // 新增tab
        Vue.prototype.$addTab = mapMutations('navTabs', ['addTab']).addTab
        // 新增一个非菜单tab
        Vue.prototype.$addNewNotMenuTab = mapMutations('navTabs', ['addNewNotMenuTab']).addNewNotMenuTab
   }
}

最后有一个小坑就是 回车事件 enter这个坑了 由于以前都是采取组件销毁的方式实现的 所以在组件里面单独实现是没问题的但是现在因为要实现很多页面都需要实现回车 这些页面又是同时存在的 所以就会出现 谁先出现谁生效,或者谁后出现谁生效的情况
解决思路
在tab切换的时候定位到tab的index关键字段 把这个字段给ref属性 即 activeName == index ==ref
所以可以使用下面的方法来实现,这个方法一定要等页面元素加载完再绑定要不会报错

          document.onkeydown = function(e) {               
                //捕捉回车事件
              let ev = (typeof event!= 'undefined') ? window.event : e;       
                if(ev.keyCode == 13) {                                    
                    if(self.$store.state.navTabs.searchList.indexOf(self.activeTabName)>-1)self.$refs[self.activeTabName][0].search && self.$refs[self.activeTabName][0].search()                   
                }
            }

不过elementUi的 tabs组件有一个最大的坑就是不能做拖拽排序和拖拽替换,这个是一个坑 到目前没找到解决方法 如有知道的小伙伴可以@一下 后面有时间再分享一个国际化的实现吧
以上就是本次文章全部内容

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

推荐阅读更多精彩内容