目录

1. 需求介绍

2. 实现过程

2.1 表单结构介绍

2.2 确定锚点组件接收的参数及使用方法

2.2.1 form-dom:需要被锚点组件控制的表单实例

2.2.2 active-anchor:默认激活的锚点

2.2.3 title-class:表单标题特有的类名

2.2.4 将 锚点组件 挂载到 body 上

2.2.5 锚点组件使用示例

2.3 实现锚点组件基本结构

2.4 锚点组件 onMounted() 时,要执行的操作

2.4.1 从表单实例中,获取锚点列表 getAnchorList()

2.4.2 激活默认锚点,滚动到指定位置

2.4.3 添加滚动事件监听

2.4.4 给滚动事件添加防抖

2.5 滚动事件实现逻辑

2.5.1 阻止事件向上传播

2.5.2 根据表单已经滚动的高度,判断激活哪个锚点

2.6 添加锚点项点击事件

2.7 实现返回顶部按钮功能

2.8 最终代码


1. 需求介绍

如图所示,锚点组件实现了以下功能:

  • 锚点组件显示表单所有的标题
  • 锚点组件存在 “返回顶部” 钮
  • 当表单滚动时,锚点组件对应目录自动高亮
  • 点击锚点组件列表项时,表单滚顶到指定章节处
  • 当前激活的锚点索引应该高亮

2. 实现过程

2.1 表单结构介绍

此项目表单需要每个模块可以折叠,所以采用 ElementPlus 中的折叠面板,如下所示:

  • 使用 div 包裹所有表单内容,并定义 ref="useAnchorFormRef" 用于获取表单实例
  • 使用 el-collapse-item 包裹每一项表单内容
  • 使用 .details-container__submenu 包裹标题,该类名后面会作为锚点内部寻找标题的依据
<div ref="useAnchorFormRef" class="details-container--scroll"><el-collapse v-model="activeNames" @change="handleCollapseChange"><!-- 任务审核意见 --><el-collapse-itemv-if="type === TaskViewPageTypeEnum.check":title="TaskViewCollapseNameEnum.taskReviewComments":name="TaskViewCollapseNameEnum.taskReviewComments"><!-- 标题 --><template #title><div class="details-container__submenu">{{ TaskViewCollapseNameEnum.taskReviewComments }}</div></template><!-- 多行文本 --><el-input v-model="reviewComments" type="textarea" :disabled="true" :rows="4"></el-input></el-collapse-item></el-collapse>
</div>

2.2 确定锚点组件接收的参数及使用方法

2.2.1 form-dom:需要被锚点组件控制的表单实例

为了让表单页面中的逻辑尽量精简,只关心表单业务本身;与业务无关的逻辑(关于表单滚动监听的事件),都考虑在锚点组件中实现,因此锚点组件需要接收表单组件实例;

2.2.2 active-anchor:默认激活的锚点

有些表单,要求一进来就定位到指定的模块,激活指定的锚点

2.2.3 title-class:表单标题特有的类名

用于判断元素的 offsetTop,此处使用 .details-container__submenu 作为标题类名,可以自己定义;简单来说,我需要获取每个标题距离可视区域顶部的范围,通过类名,获取表单标题 DOM实例,进而获取 DOM 实例的 scrollTop 属性实现

综上所述,最终接收的 props 长这个样子:

props: {// 使用锚点的表单实例formDom: {type: Object,default: () => ({}),required: true,},// 默认激活哪个锚点activeAnchor: {type: Number,default: 0,},// 章节特有的类名titleClass: {type: String,default: '.details-container__submenu',},
},

2.2.4 将 锚点组件 挂载到 body 上

锚点组件涉及到了定位,如果直接挂载到元素内部,会被父元素的 position 影响到,而导致定位位置不可控因素变多,因此使用 teleport 将他挂载到 body 上,确保位置固定

由于锚点列表依据于表单数据,因此需要在表单实例加载完成后,才能渲染锚点组件

2.2.5 锚点组件使用示例

