vue3:自定义描点定位组件(锚点定位和监听滚动切换)以及遇到的问题

news2024/11/28 13:52:32

目录

第一章 实现效果

第二章 锚点组件分析

2.1 功能分析

2.2 核心点

第三章 源代码

3.1 数据格式

3.2 代码分析

3.2.1 tab栏以及内容页面

3.2.2 逻辑

第四章 遇到的问题


第一章 实现效果

第二章 锚点组件分析

2.1 功能分析

  1. tab栏以及切换涉及逻辑
  2. 点击tab切换同时页面需要将对应的栏的内容滚动到顶部
  3. 滚动页面到对应栏的内容时tab栏也需要同时变化
  4. (小编需要做的完整需求是所有的内容都是配置生成的,这里只是其中的一部分,需要tab栏与内容除了一一对应,其他内容的多少都是不确定的,只能通过数据先做渲染),还需要知道每一个tab对应的内容距离顶部的距离(计算获取)

2.2 核心点

  1. 自定义tab
  2. addEventListener(添加监听事件:这里需要用到的是scroll滚动事件)、removeEventListener(移除监听事件)
  3. element.scrollIntoView(将列表滚动到特定的dom项上)
  4. scrollTop(获取或设置元素的垂直滚动条位置)

第三章 源代码

3.1 数据格式

  • 完整数据格式(注意:用到的是guideJson.guideInfo下的数据,如果有想要的可以从该处获取,也可以根据上面展开的数据格式造数据也可)

https://download.csdn.net/download/qq_45796592/89865179?spm=1001.2014.3001.5503

3.2 代码分析

3.2.1 tab栏以及内容页面

// 定位锚点的tab栏
<div class="preview_container">
   <div class="preview_anchor">
       <ul class="anchor_ul">
          <li
            class="anchor_li"
            v-for="item in guideJson.guideInfo" // 遍历数据,渲染tab
            :key="item.id"
            @click="handleClickAnchor(item.id)" // 点击tab实现滚动的方法
            :style="{
              color: currentAnchor === item.id ? 'red' : '',
              borderRight: currentAnchor === item.id ? '4px solid red' : ''
            }"
          >
            {{ item.title }}
          </li>
       </ul>
   </div>
   <div class="preview_wrapper" ref="preview_wrapper">
       // 这块内容是可以自定义的,如果大家没有数据跟着改造就好,也可以弄个空白页占高
       <div
          v-for="item in guideJson.guideInfo"
          :key="item.id"
          :id="'dom-' + item.id" // 这里很重要,由于小编的id是通过uuid生成,首字母是以数字开头,这种定义是不符合规范的,有可能还获取不到对应的dom,所以这里小编添加了前缀(也可自定义)
          class="previw_item"
       >
          <div class="sub_title">{{ item.title }}</div> // 标题
          <div v-if="item.type === 'text'"> // 下面就是根据不同的类别展示不同的组件对应的页面了
            <TextPreview :state="item" />
          </div>
          <div v-else-if="item.type === 'list'">
            <ListPreview :state="item" />
          </div>
          <div v-else-if="item.type === 'table'">
            <TablePreview :state="item" />
          </div>
          <div v-else-if="item.type === 'image'">
            <ImagePreview :state="item" />
          </div>
          <div v-else-if="item.type === 'file'">
            <FilePreview :state="item" />
          </div>
          <div v-else-if="item.type === 'link'">
            <LinkPreview :state="item" />
          </div>
          <div v-else-if="item.type === 'required_materialsList'">
            <RequiredFilePreview :state="item" />
          </div>
          <div v-else-if="item.type === 'result_materialsList'">
            <ResultFilePreview :state="item" />
          </div>
       </div>
   </div>
</div>


