Vue实现楼层滚动效果

效果

当滚动鼠标的时候右侧的目录会跟着一起滚动,当点击目录的时候,也会定位到相应的标题处。

这是本项目的在线浏览地址:https://chenyajun.fun/#/catalogScroll

实现

下面这张图是所有的 JS 核心代码(57-19)38行,接下来就和大家简单分享一下实现过程~

原理图

通过滚动容器的滚动距离 - 文章内容元素的 offsetTop,也就是 getBoundingClientRect().top - offsetTop,如果满足条件就处理我们的右侧目录进行相应的滚动。

getBoundingClientRect().top:用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。简单说就是元素移动时可以得到其相对于浏览器视窗的实时位置,下面是MDN的解释:

Element.getBoundingClientRect() - Web API 接口参考 | MDN (mozilla.org)

offsetTop: 它返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。

注意: offsetParent 元素是一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的元素。

代码

先看几个变量

const scrollContainer = ref(null) //内容区滚动容器元素
const catalogContent = ref(null) //标题加内容区目录元素
const catalogBox = ref(null) //目录容器
const currentCatalog = ref(0) //当前目录索引

内容容器滚动

function scrollElement() {
  const distance = Math.abs(scrollContainer.value.getBoundingClientRect().top) + 170 //增加一些距离目的为了快滚到时就激活对应目录
  catalogContent.value.forEach(function (el, index) {
    // 如果容器的滚动距离 大于或等于标题距离ul顶部的距离,就说明当前标题已经触顶
    if (distance >= el.offsetTop) {
      currentCatalog.value = index
    }
  })
  setCatalogScroll()
}

当下面这个条件成立时,就说明滚动到了相应的目录,我们只需要激活相应的目录即可;

   if (distance >= el.offsetTop) {
      currentCatalog.value = index
    }

目录容器滚动

激活之后根据当前是第几个目录进行右侧的目录移动:

scrollTo:scrollTo() 方法可以使界面滚动到给定元素的指定坐标位置。

function setCatalogScroll() {
  // 设置目录滚动距离
  if (currentCatalog.value >= 3) {
    catalogBox.value.scrollTo({
      top: (currentCatalog.value - 3) * 65,
    })
  } else {
    catalogBox.value.scrollTo({
      top: 0,
    })
  }
}

如果当前的目录超过第三个,就将目录容器往下相应的进行移动,并且始终保持在中间部位;

  if (currentCatalog.value >= 3) {
    catalogBox.value.scrollTo({
      top: (currentCatalog.value - 3) * 65,
    })
  }

 如果小于3就将其滚动到顶部即可;

catalogBox.value.scrollTo({
      top: 0,
    })

点击滚动

点击对应的目录将左侧的内容直接滚动到顶部,而右侧的目录位置和上面一致,如果当前的目录超过第三个,就将目录容器往下相应的进行移动,并且始终保持在中间部位。

点击之后先设置 目录容器 进行滚动,然后直接将对应的内容区标题进行置顶即可,通过点击的索引控制。

function jumpToCatalog(index) {
  // 点击跳转当前目录
  currentCatalog.value = index
  setCatalogScroll()
  catalogContent.value[index].scrollIntoView()
}

scrollIntoView:该方法会滚动元素的父容器,使被调用 scrollIntoView() 的元素对用户可见。 

总结

该功能类似于楼层滚动的效果,只不过对右侧的目录滚动进行优化,滚动每次基于中间位置,总体来说难度不大,主要判断当前元素是否已经滚动到顶部即可,那么核心就是怎么处理这个问题。

如果觉得以上思路对你有任何帮助或者启发,可以给作者点下赞哦~你的鼓励就是作者最大的动力呢~

源码

GitHub地址:chenyajun-create/juejinCatalogScroll: Imitate the directory scrolling effect on the juejin official website (github.com)

全部源码:

<script setup>
// 循环生成数据结构
const catalogTitle = ref([])
let i = 0
for (let index = 0; index < 12; index++) {
  if (i >= 6) {
    i = 0
  }
  catalogTitle.value.push({
    title: `文章题目${i + 1}`, //标题
    id: `${i + 1}`,
    level: `${i + 1}`, //层级
    left: (i + 1) * 15 + 'px', //左边距
  })
  i++
}

