自学内容网 自学内容网

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

目录

第一章 实现效果

第二章 锚点组件分析

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滚动到指定节点期间就添加了滚动事件。解决方法小编在代码中已添加说明。


原文地址:https://blog.csdn.net/qq_45796592/article/details/142494889

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!