vue2实现tab组件

2023/3/12 vue

使用Vue一步一步实现一个Tabs选项卡组件 (opens new window) Vue:造轮子-05:tabs组件 (opens new window)

# tab组件使用

<tabs value="name2"
      @change="changeTab">
  <tab-panel label="标签1"
             name="name1">标签一的内容</tab-panel>
  <tab-panel label="标签2"
             name="name2">标签二的内容</tab-panel>
  <tab-panel label="标签3"
             name="name3">标签三的内容</tab-panel>
</tabs>
1
2
3
4
5
6
7
8
9

# 实现tabs

<template>
  <!--tabs容器-->
  <div class="tabs">
    <!--标签页容器-->
    <div ref="navWrap"
         class="tabs-nav-wrap">
      <!--底部底部条-->
      <div class="tabs-inv-bar"
           :style="barStyle" />
      <!--标签页头label-->
      <div v-for="(item, index) in navList"
           :key="index"
           class="tabs-tab"
           @click="handleChange(index)">{{ item.label }}</div>
    </div>
    <!--所有pane组件使用的slot容器-->
    <div class="pane-content">
      <slot />
    </div>
  </div>
</template>
<script>
export default {
  name: 'Tabs',
  provide () {
    return { TabsInstance: this }
  },
  props: {
    value: {
      required: true,
      type: [String, Number]
    }
  },
  data () {
    return {
      navList: [],
      activeKey: this.value,
      barWidth: 0,
      barOffset: 0
    }
  },
  computed: {
    barStyle () {
      return {
        width: `${this.barWidth}px`,
        transform: `translate3d(${this.barOffset}px,0px,0px)`
      }
    }
  },
  watch: {
    value (val) {
      this.activeKey = val
    },
    activeKey () {
      this.updateStatus()
      this.updateBar()
    }
  },
  methods: {
    // 初始化更新
    initTabs () {
      this.updateNav()
      this.updateStatus()
      this.updateBar()
    },
    // 显示当前tab激活的content的内容
    updateStatus () {
      const tabs = this.getTabs()
      tabs.forEach(tab => (tab.show = tab.name === this.activeKey))
    },
    // 获取tabs下的所有pane实例
    getTabs () {
      return this.$children.filter(item => item.$options.name === 'TabPanel')
    },
    // 获取所有pane组件用户传入的props
    updateNav () {
      this.navList = []
      this.getTabs().forEach((pane, index) => {
        this.navList.push({
          label: pane.label,
          name: pane.name || index
        })
        // 如果不传value,默认选中第一项
        if (index === 0 && !this.activeKey) {
          this.activeKey = pane.name
        }
      })
    },
    // 改变activeKey,并监听activeKey重新更新显示状态
    handleChange (index) {
      const nav = this.navList[index]
      this.activeKey = nav.name
      this.$emit('change', index)
    },
    updateBar () {
      // 等待dom更新完毕后获取dom节点
      this.$nextTick(() => {
        // 当前选中的activeKey下标
        const index = this.navList.findIndex(nav => nav.name === this.activeKey)
        // 获取navWrap元素下的所有tab的元素
        const elemTabs = this.$refs.navWrap.querySelectorAll('.tabs-tab')
        // 获取当前选中的元素
        const elemTab = elemTabs[index]
        this.barWidth = elemTab ? elemTab.offsetWidth : 0
        // 计算需要移动的距离,当index > 0时进行累加
        if (index > 0) {
          let offset = 0
          for (let i = 0; i < index; i++) {
            offset += elemTabs[i].offsetWidth
          }
          this.barOffset = offset
        } else {
          this.barOffset = 0
        }
      })
    }

  }
}
</script>
<style>
.tabs{
  position: relative;
}
.tabs-nav-wrap {
  position: relative;
  border-bottom: 1px solid #dcdee2;
  margin-bottom: 16px;
}
.tabs-tab {
  display: inline-block;
  padding: 8px 16px;
  cursor: pointer;
}
.tabs-inv-bar {
  position: absolute;
  left: 0;
  bottom: 0;
  background-color: #2d8cf0;
  height: 2px;
  transition: transform 300ms ease-in-out;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
  • 更新 tab line css
if (index > 0) {
  let offset = elemTab.offsetWidth / 2
  for (let i = 0; i < index; i++) {
    offset += elemTabs[i].offsetWidth
  }
  this.barOffset = offset
} else {
  this.barOffset = elemTab.offsetWidth / 2
}

barStyle () {
  return {
    // width: `${this.barWidth}px`,
    transform: `translate3d(${this.barOffset}px,0,0) translateX(-50%)`
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 实现tabPanel

<template>
  <div v-show="show">
    <slot />
  </div>
</template>

<script>
export default {
  name: 'TabPanel',
  inject: ['TabsInstance'],
  props: {
    name: {
      type: String,
      required: true
    },
    label: {
      type: [String, Function],
      default: ''
    }
  },
  data () {
    return {
      show: true
    }
  },
  watch: {
    name () {
      this.TabsInstance.initTabs()
    }
  },
  mounted () {
    this.TabsInstance.initTabs()
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35