vue实现导航栏吸顶效果 + 与内容联动

大家好,我是南宫。今天写一篇博客来整理下最近刚解决的一个问题,那就是导航栏跟内容联动的问题

 

简单说一下我想要的效果:写了一个宽度为屏幕100%的div,居中的部分是一个导航栏,水平排列,默认位于banner下,如果页面滚到了banner下面,要让导航栏固定顶部。如果页面滚到了下方对应的内容,那就高亮对应的tab标记。如果点击了tab,那就要让页面滚到对应的内容,并且让该tab高亮

(效果是动态的,我随便截取一个场景来展示吧,比如我点击“应用场景”的时候,页面滚动到了对应的内容区域,并且对应的tab高亮了,也能看到导航栏固定顶部)

拆解一下,可以分为这么几个部分:①吸顶、②选择tab,可以让页面滚动到对应内容的位置、③页面滚动到了对应的内容的位置,可以设置tab的选择。

吸顶的实现:

这个是最容易的,我先简单说一下原理:默认导航栏是位于banner下方的,也就是普通的标准流元素,没有浮动没有定位,是正常占位的;而固定顶部的状态下,导航栏被固定定位(position: fixed)到了顶部,这个时候可以设置一下top。我们可以判断当前滚动的位置是否需要固定定位,如果是,那就做一下样式的切换。

显然导航栏有两种状态,所以我们可以写两个class来分别控制。(我这里使用SCSS,可以参考一下我的代码,nav-bar是默认状态,再加上fixed的是固定顶部的状态)

// 导航条
.nav-bar {
    height: 61px;
    background: url("/img/download/rect_bg.png") repeat-x;
    background-size: 5px 61px;
    &.fixed {
        position: fixed;
        top: 56px;
        left: 0;
        right: 0;
        z-index: 10;
    }
    .nav-bar-item {
        position: relative;
        margin: 0 47px;
        line-height: 55px;
        cursor: pointer;
        &:hover::after,
        &.active::after {
            content: "";
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 4px;
            background: $dark-blue;
        }
        .nav-text {
            font-size: 16px;
            color: #333;
        }
    }
}

那么怎么控制导航栏是否固定顶部呢?可以在data里放一个变量(如isFixed)来控制,默认为false。监听一下页面的滚动,如果滚动超过了某个值,就让isFixed改为true,否则改为false。然后在导航栏这边,动态绑定class,“fixed”在isFixed为true才设置上去。

 data() {
    return {
      // 是否固定
      isFixed: false,
      //...其他data
   }
}
// 处理滚动
    handleScroll() {
      var scrollTop =
        window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop
      // console.log(scrollTop) // 滑动的长度
      var offsetTop = document.querySelector('.banner').offsetHeight
      
      // 吸顶效果
        if (scrollTop > offsetTop) {
          this.isFixed = true
        } else {
          this.isFixed = false
        }
    },
<div class="nav-bar" :class="{ fixed: isFixed }">...</div>

Vue项目中如何监听页面的滚动呢?把刚才的handleScroll定义到组件的methods里面,在mounted的时候绑定window的滚动时间,注意是在mounted的时候才可以使用DOM。退出页面的时候,也要记得销毁监听。

mounted() {
    window.addEventListener('scroll', this.handleScroll) // 监听滑动事件
  },
  destroyed() {
    window.removeEventListener('scroll', this.handleScroll) // 销毁监听滑动事件
  }

点击选择tab,让页面滚动到对应内容的实现:

首先,tab会有普通状态和高亮状态,所以我们也需要用一个变量来控制当前选中的、处于高亮状态的tab是第几个,默认为0,让第一个tab高亮。

 data() {
    return {
      // ...其他data
      // 当前选择的tab
      currentNav: 0,
    }
}
  <!-- 这是导航条里的每一个tab,给它动态绑定active类,高亮才显示,并且绑定了点击事件 -->
  <div
     v-for="(item, index) in obj.contentList"
     :key="index"
     class="nav-bar-item"
     :class="{ active: currentNav == index }"
     @click="toBox(index)"
  >
     <span class="nav-text">{{ item }}</span>
  </div>

然后,页面上有好几块内容,每一块对应一个tab,我们给这几块内容的最外层加上一个class,作为标记,便于选择这些锚点元素,比如我给它们加上“j-content”这个类名。这样就实现了导航栏和每块内容的绑定。

    <!-- 这是其中的一块内容 -->
    <div class="scene j-content">
      <h3 class="production-title">应用场景</h3>
      <!-- 其他内容 -->
    </div>

点击tab的时候,我们可以获取到当前点击的tab的索引(比如第1个的索引是0),在带有j-content的div里,我们找到下标相同的div(如索引为0的时候找到第一个这样的div),获取它顶部的坐标,让页面滚动到这里。