const canRun = ref(true) //节流 防止多次执行滚动事件
function handleScroll() {
  // 内容区滚动函数
  if (canRun.value) {
    canRun.value = false
    scrollElement() //滚动元素
    setTimeout(() => {
      canRun.value = true
    }, 200)
  }
}
const scrollContainer = ref(null) //内容区滚动容器元素
const catalogContent = ref(null) //标题加内容区目录元素
const currentCatalog = ref(0) //当前目录索引
const catalogBox = ref(null) //目录容器
function scrollElement() {
  const distance = Math.abs(scrollContainer.value.getBoundingClientRect().top) + 170 //增加一些距离目的为了快滚到时就激活对应目录
  catalogContent.value.forEach((el, index) => {
    // 如果容器的滚动距离 大于或等于标题距离ul顶部的距离,就说明当前标题已经触顶
    if (distance >= el.offsetTop) {
      currentCatalog.value = index
    }
  })
  setCatalogScroll()
}
function setCatalogScroll() {
  // 设置目录滚动距离
  if (currentCatalog.value >= 3) {
    catalogBox.value.scrollTo({
      top: (currentCatalog.value - 3) * 65,
    })
  } else {
    catalogBox.value.scrollTo({
      top: 0,
    })
  }
}
function jumpToCatalog(index) {
  // 点击跳转当前目录
  currentCatalog.value = index
  setCatalogScroll()
  catalogContent.value[index].scrollIntoView()
}
</script>
<template>
  <div class="outer" @scroll="handleScroll">
    <!-- 内容区 -->
    <div class="catalog-content" ref="scrollContainer">
      <div ref="catalogContent" v-for="(item, index) in catalogTitle" :key="item.id">
        <h1 v-if="item.level === '1'">{{ item.title }}</h1>
        <h2 v-if="item.level === '2'">{{ item.title }}</h2>
        <h3 v-if="item.level === '3'">{{ item.title }}</h3>
        <h4 v-if="item.level === '4'">{{ item.title }}</h4>
        <h5 v-if="item.level === '5'">{{ item.title }}</h5>
        <h6 v-if="item.level === '6'">{{ item.title }}</h6>
        <div class="catalog-message"></div>
      </div>
      <div style="height: 1000px"></div>
    </div>

    <!-- 目录区 -->
    <div class="catalog-name">
      <div class="catalog-name-title">目录</div>
      <div ref="catalogBox" class="catalog-box">
        <div
          v-for="(item, index) in catalogTitle"
          :data-index="index"
          :key="item.id"
          :style="{
            marginLeft: item.left,
            color: currentCatalog === index ? '#1e80ff' : '#000000',
            marginTop: index === 0 ? '10px' : '30px',
          }"
          class="catalog-title"
          @click="jumpToCatalog(index)"
        >
          {{ item.title }}
        </div>
      </div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
html,
body {
  margin: 0;
  height: 100%;
}
$i: 7;
@while $i > 0 {
  h#{$i} {
    margin-left: 10px;
  }
  $i: $i - 1;
}
.outer {
  width: 100%;
  height: 100%;
  position: relative;
  background-color: #f2f3f5;
  display: flex;
  justify-content: center;
  overflow-y: scroll;
}
.catalog-content {
  display: inline-block;
  background-color: #ffffff;
  width: 800px;
  border-radius: 10px;
  margin-top: 20px;
  .catalog-message {
    height: 200px;
    background-color: #bfa;
  }
}
.catalog-name {
  position: fixed;
  right: 20px;
  top: 20px;
  width: 20%;
  border-radius: 10px;
  background-color: #ffffff;
  .catalog-box {
    max-height: 312px;
    overflow-y: auto;
    .catalog-title {
      margin-left: 15px;
      margin-bottom: 15px;
      &:hover {
        color: #1e80ff;
        cursor: pointer;
      }
    }
  }

  .catalog-name-title {
    font-weight: 500;
    margin-top: 15px;
    margin-left: 15px;
    margin-bottom: 35px;
    position: relative;
    &:after {
      content: '';
      width: 95%;
      position: absolute;
      height: 1px;
      top: 32px;
      left: 0;
      background-color: #e4e6eb;
    }
  }
}
</style>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JacksonChen_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值