<!-- 锚点组件 -->
<teleport :disabled="false" to="body"><!-- form-dom:需要被锚点组件控制的表单实例 --><!-- active-anchor:默认激活的锚点,设置此项后,进入表单会自动定位锚点,并滚动到相应位置 --><!-- v-if="useAnchorFormRef" 此判断必须存在,防止传入 表单实例DOM 为空的问题 --><anchor-point v-if="useAnchorFormRef" :form-dom="useAnchorFormRef" :active-anchor="0"></anchor-point>
</teleport>

2.3 实现锚点组件基本结构

如下所示,除了需要展示锚点列表,还需要展示 返回顶部 的按钮

<div class="anchor__container"><template v-if="anchorList.length"><divv-for="node in anchorList":key="node.index"class="anchor__item":label="node.label":class="{ active: currentAnchor === node.index }"@click="handleAnchorClick(node)">{{ node.label }}</div></template><div class="anchor__return-top" @click="handleReturnTop">返回顶部</div>
</div>

2.4 锚点组件 onMounted() 时,要执行的操作

2.4.1 从表单实例中,获取锚点列表 getAnchorList()

先定义三个变量:

  • 锚点列表
  • 当前激活的锚点索引
  • 表单实例中,标题的 DOM 实例列表

响应式变量如下所示:

// 响应式变量
const state = reactive({// 锚点列表anchorList: [] as any[],// 当前激活的锚点索引currentAnchor: 0,// 表单实例中,章节 DOM 列表(锚点列表的内容就是通过这个变量填充的)titleListInForm: [] as any[],
});

接下来要执行这些操作:

  • 清空锚点列表
  • 根据 props.title-class 以及 querySelectorAll() 获取全部表单标题 DOM 实例,为了让 DOM 实例列表变成数组,使用 Array.form 处理他们
  • 遍历标题 DOM 列表,填充锚点列表;需要注意:要实现点击锚点,滚动到表单指定区域,就要在每一项锚点数据中,填充上当前锚点需要让表单滚动多大距离,也就是此处的 top;
/*** 从表单实例中,获取章节列表,并填充锚点列表*/
const getAnchorList = () => {// 清空锚点列表state.anchorList = [];// 获取表单实例中的章节 DOM 列表state.titleListInForm = Array.from(props.formDom.querySelectorAll(props.titleClass));// console.log('获取表单实例中的章节 DOM 列表 titleListInForm ===', titleListInForm);// 遍历章节 DOM 列表,填充锚点列表state.titleListInForm.forEach((item: any, index) => {// console.log('当前遍历的 章节 DOM item ===', item);state.anchorList.push({index, // 章节索引label: item.innerHTML || '--', // 章节内容top: item.offsetTop,titleDOM: item, // 章节完整 DOM 信息});});// console.log('填充锚点列表 state.anchorList ===', state.anchorList);
};

2.4.2 激活默认锚点,滚动到指定位置

实现思路:

  • 如果不是默认激活第一项,则要手动激活锚点项,并滚动到指定位置
  • 遍历锚点列表,寻找和当前表单所处位置(第几个)一致的锚点索引,将该锚点对应的标题组件存到临时变量中
  • 如果找到了对应的标题 DOM,则使用el.scrollIntoView()方法,平滑的滚动到对应位置

注意:此处应该使用定时器,否则会导致滚动不生效

// 如果默认激活的锚点,不是第一个,则要先进行一次滚动
if (props.activeAnchor !== 0) {state.currentAnchor = props.activeAnchor;// 即将滚动到的目标章节 DOMlet showTitleDomStart: any;state.anchorList.forEach((item: any) => {const indexTemp = item.index;if (props.activeAnchor === indexTemp) {showTitleDomStart = item.titleDOM;console.log('默认滚动到的 章节DOM', item.titleDOM);}});// 如果找到了符合条件的章节 DOMif (showTitleDomStart) {setTimeout(() => {// 平滑滚动showTitleDomStart.scrollIntoView({behavior: 'smooth',block: 'start',});}, 500);}
}

2.4.3 添加滚动事件监听

