(js+css+html、vue)分别做了个一个随着鼠标滚动,菜单根据指定距离切换选中状态,已经点击菜单定位到对应视图位置;先制造是个菜单,且每个菜单对应的元素高度一样,动态生成的的
·
技术实现:滚动菜单联动与定位
核心思路
通过监听滚动事件计算当前视口位置,动态匹配对应菜单项并高亮显示。点击菜单时平滑滚动到对应区块,实现双向联动效果。以下是三种技术栈的实现方案。
原生JS+CSS+HTML方案
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ScrollSpy Demo</title>
<style>
body {
margin: 0;
font-family: sans-serif;
display: flex;
}
/* 左侧菜单 */
.menu {
position: fixed;
top: 0;
left: 0;
width: 150px;
height: 100vh;
background: #f7f7f7;
border-right: 1px solid #ddd;
display: flex;
flex-direction: column;
}
.menu-item {
padding: 12px;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #eee;
}
.menu-item.active {
background: #007bff;
color: white;
font-weight: bold;
}
/* 右侧内容 */
.content {
margin-left: 160px;
padding: 20px;
width: calc(100% - 160px);
}
.section {
margin-bottom: 40px;
border: 1px solid #ccc;
border-radius: 6px;
padding: 20px;
background: #fafafa;
}
.section:nth-child(odd) {
background: #f0f8ff;
}
</style>
</head>
<body>
<!-- 菜单 -->
<div class="menu" id="menu"></div>
<!-- 内容 -->
<div class="content" id="content"></div>
<script>
const menuEl = document.getElementById("menu");
const contentEl = document.getElementById("content");
// 生成 10 个菜单和内容块
const sectionCount = 10;
const sections = [];
for (let i = 0; i < sectionCount; i++) {
// 菜单项
const menuItem = document.createElement("div");
menuItem.className = "menu-item";
menuItem.innerText = `菜单 ${i + 1}`;
menuItem.dataset.index = i;
menuEl.appendChild(menuItem);
// 内容区块
const section = document.createElement("div");
section.className = "section";
section.id = `section-${i}`;
section.innerHTML = `<h2>内容区 ${i + 1}</h2>
<p>这里是第 ${i + 1} 个内容块,高度随机。</p>`;
// 随机高度
section.style.height = `${200 + Math.random() * 400}px`;
contentEl.appendChild(section);
sections.push(section);
}
const menuItems = document.querySelectorAll(".menu-item");
// 点击菜单 → 滚动到对应区块
menuItems.forEach(item => {
item.addEventListener("click", () => {
const index = parseInt(item.dataset.index, 10);
sections[index].scrollIntoView({ behavior: "smooth", block: "start" });
});
});
// 滚动监听 → 切换菜单高亮
window.addEventListener("scroll", () => {
let current = 0;
sections.forEach((sec, i) => {
const rect = sec.getBoundingClientRect();
if (rect.top <= 100) { // 进入视口一定范围时认为是当前
current = i;
}
});
menuItems.forEach((item, i) => {
item.classList.toggle("active", i === current);
});
});
</script>
</body>
</html>
Vue 3 方案(Composition API)
组件结构
<template>
<div class="menu">
<ul>
<li v-for="(item, index) in sections" :key="index">
<a
:href="`#${item.id}`"
:class="{ active: activeSection === item.id }"
@click.prevent="scrollTo(item.id)"
>{{ item.title }}</a>
</li>
</ul>
</div>
<div class="content">
<section
v-for="(item, index) in sections"
:id="item.id"
:key="index"
class="section"
>{{ item.content }}</section>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const sections = ref([
{ id: 'sec1', title: 'Section 1', content: '...' },
{ id: 'sec2', title: 'Section 2', content: '...' },
{ id: 'sec3', title: 'Section 3', content: '...' }
]);
const activeSection = ref('');
const handleScroll = () => {
sections.value.forEach(section => {
const el = document.getElementById(section.id);
const rect = el.getBoundingClientRect();
if (rect.top <= 100 && rect.bottom >= 100) {
activeSection.value = section.id;
}
});
};
const scrollTo = (id) => {
const el = document.getElementById(id);
el.scrollIntoView({ behavior: 'smooth' });
};
onMounted(() => {
window.addEventListener('scroll', handleScroll);
// 初始化检测第一个可见区块
handleScroll();
});
</script>
动态高度处理
通过ResizeObserver监听区块高度变化:
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
const sectionId = entry.target.id;
// 更新对应section的高度记录
});
});
onMounted(() => {
sections.value.forEach(section => {
observer.observe(document.getElementById(section.id));
});
});
性能优化建议
节流处理滚动事件
let isScrolling;
window.addEventListener('scroll', () => {
window.clearTimeout(isScrolling);
isScrolling = setTimeout(() => {
handleScroll();
}, 100);
});
Intersection Observer API替代
现代浏览器推荐使用IntersectionObserver实现更高效的元素可见性检测:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
activeSection.value = entry.target.id;
}
});
}, { threshold: 0.5 });
onMounted(() => {
sections.value.forEach(section => {
observer.observe(document.getElementById(section.id));
});
});
关键问题解决方案
动态生成菜单与区块
- 通过v-for循环渲染动态数据
- 使用唯一ID作为锚点标识
- 通过计算属性生成菜单项数据
高度不一致处理
- 使用ResizeObserver监听高度变化
- 存储各区块的offsetTop位置信息
- 滚动时实时计算相对位置
跨浏览器兼容性
- 添加scroll-margin-top的polyfill
- 平滑滚动使用behavior: 'smooth'的polyfill
- 使用requestAnimationFrame优化滚动性能
更多推荐

所有评论(0)