- 功能
-
滚动右侧内容 → 左侧菜单自动跟随高亮。
-
点击左侧菜单 → 平滑跳转到对应内容。
-
懒加载 & 无限滚动 保持不变。
- 效果
- 代码
<template>
<div class="app">
<!-- 左侧菜单 -->
<div class="sidebar">
<el-menu :default-active="activeMenu" @select="handleMenuClick">
<el-menu-item index="info">基本信息</el-menu-item>
<el-menu-item index="work">工作经历</el-menu-item>
<el-menu-item index="edu">教育背景</el-menu-item>
<el-menu-item index="skills">技能专长</el-menu-item>
<el-menu-item index="paper">论文成果</el-menu-item>
<el-menu-item index="project">项目经验</el-menu-item>
<el-menu-item index="awards">获奖情况</el-menu-item>
<el-menu-item index="contact">联系方式</el-menu-item>
</el-menu>
</div>
<!-- 右侧内容 -->
<div
class="content"
ref="contentRef"
v-infinite-scroll="loadMore"
:infinite-scroll-disabled="noMore"
:infinite-scroll-distance="50"
@scroll="onScroll"
>
<!-- 固定模块 -->
<section id="info" class="section">基本信息内容...</section>
<section id="work" class="section">工作经历内容...</section>
<section id="edu" class="section">教育背景内容...</section>
<section id="skills" class="section">技能专长内容...</section>
<section id="paper" class="section">论文成果内容...</section>
<section id="project" class="section">项目经验内容...</section>
<section id="awards" class="section">获奖情况内容...</section>
<section id="contact" class="section">联系方式内容...</section>
<!-- 动态加载模块 -->
<section
v-for="(item, index) in moreSections"
:key="index"
:id="'extra-' + index"
class="section"
>
<div v-if="item.loading" class="loading-box">
<el-icon class="is-loading"><Loading /></el-icon> 加载中...
</div>
<div v-else>
{{ item.content }}
</div>
</section>
<p v-if="noMore" class="end-text">没有更多数据了</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import { Loading } from "@element-plus/icons-vue";
import "element-plus/es/components/infinite-scroll/style/css";
const activeMenu = ref("info");
const contentRef = ref(null);
let isClickScrolling = false;
const moreSections = ref([]);
const noMore = ref(false);
// 左侧点击
const handleMenuClick = (key) => {
activeMenu.value = key;
const el = document.getElementById(key);
if (el) {
isClickScrolling = true;
el.scrollIntoView({ behavior: "smooth", block: "start" });
setTimeout(() => {
isClickScrolling = false;
}, 500);
}
};
// 滚动高亮
const onScroll = () => {
if (isClickScrolling) return;
const sections = [
"info",
"work",
"edu",
"skills",
"paper",
"project",
"awards",
"contact",
...moreSections.value.map((_, i) => "extra-" + i),
];
let current = "info";
const scrollTop = contentRef.value.scrollTop;
for (let id of sections) {
const el = document.getElementById(id);
if (el && el.offsetTop - 80 <= scrollTop) {
current = id;
}
}
activeMenu.value = current;
};
// 单模块加载
const loadMore = () => {
if (noMore.value) return;
if (moreSections.value.length >= 5) {
noMore.value = true;
return;
}
const newItem = {
loading: true,
content: "",
};
moreSections.value.push(newItem);
// 模拟异步加载
setTimeout(() => {
newItem.loading = false;
newItem.content = `动态加载模块 ${moreSections.value.length} 的内容...`;
}, 1000);
};
// 检查是否填满一屏
const checkFillScreen = async () => {
await nextTick();
const container = contentRef.value;
if (container.scrollHeight <= container.clientHeight && !noMore.value) {
loadMore();
checkFillScreen(); // 递归继续加载,直到填满或 noMore
}
};
onMounted(() => {
activeMenu.value = "info";
checkFillScreen();
});
</script>
<style scoped>
.app {
display: flex;
height: 100vh;
}
.sidebar {
width: 220px;
border-right: 1px solid #ddd;
}
.content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.section {
height: 300px;
margin-bottom: 20px;
border: 1px solid #eee;
background: #fafafa;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
}
.loading-box {
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.end-text {
text-align: center;
padding: 20px;
color: #888;
}
</style>