Vue实现顶部导航跟随页面联动效果

需求背景

最近也是应我一位同学的要求,给他的公司制作一个官网,那也就是拾起了一些基础的知识,搜索了一些其它的文章有实现这种效果的,但大多导航都是在侧边,然后点击导航对应的部分内容滑到最上面,跟我这次设计的交互有些偏差,所以自己也是实现了一把,比较简单,大体逻辑跟其它的比较类似。

要实现的效果预览

  1. 导航固定在顶部,背景色透明,向上滑动时背景色变为白色
  2. 向上滑动时候当相应的内容即将没过头部时,对应的导航变为激活态
  3. 当滑到最底部时,最后的导航设为激活态,因为有的设计稿最后一块儿的区域高度是撑不开一屏的,所以最后一个永远不会高亮
  4. 点击导航滑到对应的区域上,实现两者的联动效果

代码实现

vue 复制代码
<template>
  <div
    class="header"
    :style="{
      backgroundColor: scrollTop || navActive > 1 ? '#fff' : 'transparent'
    }"
  >
    <div class="header-box">
      <div class="logo-container">logo</div>
      <div class="navBox">
        <div
          v-for="item in navList"
          :key="item.value"
          :class="['navItem', navActive == item.value ? 'active' : '']"
          @click="goAnchor(item.id, item.value)"
        >
          {{ item.label }}
        </div>
      </div>
    </div>
  </div>
  <div class="box-col" id="box">
    <div id="part1">核心产品</div>
    <div id="part2">应用场景</div>
    <div id="part3">服务领域</div>
    <div id="part4">联系我们</div>
  </div>
</template>
js 复制代码
<script>
export default {
  name: 'Anchor',
  data() {
    return {
      scrollTop: 0,
      //锚点点击标识, 不然active就会来回抖动
      navClickFlag: false,
      // 滚动元素id
      scrollWrap: 'box',
      navActive: 1,
      navList: [
        {
          label: '核心产品',
          value: 1,
          id: 'part1'
        },
        {
          label: '应用场景',
          value: 2,
          id: 'part2'
        },
        {
          label: '服务领域',
          value: 3,
          id: 'part3'
        },
        {
          label: '联系我们',
          value: 4,
          id: 'part4'
        }
      ],
      // 各锚点的高度
      offTopLs: []
    }
  },
  mounted() {
    // 监听滚动
    window.addEventListener('scroll', this.handleScroll, true)
    this.$nextTick(() => {
      this.getoffTop()
    })
  },
  beforeDestroy() {
    // 销毁监听
    window.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    // 计算锚点的高度
    getoffTop() {
      let offTop = []
      for (let index = 0; index < this.navList.length; index++) {
        const element = document.getElementById(this.navList[index].id)

        if (element) offTop.push(element.offsetTop)
      }
      this.offTopLs = offTop
    },
    // 监听滚动
    handleScroll(event) {
      console.log(document.querySelector('.header').clientHeight)
      const { scrollTop, scrollHeight, clientHeight } = event.target
      const scrollWrap = document.getElementById(this.scrollWrap)
      if (!this.navClickFlag && scrollWrap) {
        let scTop = scrollWrap.scrollTop
        this.scrollTop = scTop
        for (let index = 0; index < this.offTopLs.length; index++) {
          const element = this.offTopLs[index]
          let offsetTop = scrollWrap.offsetTop + 88
          if (scTop + offsetTop >= element) {
            this.navActive = index + 1
          }
          if (scrollTop + clientHeight >= scrollHeight) {
            // 滚动到底部,高亮最后一个
            this.navActive = this.navList.length
          }
        }
      }
    },
    // 锚点导航
    goAnchor(keyId, val) {
      if (this.navClickFlag || this.navActive === val) return
      if (val === 1) this.scrollTop = 0
      this.navClickFlag = true
      this.navActive = val

      const targetElement = document.getElementById(keyId)
      if (targetElement) {
        const offsetTop = targetElement.offsetTop
        const scrollContainer = document.getElementById(this.scrollWrap)
        if (scrollContainer) {
          scrollContainer.scrollTo({
            top: offsetTop - 88,
            behavior: 'smooth'
          })
        }
      }

      setTimeout(() => {
        this.navClickFlag = false
      }, 1000)
    }
  }
}
</script
css 复制代码
<style scoped lang="scss">
html {
  overflow: hidden;
}
.box-col {
  height: 100vh;
  overflow-x: hidden;
  overflow-y: auto;
  box-sizing: border-box;
  scroll-behavior: smooth;
}

.header {
  width: 100vw;
  background-color: #fff;
  height: 88px;
  position: fixed;
  z-index: 10;
  transition: background-color 0.3s;

  &-box {
    width: 1440px;
    height: 100%;
    display: flex;
    margin: 0 auto;
    justify-content: space-between;
    align-items: center;

    .logo-container {
      width: 199px;
      height: 42px;
      font-size: 24px;
    }

    .navBox {
      margin-left: 10px;
      display: flex;

      .navItem {
        color: #000000;
        padding: 8px 35px;
        cursor: pointer;
        &.active {
          border-radius: 5px;
          color: #fff;
          background: rgb(0, 108, 238);
        }
      }
    }
  }
}

#part1,
#part2,
#part3,
#part4 {
  height: 500px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
}

#part1 {
  background-color: #ccc;
}
#part2 {
  background-color: #ccd;
}
#part3 {
  background-color: #cad;
}
#part4 {
  background-color: #cba;
}
</style>

遇到的问题及解决

一开始采用的是scrollIntoView,即:document.getElementById(keyId).scrollIntoView(true),但是这种方式有个弊端,不太灵活,会滚动到最顶部,导致有偏差,所以后面换成scrollTo的方式,灵活控制需要滚动的数值

结语

这里也是记录下,有需求的同学直接代码复制到项目中就能使用,避免了重复造轮子的缺陷,希望能帮助到大家。

相关推荐
子兮曰5 小时前
async/await高级模式:async迭代器、错误边界与并发控制
前端·javascript·github
恋猫de小郭5 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
GIS之路7 小时前
ArcGIS Pro 中的 Notebooks 入门
前端
IT_陈寒8 小时前
React状态管理终极对决:Redux vs Context API谁更胜一筹?
前端·人工智能·后端
lemon_yyds9 小时前
《vue 2 升级vue3 父组件 子组件 传值: value 和 v-model
vue.js
Kagol9 小时前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉9 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau9 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生9 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼9 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范