用过Element ui库的童鞋肯定知道<el-tabs>组件,简单、好用、可以自定义标签页,不知道广大童鞋们在刚开始使用<el-tabs>组件的时候有没有想过它是如何实现的?我咋刚开始使用<el-tabs>组件的时候就有去想过,也想去实现一个超级简单的tabs选项卡组件,无奈当时功力不够,未能实现。最近的一个简单项目中正好要用到选项卡组件,由于项目简单也就没有使用任何第三方库,于是就自己动手写了个选项卡组件。

1、实现tabs选项卡组件的思考

<el-tabs v-model="activeName" @tab-click="handleClick">    <el-tab-pane label="用户管理" name="first">用户管理</el-tab-pane>    <el-tab-pane label="配置管理" name="second">配置管理</el-tab-pane>    <el-tab-pane label="角色管理" name="third">角色管理</el-tab-pane>    <el-tab-pane label="定时任务补偿" name="fourth">定时任务补偿</el-tab-pane></el-tabs>

问题:

  1. 如何根据<el-tab-pane>来生成标签页?
  2. 如何过滤<el-tabs>组件中的子元素,使得在使用的时候只显示<el-tab-pane>,而不会显示其他组件或div之类的元素?

2、实现思路

  1. 想根据<el-tab-pane>来生成标签页就需要使用到<slot>,使用<slot><template>的形式肯定是不行的,因为无法获取到<slot>的数量;使用<template>的形式行不通,那就只有使用render函数了
  2. 过滤<el-tabs>组件中的子元素也需要使用render函数

3、代码实现

index.js

import PTabs from './PTabs';import PTabPane from './PTabPane';export default function tabsInstall(Vue) {  if(tabsInstall.installed){    return;  }  Vue.component('PTabs', PTabs);  Vue.component('PTabPane', PTabPane);}

PTabs.vue

