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 组件的插槽内容完全动态化,且灵活地通过数据控制内容显示和渲染。
 
 