// 样式
.preview_container {
  margin: 0 auto;
  width: 1200px;
  height: 100%;
  .preview_anchor {
    text-align: right;
    font-size: 14px;
    color: rgba(0, 0, 0, 0.45);
    position: fixed;
    transform: translateX(-150px);
    top: 389px;
    .anchor_ul {
      .anchor_li {
        padding: 4px 16px;
        max-width: 150px;
        border-right: 2px solid rgba(0, 0, 0, 0.06);
        cursor: pointer;
      }
    }
  }
  .preview_wrapper {
    height: calc(100% - 251px);
    overflow: auto;
    .previw_item {
      .sub_title {
        width: 100%;
        height: 32px;
        font-size: 16px;
        font-weight: bold;
        color: #3d3d3d;
        display: flex;
        align-items: center;
        justify-content: flex-start;
        margin-bottom: 10px;
        margin-top: 24px;
      }
    }
  }
}

@media (max-width: 1200px) {
  .preview_anchor {
    display: none;
  }
}
::-webkit-scrollbar {
  width: 0;
  height: 0;
}

3.2.2 逻辑

const currentAnchor = ref('') // 点击的当前tab
const preview_wrapper = ref(null) // preview_wrapper 定义dom元素
const staticHeight = ref(0) // 滚动盒子preview_wrapper距离顶部的高度
let onScrollFunction = null // 初始化方法为null

// ===== scroll 滚动带动 tab 切换的逻辑 =====
// 页面首次进来时执行的逻辑
onMounted(async () => {
  query.value = route.query // 获取路由的参数
  await thingApi.guideById({ id: query.value.id }).then((res) => { // 接口请请求,为了获取数据,大家用的时候可以直接根据前面的图造数据就行
    guideJson.value = res ? JSON.parse(res.guideJson) : {} // 数据复制,大家针对自己造的数据进行除了
    console.log('预览数据', guideJson.value)
    const { guideInfo } = guideJson.value // 获取到我们需要的数据
    currentAnchor.value = guideInfo[0].id
    nextTick(() => {
      const wrapper = preview_wrapper.value // dom元素赋值
      staticHeight.value = wrapper.offsetTop // 距离元素最近的一个具有定位的祖宗元素,没有定义则是body
      genHeadingsOffset(wrapper) // 首次进来初始化,获取每一个tab对应的dom距离顶部的距离(有优化空间,最后给出)
      // 滚动逻辑函数,赋值 控制是用一个滚动函数,方能移除
      onScrollFunction = function onScroll(e) {
        const target = e.target
        const offsetTop = target.scrollTop
        const offsetList = Object.keys(offsetMap.value).map((item) => +item)
        for (let i = 0; i < offsetList.length; i++) { // 从第一个元素往后遍历
          if (offsetTop + staticHeight.value <= offsetList[i]) { // 如果滚动的高度+静态不变的高度小于某个元素的高度,获取对应元素赋值,带动tab切换,结束遍历(效果有缺陷,大家后续自己看效果)
            const activeId = `#${offsetMap.value[offsetList[i]]}`
            const findItem = anchors.value.find((item) => item.href === activeId)
            if (findItem) {
              currentAnchor.value = findItem.href.split('#dom-').join('')
            }
            break
          }
        }
      }
      wrapper?.addEventListener && wrapper.addEventListener('scroll', onScrollFunction)
    })
  })
})


// 监听 getContainer 获取容器的滚动事件,更新当前的锚点信息
const offsetMap = ref({}) // 每一个dom节点对应顶部的高度
const headingsEl = ref([]) // 每一个dom节点
const anchors = ref([]) // 动态保存每一个id对应的dom元素名称(注意要与写样式时的id一致)
const genHeadingsOffset = (wrapper) => {
  nextTick(() => {
    const { guideInfo } = guideJson.value
    anchors.value = guideInfo.map((item) => { // map遍历保存id
      return {
        href: `#dom-${item.id}` // id命名
      }
    })

    const headingsElCache = [] // 缓存每一个dom节点
    anchors.value.forEach((item, index) => {
      headingsElCache[index] = wrapper.querySelector(item.href)
    })

    const offsetMapCache = {} // 缓存每一个dom节点对应顶部的高度
    headingsElCache.forEach((head) => {
      offsetMapCache[head.offsetTop] = head.id
    })
    offsetMap.value = offsetMapCache
    headingsEl.value = headingsElCache
  })
}

