Vue 3 + Elementui + TypeScript 实现左侧菜单定位右侧内容

Vue 3 + Elementui + TypeScript 实现左侧菜单定位右侧内容

下面是一个完整的 Vue 3 组合式 API + TypeScript 实现方案,包含平滑滚动和当前激活项高亮功能。

样式展示

完整组件实现

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>

关键功能说明

  1. TypeScript 类型定义

    • 定义了 Section 接口来描述每个内容区域的类型
    • 使用泛型 ref<Section[]>ref<number> 确保类型安全
  2. 响应式引用管理

    • 使用 :ref="(el) => setSectionRef(el, index)" 动态设置引用
    • 将引用存储在 sectionRefs 数组中以便访问
  3. 平滑滚动

    • scrollTo 方法使用 scrollIntoView 实现平滑滚动
    • 包含 behavior: 'smooth'block: 'start' 选项
  4. Intersection Observer

    • 自动检测当前可见区域
    • 当内容区域进入视口时更新激活状态
    • 在组件卸载时正确清理观察器
  5. 样式处理

    • 使用 sticky 定位左侧菜单
    • 为固定导航栏添加了 padding 和 margin 补偿
    • 添加了平滑的过渡效果

使用注意事项

  1. 如果需要支持旧版浏览器,应添加 Intersection Observer 的 polyfill
  2. 可以根据实际需求调整观察器的 threshold
  3. 如果内容区域高度很大,可以考虑使用虚拟滚动优化性能
  4. 在移动设备上可能需要调整布局为垂直排列
相关推荐
两个西柚呀18 分钟前
未在props中声明的属性
前端·javascript·vue.js
一个处女座的程序猿O(∩_∩)O4 小时前
Vue-Loader 深度解析:原理、使用与最佳实践
前端·javascript·vue.js
还是大剑师兰特5 小时前
TypeScript 面试题及详细答案 100题 (91-100)-- 工程实践与框架集成
前端·javascript·typescript·1024程序员节
麦麦大数据6 小时前
F034 vue+neo4j 体育知识图谱系统|体育文献知识图谱vue+flask知识图谱管理+d3.js可视化
javascript·vue.js·知识图谱·neo4j·文献·体育·知识图谱管理
fhsWar8 小时前
Vue3 props: `required: true` 与 vant 的`makeRequiredProp`
前端·javascript·vue.js
fruge15 小时前
TypeScript 基础类型与接口详解
javascript·ubuntu·typescript
阿珊和她的猫18 小时前
深入剖析 Vue Router History 路由刷新页面 404 问题:原因与解决之道
前端·javascript·vue.js
@AfeiyuO20 小时前
el-table 表格嵌套表格
前端·elementui·vue
麦麦大数据1 天前
F032 材料科学文献知识图谱可视化分析系统(四种知识图谱可视化布局) | vue + flask + echarts + d3.js 实现
vue.js·flask·知识图谱·数据可视化·论文文献·1024程序员节·科研图谱
web打印社区1 天前
使用React如何静默打印页面:完整的前端打印解决方案
前端·javascript·vue.js·react.js·pdf·1024程序员节