轻松实现多层锚点导航,让您的网站更加易用!
前言
最近开发H5页面中遇到这样一个需求,需要实现一个多层级的筛选组件,左侧为主要导航,右侧为筛选项目,且有锚点效果,右侧滚动时左侧要有相应的联动效果,我本能地打开vant组件库查找对应的组件,最后发现,组件库里的Sidebar 侧边导航只有一层,且不能实现左右滚动联动,只能通过点击左侧导航切换右侧页面。这不经让我联想到之前做的一个PC web页面也是有类似的导航需求,只需要把右侧的筛选项替换成相应的页面组件节点即可。因此,决定产出了一个公共的锚点导航组件,让后面的开发不需要再重复造轮子。先上效果图:
实现原理
传统的锚点导航大部分都是利用a标签中的href直接链接到对应的dom节点的id,如下所示:
xml
<template>
<a href="#test"></a>
<div id="test"></div>
</template>
但这样如果我们的路由使用的hash模式,则会改变页面路由,页面刷新时会有问题。因此,手动计算对应节点距离页面顶部的滚动距离,来进行滚动,这样的方式才是目前的最优解。对于右侧滚动联动左侧状态切换,也是同样的原理,通过监听滚动时距离窗口顶部最近的节点来改变左侧的状态。具体实现方式,请看下文。
实现步骤
dom结构实现
以下是dom结构部分实现代码,主要为遍历树状结构的数据,将一级节点和二级节点遍历为左侧导航,右侧主要为三级节点展示,采用插槽的方式,这样方便我们可以自定义右侧的展示形式。
ini
<template>
<div class="anchor-point-page">
<div class="anchor-point">
<div class="anchor-li" :class="{ active: menuActive.parent == item.id }"
@click.stop="menuClick(item.id, item.children[0].id)" v-for="item in authorData" :key="item.id">
<span class="text" v-if="item.children.length == 0">{{ item.label }}</span>
<div class="open-wrap" v-if="item.children.length > 0">
<div class="first">
<div class="text">{{ item.label }}</div>
<i v-show="menuActive.parent === item.id" class="arrow-up" />
<i v-show="menuActive.parent !== item.id" class="arrow-down" />
</div>
<div class="second" :class="{ active: menuActive.parent == item.id }">
<div :class="{ active: menuActive.child == el.id }" class="text"
@click.stop="menuClick(item.id, el.id)" v-for="el in item.children" :key="el.id">{{ el.label
}}</div>
</div>
</div>
</div>
</div>
<div class="anchor-content" @scroll="contentScroll">
<div class="select-item" v-for="first in authorData" :key="first.id" :id="'id_' + first.id">
<div class="first-wrap">
<div class="first-title">{{ first.label }}</div>
</div>
<div class="second-item" v-for="second in first.children" :key="second.id" :id="'id_' + second.id">
<div class="second-title">
{{ second.label }}<span v-show="second.createDeptName">【{{ second.createDeptName }}】</span>
</div>
<div class="three-wrap">
<div class="three-item" v-for="three in second.children" :key="three.id" :id="'id_' + three.id">
<div class="three-label">
<slot name="slot-scope" :data="three"></slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
接收的数据形式
页面中接收的authorData结构如下:
yaml
{ id: 1, label: '一级 1', children: [{ id: 4, label: '二级 1-1', children: [{ id: 9, label: '三级 1-1-1' }, { id: 10, label: '三级 1-1-2' }] }] }, { id: 2, label: '一级 2', children: [{ id: 5, label: '二级 2-1' }, { id: 6, label: '二级 2-2' }] }, { id: 3, label: '一级 3', children: [{ id: 7, label: '二级 3-1' }, { id: 8, label: '二级 3-2', children: [{ id: 11, label: '三级 3-2-1' }, { id: 12, label: '三级 3-2-2' }, { id: 13, label: '三级 3-2-3' }] }] }
锚点点击事件
当点击锚点时需要改变对应活跃的一级节点和二级节点,计算对应右侧节点距离页面顶部的距离,通过offsetTop
可以获取对应的数值,这里为了防止与单独滚动右侧时触发左侧联动事件相冲突,所以增加了canScroll
标志位进行判断。
ini
//点击导航锚点
const menuClick = (pId: string, id: string) => {
menuActive.value.parent = pId;
menuActive.value.child = id;
canScroll.value = false;
let top: any = document.querySelector('#id_' + id);
console.log(top?.offsetTop)
document.querySelector('.anchor-content')?.scrollTo(0, top?.offsetTop);
setTimeout(() => {
canScroll.value = true;
}, 1000);
}
右侧滚动联动
监听右侧滚动事件,通过getBoundingClientRect().top
实时计算距离窗口顶部最近的一级和二级节点,最终来改变左侧的active状态。
typescript
//右侧面板滚动
const contentScroll = () => {
if (!canScroll.value) {
return;
}
let tempArr: any = [];
let secTempArr: any = [];
Array.from(document.querySelectorAll('.anchor-content .select-item')).forEach((item) => {
tempArr.push({ id: item.getAttribute('id'), top: Math.abs(item.getBoundingClientRect().top) });
});
Array.from(document.querySelectorAll('.anchor-content .select-item .second-item')).forEach((item) => {
secTempArr.push({ id: item.getAttribute('id'), top: Math.abs(item.getBoundingClientRect().top) });
});
tempArr.sort((a: any, b: any) => {
return a.top - b.top;
});
secTempArr.sort((a: any, b: any) => {
return a.top - b.top;
});
menuActive.value.parent = tempArr[0]?.id?.replace('id_', '') || '';
menuActive.value.child = secTempArr[0]?.id?.replace('id_', '') || '';
}
总结
本文通过简单的两个方法和dom结构,将一个看似比较复杂的功能分离开来,相信大家很容易就能看懂对应的原理,有需要的小伙伴可以参考源码根据需求自定义自己的导航组件,git地址:gitee.com/fcli/anchor... 也可以通过npm直接安装使用,具体请见README.md,今天的分享就到这了,欢迎大家点个关注,不定时分享前端相关知识。