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

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

前言

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

相关推荐
咖啡の猫38 分钟前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲3 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5814 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路4 小时前
GeoTools 读取影像元数据
前端
ssshooter4 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry5 小时前
Jetpack Compose 中的状态
前端
dae bal6 小时前
关于RSA和AES加密
前端·vue.js
柳杉6 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog6 小时前
低端设备加载webp ANR
前端·算法
LKAI.6 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi