封装一个Tabs组件

3,226 阅读1分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

TIP 👉 生于优患,死于安乐。《孟子·告于下》

前言

在我们日常项目开发中,我们经常会写一些选项卡,所以封装了这款tab项组件。

标签页

Tabs组件属性

1. value
  • 选中标签页值(即选中TabPane的name值)
  • 值为字符串类型
  • 非必填默认为第一个TabPane的name
2. lazy
  • 未显示的内容面板是否延迟渲染
  • 值为布尔类型
  • 默认为false
样式要求
  • 组件外面需要包裹可以相对定位的元素,增加样式:position: relative

TabPane组件属性

1. name
  • 标签页英文名称
  • 值为字符串类型
  • 必填
2. label
  • 标签页导航标签显示的文字
  • 值为字符串类型
  • 必填
3. scroll
  • 是否有滚动条
  • 值为布尔类型
  • 默认值为:true

示例

<template>
  <div class="tabs-wrap">
    <Tabs v-model="curTab" :lazy="true">
      <TabPane name="list1" label="列表1"><TabPaneList1></TabPaneList1></TabPane>
      <TabPane name="list2" label="列表2"><TabPaneList2></TabPaneList2></TabPane>
      <TabPane name="list3" label="列表3"><TabPaneList3></TabPaneList3></TabPane>
    </Tabs>
  </div>
</template>

<script>
import { Tabs, TabPane } from '@/components/m/tabs'
import TabPaneList1 from './TabPaneList1.vue'
import TabPaneList2 from './TabPaneList2.vue'
import TabPaneList3 from './TabPaneList3.vue'

export default {
  name: 'TabsDemo',
  components: {
    Tabs,
    TabPane,
    TabPaneList1,
    TabPaneList2,
    TabPaneList3
  },
  data () {
    return {
      // 当前选中的标签
      curTab: ''
    }
  }
}
</script>

<style lang="scss" scoped>
.tabs-wrap {
  position: absolute;
  top: $app-title-bar-height;
  bottom: 0;
  left: 0;
  right: 0;
}
</style>

实现tabs.vue

<template>
  <div class="tabs">
    <div class="tab-list" ref="tabWrap">
      <div class="tab-item"
           v-for="info in tabInfos"
           :key="info.name"
           :class="{active: info.name === curValue}"
           @click="handleClickTab(info.name)">
        {{info.label}}
      </div>
      <div v-if="tabLineX" class="tab-line" :style="{ transform: `translateX(${tabLineX}px) translateX(-50%)`}"></div>
    </div>
    <div class="tab-pane-wrap">
      <slot></slot>
    </div>
  </div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
  name: 'Tabs',
  props: {
    // 选中标签页值(页签的英文名)
    value: String,
    // 未显示的内容面板是否延迟渲染
    lazy: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      // 组件实例的唯一ID
      id: generateUUID(),
      // 当前选中的标签页值(标签页的英文名)
      curValue: this.value,
      // 标签页信息数组
      tabInfos: [],
      // 标签页面板vue实例数组
      panes: [],
      // 表示当前标签页的蓝线x坐标位置
      tabLineX: 0
    }
  },
  watch: {
    value (val) {
      this.curValue = val
    },
    curValue (val) {
      this.$eventBus.$emit('CHANGE_TAB' + this.id, val)
      this.$emit('change', val)
      this.calcTabLineX()
    }
  },
  computed: {
    // 当前选中标签页索引
    curIndex () {
      let index = -1
      for (let i = 0; i < this.tabInfos.length; i++) {
        if (this.tabInfos[i].name === this.curValue) {
          index = i
          break
        }
      }
      return index
    }
  },
  mounted () {
    this.calcPaneInstances()
  },
  beforeDestroy () {
    this.$eventBus.$off('CHANGE_TAB' + this.id)
  },
  methods: {
    // 计算标签页面板实例信息
    calcPaneInstances () {
      if (this.$slots.default) {
        const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
          vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'TabPane')
        const panes = paneSlots.map(({ componentInstance }) => componentInstance)
        const tabInfos = paneSlots.map(({ componentInstance }) => {
          console.log(componentInstance.name, componentInstance)
          return {
            name: componentInstance.name,
            label: componentInstance.label
          }
        })
        this.tabInfos = tabInfos
        this.panes = panes
        if (!this.curValue) {
          if (tabInfos.length > 0) {
            this.curValue = tabInfos[0].name
          }
        } else {
          this.$eventBus.$emit('CHANGE_TAB' + this.id, this.curValue)
          this.calcTabLineX()
        }
      }
    },
    // 标签页点击事件处理方法
    handleClickTab (val) {
      this.curValue = val
    },
    // 计算表标识表示当前标签页蓝线的位置
    calcTabLineX () {
      let wrapWidth = this.$refs.tabWrap.getBoundingClientRect().width
      let tabWidth = this.panes.length > 0 ? wrapWidth / this.panes.length : wrapWidth
      let positionX = tabWidth * (this.curIndex + 0.5)
      this.tabLineX = positionX
    }
  }
}
</script>
<style lang="scss" scoped>
.tabs {
  display: flex;
  flex-direction: column;
  height: 100%;
  .tab-list {
    position: relative;
    flex: none;
    display: flex;
    background-color: #FFF;
    .tab-item {
      flex: 1;
      line-height: 80px;
      text-align: center;
      color: #8E8E8E;
      &.active {
        position: relative;
        color: $base-color;
      }
    }
    .tab-line {
      position: absolute;
      bottom: 0;
      left: 0;
      z-index: 1;
      width: 4em;
      height: 4px;
      background-color: $base-color;
      transition-duration: 0.3s;
    }
  }
  .tab-pane-wrap {
    flex: 1;
  }
}
</style>

感谢评论区大佬的点拨。

希望看完的朋友可以给个赞,鼓励一下