[场景实现]:左侧目录树右侧内容联动

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);});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值