// =========== onScrollFunction方法优化方案 ============
// onScrollFunction = function onScroll(e) {
//   Object.keys(offsetMap.value).length ? offsetMap.value : genHeadingsOffset(wrapper) // 针对首次没有初始化数据时初始化数据,有了数据之后不在执行函数逻辑直接赋值
//   const target = e.target
//   const offsetTop = target.scrollTop
//   const offsetList = Object.keys(offsetMap.value).map((item) => +item)
//   for (let i = offsetList.length; i > 0; i--) { // 从最后一个元素往前遍历
//     if (offsetTop + staticHeight.value > offsetList[i]) { // 如果滚动的高度+静态不变的高度大于某个元素的高度,获取对应元素赋值,带动tab切换(相比前面的方法,效果更合理),结束遍历
//       const activeId = `#${offsetMap.value[offsetList[i]]}`
//       const findItem = anchors.value.find((item) => item.href === activeId)
//       if (findItem) {
//         currentAnchor.value = findItem.href.split('#dom-').join('')
//       }
//       break
//     }
//   }
// }

// ============= 点击tab 切换 滚动的逻辑 ===================
//点击tab切换的逻辑
const handleClickAnchor = async (e) => {
    // 先移除滚动监听(由于我们在onMounted已经注册添加过滚动事件了,如果不移除就会造成事件叠加的问题,以及我们的本意是想点击直接切换tab,由于前面的滚动函数还存在,还是会有滚动带动tab切换的效果)
    const wrapper = preview_wrapper.value
    wrapper.removeEventListener('scroll', onScrollFunction)
    currentAnchor.value = e
    // 滑动
    const element = document.querySelector(`#dom-${e}`)
    // 确定element.scrollIntoView滚动完全完成后再开启滚动监听,否则提前触发滚动逻辑也会有滚动带动tab切换的现象
    scrollIntoViewWithListener(element, { behavior: 'smooth' }).then(() => {
      // 添加延迟二次确定
      setTimeout(() => {
        // 执行完成后添加滚动监听
        wrapper.addEventListener('scroll', onScrollFunction)
      }, 500)
    })
}

// 确定element.scrollIntoView滚动完全完成
function scrollIntoViewWithListener(element, scrollOptions) {
  return new Promise((resolve) => {
    // 使用IntersectionObserver来检测滚动是否真正发生
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          // 元素滚动到视口中时,停止观察并调用resolve
          observer.unobserve(element)
          resolve()
        }
      },
      {
        // 这些选项可以根据需要进行调整
        root: null,
        threshold: 0
      }
    )
    // 开始观察元素
    observer.observe(element)
    // 滚动到指定元素
    element.scrollIntoView(scrollOptions)
  })
}


// 最后注意移除滚动事件
onBeforeUnmount(() => {
    const wrapper = preview_wrapper.value
    wrapper.removeEventListener('scroll', onScrollFunction)
})

第四章 遇到的问题

  • 使用了addEventListener添加事件后removeEventListener移除不掉。解决原理小编在该文章。

js基础:addEventListener与removeEventListener使用时,涉及的问题(包括事件捕获、冒泡,removeEventListener不生效问题)-CSDN博客

  • 点击切换时没有移除事件以及使用scrollIntoView滚动到指定节点期间就添加了滚动事件。解决方法小编在代码中已添加说明。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2201160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app 打包成app时 限制web-view大小

今天对接一个uni-app的app 内置对方h5 web-view的形式 需要对方在web-view顶部加点东西 对方打的app的web-view始终是全屏的状态&#xff0c;对方表示做不到我要的效果 emmmmmm。。。。。。 于是乎 自己搭了个demo 本地h5跑起来审查了下代码&#xff0c;发现web-view是给绝对定…

