signed

QiShunwang

“诚信为本、客户至上”

超详细Vue实现菜单绑定内容锚点和滚动动画

2021/1/28 13:48:37   来源:

在原文 超详细Vue实现导航栏绑定内容锚点+滚动动画+vue-router(hash模式可用)  基础上优化一下  因为有的菜单很长 需要菜单滚动到一定位置后 还能显示到接下来的菜单  动态滚动 因此加了逻辑处理

先上效果图  如果效果是你所需要的 可继续看代码

 

 

 

 

 

 

 

 

<template>
  <div>
    <!--左侧菜单区导航区域 -->

    <ul class="menu" id="menu">
      <li
        v-for="(menuItem, menukey) in menuList"
        :key="menukey"
        :class="{ active: active === menukey }"
        @click="scrollTo(menukey)"
      >
        {{ menuItem.name }}
      </li>
    </ul>
    <!-- 右侧内容区域 -->
    <div class="content">
      <div>content-0</div>
      <div>content-1</div>
      <div>content-2</div>
      <div>content-3</div>
      <div>content-4</div>
      <div>content-5</div>
      <div>content-6</div>
      <div>content-7</div>
      <div>content-8</div>
      <div>content-9</div>
      <div>content-10</div>
      <div>content-11</div>
      <div>content-12</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      active: 0, // 当前激活的导航索引
      menuList: [
        { name: '菜单0' },
        { name: '菜单1' },
        { name: '菜单2' },
        { name: '菜单3' },
        { name: '菜单4' },
        { name: '菜单5' },
        { name: '菜单6' },
        { name: '菜单7' },
        { name: '菜单8' },
        { name: '菜单9' },
        { name: '菜单10' },
        { name: '菜单11' },
        { name: '菜单12' },
      ]
    }
  },
  mounted() {
    // 监听滚动事件
    window.addEventListener('scroll', this.onScroll)
  },
  destroy() {
    // 必须移除监听器,不然当该vue组件被销毁了,监听器还在就会出错
    window.removeEventListener('scroll', this.onScroll)
  },

  methods: {
    // 滚动监听器
    onScroll() {
      // 获取所有锚点元素
      const navContents = document.querySelectorAll('.content div')
      // 所有锚点元素的 offsetTop
      const offsetTopArr = []
      navContents.forEach(item => {
        // item.style.height =  window.innerHeight +'px' ;
        // item.style.lineHeight =  window.innerHeight +'px' ;
        // console.log('item',window.innerHeight )
        offsetTopArr.push(item.offsetTop)
      })
      // 获取当前文档流的 scrollTop
      const scrollTop = document.documentElement.scrollTop + 40 || document.body.scrollTop + 40
      // 定义当前点亮的导航下标
      let navIndex = 0
      let count = 0
      let menu = document.getElementById('menu');
      for (let n = 0; n < offsetTopArr.length; n++) {
        // 如果 scrollTop 大于等于第 n 个元素的 offsetTop 则说明 n-1 的内容已经完全不可见
        // 那么此时导航索引就应该是 n 了
        if (scrollTop >= offsetTopArr[n]) {
          navIndex = n
          if (navIndex >= 8) count++
        }
      }
      //当前active菜单赋值
      this.active = navIndex
      // 滚动到一定位置 将菜单整体向上移  确保能看的到更多的菜单
      if (navIndex >= 8) {
        menu.style.top = - 100 * count + 'px'
      } else {
        menu.style.top = 0 + 'px'
      }

    },
    // 跳转到指定索引的元素
    scrollTo(index) {
      // 获取目标的 offsetTop
      // css选择器是从 1 开始计数,我们是从 0 开始,所以要 +1
      const targetOffsetTop = document.querySelector(`.content div:nth-child(${index + 1})`).offsetTop
      // 获取当前 offsetTop
      let scrollTop = document.documentElement.scrollTop  || document.body.scrollTop
      // 定义一次跳 50 个像素,数字越大跳得越快,但是会有掉帧得感觉,步子迈大了会扯到蛋
      const STEP = 70
      // 判断是往下滑还是往上滑
      if (scrollTop > targetOffsetTop) {
        // 往上滑
        smoothUp()
      } else {
        // 往下滑
        smoothDown()
      }
      // 定义往下滑函数
      function smoothDown() {
        // 如果当前 scrollTop 小于 targetOffsetTop 说明视口还没滑到指定位置
        if (scrollTop < targetOffsetTop) {
          // 如果和目标相差距离大于等于 STEP 就跳 STEP
          // 否则直接跳到目标点,目标是为了防止跳过了。
          if (targetOffsetTop - scrollTop >= STEP) {
            scrollTop += STEP
          } else {
            scrollTop = targetOffsetTop
          }
          document.body.scrollTop = scrollTop
          document.documentElement.scrollTop = scrollTop
          // 关于 requestAnimationFrame 可以自己查一下,在这种场景下,相比 setInterval 性价比更高
          requestAnimationFrame(smoothDown)
        }
      }
      // 定义往上滑函数
      function smoothUp() {
        if (scrollTop > targetOffsetTop) {
          if (scrollTop - targetOffsetTop >= STEP) {
            scrollTop -= STEP
          } else {
            scrollTop = targetOffsetTop
          }
          document.body.scrollTop = scrollTop
          document.documentElement.scrollTop = scrollTop
          requestAnimationFrame(smoothUp)
        }
      }
    }
  }
}
</script>

<style scoped>
/* 内容区的样式 */
.content {
  background-color: white;
  border-radius: 4px;
  margin: 10px 10px 0 150px;
}
.content div {
  height: 960px;
  line-height:960px;
  width: 100%;
  text-align: center;
  font-size: 100px;
  padding: 20px;
  background: #ffffff;
  box-shadow: 0 3px 10px 0 rgba(219, 219, 219, 0.5);
  border-radius: 6px;
}
.content div:not(:last-child) {
  margin-bottom: 2px;
}

/* 导航栏的样式 */
.menu {
  position: fixed;
  top: 0;
  left: 0px;
  background-color: #efefef;
  width: 140px;
}
.menu li {
  text-align: center;
  padding: 20px;
  font-size: 24px;
  cursor: pointer;
  line-height: 50px;
  height: 50px;
  font-weight: 600;
  color: #727475;
  list-style: none;
}
.menu li:not(:last-child) {
  border-bottom: 1px solid #e0e0e0;
}
.menu li:hover {
    -webkit-box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.2);
    box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.2);
}
/* 当导航被点亮后改变颜色 */
.menu .active {
  color: #847ec3;
  background-color: #e2e2e2;
}
</style>