轻松实现多层锚点导航,让您的网站更加易用!

轻松实现多层锚点导航,让您的网站更加易用!

前言

最近开发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,今天的分享就到这了,欢迎大家点个关注,不定时分享前端相关知识。

相关推荐
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀3 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端
永远不打烊3 小时前
librtmp 原生API做直播推流
前端