1、需求描述
左侧是目录,部分目录项有子项,右侧是内容。
当滑动右侧内容区域的时候,最上部分的内容对应的左侧目录项会有样式背景色区分。
当点击左侧目录项的时候,右侧对应的内容会滚动到顶部。
2、实现思路
锚点来做对应。
左侧目录树用的el-menu,用.el-menu-item.is-active来做样式区分。
点击和滚动的冲突用setTimeout解决。
该部分实现在弹窗上,用ref和watch。避免挂载时据id找不到元素。
最后一个目录项无法滚动到顶部,但还是需要样式显示,单独设置。
3、关键代码
总体布局样式等
...
<!--弹窗等已省略-->
<div class="editBox">
<!--左侧-->
<div class="sidebar">
<el-menu
:default-active="activeIndex"
style="margin-top: 22px;padding-right: 10px;border:none"
>
<template v-for="(item, index) in menuItems">
<el-menu-item
v-if="!item.subItems"
:key="`menu-item-${index}`"
:index="`${index}`"
@click="handleMenuItemClick(index)"
>
{{ item.title }}
</el-menu-item>
<el-sub-menu
v-else
:key="`submenu-${index}`"
:index="`${index}`"
>
<template #title>
{{ item.title }}
</template>
<el-menu-item
v-for="(subItem, subIndex) in item.subItems"
:key="`sub-item-${index}-${subIndex}`"
:index="`${index}-${subIndex}`"
@click="handleMenuItemClick(index, subIndex)"
>
{{ subItem.title }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</div>
<!--右侧-->
<div
id="contentMain"
ref="contentMain"
class="content"
@scroll="handleScroll"
>
<!-- 内容子组件start -->
<partOne/>
<partTwo/>
<partThree/>
...
<!-- 内容子组件end -->
</div>
</div>
<!-- 样式/deep/或::v-deep -->
...
/deep/ .el-menu-item.is-active {
background-color: #6991FF !important;
border-radius: 4px;
color: #fff;
span {
color: #fff !important;
}
}
...
内容子组件示例
//目录项 对应menuItems 设置id和class
<div>
<div id="partTwo" class="content-section">内容二</div>
...
//如果有目录子项,对应menuItems 设置id和class
<el-row id="subOne"
class="sub-section-1">
<el-col class="titleItem">子项1 </el-col>
</el-row>
...
</div>
滚动点击事件等
const contentMain= ref(null);//对应该部分的ref
//anchor即分别对应内容部分开始处的id
const menuItems = [
{
title: '内容一',
anchor: 'partOne'
},
{
title: '内容二',
anchor: 'partTwo',
subItems: [
{
title: '子项1',
anchor: 'subOne'
},
{
title: '子项2',
anchor: 'subTwo'
},
{
title: '子项3',
anchor: 'subThree'
}
]
},
...
];
const activeIndex = ref('0');//当前激活项
const scroll = ref(true); //是否可滚动
//点击事件
const handleMenuItemClick = (index, subIndex = null) => {
scroll.value = false; // 禁止滚动
activeIndex.value = index; // 设置当前菜单项的索引
if (subIndex !== null) {
activeIndex.value = `${index}-${subIndex}`; // 设置当前子菜单项的索引
}
const { anchor } = subIndex !== null ? menuItems[index].subItems[subIndex] : menuItems[index]; // 获取菜单项或子菜单项对应的锚点
const content = document.getElementById(anchor); // 找到锚点对应的内容
//利用setTimeout来避免点击之后滚动与点击带来的跳动,区分最后一个目录项
if (content && !scroll.value) {
if (index === menuItems.length - 1) {
const contentContainer = document.getElementById('contentMain');
const offset = contentContainer.scrollHeight - contentContainer.clientHeight;
contentContainer.scrollTo({ top: offset, behavior: 'smooth' });
} else {
content.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
setTimeout(() => {
scroll.value=true;
}, 2000);
}
};
//滚动事件
const handleScroll = () => {
//目录项class对应元素
const contentSections = document.querySelectorAll('.content-section');
let minDistance = Infinity;
let currentSectionIndex = 0;//当前默认选中的父项
let currentSubSectionIndex = 0;//当前默认选中的子项
//内容区域的元素
const dialog = document.getElementById('contentMain');
//该部分实现在弹框而非整个视口,要减去
contentSections.forEach((section, index) => {
const distanceMain = Math.abs(section.getBoundingClientRect().top - dialog.getBoundingClientRect().top);
if (distanceMain < minDistance) {
minDistance = distanceMain;
currentSectionIndex = index;
if (menuItems[currentSectionIndex].subItems) {
//子目录项对应class的元素
const subSections = document.querySelectorAll(`.sub-section-${currentSectionIndex}`);
subSections.forEach((subSection, subIndex) => {
const distanceSub = Math.abs(subSection.getBoundingClientRect().top - dialog.getBoundingClientRect().top);
if (distanceSub < minDistance) {
//比较所有的目录项及子目录项找到离区域顶部最近的
minDistance = distanceSub;
currentSectionIndex = index;
currentSubSectionIndex = subIndex;
}
});
}
}
});
//避免点击之后滚动与点击带来的跳动
if (scroll.value) {
//当前被激活项赋值
activeIndex.value = currentSectionIndex;
if (menuItems[currentSectionIndex].subItems) {
activeIndex.value = `${currentSectionIndex}-${currentSubSectionIndex}`;
}
const content = document.getElementById('contentMain');
//为最后一个目录项单独设置
if (content.scrollTop + content.clientHeight >= content.scrollHeight) {
activeIndex.value = menuItems.length - 1;
}
}
};
//该部分实现在弹框上,用watch避免直接在挂载的时候找元素找不到
watch(() => contentMain.value, (newValue, oldValue) => {
if (newValue) {
contentMain.value.addEventListener('scroll', handleScroll);
}
});
//如果非弹框,挂载的时候通过id来找元素监听即可
//onMounted(() => //{document.getElementById('contentMain').addEventListener('scroll',handleScroll);});