使用Vue开发Tabs组件(二)

一个 Tabs 的组成

Tabs

-Tab Header
-Tab Panel

一、TabPanel 组件开发

TabPanel Template 基本结构

1
2
3
<div :class="panelCls()">
<slot></slot>
</div>

因为每个 Tab Header label name 需要在子组件上配置,故该子组件的 props 应该有以下俩值

1
2
3
4
5
6
7
props: {
label: {
default: "",
type: String
},
name: [String, Number]
}

为了判断 TabPanel 显示和隐藏,故在 data 中定义以下值

1
2
3
4
5
6
data() {
return {
show: true,
currentName: this.name
}
},

随之便引出了控制隐藏显示的样式方法

1
2
3
panelCls(){
return ["tab-panel-content", {['panel-active']: this.show === true}];
}

TabPanel name label 值发生变化后, Tab Header 也需要相应的进行更新渲染,定义一个 updateNav 函数 直接去调用父类的 updateNav 函数进行更新。随后在添加上要监听的属性和基本配置得出 TabPanel 组件所有代码

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
<template>
<div :class="panelCls()">
<slot></slot>
</div>
</template>
<script>
export default {
data() {
return {
show: true,
currentName: this.name
};
},
name: "TabPanel",
props: {
label: {
type: [String, Function],
default: ""
},
name: [String, Number]
},
watch: {
label() {
this.updateNav();
},
name(val) {
this.currentName = val;
this.updateNav();
}
},
inject: ["TabsInstance"],
computed: {},
methods: {
updateNav() {
this.TabsInstance.updateNav();
},
panelCls() {
return ["tab-panel-content", {['panel-active']: this.show === true}];
}
},
mounted() {
this.updateNav();
}
};
</script>

二、编写父类组件 Tabs

Tabs Template 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="tab">
<div class="tab-header">
<ul class="tab-item" ref="nav">
<li
:class="tabCls(item)"
v-for="(item, index) in navList"
:key="index"
@click="handleChange(index)"
>
{{item.label}}
</li>
</ul>
<div class="active-bar-link bar-animated" :style="barStyle"></div>
</div>
<div class="tab-panel" ref="panels" :style="contentStyle">
<slot></slot>
</div>
</div>

在之前实现的 Html 的基础稍加改造。使用 for 循环渲染 Tab Header , class 改为函数,更好的控制选中非选中样式变动。根据上面可在JS中定义以下结构

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
export default {
name: "Tabs",
data() {
return {
navList: [],
activeKey: this.value,
};
},
props: {
value: [String, Number]
},
watch: {},
computed: {},
mounted() {},
methods: {
handleChange(index) {
this.activeKey = this.navList[index].name;
},
tabCls(item) {
return [
`tab-item-title`,
{
[`tab-active`]: item.name === this.activeKey
}
];
},
}
};

接下来编写很重要的一个函数 updateNav ,在子元素,挂载更新时都需要调用到

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
// 先获取所有面板
getPanels() {
const TabPanels = this.$children.filter(
item => item.$options.name === "TabPanel"
);
TabPanels.sort((a, b) => {
if (a.index && b.index) {
return a.index > b.index ? 1 : -1;
}
});
return TabPanels;
},
updateNav() {
this.navList = [];
this.getPanels().forEach((panel, index) => {
this.navList.push({
label: panel.label,
name: panel.name || index
});
if (!panel.currentName) panel.currentName = index;
if (index === 0) {
if (!this.activeKey)
this.activeKey = panel.currentName || index;
}
});
this.updateStatus();
},
updateStatus() {
const navs = this.getPanels();
navs.forEach(
tab => (tab.show = tab.currentName === this.activeKey)
);
}

