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

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

前言

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

相关推荐
智能运维指南25 分钟前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
白山编程大哥2 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Flynt3 小时前
我把热榜那篇"禁用 axios"逐条实测了一遍:结论大体对,坑在它没讲的地方
前端·javascript·axios
Csvn4 小时前
bundle优化
前端
北冥有鱼4 小时前
发版后动态路由点击报 `Failed to fetch dynamically imported module`」问题上的完整排查与修复过程
前端·vue.js
静默回滚4 小时前
视频压到 25 MB 以内,码率要算、音轨要扣、余量要留
前端
用户37203749260184 小时前
把 10MB 中文语义模型塞进浏览器:STSB 做到教师模型的 93%,但线上 top1 只有 42%
前端
光锥智能4 小时前
海康机器人举行2026移动机器人新品发布会:箱式存拣升级,「明智」系列发布
java·前端·机器人
计算机魔术师4 小时前
Anthropic 上市文件最吓人的一幕,不是亏损 807 亿
前端
alsmile4 小时前
AI友好的可视化图形引擎自我革命:Meta2d.js 2.0 历时一年全新重写,性能与多人协作全面升级
前端·开源·数据可视化