这里需要注意:props 传进来的 表单 DOM 实例,可以直接使用,不要添加 .value

挂载时,需要添加滚动事件监听,卸载时,要记得取消滚动事件监听

onMounted(() => {// 给表单添加滚动监听props.formDom.addEventListener('scroll', handleDebounceScroll);
});onUnmounted(() => {// 移除表单滚动监听props.formDom.removeEventListener('scroll', handleDebounceScroll);
});

2.4.4 给滚动事件添加防抖

只要页面发生变化,就会触发滚动事件;因此,一定要添加防抖事件,避免影响性能

/*** 防抖 在事件被触发一定时间后再执行回调,如果在这段事件内又被触发,则重新计时* 使用场景:* 1、搜索框中,用户在不断输入值时,用防抖来节约请求资源* 2、点击按钮时,用户误点击多次,用防抖来让其只触发一次* 3、window 触发 resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次* @param fn 回调* @param duration 时间间隔的阈值(单位:ms) 默认1000ms*/
export function useDebounce<F extends(...args: unknown[]) => unknown> (fn: F, duration = 1000):
() => void {let timeoutId: ReturnType<typeof setTimeout> | undefined;const debounce = (...args: Parameters<F>) => {if (timeoutId) {clearTimeout(timeoutId);}timeoutId = setTimeout(() => {fn(...args);timeoutId = undefined;}, duration);};return debounce;
}/*** 对滚动事件进行防抖处理,节约性能*/
const handleDebounceScroll = useDebounce(handleScroll, 200);

2.5 滚动事件实现逻辑

2.5.1 阻止事件向上传播

/*** 处理滚动事件*/
const handleScroll = (e: any) => {// console.log('处理滚动事件', e);e.stopPropagation();// 根据表单已经滚动的高度,判断激活哪个锚点activeFixedAnchor();
};

2.5.2 根据表单已经滚动的高度,判断激活哪个锚点

遍历锚点列表,如果符合以下条件,则修改激活的锚点项

  • 如果 表单滚动的高度 等于 表单标题的 offsetTop
  • 如果 表单滚动的高度 介于 当前标题节点的 offsetTop 和 下一个标题节点的 offsetTop 之间(也就是当前标题看不到了,但下一个标题还没滚动到头部)

注意:由第二条可知,我们要对比下一个节点和当前节点的 offsetTop,所以最后一个节点不可以用上述方法判断是否激活

如何判断最后一个节点呢?

如果当前表单滚动的高度 大于 最后一个标题节点的 offsetTop,则直接激活

注意:这个判断方法存在 bug,如果最后的表单内容没有那么厂,就永远不会激活最后一个节点,但是目前没找到好的解决方案

/*** 根据表单已经滚动的高度,判断激活哪个锚点*/
const activeFixedAnchor = () => {// 这里需要注意一个问题,表单实例的 scrollTop 是相对于编辑页面头部的下方开始的,而标题的 offsetTop 是相对于 微应用容器 计算的,因此要加上 65const formScrollTop = props.formDom.scrollTop + 65; // 表单的 scrollTop,默认为 0for (let k = 0; k < state.anchorList.length; k++) {if (// 如果 scrollTop 正好和标题节点的 offsetTop 相等formScrollTop === state.anchorList[k].top// 由于需要和下一个标题节点作比较,所以当前标题节点不能是最后一个|| (k < state.anchorList.length - 1// scrollTop 介于当前判断的标题节点和下一个标题节点之间&& formScrollTop > state.anchorList[k].top&& formScrollTop < state.anchorList[k + 1].top)) {// console.log('表单的 scrollTop,激活标题的 offsetTop,激活id ===', formScrollTop, state.anchorList[k].top, k);state.currentAnchor = k;break;// 如果是最后一个标题节点,只要 scrollTop 大于节点的 offsetTop 即可} else if (k === state.anchorList.length - 1) {if (formScrollTop > state.anchorList[k - 1].top) {state.currentAnchor = k;break;}}}
};

2.6 添加锚点项点击事件

参考 2.4.2 逻辑,基本一致