<script>  import PTabNav from './PTabNav';  export default {    name: "PTabs",    props: {      value: {        type: [String, Number],        default: ''      },      beforeClick: {        type: Function,        default(){          return function () {};        }      }    },    components: {      PTabNav    },    data(){      return {        pTabPanes: [],        currentName: this.value || 0      }    },    methods: {      addPane(pane){        this.pTabPanes.push(pane);        if(!this.currentName){          this.setCurrentName(this.pTabPanes[0].name);        }      },      removePane(pane){        let index = this.pTabPanes.indexOf(pane);        if(index > -1){          this.pTabPanes.splice(index, 1);        }      },      setCurrentName(name){        if(this.currentName !== name){          this.currentName = name;          this.$emit('input', name);        }      },      // 标签页点击事件      handTabNavClick(name, pane, e){        if(this.currentName === name || pane.disabled){          return;        }        let before = this.beforeClick();        if(before && before.then){          before.then(() => {            this.setCurrentName(name);            this.$emit('tabClick', pane, e);          })        }else{          this.setCurrentName(name);          this.$emit('tabClick', pane, e);        }      }    },    watch: {      value(newVal){        this.setCurrentName(newVal);      },      currentName(){        this.$nextTick(() => {          this.$refs.p_tab_nav.scrollToActiveTab();        });      }    },    render(h) {      let {$scopedSlots} = this;      let $default = $scopedSlots.default();      let qTabPanes = $default.map(item => {        /* 过滤<PTabs>xxx</PTabs>中传递的xxx内容。这里只接收<PTabPane>组件,因为我们需要根据<PTabPane>组件的数量来生成         * <PTabNav>组件,如果参差了其它节点则会导致不能正确生成<PTabNav>组件 */        if(item.componentOptions && item.componentOptions.tag === 'PTabPane'){          return item;        }      });      let qTab = h('PTabNav', {        props: {          // 将tab-pane传递给 <PTabNav>组件,<PTabNav>组件就知道要有多少个tab-item了          tabPanes: this.pTabPanes,          handTabNavClick: this.handTabNavClick        },        ref: 'p_tab_nav'      });      let qTabBody = h('div', {        staticClass: 'p-tabs_content'      }, qTabPanes);      console.log($default)      return h('div', {        staticClass: 'p-tabs'      }, [qTab, qTabBody]);    },    mounted() {      //console.log(this)      this.$nextTick(() => {        this.$refs.p_tab_nav.scrollToActiveTab();      });    }  }</script><style lang="stylus">.p-tabs{  .p-tabs_header{    position: relative;    margin-bottom: 15px;    &.is-scrollable{      padding-left: 20px;      padding-right: 20px;    }  }  .p-tabs_nav-prev,  .p-tabs_nav-next{    position: absolute;    top: 0;    width: 20px;    height: 100%;    display: none;    &::before{      position: absolute;      content: ' ';      font-size: 0;      line-height: 0;      width: 10px;      height: 10px;      top: 50%;      left: 50%;      border-top: 1px solid #eee;      border-left: 1px solid #eee;      margin: -5px 0 0 -5px;    }    cursor: pointer;    &.disabled{      cursor: default;      border-color: #aaa;    }  }  .p-tabs_nav-prev{    left: 0;    &:before{      transform: rotate(-45deg);    }  }  .p-tabs_nav-next{    right: 0;    &:before{      transform: rotate(135deg);    }  }  .p-tabs_header{    &.is-scrollable{      .p-tabs_nav-prev,      .p-tabs_nav-next{        display: block;      }    }  }  .p-tabs_nav-scroll{    overflow: hidden;  }  .p-tabs_nav-list{    position: relative;    float: left;    white-space: nowrap;    transition: transform .3s;  }  .p-tabs_nav-item{    display: inline-block;    height: 40px;    line-height: 40px;    padding: 0 20px;    color: #fff;    cursor: pointer;    &.active,    &:hover{      color: #ffb845;    }    &.disabled{      cursor: not-allowed;      color: #aaa;      &:hover{        color: #aaa;      }    }  }  .p-tabs_content{    position: relative;    overflow: hidden;  }  .p-tabs-pane{    color: #fff;  }}</style>

PTabPane.vue

<template>  <div class="p-tabs-pane" v-show="show">    <slot></slot>  </div></template><script>  export default {    name: "PTabPane",    props: {      label: {        type: String,        default: ''      },      name: {        type: [String, Number],        default: ''      },      disabled: {        type: Boolean,        default: false      }    },    data(){      return {        loaded: false      }    },    computed: {      show(){        if(this.$parent.currentName === this.name){          if(!this.loaded){            this.loaded = true;          }          return true;        }        return false;      }    },    watch: {      label(){        // label更新的时候强制更新父组件,以触发PTabNav才能更新        this.$parent.$forceUpdate();      }    },    mounted() {      // 当当前组件创建的时候将当前组件添加到父组件的pTabPanes中,以触发PTabNav才能更新      this.$parent.addPane(this);    },    destroyed() {      if(this.$el && this.$el.parentNode){        this.$el.parentNode.removeChild(this.$el);      }      // 当当前组件销毁时需从父组件中的pTabPanes中移除当前组件,以触发PTabNav才能更新      this.$parent.removePane(this);    }  }</script>

PTabNav.vue

<script>  function noop() {};  export default {    name: "PTabNav",    props: {      tabPanes: {        type: Array,        default(){          return [];        }      },      handTabNavClick: {        type: Function,        default(){          return function () {};        }      }    },    data(){      return {        navPrevDisabled: true,        navNextDisabled: true,        // 控制左右箭头显示        scrollable: false,        listOffset: 0      }    },    methods: {      navPrevClickEvent(){        if(!this.navPrevDisabled){          let navScrollW = this.$refs.nav_scroll.offsetWidth;          let navListW = this.$refs.nav_list.offsetWidth;          let maxTransformX = 0;          let transformX = this.listOffset - navScrollW;          if(transformX < maxTransformX){            transformX = maxTransformX;          }          if(transformX === this.listOffset){            return;          }          console.log('上一页按钮点击了', transformX);          this.listOffset = transformX;          if(transformX === 0){            this.navPrevDisabled = true;            this.navNextDisabled = false;          }else if(transformX === (navListW - navScrollW)){            this.navPrevDisabled = false;            this.navNextDisabled = true;          }else{            this.navPrevDisabled = false;            this.navNextDisabled = false;          }        }      },      navNextClickEvent(){        if(!this.navNextDisabled){          let navScrollW = this.$refs.nav_scroll.offsetWidth;          let navListW = this.$refs.nav_list.offsetWidth;          let maxTransformX = navListW - navScrollW;          let transformX = this.listOffset + navScrollW;          if(transformX > maxTransformX){            transformX = maxTransformX;          }          if(transformX === this.listOffset){            return;          }          console.log('下一页按钮点击了', transformX);          this.listOffset = transformX;          if(transformX === 0){            this.navPrevDisabled = true;            this.navNextDisabled = false;          }else if(transformX === (navListW - navScrollW)){            this.navPrevDisabled = false;            this.navNextDisabled = true;          }else{            this.navPrevDisabled = false;            this.navNextDisabled = false;          }        }      },      // 计算 .p-tabs_nav-list 是否溢出      calculateListSpilled(){        let navScrollW = this.$refs.nav_scroll.offsetWidth;        let navListW = this.$refs.nav_list.offsetWidth;        if(navScrollW < navListW){          this.scrollable = true;        }else{          if(this.listOffset > 0){            this.listOffset = 0;          }          this.scrollable = false;        }      },      // 滚动条滚动到激活的tab      scrollToActiveTab(){        if(this.scrollable){          this.$nextTick(() => {            let navScrollW = this.$refs.nav_scroll.offsetWidth;            let navList = this.$refs.nav_list;            let activeTab = navList.querySelector('.active');            let activeTabOffsetLeft = 0;            if(activeTab){              activeTabOffsetLeft = activeTab.offsetLeft;            }            let transformX = activeTabOffsetLeft + activeTab.offsetWidth - navScrollW;            transformX = transformX < 0 ? 0 : transformX;            this.listOffset = transformX;            if(transformX === 0){              this.navPrevDisabled = true;              this.navNextDisabled = false;            }else if(transformX === (navList.offsetWidth - navScrollW)){              this.navPrevDisabled = false;              this.navNextDisabled = true;            }else{              this.navPrevDisabled = false;              this.navNextDisabled = false;            }          });        }      }    },    computed: {      listOffsetTran(){        console.log('dddd',`translateX(-${this.listOffset}px);`)        return {          transform: `translateX(-${this.listOffset}px)`        }      }    },    render(h) {    /*dom结构    <div class="p-tabs_header is-scrollable">        <span class="p-tabs_nav-prev disabled"></span>        <span class="p-tabs_nav-next"></span>      <div class="p-tabs_nav-scroll">        <div class="p-tabs_nav-list">          <div class="p-tabs_nav-item active">全部</div>          <div class="p-tabs_nav-item disabled">技术教学</div>          <div class="p-tabs_nav-item">新手教学</div>        </div>      </div>    </div>    */      let navPrev = h('span', {        staticClass: 'p-tabs_nav-prev',        'class': {          disabled: this.navPrevDisabled        },        on: {          click: this.navPrevClickEvent        }      });      let navNext = h('span', {        staticClass: 'p-tabs_nav-next',        'class': {          disabled: this.navNextDisabled        },        on: {          click: this.navNextClickEvent        }      });      // 生成标签页      let navItems = this.tabPanes.map(item => {        let $labelSlot = item.$scopedSlots.label ? item.$scopedSlots.label() : null;        let labelContent = $labelSlot ? $labelSlot : item.label;        return h('div', {          staticClass: 'p-tabs_nav-item',          'class': {            active: this.$parent.currentName === item.name,            disabled: item.disabled,          },          on: {            click: (e) => {              this.handTabNavClick(item.name, item, e);            }          }        }, [labelContent]);      });      let navScroll = h('div', {        staticClass: 'p-tabs_nav-scroll',        ref: 'nav_scroll'      }, [        h('div', {          staticClass: 'p-tabs_nav-list',          ref: 'nav_list',          style: this.listOffsetTran        }, [navItems])      ]);      return h('div', {        staticClass: 'p-tabs_header',        'class': {          'is-scrollable': this.scrollable        },      }, [navPrev, navNext, navScroll]);    },    updated(){      this.calculateListSpilled();    },    mounted() {      this.calculateListSpilled();    }  }</script>

4、使用

main.js

// 引入tabs组件import tabs from './components/p-tabs';// 全局注册p-tabs组件Vue.use(tabs);

页面中使用

<PTabs v-model="activeName">    <PTabPane label="用户管理" name="first">用户管理</PTabPane>    <PTabPane label="配置管理" name="second">配置管理</PTabPane>    <PTabPane label="角色管理" name="third">角色管理</PTabPane>    <PTabPane label="定时任务补偿" name="fourth">定时任务补偿</PTabPane></PTabs>