(看下面的代码,从所有的带有j-content的class里找到对应当前索引的div,获取它的offsetTop,然后用window.scrollTo平滑滚动到指定地方,这里用behavior设置了平滑滚动)

    // 滚动到哪一块
    toBox(index) {
      this.currentNav = index
      const DOM = document.querySelectorAll('.j-content')[index]
      const offsetTop = DOM.offsetTop - 25
      // console.log('滚动到哪里', DOM)
      window.scrollTo({
        top: offsetTop,
        behavior: 'smooth'
      })
    },

页面滚动到对应内容,设置tab高亮的实现:

首页,要判断页面滚到到了哪一块内容,就得监听页面的滚动事件,所以判断的代码需要写到handleScroll方法里。

然后滚动的时候,滚动的位置跟哪些值比较呢?这就需要我们记录每一块内容的offsetTop的位置。根据之前定好的class,找到每一块内容的div,获取一下它们各自的offsetTop,并保存。

// 获取所有锚点元素
const divs = [...document.querySelectorAll('.j-content')]
// 将所有锚点元素offsetTop push到数组内
divs.forEach((item, index) => {
  this.contentTopList[index] = item.offsetTop - 25
})

接着,在滚动的过程中,怎么确定当前要高亮的tab是哪个呢?我们可以把当前的scrollTop值与每一个div的offsetTop比较,找到“小于但又最接近”的那个值,把这个下标作为要高亮的tab的下标。

(我补充了一个判断,假如刚开始滚,还没有滚到第一个内容区域的话,navIndex会算出来undefined,为了让这个时候也有tab被高亮,我认为当前高亮的是0.)

let navIndex
// 滚动定位tab高亮的状态
for (let i = 0; i < this.contentTopList.length; i++) {
   // 如果当前滚动的top坐标大于第i个的top坐标,就记录下i。
   // 记录到最后,i就会是最后一个满足条件的i,也就是刚刚好的那个值
   if (scrollTop >= this.contentTopList[i]) {
      navIndex = i
   }
}
 // 把下标赋值给 vue 的 data
this.currentNav = navIndex
if (typeof navIndex !== 'number') {
    this.currentNav = 0
}

这一步,就已经做到了滚动吸顶、点击tab滚动到相应的锚点、滚动高亮对应的tab。但是在调试的时候我又发现了一个问题——点击tab,页面滚动的过程中,经过其他区域的时候,也会点亮对应的tab,这就显得效果有些拖泥带水,不像是被直接定位过去的

于是我觉得,需要区分这两种情况:“因为点击而直接滚动到这里”  和 “页面自己滚动的时候路过这里”。我在data里加了一个新的变量,叫isClick,默认为false,表示不是点击定位的。在点击tab后,瞬间把isClick赋值为true。然后在“根据滚动位置,判断高亮tab”之前,先判断isClick,确定不是点击定位的才判断。在点击动作完成、滚动完毕后,过一小段时间,把isClick还原成true,以便恢复后续的滚动高亮效果。

完整的handleScroll和toBox代码在这里:

// 处理滚动
    handleScroll() {
      var scrollTop =
        window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop
      // console.log(scrollTop) // 滑动的长度
      var offsetTop = document.querySelector('.banner').offsetHeight
      // 判断是否已经记录了每个内容的top,如果不是,就记录一下。如果是,就直接使用
      // 获取所有锚点元素
      const divs = [...document.querySelectorAll('.j-content')]
      // 将所有锚点元素offsetTop push到数组内
      divs.forEach((item, index) => {
        this.contentTopList[index] = item.offsetTop - 25
      })
      // if (this.contentTopList.length === 0) {

      // }
      // 判断当前是否是点击定位的,如果不是,才有滚动定位的效果
      if (!this.isClick) {
        let navIndex
        // 滚动定位tab高亮的状态
        for (let i = 0; i < this.contentTopList.length; i++) {
          // 如果当前滚动的top坐标大于第i个的top坐标,就记录下i。记录到最后,i就会是最后一个满足条件的i,也就是刚刚好的那个值
          if (scrollTop >= this.contentTopList[i]) {
            navIndex = i
          }
        }
        // 把下标赋值给 vue 的 data
        this.currentNav = navIndex
        if (typeof navIndex !== 'number') {
          this.currentNav = 0
        }
      }
      // 吸顶效果
      if (scrollTop > offsetTop) {
        this.isFixed = true
      } else {
        this.isFixed = false
      }
    },
    // 滚动到哪一块
    toBox(index) {
      // 点击滚动到指定的位置,要去掉滚动的过程中因为位置变化带来的效果
      this.isClick = true
      this.currentNav = index
      const DOM = document.querySelectorAll('.j-content')[index]
      const offsetTop = DOM.offsetTop - 25
      // console.log('滚动到哪里', DOM)
      window.scrollTo({
        top: offsetTop,
        behavior: 'smooth'
      })
      // 过一段时间,把isClick还原
      setTimeout(() => {
        this.isClick = false
      }, 800)
    },

 还有20分钟就是新年了,我总算赶在2021年写完了这一篇博客,欢迎有类似需要的小伙伴来探讨哦,谢谢大家!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>