关于 CAM350打开钻孔文件时提示出错处理

自动导入CAM350出错时&#xff0c;需要单独导入通孔文件或者槽孔文件查看 第一步&#xff1a; 第二步&#xff1a; 下图中的几处值要与出Allegro中导出文件时的设置一致。 这样应该就能正常查看钻孔文件。

WPF中的Window类

控件分类 在第一篇文章.Net Core和WPF介绍中的WPF的功能和特性部分根据功能性介绍了WPF的控件 名称。 在接下来的文章中&#xff0c;将会详细的介绍各个控件的概念及使用。 主要包括&#xff1a; 内容控件&#xff1a;Label、Button、CheckBox、ToggleButton、RadioButton、…

【长文梳理Webserver核心】框架篇

感谢前人的总结&#xff0c;让一个小白快速成长&#xff0c;那我也贡献一份自己的力量~ 大框架梳理从main函数开始学习 大框架梳理 先摆图&#xff1a; 目光先放到最上面的两个小框架&#xff0c;半同步/半反应堆线程池和异步日志系统&#xff0c;日志系统晓得伐&#xff1f;…

jvm垃圾收集器简介

串行垃圾收集器 串行垃圾收集器&#xff0c;是指使用单线程进行垃圾回收&#xff0c;垃圾回收时&#xff0c;只有一个线程在工作&#xff0c;Java应用中的所有线程都要暂停&#xff0c;等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)&#xff0c;一般的javaweb应用中…

深入理解 pnpm(Performant NPM) 的实现原理及其与 npm 的区别

深入理解 pnpm 的实现原理及其与 npm 的区别 在 JavaScript 生态系统中&#xff0c;包管理器是开发者日常工作中不可或缺的工具。npm&#xff08;Node Package Manager&#xff09;作为 Node.js 的默认包管理器&#xff0c;已经广泛应用于各种项目中。然而&#xff0c;随着项目…

略谈发展测量方法论-高敏雪老师的文章解读与收获

历史地看&#xff0c; GDP 被誉为“世纪性发明”&#xff0c;我们曾经将其视为衡量一国经济发展的重要工具&#xff1b;现实地看&#xff0c;“超越 GDP”是当前出现的带有国际性的口号&#xff0c;内在地包含着对 GDP作为发展指标的批评和替代。 如何看到发展&#xff1f; 如…

apisix云原生网关

定义 企业级网关通过域名、路由将请求分发到对应的应用上&#xff0c;通常承载数千个服务的流量&#xff0c;对稳定性有较高要求。 CNCF全景图 选型 Kubernetes抽象出两个核心概念&#xff1a;Service&#xff0c;为多个Pod提供统一的访问入口&#xff1b;Ingress&#xff…

【STM32-HAL库】实现微秒、毫秒、纳秒延时。(STM32F4系列)(附带工程下载链接)

使用了本代码后不能使用HAL库自带的HAL_Delay函数 使用了本代码后不能使用HAL库自带的HAL_Delay函数 使用了本代码后不能使用HAL库自带的HAL_Delay函数 一、新建工程 可以参考我的新建工程系列教程 stm32-HAL库cubeMX新建工程教程&#xff08;以F103C8T6为例&#xff09;ht…

【长文梳理webserver核心】核心类篇

前言 有三个核心组件支撑一个reactor实现 [持续] 的 [监听] 一组fd&#xff0c;并根据每个fd上发生的事件 [调用] 相应的处理函数。这三个组件就是 EventLoop 、Channel 以及 Poller 三个类&#xff0c;其中 EventLoop 可以看作是对业务线程的封装&#xff0c;而 Channel 可以看…

从零开始搭建一个node.js后端服务项目

