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

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

前言

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

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax