171 lines
3.3 KiB
Vue
171 lines
3.3 KiB
Vue
<template>
|
|
<div class="tabs-container">
|
|
<!-- 左箭头 -->
|
|
<div
|
|
v-if="showArrows && canScrollLeft"
|
|
class="arrow arrow-left"
|
|
@click="scrollLeft"
|
|
>left</div>
|
|
<!-- 滚动容器 -->
|
|
<div class="tabs-scroll" ref="scrollRef" @scroll="handleScroll">
|
|
<div class="tabs">
|
|
<div
|
|
v-for="(tab, index) in tabs"
|
|
:key="index"
|
|
:class="['tab-item', { active: activeTab === index }, tab.class]"
|
|
@click="setActiveTab(index)"
|
|
>
|
|
<slot :name="`tab-${index}`"></slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 右箭头 -->
|
|
<div
|
|
v-if="showArrows && canScrollRight"
|
|
class="arrow arrow-right"
|
|
@click="scrollRight"
|
|
>right</div>
|
|
<!-- 内容区域 -->
|
|
<div class="tabs-content">
|
|
<slot :name="`content-${activeTab}`"></slot>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, watch } from 'vue';
|
|
|
|
// 接收默认激活的 tab 索引
|
|
const props = defineProps({
|
|
defaultActiveTab: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
});
|
|
|
|
// 当前激活的 tab 索引
|
|
const activeTab = ref(props.defaultActiveTab);
|
|
// 滚动容器的引用
|
|
const scrollRef = ref(null);
|
|
// 是否可以向左滚动
|
|
const canScrollLeft = ref(false);
|
|
// 是否可以向右滚动
|
|
const canScrollRight = ref(false);
|
|
// 是否显示箭头
|
|
const showArrows = ref(false);
|
|
|
|
// 设置激活的 tab
|
|
const setActiveTab = (index) => {
|
|
activeTab.value = index;
|
|
};
|
|
|
|
// 收集所有 tab 项
|
|
const tabs = [];
|
|
const slots = useSlots();
|
|
for (const key in slots) {
|
|
if (key.startsWith('tab-')) {
|
|
const index = parseInt(key.split('-')[1]);
|
|
const classProp = slots[`class-${index}`]?.()[0]?.props?.innerHTML;
|
|
tabs[index] = { class: classProp };
|
|
}
|
|
}
|
|
|
|
// 处理滚动事件
|
|
const handleScroll = () => {
|
|
const scrollEl = scrollRef.value;
|
|
canScrollLeft.value = scrollEl.scrollLeft > 0;
|
|
canScrollRight.value = scrollEl.scrollLeft < scrollEl.scrollWidth - scrollEl.clientWidth;
|
|
};
|
|
|
|
// 向左滚动
|
|
const scrollLeft = () => {
|
|
const scrollEl = scrollRef.value;
|
|
scrollEl.scrollLeft -= 200;
|
|
};
|
|
|
|
// 向右滚动
|
|
const scrollRight = () => {
|
|
const scrollEl = scrollRef.value;
|
|
scrollEl.scrollLeft += 200;
|
|
};
|
|
|
|
const checkArrowsVisibility = () => {
|
|
const scrollEl = scrollRef.value;
|
|
showArrows.value = scrollEl.scrollWidth > scrollEl.clientWidth;
|
|
};
|
|
|
|
onMounted(() => {
|
|
checkArrowsVisibility();
|
|
handleScroll();
|
|
});
|
|
|
|
// 监听滚动容器宽度变化
|
|
watch(
|
|
() => scrollRef.value?.scrollWidth,
|
|
() => {
|
|
checkArrowsVisibility();
|
|
handleScroll();
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tabs-container {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.arrow {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 30px;
|
|
height: 30px;
|
|
background-color: rgba(0, 0, 0, 0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
z-index: 1;
|
|
}
|
|
|
|
.arrow-left {
|
|
left: 0;
|
|
}
|
|
|
|
.arrow-right {
|
|
right: 0;
|
|
}
|
|
|
|
.tabs-scroll {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
scroll-behavior: smooth;
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
|
|
.tabs-scroll::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tab-item {
|
|
padding: 10px 15px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tab-item.active {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.tabs-content {
|
|
padding: 15px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
</style> |