功能介绍:
- 切换动画
- 手势滑动切换
- lazyRender
- 阿拉伯语等页面镜像后的动画调整(isRtl替换为项目中自己定义的属性,此处未做props)
// 样式中的颜色自行更改
<template>
<div class="tabs">
<div class="tab-list">
<div
class="tab"
:class="{'tab_active': index === activeIndex}"
v-for="(tab, index) in tabs" :key="index"
@click="handleTabClick(index)"
:style="{
'background-color': index === activeIndex ? activeBgColor : '',
'color': index === activeIndex ? activeColor : ''
}"
>
{{ tab }}
</div>
</div>
<div
class="tab-content"
ref="conetnt"
@touchstart="handleTouchStart"
@touchmove="handleTouchmove"
@touchend="handleTouchEnd"
>
<div
ref="lazyContainer"
class="tab-pane"
v-for="(_, index) in tabs" :key="index"
:data-index="index"
:style="{
display: (animated || swipeable) ? 'block' : index === activeIndex ? 'block' : 'none',
// 用于 lazy render
marginLeft: index === activeIndex && $store.state.params.isRtl ? `${marginValue}px` : '',
marginRight: index === activeIndex && !$store.state.params.isRtl ? `${marginValue}px` : '',
}"
:class="[{'tab-pane_animated': animated || swipeable}, {'current': index === activeIndex}]"
>
<slot v-if="loadedTabs.includes(`${index}`)"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
// tab 列表
tabs: {
type: Array,
default: () => []
},
// 选中的背景颜色
activeBgColor: {
type: String,
default: ''
},
// 选中的字体颜色
activeColor: {
type: String,
default: ''
},
// 默认选中的 tab
active: {
type: Number,
default: 0
},
// 是否开启切换动画
animated: {
type: Boolean,
default: false
},
// 是否开启滑动切换
swipeable: {
type: Boolean,
default: false
},
// 是否开启懒加载
lazyRender: {
type: Boolean,
default: true
},
},
computed: {
istest(index) {
return this.loadedTabs.includes(index);
}
},
data() {
return {
activeIndex: this.active,
startX: 0,
endX: 0,
loadedTabs: [],
observer: null,
marginValue: 0,
};
},
methods: {
handleTabClick(index) {
this.activeIndex = index;
// 开启切换动画
this.animated && this.handleChangeTab(index);
this.$emit('click', index);
},
handleChangeTab(index) {
const { isRtl } = this.$store.state.params;
const content = this.$refs.conetnt;
content.style.transform = `translateX(${isRtl ? '+' : '-'}${index * 100}%)`;
content.style.transition = 'all .3s';
this.$emit('change', index);
},
handleTouchStart(event) {
if (!this.swipeable) return;
this.startX = event.touches[0].clientX;
},
handleTouchmove(event) {
if (!this.swipeable) return;
this.endX = event.touches[0].clientX;
},
handleTouchEnd() {
if (!this.swipeable) return;
const deltaX = this.startX - this.endX;
if (deltaX > 100 && this.activeIndex < this.tabs.length - 1 && this.endX) {
this.activeIndex += 1;
this.handleChangeTab(this.activeIndex);
}
else if (deltaX < -100 && this.activeIndex > 0 && this.endX) {
this.activeIndex -= 1;
this.handleChangeTab(this.activeIndex);
}
},
initObserver() {
this.observer = new IntersectionObserver(entries => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = this.loadedTabs.indexOf(entry.target.getAttribute('data-index'));
if (index === -1) {
this.loadedTabs.push(entry.target.getAttribute('data-index'));
}
}
});
});
const target = this.$refs.lazyContainer;
target.forEach(item => {
this.observer.observe(item);
});
},
disconnectObserver() {
this.observer && this.observer.disconnect();
}
},
beforeDestroy() {
this.disconnectObserver();
},
mounted() {
// 是否开启 lazy render
if (this.lazyRender) {
this.initObserver();
}
else {
this.loadedTabs = this.tabs.map((_, index) => {
return `${index}`;
});
}
// 获取当前 tab 占据满屏幕所需的差值
const content = this.$refs.conetnt;
this.marginValue = (window.screen.width - content.offsetWidth) / 2;
},
};
</script>
<style lang="less" scoped>
.tabs {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.tab-list {
display: flex;
align-items: center;
margin-bottom: 20px;
.tab {
padding: 2px 10px;
color: var(--color-content-2);
}
.tab_active {
font-weight: bold;
border-radius: 4px;
}
}
.tab-content {
display: flex;
flex: 1;
.tab-pane {
flex: 1;
display: none;
}
.tab-pane_animated {
flex: 1 0 100%;
}
}
}
</style>