Vue 3 + Elementui + TypeScript 实现左侧菜单定位右侧内容
下面是一个完整的 Vue 3 组合式 API + TypeScript 实现方案,包含平滑滚动和当前激活项高亮功能。
样式展示
完整组件实现
<template>
<div class="container">
<el-container>
<!-- 左侧菜单 -->
<el-aside class="left-menu">
<button v-for="(section, index) in sections" :key="section.id" @click="scrollTo(index)" :class="{ active: activeSection === index }">
{{ section.title }}
</button>
</el-aside>
<!-- 右侧内容 -->
<el-main class="right-content">
<section v-for="(section, index) in sections" :key="section.id" :ref="(el) => setSectionRef(el, index)">
<h2 :id="section.id">{{ section.title }}</h2>
<p>{{ section.content }}</p>
</section>
</el-main>
</el-container>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onBeforeUnmount } from 'vue';
interface Section {
id: string;
title: string;
content: string;
}
export default defineComponent({
name: 'MenuContentNavigation',
setup() {
// 定义内容区域数据
const sections = ref<Section[]>([
{
id: 'section-1',
title: '第一部分',
content: '这是第一部分的内容...',
},
{
id: 'section-2',
title: '第二部分',
content: '这是第二部分的内容...',
},
{
id: 'section-3',
title: '第三部分',
content: '这是第三部分的内容...',
},
]);
const activeSection = ref<number>(0);
const sectionRefs = ref<HTMLElement[]>([]);
let observer: IntersectionObserver | null = null;
// 设置内容区域引用
const setSectionRef = (el: any, index: number) => {
if (el) {
sectionRefs.value[index] = el;
}
};
// 滚动到指定区域
const scrollTo = (index: number) => {
if (sectionRefs.value[index]) {
sectionRefs.value[index].scrollIntoView({
behavior: 'smooth',
block: 'start',
});
activeSection.value = index;
}
};
// 初始化 Intersection Observer
const initObserver = () => {
const options = {
root: document.querySelector('.right-content'),
rootMargin: '0px',
threshold: 0.5,
};
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = sectionRefs.value.findIndex((ref) => ref === entry.target);
if (index !== -1) {
activeSection.value = index;
}
}
});
}, options);
sectionRefs.value.forEach((section) => {
if (section) {
observer?.observe(section);
}
});
};
onMounted(() => {
initObserver();
});
onBeforeUnmount(() => {
if (observer) {
observer.disconnect();
}
});
return {
sections,
activeSection,
setSectionRef,
scrollTo,
};
},
});
</script>
<style scoped>
.container {
display: flex;
height: 100vh;
}
.left-menu {
width: 200px;
padding: 20px;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
gap: 10px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.left-menu button {
padding: 10px 15px;
text-align: left;
border: none;
background: none;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
}
.left-menu button:hover {
background-color: #e0e0e0;
}
.left-menu button.active {
background-color: #2196f3;
color: white;
font-weight: bold;
}
.right-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.right-content section {
min-height: 100vh;
padding: 20px 0;
border-bottom: 1px solid #eee;
}
.right-content h2 {
margin-top: 0;
padding-top: 60px; /* 为固定导航栏留出空间 */
margin-top: -40px; /* 抵消部分padding-top */
}
</style>
关键功能说明
TypeScript 类型定义:
- 定义了
Section
接口来描述每个内容区域的类型 - 使用泛型
ref<Section[]>
和ref<number>
确保类型安全
- 定义了
响应式引用管理:
- 使用
:ref="(el) => setSectionRef(el, index)"
动态设置引用 - 将引用存储在
sectionRefs
数组中以便访问
- 使用
平滑滚动:
scrollTo
方法使用scrollIntoView
实现平滑滚动- 包含
behavior: 'smooth'
和block: 'start'
选项
Intersection Observer:
- 自动检测当前可见区域
- 当内容区域进入视口时更新激活状态
- 在组件卸载时正确清理观察器
样式处理:
- 使用 sticky 定位左侧菜单
- 为固定导航栏添加了 padding 和 margin 补偿
- 添加了平滑的过渡效果
使用注意事项
- 如果需要支持旧版浏览器,应添加 Intersection Observer 的 polyfill
- 可以根据实际需求调整观察器的
threshold
值 - 如果内容区域高度很大,可以考虑使用虚拟滚动优化性能
- 在移动设备上可能需要调整布局为垂直排列