/*** 点击锚点列表项*/
const handleAnchorClick = (anchorInfo: any) => {// console.log('当前点击的锚点列表项 ===', anchorInfo);// 修改当前选中的锚点state.currentAnchor = anchorInfo.index;// 即将滚动到的目标章节 DOMlet showTitleDom: any;state.titleListInForm.forEach((item: any, index) => {const labelTemp = item.innerHTML;if (anchorInfo.label === labelTemp) {showTitleDom = item;}});// 如果找到了符合条件的章节 DOMif (showTitleDom) {// 平滑滚动showTitleDom.scrollIntoView({behavior: 'smooth',block: 'start',});}
};

2.7 实现返回顶部按钮功能

修改表单的 scrollTop 即可

/*** 返回顶部*/
const handleReturnTop = () => {// eslint-disable-next-line no-param-reassign, vue/no-mutating-propsprops.formDom.scrollTop = 0;
};

2.8 最终代码

<template><div class="anchor__container"><template v-if="anchorList.length"><divv-for="node in anchorList":key="node.index"class="anchor__item":label="node.label":class="{ active: currentAnchor === node.index }"@click="handleAnchorClick(node)">{{ node.label }}</div></template><div class="anchor__return-top" @click="handleReturnTop">返回顶部</div></div>
</template><script lang="ts">
import { reactive, toRefs, defineComponent, onMounted, onUnmounted } from 'vue';
// hooks 防抖
import { useDebounce } from '../hooks/common/use-debounce';export default defineComponent({name: 'AnchorPoint',props: {// 使用锚点的表单实例formDom: {type: Object,default: () => ({}),required: true,},// 默认激活哪个锚点(项目中有要求一进入某个表单时,就定位到指定锚点的需求,默认激活第一个节点)activeAnchor: {type: Number,default: 0,},// 章节特有的类名(用于判断元素的 offsetTop,5.0 里使用 .details-container__submenu 作为章节类名,也可以自己定义)titleClass: {type: String,default: '.details-container__submenu',},},setup(props, { emit }) {// 响应式变量const state = reactive({// 锚点列表anchorList: [] as any[],// 当前激活的锚点索引currentAnchor: 0,// 表单实例中,章节 DOM 列表(锚点列表的内容就是通过这个变量填充的)titleListInForm: [] as any[],});/*** 返回顶部*/const handleReturnTop = () => {// eslint-disable-next-line no-param-reassign, vue/no-mutating-propsprops.formDom.scrollTop = 0;};/*** 从表单实例中,获取章节列表,并填充锚点列表*/const getAnchorList = () => {// 清空锚点列表state.anchorList = [];// 获取表单实例中的章节 DOM 列表state.titleListInForm = Array.from(props.formDom.querySelectorAll(props.titleClass));// console.log('获取表单实例中的章节 DOM 列表 titleListInForm ===', titleListInForm);// 遍历章节 DOM 列表,填充锚点列表state.titleListInForm.forEach((item: any, index) => {// console.log('当前遍历的 章节 DOM item ===', item);state.anchorList.push({index, // 章节索引label: item.innerHTML || '--', // 章节内容top: item.offsetTop,titleDOM: item, // 章节完整 DOM 信息});});// console.log('填充锚点列表 state.anchorList ===', state.anchorList);};/*** 点击锚点列表项*/const handleAnchorClick = (anchorInfo: any) => {// console.log('当前点击的锚点列表项 ===', anchorInfo);// 修改当前选中的锚点state.currentAnchor = anchorInfo.index;// 即将滚动到的目标章节 DOMlet showTitleDom: any;state.titleListInForm.forEach((item: any, index) => {const labelTemp = item.innerHTML;if (anchorInfo.label === labelTemp) {showTitleDom = item;}});// 如果找到了符合条件的章节 DOMif (showTitleDom) {// 平滑滚动showTitleDom.scrollIntoView({behavior: 'smooth',block: 'start',});}};/*** 根据表单已经滚动的高度,判断激活哪个锚点*/const activeFixedAnchor = () => {// 这里需要注意一个问题,表单实例的 scrollTop 是相对于编辑页面头部的下方开始的,而标题的 offsetTop 是相对于 微应用容器 计算的,因此要加上 65const formScrollTop = props.formDom.scrollTop + 65; // 表单的 scrollTop,默认为 0for (let k = 0; k < state.anchorList.length; k++) {if (// 如果 scrollTop 正好和标题节点的 offsetTop 相等formScrollTop === state.anchorList[k].top// 由于需要和下一个标题节点作比较,所以当前标题节点不能是最后一个|| (k < state.anchorList.length - 1// scrollTop 介于当前判断的标题节点和下一个标题节点之间&& formScrollTop > state.anchorList[k].top&& formScrollTop < state.anchorList[k + 1].top)) {// console.log('表单的 scrollTop,激活标题的 offsetTop,激活id ===', formScrollTop, state.anchorList[k].top, k);state.currentAnchor = k;break;// 如果是最后一个标题节点,只要 scrollTop 大于节点的 offsetTop 即可} else if (k === state.anchorList.length - 1) {if (formScrollTop > state.anchorList[k - 1].top) {state.currentAnchor = k;break;}}}};/*** 处理滚动事件*/const handleScroll = (e: any) => {// console.log('处理滚动事件', e);e.stopPropagation();// 根据表单已经滚动的高度,判断激活哪个锚点activeFixedAnchor();};/*** 对滚动事件进行防抖处理,节约性能*/const handleDebounceScroll = useDebounce(handleScroll, 200);onMounted(() => {// console.log('锚点组件内,获取滚动表单实例 ===', props.formDom);// 从表单实例中,获取章节列表,并填充锚点列表getAnchorList();// 如果默认激活的锚点,不是第一个,则要先进行一次滚动if (props.activeAnchor !== 0) {state.currentAnchor = props.activeAnchor;// 即将滚动到的目标章节 DOMlet showTitleDomStart: any;state.anchorList.forEach((item: any) => {const indexTemp = item.index;if (props.activeAnchor === indexTemp) {showTitleDomStart = item.titleDOM;console.log('默认滚动到的 章节DOM', item.titleDOM);}});// 如果找到了符合条件的章节 DOMif (showTitleDomStart) {setTimeout(() => {// 平滑滚动showTitleDomStart.scrollIntoView({behavior: 'smooth',block: 'start',});}, 500);}}// 给表单添加滚动监听props.formDom.addEventListener('scroll', handleDebounceScroll);});onUnmounted(() => {// 移除表单滚动监听props.formDom.removeEventListener('scroll', handleDebounceScroll);});return {...toRefs(state),handleReturnTop,handleAnchorClick,};},
});
</script><style lang="scss" scoped>
.anchor__container {position: fixed;top: 50%;right: 46px;overflow: auto;box-sizing: border-box;width: 300px;height: 180px;padding: 12px;background: rgba(255, 0, 0, 0.4);transform: translate(0, -50%);
}.anchor__item {overflow: hidden;box-sizing: border-box;width: 100%;margin: 4px 0;padding: 4px;background: rgba(255, 255, 0, 0.2);text-overflow: ellipsis;white-space: nowrap;cursor: pointer;
}.anchor__return-top {position: absolute;bottom: 0;padding: 4px 0;background: rgba(0, 0, 255, 0.2);color: blue;cursor: pointer;
}.active {color: yellow;
}
</style>

使用 Vue3 实现锚点组件相关推荐

  1. 学会使用ant design封装一个锚点组件

    我是歌谣 放弃很容易 但是坚持一定很酷 封装一个锚点组件就是要知道一个父子组件的一个传值 很显然 父亲这边传过去一个数组 然后就可以进行循环遍历得到一个新的数值 这边注意 当我们进行一个map返回值得 ...

  2. echarts vue 柱状图实例_「源码学习」适用于 Vue3 的 ECharts 包装组件

    距离 Vue3 发布已经有近一周的时间,不知道大家源码都学习的怎么样了呢?今天 Gitee 为开发者们推荐一个新的学习资源,就是下面要介绍的这个同时适用于 Vue2 和 Vue3 的 EChatrts ...

  3. vue3.2+ 滑动验证组件,pc/手机通用,即插即用

    vue3.2+ 滑动验证组件,pc/手机通用,即插即用 一.前言 二.成果展示 三.组件使用 四.vue3.2+ 滑动验证组件 源码 五.最后,点个赞 一.前言 vue已经更新到3.2+,使用了scr ...

  4. Vue3封装Video.js组件(基于video.js)

    Vue3封装Video.js组件 话不多说直接上代码 在项目中安装Video.js 通过npm安装video.js npm install video.js --save Video.js组件的封装 ...

  5. 基于vue3的京东nutui组件库的表单校验规则:怎样进行表单验证?怎样只使用指定的某一个规则进行校验呢?

    官网: NutUI - 移动端 Vue2.Vue3.小程序 组件库京东风格的轻量级移动端 Vue.React 组件库https://nutui.jd.com/#/component/form 用法: ...

  6. 16.0 vue3 Teleport---自定义dialog组件

    上一篇: 15.0 vue3 provide&inject跨组件通信方式_十一月的萧邦-CSDN博客上一篇:14.0 vue3 customRef的使用_十一月的萧邦-CSDN博客上一篇:vu ...

  7. 分享一个基于Vue3+TS构建Cesium组件库

    分享一个基于Vue3+TS构建Cesium组件库 点击进入 Vue Cesium官网 //vc-navigation <template><el-row ref="view ...

  8. anchor iview 悬浮_iView3.x Anchor(锚点)组件 导航锚点

    iView3.x Anchor(锚点)组件 导航锚点 iview 3.x框架中新添了一个Anchor(锚点组件),用这个组件去做页面的分类导航正好合适,但是苦于官方文档太过抽象研究了一整天,才勉强可以 ...

  9. Webpack的代码分包Vue3中定义异步组件分包refs的使用

    一.默认的打包过程: 默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么webpack在打包时就会将组件模块打包到一起(比如一个app.js文件中): 这个时候随着项 ...

最新文章

  1. 5行Python提取海量新闻网站内容
  2. java工具类去掉字符串String中的.点。android开发java程序员常用工具类
  3. hive脚本执行方式
  4. 为Tiny4412设备驱动在proc目录下添加一个可读版本信息的文件
  5. python在eclipse下中文乱码问题zz
  6. __thread 和 __typeof__关键字
  7. 基于android的视频采集系统的设计与实现,基于Android的视频采集系统的设计与实现...
  8. 携程App的网络性能优化实践
  9. pytorch visdom可视化工具学习—1—详细使用-3-Generic Plots和Others
  10. 阅读YYKit之YYImage实现gif展示
  11. java ibm notes_使用Java API从Lotus Notes NSF文件中提取电子邮件
  12. [USACO06DEC]最少的硬币The Fewest Coins
  13. drools -规则语法
  14. 阿里巴巴矢量图标库(网页)
  15. C指针Pointers
  16. 常微分方程(Ordinary differential equation)
  17. 路由与交换-华为eNSP-交换机上配置DHCP技术
  18. 观其关键字排名查询工具_获取Ahrefs SEO工具栏
  19. 浙江学计算机的有哪些大学,浙江哪些大学有人工智能专业
  20. 计算机导论课后总结四

热门文章

  1. Atcoder Regular contest 085F NRE 线段树+DP
  2. 恶意网页病毒十三大症状及修复方法
  3. 解决django4.0 跨域报 Cross-Origin Opener Policy错误
  4. 数学建模森林着火问题Matlab,数学建模森林救火问题.doc
  5. 色标传感器和颜色传感器
  6. 鸿蒙系统环境搭建、源码编译与烧写之经典
  7. MAC层与llc层的大不同
  8. Alfred 配置google翻译
  9. 军师联盟之稳略军师联盟 x 版权猫:塔链科技“鲸确”精确云清算支持智力与文化战略领域
  10. SlimDX和WPF的合作应用