一、下载node.js及配置环境 网上很多安装教程&#xff0c;此处就不再赘述了 版本信息 C:\Users\XXX>node -v v20.15.0C:\Users\XXX>npm -v 10.7.0 二、搭建node.js项目及安装express框架 在任意位置创建一个项目文件夹&#xff0c;此处项目文件夹名为test&#xff0…

右键菜单添加cmd

regedit 计算机\HKEY_CLASSES_ROOT\Directory\Background\shell\命令提示符\command 数据&#xff1a;cmd.exe /s /k title 命令提示符 或软件商店安装 Windows Terminal

基于element-ui的upload组件与阿里云oss对象存储的文件上传(采用服务端签名后直传的方式)

服务端签名后直传图解 步骤 1 开通阿里云OSS对象存储服务&#xff0c;创建新的Bucket 2 创建子账户获取密钥 创建用户 添加权限 后端 1 新建一个第三方服务的模块 third-party pom文件 <?xml version"1.0" encoding"UTF-8"?> <project x…

HAL+M4学习记录_3

一、HAL库开发框架 记录HAL学习过程 1.1 CMSIS CMSIS&#xff08;Cortex微控制器软件接口标准&#xff09;&#xff0c;用于提供用户和硬件间的接口&#xff0c;用户通过CMSIS标准对Cortex微控制器内部寄存器单元进行读写 1.2 HAL库 HAL&#xff08;硬件抽象层&#xff09;为…

加固与脱壳04 - 一些简单的脱壳方法

这里只讨论一些简单壳的脱壳方法及其原因。 FRIDA-DEXDump https://github.com/hluwa/FRIDA-DEXDump 适用于不需要研究那些被强保护起来的代码&#xff0c;只是想单纯的看看某个地方的业务逻辑。 原理&#xff1a; 对于完整的 dex&#xff0c;采用暴力搜索 dex035&#xf…

云栖实录 | Hologres3.0全新升级:一体化实时湖仓平台

本文根据2024云栖大会实录整理而成&#xff0c;演讲信息如下&#xff1a; 演讲人&#xff1a; 姜伟华 | 阿里云智能集团资深技术专家、Hologres 负责人 丁 烨 | 阿里云智能集团产品专家、Hologres 产品负责人 活动&#xff1a; 2024 云栖大会 - 商用大数据计算与分析平台论…

基于Arduino的超声波和舵机模块集成使用

一.超声波模块和舵机模块集成使用 超声波模块&#xff1a;HC-SR04舵机模块&#xff1a;SG90目的&#xff1a;通过Arduino UNO核心板控制舵机旋转到不同位置&#xff0c;同时获取不同位置超声波模块和障碍物间的距离&#xff0c;配合控制算法&#xff0c;进行基于超声波智能避障…

需求10——通过改一个小bug来学习如何定位问题

在浏览我之前完成的一些小需求时&#xff0c;我发现了一个非常有价值的需求。这个需求可以让我深入了解系统中关于故障上报的功能。通过完善这个需求&#xff0c;我能够全面掌握整个故障上报的流程。 这个需求主要是关于故障上报流程中出现的问题。当前的流程如下&#xff1a;…

ML 系列:机器学习和深度学习的深层次总结(16) — 提高 KNN 效率-使用 KD 树和球树实现更快的算法

一、说明 在机器学习系列的第 16 节&#xff0c;我们重点介绍了提高 K 最近邻 &#xff08;KNN&#xff09; 算法的效率&#xff0c;这是一种广泛用于分类和回归任务的方法。虽然 KNN 简单有效&#xff0c;但对于大型数据集来说&#xff0c;其计算成本可能会令人望而却步。为了…

基于SpringBoot问卷调查系统小程序【附源码】

基于SpringBoot问卷调查系统小程序 效果如下&#xff1a; 管理员登录界面 管理员功能界面 调查人管理界面 问卷调查管理界面 问卷题目管理界面 用户登录界面 APP首页界面 公告信息界面 研究背景 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&…