记录下手写带定位功能的滑动置顶菜单的实现

1,297 阅读3分钟

项目中自己写的一个小功能,先看看效果

test2.gif

上面的菜单部分滑动时会自动置顶,点击部分后会定位到页面指定位置,且标题随着滚动的页面内容实时高亮。这个功能大部分官网都有,一说就懂。下面说一下实现过程。

实现过程

1.标题栏的定位

标题栏位置按照正常文档流写入,使用sticky定位,top设置为0就可以实现。过去在sticky出现之前,一般会使用动态class设置粘性布局,但是这太不优雅了,还需要js逻辑辅助。但是sticky的出现完美解决了这一问题。代码如下

html

其中scrollToView是绑定的点击跳转事件,menuIndex用于高亮

<div ref="menuBox" class="menu-box">
    <ul class="menu-box__list">
      <li
        v-for="(item, index) in menuList"
        :key="index"
        :class="menuIndex === index ? 'check' : ''"
        @click="scrollToView(index)"
      >
        {{ item.name }}
      </li>
    </ul>
</div>

css

.menu-box {
    position: sticky;
    top: 0;
    z-index: 999;
    width: 100%;
    margin-top: -40px;
    color: #fff;
    background: linear-gradient(90deg, #1e2e45 0%, rgba(12, 60, 109, 0.5) 50%, #162131 100%);

    ul {
      display: flex;
      justify-content: space-between;
      width: 1200px;
      min-width: 1200px;
      height: 40px;
      margin: 0 auto;
    }

    li {
      display: inline-block;
      line-height: 40px;
      cursor: pointer;
    }

    .check {
      color: #a4ddff;
      position: relative;
    }

    .check::after {
      position: absolute;
      content: '';
      bottom: 2px;
      height: 1px;
      width: 40px;
      right: 50%;
      transform: translateX(50%);
      background: #a4ddff;
    }
  }

2.点击滚动到指定位置切高亮

点击定位实现是通过滚动监听,设置ref动态读取位置实现的。代码如下:

html

html部分在上面的基础上,增加底下的页面内容元素,这里的css就不放了

<div ref="jjfa" class="content-box jjfa">
    <div class="content-box__title">{{ menuList[0].name }}</div>
    <div class="back"></div>
</div>
<div ref="jcfw" class="content-box jcfw">
    <div class="content-box__title">{{ menuList[1].name }}</div>
    <div class="back"></div>
</div>
<div ref="bafw" class="content-box bafw">
    <div class="content-box__title">{{ menuList[2].name }}</div>
    <div class="back"></div>
</div>
<div ref="sffw" class="content-box sffw">
    <div class="content-box__title">{{ menuList[3].name }}</div>
    <div class="back"></div>
</div>
<div ref="hzfw" class="content-box hzfw">
    <div class="content-box__title">{{ menuList[4].name }}</div>
    <div class="back"></div>
</div>
<div ref="ldqk" class="content-box ldqk">
    <div class="content-box__title">{{ menuList[5].name }}</div>
    <div class="back"></div>
</div>
<div ref="dxal" class="content-box dxal">
    <div class="content-box__title">{{ menuList[6].name }}</div>
    <div class="back"></div>
</div>

实现逻辑

以下内容分部分介绍

1.data部分,menuList用来存放页面ref值以及各计算出来的ref部分到首和尾到页面顶部的高度(高度在加载时赋值),menuIndex用来动态给标题高亮

data() {
    return {
      menuList: [
        { name: '标题1', topToTop: 0, botToTop: 0, ref: 'jjfa' },
        { name: '标题2', topToTop: 0, botToTop: 0, ref: 'jcfw' },
        { name: '标题3', topToTop: 0, botToTop: 0, ref: 'bafw' },
        { name: '标题4', topToTop: 0, botToTop: 0, ref: 'sffw' },
        { name: '标题5', topToTop: 0, botToTop: 0, ref: 'hzfw' },
        { name: '标题6', topToTop: 0, botToTop: 0, ref: 'ldqk' },
        { name: '标题7', topToTop: 0, botToTop: 0, ref: 'dxal' }
      ],
      menuIndex: 0
    }
  },

2.设置监听,页面加载addEventListener滚动事件,滚动监听事件用来高亮对应标题使用

mounted() {
    window.addEventListener('scroll', this.handleScroll)
},
destroyed() {
    window.removeEventListener('scroll', this.handleScroll)
},
methods: {
    handleScroll() {
      let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
      this.menuList.forEach((item, index) => {
        if (scrollTop >= item.topToTop && scrollTop < item.botToTop) {
          this.menuIndex = index
          return
        }
      })
    },
}


3.页面加载时获取各元素到top的高度

mounted() {
    this.initContentBoxDistance()
},
methods: {
    initContentBoxDistance() {
      this.menuList.forEach((item, index) => {
        this.menuList[index].topToTop = getElementViewTop(this.$refs[item.ref])
        this.menuList[index].botToTop = this.$refs[item.ref].clientHeight + this.menuList[index].topToTop
      })
    },
    getElementViewTop(element){
      let actualTop = element.offsetTop
      let current = element.offsetParent
      while (current !== null) {
        actualTop += current.offsetTop
        current = current.offsetParent
      }
      return actualTop
    }
}

4.为按钮添加点击滚动事件

scrollToView(index) {
  document.getElementsByClassName('content-box')[index].scrollIntoView({
    behavior: 'smooth',
    block: 'start',
    inline: 'nearest'
  })
},

5.完整代码

export default {
  name: 'Home',
  components: {
    TitleBanner
  },
  data() {
    return {
      menuList: [
        { name: '标题1', topToTop: 0, botToTop: 0, ref: 'jjfa' },
        { name: '标题2', topToTop: 0, botToTop: 0, ref: 'jcfw' },
        { name: '标题3', topToTop: 0, botToTop: 0, ref: 'bafw' },
        { name: '标题4', topToTop: 0, botToTop: 0, ref: 'sffw' },
        { name: '标题5', topToTop: 0, botToTop: 0, ref: 'hzfw' },
        { name: '标题6', topToTop: 0, botToTop: 0, ref: 'ldqk' },
        { name: '标题7', topToTop: 0, botToTop: 0, ref: 'dxal' }
      ],
      menuIndex: 0
    }
  },
  mounted() {
    window.addEventListener('scroll', this.handleScroll)
    this.initContentBoxDistance()
  },
  destroyed() {
    window.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    scrollToView(index) {
      document.getElementsByClassName('content-box')[index].scrollIntoView({
        behavior: 'smooth',
        block: 'start',
        inline: 'nearest'
      })
    },
    handleScroll() {
      let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
      this.menuList.forEach((item, index) => {
        if (scrollTop >= item.topToTop && scrollTop < item.botToTop) {
          this.menuIndex = index
          return
        }
      })
    },
    initContentBoxDistance() {
      this.menuList.forEach((item, index) => {
        this.menuList[index].topToTop = getElementViewTop(this.$refs[item.ref])
        this.menuList[index].botToTop = this.$refs[item.ref].clientHeight + this.menuList[index].topToTop
      })
    },
    getElementViewTop(element){
      let actualTop = element.offsetTop
      let current = element.offsetParent
      while (current !== null) {
        actualTop += current.offsetTop
        current = current.offsetParent
      }
      return actualTop
    }
  }
}

结尾

原创内容,多多批评都铎优化建议多多指正。一起讨论,thanks