vue3 element-plus实现长滚动,一个一个内容块 和tab联动
简单实现
在 Vue 3 中使用 Element Plus 来实现长滚动页面,并且让每个内容块与 Tab
组件联动,可以通过以下步骤实现:
布局设计:
- 使用
ElTabs
作为导航栏,每个ElTabPane
对应一个内容块。 - 使用
IntersectionObserver
或者基于滚动事件检测滚动位置,并同步到Tab
组件。
- 使用
使用 ElTabs 组件:
ElTabs
组件用于显示不同的内容块,并且可以通过监听选项卡的切换和手动滚动触发 tab 切换。使用 IntersectionObserver 检测滚动: 可以使用
IntersectionObserver
来检测当前页面的哪个内容块在视图区域内,并同步到ElTabs
。
代码示例
<template>
<div>
<!-- Tab导航 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane v-for="(item, index) in tabs" :key="index" :label="item.label" :name="item.name"></el-tab-pane>
</el-tabs>
<!-- 滚动区域 -->
<div ref="scrollContainer" class="scroll-container" @scroll="onScroll">
<div v-for="(item, index) in tabs" :key="index" :ref="`block-${index}`" class="content-block">
<h2>{{ item.label }}</h2>
<p>{{ item.content }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const activeTab = ref('tab-1'); // 默认激活第一个Tab
const tabs = ref([
{ label: 'Tab 1', name: 'tab-1', content: '内容 1' },
{ label: 'Tab 2', name: 'tab-2', content: '内容 2' },
{ label: 'Tab 3', name: 'tab-3', content: '内容 3' },
{ label: 'Tab 4', name: 'tab-4', content: '内容 4' },
]);
// 监听Tab点击事件,滚动到对应内容块
const handleTabClick = (tab) => {
const targetBlock = document.querySelector(`[ref="block-${tabs.value.findIndex(t => t.name === tab.paneName)}"]`);
if (targetBlock) {
targetBlock.scrollIntoView({ behavior: 'smooth' });
}
};
// 监听滚动事件,更新Tab激活状态
const onScroll = () => {
const scrollContainer = document.querySelector('.scroll-container');
const blocks = tabs.value.map((_, index) => document.querySelector(`[ref="block-${index}"]`));
blocks.forEach((block, index) => {
const rect = block.getBoundingClientRect();
if (rect.top >= 0 && rect.top < scrollContainer.offsetHeight / 2) {
activeTab.value = tabs.value[index].name;
}
});
};
</script>
<style>
.scroll-container {
height: 400px;
overflow-y: auto;
}
.content-block {
padding: 20px;
height: 400px;
border: 1px solid #dcdcdc;
margin-bottom: 10px;
}
</style>
关键点解释:
ElTabs:
- 使用
v-model
绑定activeTab
来控制当前激活的标签页。 - 监听
tab-click
事件,通过点击 Tab 切换内容块。
- 使用
滚动联动:
- 使用
scroll
事件监听滚动条的位置,在滚动时计算当前可视区域的内容块并同步Tab
激活状态。 - 通过
scrollIntoView
实现点击 Tab 时的平滑滚动效果。
- 使用
样式:
- 设置
.scroll-container
高度和overflow-y: auto
,保证内容可以滚动。
- 设置
这样你就可以实现一个内容块与 Tab 组件联动的滚动效果。
封装为一个通用组件
可以将这个滚动联动的功能封装为一个通用的组件,这样就可以在不同的页面和项目中复用。我们可以将 ElTabs
与内容区域的联动功能封装为一个组件 ScrollableTabs
,然后通过 props
来传递数据和选项。
封装通用组件 ScrollableTabs
<template>
<div class="scrollable-tabs">
<!-- Tab导航 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane
v-for="(item, index) in tabs"
:key="index"
:label="item.label"
:name="item.name"
></el-tab-pane>
</el-tabs>
<!-- 滚动区域 -->
<div ref="scrollContainer" class="scroll-container" @scroll="onScroll">
<div
v-for="(item, index) in tabs"
:key="index"
:ref="`block-${index}`"
class="content-block"
>
<!-- 插槽内容,支持自定义 -->
<slot :name="item.name" :content="item.content"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
// 定义 props
const props = defineProps<{
tabs: { label: string; name: string; content: string }[];
initialTab?: string; // 默认激活的Tab
}>();
// 当前激活的Tab
const activeTab = ref(props.initialTab || props.tabs[0].name);
// 监听 activeTab 的变化,并滚动到对应内容块
const handleTabClick = (tab: any) => {
const index = props.tabs.findIndex((t) => t.name === tab.paneName);
scrollToBlock(index);
};
// 滚动到指定内容块
const scrollToBlock = (index: number) => {
const targetBlock = document.querySelector(`[ref="block-${index}"]`);
if (targetBlock) {
targetBlock.scrollIntoView({ behavior: 'smooth' });
}
};
// 监听滚动事件,更新Tab激活状态
const onScroll = () => {
const scrollContainer = document.querySelector('.scroll-container');
const blocks = props.tabs.map((_, index) =>
document.querySelector(`[ref="block-${index}"]`)
);
blocks.forEach((block, index) => {
const rect = block.getBoundingClientRect();
if (rect.top >= 0 && rect.top < (scrollContainer as HTMLElement).offsetHeight / 2) {
activeTab.value = props.tabs[index].name;
}
});
};
// 监听传入的initialTab变化,滚动到对应内容块
watch(() => props.initialTab, (newVal) => {
if (newVal) {
const index = props.tabs.findIndex((tab) => tab.name === newVal);
scrollToBlock(index);
}
});
onMounted(() => {
// 初次加载滚动到初始 Tab
const index = props.tabs.findIndex((tab) => tab.name === activeTab.value);
scrollToBlock(index);
});
</script>
<style scoped>
.scrollable-tabs {
width: 100%;
}
.scroll-container {
height: 400px;
overflow-y: auto;
border: 1px solid #ebeef5;
margin-top: 20px;
}
.content-block {
padding: 20px;
height: 400px;
border: 1px solid #dcdcdc;
margin-bottom: 10px;
}
</style>
使用 ScrollableTabs
组件
在父组件中,我们可以直接使用封装好的 ScrollableTabs
组件,并通过 props
来传递 tabs
数据和 initialTab
。
<template>
<div>
<ScrollableTabs
:tabs="tabData"
initialTab="tab-2"
>
<!-- 内容块的自定义插槽 -->
<template #tab-1>
<h2>自定义内容 1</h2>
<p>这是内容块 1 的自定义内容。</p>
</template>
<template #tab-2>
<h2>自定义内容 2</h2>
<p>这是内容块 2 的自定义内容。</p>
</template>
<template #tab-3>
<h2>自定义内容 3</h2>
<p>这是内容块 3 的自定义内容。</p>
</template>
<template #tab-4>
<h2>自定义内容 4</h2>
<p>这是内容块 4 的自定义内容。</p>
</template>
</ScrollableTabs>
</div>
</template>
<script setup lang="ts">
import ScrollableTabs from './components/ScrollableTabs.vue';
const tabData = [
{ label: 'Tab 1', name: 'tab-1', content: '内容 1' },
{ label: 'Tab 2', name: 'tab-2', content: '内容 2' },
{ label: 'Tab 3', name: 'tab-3', content: '内容 3' },
{ label: 'Tab 4', name: 'tab-4', content: '内容 4' },
];
</script>
组件设计思路
Props 设计:
tabs
: 内容块的数据数组,每个项包含label
,name
和content
。initialTab
: 默认激活的 Tab 名称。
插槽设计:
- 允许父组件通过具名插槽的方式传入自定义内容,使得每个内容块可以灵活配置。
联动功能:
- 滚动和 Tab 的联动通过监听
scroll
事件来动态更新activeTab
,并通过点击 Tab 实现滚动。
- 滚动和 Tab 的联动通过监听
这种设计可以满足各种场景的需求,包括内容块内容的自定义和 Tab 切换的同步联动。
动态插槽内容实现
template
也可以是动态的。可以通过数据驱动的方式动态生成插槽内容,而不是手动定义每个插槽。可以在父组件中传递一个内容对象数组,并在 ScrollableTabs
组件中使用 v-for
动态渲染内容。
动态插槽内容实现
我们可以在 ScrollableTabs
组件中通过 v-for
渲染内容块,并通过 slot
插槽属性传递内容数据。
1. 更新 ScrollableTabs
组件
<template>
<div class="scrollable-tabs">
<!-- Tab导航 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane
v-for="(tab, index) in tabs"
:key="index"
:label="tab.label"
:name="tab.name"
></el-tab-pane>
</el-tabs>
<!-- 滚动区域 -->
<div ref="scrollContainer" class="scroll-container" @scroll="onScroll">
<div
v-for="(tab, index) in tabs"
:key="index"
:ref="`block-${index}`"
class="content-block"
>
<!-- 使用插槽显示内容,动态插槽名 -->
<slot :name="tab.name" :data="tab.content"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
// 定义 props
const props = defineProps<{
tabs: { label: string; name: string; content: any }[];
initialTab?: string; // 默认激活的Tab
}>();
const activeTab = ref(props.initialTab || props.tabs[0].name);
// 监听 Tab 点击事件
const handleTabClick = (tab: any) => {
const index = props.tabs.findIndex((t) => t.name === tab.paneName);
scrollToBlock(index);
};
// 滚动到指定内容块
const scrollToBlock = (index: number) => {
const targetBlock = document.querySelector(`[ref="block-${index}"]`);
if (targetBlock) {
targetBlock.scrollIntoView({ behavior: 'smooth' });
}
};
// 监听滚动事件,更新 Tab 激活状态
const onScroll = () => {
const scrollContainer = document.querySelector('.scroll-container');
const blocks = props.tabs.map((_, index) =>
document.querySelector(`[ref="block-${index}"]`)
);
blocks.forEach((block, index) => {
const rect = block.getBoundingClientRect();
if (rect.top >= 0 && rect.top < (scrollContainer as HTMLElement).offsetHeight / 2) {
activeTab.value = props.tabs[index].name;
}
});
};
// 监听 initialTab 的变化
watch(() => props.initialTab, (newVal) => {
if (newVal) {
const index = props.tabs.findIndex((tab) => tab.name === newVal);
scrollToBlock(index);
}
});
onMounted(() => {
// 初次加载滚动到初始 Tab
const index = props.tabs.findIndex((tab) => tab.name === activeTab.value);
scrollToBlock(index);
});
</script>
<style scoped>
.scrollable-tabs {
width: 100%;
}
.scroll-container {
height: 400px;
overflow-y: auto;
border: 1px solid #ebeef5;
margin-top: 20px;
}
.content-block {
padding: 20px;
height: 400px;
border: 1px solid #dcdcdc;
margin-bottom: 10px;
}
</style>
2. 父组件使用示例
在父组件中,我们可以将内容对象数组传递给 ScrollableTabs
组件,然后通过默认插槽中的逻辑渲染内容
<template>
<div>
<ScrollableTabs :tabs="tabData" initialTab="tab-2">
<template v-for="tab in tabData" :slot="tab.name" :key="tab.name" v-slot="{ data }">
<h2>{{ tab.label }}</h2>
<!-- 动态内容渲染 -->
<p>{{ data }}</p>
</template>
</ScrollableTabs>
</div>
</template>
<script setup lang="ts">
import ScrollableTabs from './components/ScrollableTabs.vue';
const tabData = [
{ label: 'Tab 1', name: 'tab-1', content: '这是内容块 1 的内容,可以是任何数据类型。' },
{ label: 'Tab 2', name: 'tab-2', content: '这是内容块 2 的内容,可以是任何数据类型。' },
{ label: 'Tab 3', name: 'tab-3', content: '这是内容块 3 的内容,可以是任何数据类型。' },
{ label: 'Tab 4', name: 'tab-4', content: '这是内容块 4 的内容,可以是任何数据类型。' },
];
</script>
说明
动态插槽渲染:
- 在父组件中使用
v-for
遍历tabData
,通过:slot
属性动态指定插槽名,并通过v-slot
获取传递的data
内容。
- 在父组件中使用
插槽内容传递:
ScrollableTabs
组件内部使用<slot :name="tab.name" :data="tab.content"></slot>
的方式,将每个tab
的content
作为slot
的数据传递。
内容数据灵活性:
- 通过
props
传递的tabs
数据集,可以包含任意类型的数据(如字符串、对象、HTML),这样父组件可以根据需要动态渲染内容,而不需要定义多个固定模板。
- 通过
这种方法可以让 ScrollableTabs
组件的插槽内容完全动态化,且灵活地通过数据控制内容显示和渲染。