最后在进行一些面板切换和选中的样式细节优化,再加上一个简单的 click 事件,调整的代码为

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
<template>
<div class="tab">
<div class="tab-header">
<ul class="tab-item" ref="nav">
<li
:class="tabCls(item)"
v-for="(item, index) in navList"
:key="index"
@click="handleChange(index)"
>
{{item.label}}
</li>
</ul>
<div class="active-bar-link bar-animated" :style="barStyle"></div>
</div>
<div class="tab-panel" ref="panels" :style="contentStyle">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "Tabs",
data() {
return {
navList: [],
activeKey: this.value,
barWidth: 0,
barOffset: 0
};
},
props: {
value: [String, Number]
},
provide() {
return { TabsInstance: this };
},
watch: {
activeKey() {
this.updateStatus();
this.updateBar();
}
},
computed: {
barStyle(){
let style = {
visibility: 'visible',
width: `${this.barWidth}px`
}
style.transform = `translate3d(${this.barOffset}px, 0px, 0px)`;
return style;
},
contentStyle(){
const index = this.getTabIndex(this.activeKey);
return {'margin-left': index>0 ?`-${index}00%`:0};
}
},
mounted() {},
methods: {
handleChange(index) {
this.activeKey = this.navList[index].name;
const nav = this.navList[index];
this.$emit('tab-click', nav.name)
},
tabCls(item) {
return [
`tab-item-title`,
{
[`tab-active`]: item.name === this.activeKey
}
];
},
getPanels() {
const TabPanels = this.$children.filter(
item => item.$options.name === "TabPanel"
);
TabPanels.sort((a, b) => {
if (a.index && b.index) {
return a.index > b.index ? 1 : -1;
}
});
return TabPanels;
},
updateNav() {
this.navList = [];
this.getPanels().forEach((panel, index) => {
this.navList.push({
label: panel.label,
name: panel.name || index
});
if (!panel.currentName) panel.currentName = index;
if (index === 0) {
if (!this.activeKey)
this.activeKey = panel.currentName || index;
}
});
this.updateStatus();
this.updateBar();
},
updateBar() {
this.$nextTick(() => {
const index = this.getTabIndex(this.activeKey);
if (!this.$refs.nav) return;
const prevTabs = this.$refs.nav.querySelectorAll('.tab-item-title');
const tab = prevTabs[index];
this.barWidth = tab? parseFloat(tab.offsetWidth): 0;
if(index > 0){
let offset = 0;
for(let i = 0;i < index;i++){
offset += prevTabs[i].offsetWidth
}
this.barOffset = offset;
}else{
this.barOffset = 0;
}
});
},
getTabIndex(name) {
return this.navList.findIndex(nav => nav.name === name);
},
updateStatus() {
const navs = this.Panels();
navs.forEach(
tab => (tab.show = tab.currentName === this.activeKey)
);
}
}
};
</script>

Css 样式

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
html {
height: 100%;
padding: 0;
margin: 0;
}

a {
text-decoration: none;
}

a:visited {
color: #000;
}

.clear {
clear: both;
}

.tab {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
-webkit-font-feature-settings: "tnum";
font-feature-settings: "tnum";
position: relative;
overflow: hidden;
zoom: 1;
}

.tab-header{
border-bottom: 1px solid #e8e8e8;
}

.tab-item {
list-style: none;
padding-inline-start: 0;
margin-block-end: 0;
}

.tab-item > .tab-item-title {
position: relative;
float: left;
padding: 10px 15px;
text-align: center;
font-weight: 500;
color: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
}

.tab-item > .tab-item-title.tab-active {
color: #1890ff;
}


.tab-item > .tab-item-title::before {
content: "";
position: absolute;
left: 0;
bottom: 0;
height: 2px;
width: 0;
}

.active-bar-link{
background-color: #1890ff;
height: 2px;
clear: both;
}

.active-bar-link.bar-animated{
-webkit-transition: width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1),-webkit-transform .3s cubic-bezier(.645, .045, .355, 1);
transition: width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1),-webkit-transform .3s cubic-bezier(.645, .045, .355, 1);
transition: transform .3s cubic-bezier(.645, .045, .355, 1),width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1);
transition: transform .3s cubic-bezier(.645, .045, .355, 1),width .3s cubic-bezier(.645, .045, .355, 1),left .3s cubic-bezier(.645, .045, .355, 1),-webkit-transform .3s cubic-bezier(.645, .045, .355, 1);
}

.tab-item > .tab-item-title.tab-active > a {
color: #1890ff;
}

.tab-panel {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
transition: margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
will-change: margin-left;
width: 100%;
}

.tab-panel::before {
display: block;
overflow: hidden;
content: "";
}

.tab-panel-content {
height: 0;
padding: 0 !important;
opacity: 0;
pointer-events: none;
flex-shrink: 0;
width: 100%;
-webkit-transition: opacity 0.45s;
transition: opacity 0.45s;
}

.tab-panel-content.panel-active {
flex-shrink: 0;
height: auto;
width: 100%;
opacity: 1;
-webkit-transition: opacity 0.45s;
transition: opacity 0.45s;
}

结语

以上一个基本的 Tabs 组件就实现了。

Tabs代码