一个 Tabs
的组成
Tabs
-Tab Header
-Tab Panel
一、TabPanel 组件开发
TabPanel
的 Template
基本结构
<div :class="panelCls()">
<slot></slot>
</div>
因为每个 Tab Header
的 label
和 name
需要在子组件上配置,故该子组件的 props
应该有以下俩值
props: {
label: {
default: "",
type: String
},
name: [String, Number]
}
为了判断 TabPanel
显示和隐藏,故在 data
中定义以下值
data() {
return {
show: true,
currentName: this.name
}
},
随之便引出了控制隐藏显示的样式方法
panelCls(){
return ["tab-panel-content", {['panel-active']: this.show === true}];
}
当 TabPanel
的 name
, label
值发生变化后, Tab Header
也需要相应的进行更新渲染,定义一个 updateNav
函数 直接去调用父类的 updateNav
函数进行更新。随后在添加上要监听的属性和基本配置得出 TabPanel
组件所有代码
<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
基本结构
<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中定义以下结构
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
,在子元素,挂载更新时都需要调用到
// 先获取所有面板
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
事件,调整的代码为
<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
样式
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
组件就实现了。