my-tabs/src/Tabs.vue

<template>
  <MyContainer class="my-tabs" :class="classes" :fit="isFit" v-bind="$attrs">
    <Tabs v-if="tabs"
          :type="cardType"
          class="my-tabs__tabs"
          v-model="tabName"
          :tab-position="currentPosition"
          :stretch="stretch">
      <TabPane v-for="(tab, index) in tabs"
               :key="tab.name"
               :name="tab.name"
               :label="tab.label"
               lazy>
        <slot slot="label" name="label" v-bind="{tab, index}">{{tab.label}}</slot>
      </TabPane>
    </Tabs>
    <div class="my-tabs__body">
      <slot :tab="currentTab">
        <component v-if="tabComponent" :is="tabComponent" v-bind="currentTab.props" v-on="currentTab.on" :ref="currentTab.ref"></component>
      </slot>
    </div>
  </MyContainer>
</template>

<script>
  /**
   * 选项卡容器组件
   * @module $ui/components/my-tabs
   */
  import {MyContainer} from '$ui'
  import {Tabs, TabPane} from 'element-ui'
  import {addResizeListener, removeResizeListener} from 'element-ui/lib/utils/resize-event'

  /**
   * 插槽
   * @member slots
   * @property {string} label 作用域插槽,定义选项卡的头部,参数 tab:选项卡项数据,index:选项卡索引
   * @property {string} default 作用域插槽,定义选项卡内容,参数 tab:选项卡项数据
   */
  export default {
    name: 'MyTabs',
    components: {
      MyContainer,
      Tabs,
      TabPane
    },
    /**
     * 属性参数, 继承 MyContainer 的全部参数,如 fit、shadow、border
     * @member props
     * @property {array} [tabs] 选项卡数据数组, 项对象 {name,label,component, props}
     * @property {string} [tabs.name] 选项卡名称,用来标识选项卡
     * @property {string} [tabs.label] 选项卡文本,复杂内容可用插槽定义
     * @property {string|VueComponent} [tabs.component] 选项卡加载的组件,可选。定义内容插槽,该设置将会失效
     * @property {object} [tabs.props] 组件的实例化参数,对 设置 tabs.component 有效
     * @property {string} [defaultActive] 初始激活的选项卡名称
     * @property {string} [position=left] tab显示位置,可选 'left', 'right', 'top'
     * @property {boolean} [stretch] 标签的宽度是否自撑开,对 position 为 top 有效
     * @property {number} [responsiveWidth] 响应式的宽度,小于该值后变成position=top
     * @property {boolean} [fixed] 固定tabs,不随页面滚动,对 position 为 left 或 right 有效
     * @property {boolean} [card] 开启卡片模式,仅对 position 为 top 有效
     *
     */
    props: {
      // [{name, label, component, props}]
      tabs: {
        type: Array
      },
      // 绑定值,选中选项卡的 name
      defaultActive: {
        type: String
      },
      // tab 位置
      position: {
        type: String,
        default: 'left',
        validator(val) {
          return ['left', 'right', 'top'].includes(val)
        }
      },
      // 标签的宽度是否自撑开,对 position 为 top 有效
      stretch: Boolean,
      // 开启响应式
      responsiveWidth: Number,
      // 固定tabs
      fixed: Boolean,
      // 卡片模式
      card: Boolean
    },
    data() {
      return {
        tabName: null,
        currentPosition: this.position
      }
    },
    computed: {
      currentTab() {
        if (!this.tabs) return null
        return this.tabs.find(t => t.name === this.tabName)
      },
      tabComponent() {
        if (!this.currentTab) return null
        return this.currentTab.component
      },
      classes() {
        return {
          [`is-${this.currentPosition}`]: !!this.currentPosition,
          'is-fixed': this.fixed,
          'is-card': !!this.cardType
        }
      },
      isFit() {
        return (this.currentPosition === 'top' && this.fixed) || this.$attrs.fit
      },
      cardType() {
        return (this.currentPosition === 'top' && this.card) ? 'card' : null
      },
      isResponsive() {
        return this.responsiveWidth > 0 && this.currentPosition !== 'top'
      }
    },
    watch: {
      defaultActive: {
        immediate: true,
        handler(val) {
          if (!val && this.tabs && this.tabs[0]) {
            this.tabName = this.tabs[0].name
          } else {
            this.tabName = val
          }
        }
      },
      tabName(val) {
        /**
         * 选项卡切换时触发
         * @event change
         * @param {string} name 选项卡名称
         * @param {Object} tab 选项卡数据对象
         */
        this.$emit('change', val, this.currentTab)
      },
      position: {
        immediate: true,
        handler(val) {
          this.currentPosition = val
        }
      }
    },
    methods: {
      responsive() {
        const rect = this.$el.getBoundingClientRect()
        this.currentPosition = rect.width < this.responsiveWidth
          ? 'top'
          : this.position
      }
    },
    mounted() {
      this.isResponsive && addResizeListener(this.$el, this.responsive)
    },
    beforeDestroy() {
      this.isResponsive && removeResizeListener(this.$el, this.responsive)
    }
  }
</script>