tabs.vue
<template>
<div class="tabs">
<div class="tabs__titles" ref="tabsTitlesRef">
<div class="tabs__titles-item"
:class="{active : index === activeIndex}"
@click="tabChange(item, index)"
v-for="(item, index) in titles"
:key="item.index">
<div class="tabs__titles-item__line"></div>
<slot v-if="$slots.title" name="title" :row="item"></slot>
<div v-else class="tabs__titles-item__text">{{ item }}</div>
</div>
</div>
<div class="tabs__content" :style="{'transform': translate}">
<slot name="default"></slot>
</div>
</div>
</template>
<script setup>
import {
ref,
useSlots,
provide,
defineProps,
} from 'vue';
// eslint-disable-next-line
const props = defineProps({
modelValue: {
default: '',
},
});
const $slots = useSlots();
const titles = ref([]);
const tabsTitles = ref(null);
const activeIndex = ref(0);
const translate = ref('translateX(0%)');
const getTitles = (val) => {
if (!$slots.title && typeof val !== 'string') {
console.error('非自定义标题内容不允许传递对象类型');
} else {
titles.value.push(val);
}
};
provide('getTitles', getTitles);
provide('activeKey', props.modelValue);
const scrollIntoView = (index) => {
const { length } = document.querySelectorAll('.tabs__titles-item');
const activeNodeNextNodeWidth = length === index + 1 ? 0 : document.querySelectorAll('.tabs__titles-item')[index + 1].offsetWidth;
const width = window.innerWidth / 2 - activeNodeNextNodeWidth;
let offset = 0;
for (let i = length - 1; i > index + 1; i--) {
offset += document.querySelectorAll('.tabs__titles-item')[i].offsetWidth;
}
if (offset > width) {
document.querySelectorAll('.tabs__titles-item')[index].scrollIntoView({ inline: 'center' });
} else {
document.querySelectorAll('.tabs__titles-item')[length - 1].scrollIntoView({ inline: 'end' });
}
};
const tabChange = (item, index) => {
activeIndex.value = index;
translate.value = `translateX(${-100 * index}%)`;
scrollIntoView(index);
};
</script>
<style lang="scss">
.tabs {
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabs__titles {
display: flex;
flex-wrap: nowrap;
align-items: center;
background-color: #f8f8f8;
flex-shrink: 0;
overflow-x: auto;
overflow-y: hidden;
padding: 0 10px;
scroll-behavior: smooth;
.tabs__titles-item {
width: auto!important;
position: relative;
flex: 1 0 auto;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
min-width: 70px;
padding: 0 10px;
}
}
.tabs__titles-item.active {
font-weight: 600;
.tabs__titles-item__line {
width: 40px;
transition: width .3s ease;
}
}
.tabs__titles-item__line {
position: absolute;
bottom: 15%;
left: 50%;
width: 0;
height: 3px;
background: -webkit-gradient(linear,left top,right top,from(#fa2c19),to(rgba(250,44,25,.15)));
background: linear-gradient(90deg,#fa2c19 0%,rgba(250,44,25,.15) 100%);
transform: translate(-50%);
overflow: hidden;
}
.tabs__content {
display: flex;
flex-wrap: nowrap;
transition-duration: 300ms;
}
.tabs__titles::-webkit-scrollbar {
display: none;
width: 0;
background: transparent;
}
</style>
tabpane.vue
<template>
<div class="tabpane">
<slot></slot>
</div>
</template>
<script setup>
import {
defineProps,
inject,
onMounted,
} from 'vue';
// eslint-disable-next-line
const props = defineProps({
title: [String, Object],
paneKey: [String, Number],
});
const getTitles = inject('getTitles');
onMounted(() => {
getTitles(props.title);
});
</script>
<style>
.tabpane {
width: 100%;
padding: 20px;
flex-shrink: 0;
overflow: auto;
height: 100%;
}
</style>
使用
<template>
<tabs v-model="state.tabValue">
<tab-pane title="tab1">tab1内容</tab-pane>
<tab-pane title="tab2">tab2内容</tab-pane>
<tab-pane title="tab3">tab3内容</tab-pane>
</tabs>
</template>
<script setup>
import { reactive } from 'vue';
const state = reactive({
tabValue: '0'
})
</script>