需求
  vue项目PC端详情页内容过多,在右侧或左侧加一个导航栏,通过点击某一项,页面平滑滚动到具体的内容上。并把这个功能封装成组件。

在这里插入图片描述

思路:

  1. 封装成组件复用的话,首先快捷键的每一项数据需要父组件传入;在父组件定义一个数组作为右侧导航栏的数据;由于左侧区域的某一块内容没有数据时,其对应的导航项不显示;
  2. 要给每一块的内容最外层绑定ref,是为了获取该dom元素,通过点击导航栏某一项时,让页面可以滚动到对应的元素内容;
  3. 导航在抽屉里做的,给最外层绑定ref=“drawer”,可以获取到当前页面最外层的dom元素,根据最外层容器的dom元素来获取滚动高度;
  4. 组件要接收导航栏数据、接收当前是否是抽屉(默认为抽屉,传false为普通正常页面)、接收当前页面所有的dom元素、接收样式(显示在页面的左侧还是右侧)
  5. 统一封装滚动方法:将滚动方法放在定时器中,在获取到最新DOM元素时,添加该定时器;在组件销毁时,清除定时器;定时器中主要功能:当滚动上去的高度+可见区域的高度==全文高度,导航栏选中最后一项;当第一项的距离可视窗口顶部距离>0,导航栏选中第一项。
  6. 点击方法:点击导航栏某一项,内容区域滚动到可视区域的顶部,导航栏选中该项。

代码:

具体页面
在这里插入图片描述

组件:

<template>
  <div>
    <div class="leftKey" :style="{ left: left || 'auto', right: right || 'auto', opacity: opacity }">
      <span v-for="item in titleKey" :key="item.id">
        <a v-if="item.flag" @click="goAnchor(item.id)" :class="{checkKey: item.checked}">{{item.name}}</a>
      </span>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      timer: null,
      scrollTop: 0,
      anchorFlag: false,
      ids: ''
    }
  },
  created () {
    // 在组件调用之前将所有项设置未选中,将第一项设置默认选中
    this.titleKey.forEach(item => {
      item.checked = false
    })
    this.titleKey[0].checked = true
  },
  mounted () {
    // console.log('7777', this.parentRef)
    this.fun = () => {
      clearTimeout(this.timer)
      this.timer = setTimeout(() => {
        // scrollTop 网页被卷去的高
        // clientHeight 网页可见区域高
        // scrollHeight 网页正文全文高
        // this.parentRef.drawer.$refs.drawer.scrollTop 获取当前抽屉滚动的高度
        // document.documentElement.scrollTop || document.body.scrollTop 获取当前页面滚动的高度
        var scrollTop = this.drawerFLag ? this.parentRef.drawer.$refs.drawer.scrollTop : (document.documentElement.scrollTop || document.body.scrollTop)
        var clientHeight = document.documentElement.clientHeight || document.body.clientHeight
        var scrollHeight = this.drawerFLag ? this.parentRef.drawer.$refs.drawer.scrollHeight : (document.documentElement.scrollHeight || document.body.scrollHeight)
        this.scrollTop = scrollTop
        // 网页被卷去的高+网页可见的高===网页全文高,就是滚动到最底部了,默认选中最后一项
        // 为什么不能使用forEach循环,因为最后一项可能是false不显示,这个时候如果只遍历将全部设置为false,而把最后一项设为true此时没有可选中的项
        // 设置一个变量为true,倒着遍历,若该项存在且变量为true,则将这一项设置为true,将变量设置为false;剩下的for循环会将每一项都设为false
        if (scrollTop + clientHeight === scrollHeight) {
          let lastFlag = true
          for (let i = this.titleKey.length - 1; i >= 0; i--) {
            if (this.titleKey[i].flag && lastFlag) {
              this.titleKey[i].checked = true
              lastFlag = false
            } else {
              this.titleKey[i].checked = false
            }
          }
          return
        }
        // 监听id对应DOM与屏幕顶部的距离,第一项距离大于0,就默认选中第一项
        // getBoundingClientRect().top 元素的上边 距离 可视窗口顶部的距离
        if (this.parentRef[this.titleKey[0].id].getBoundingClientRect().top > 0) {
          this.titleKey.forEach(item => {
            item.checked = false
          })
          this.titleKey[0].checked = true
        }
      }, 50)
    }
    // vue使用$once清除定时器的问题,以防其他页面滚动时触发该定时器导致报错
    // 通过$once来监听生命周期beforeDestroy钩子,在组件销毁前清除定时器
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('scroll', this.fun, true)
    })
    // 通过$nextTick获取最新更新的DOM元素
    this.$nextTick(() => {
      window.addEventListener(
        'scroll',
        this.fun,
        true
      )
    })
  },
  methods: {
    // 点击导航栏的某一项,左侧内容区域滚动到对应的位置
    goAnchor (id) {
      // 如果点击选中的这一项id在传过来的dom元素中
      // scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域内
      // 让选中id对应的DOM置顶,即DOM与可视屏幕顶部重合
      if (this.parentRef[id]) {
        this.parentRef[id].scrollIntoView({
          // 平滑过渡
          behavior: 'smooth',
          // 上边框与视窗顶部平齐。默认值
          block: 'start'
        })
        // 遍历让所有项为false,点击项为true
        this.titleKey.forEach(item => {
          if (item.id === id) {
            item.checked = true
          } else {
            item.checked = false
          }
        })
      }
    },
  },

  props: {
    // 父组件传入的数据属性名
    titleKey: {
      // 类型
      type: Array,
      // 默认值
      default: () => []
    },
    // 父组件传来的值是否是抽屉
    // 默认是true也就是抽屉,如果不是则传false
    drawerFLag: {
      type: Boolean,
      default: true
    },
    // 父组件的ref
    parentRef: {
      type: Object,
      default: () => {}
    },
    // 居左显示
    left: {
      type: String,
      default: ''
    },
    // 居右显示
    right: {
      type: String,
      default: ''
    },
    // 设置透明度
    opacity: {
      type: Number,
      default: 0
    }
  }
}
</script>

<style lang="scss" scoped>
@import "~@/styles/variables.scss";
@import "~@/styles/index.scss";

    .clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
      content: "";
      display: block;
      height: 0;
      clear:both;
      visibility: hidden;
    }
    .clearfix{
      *zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
    }

    .checkKey {
        color: #E8380D;
        border-bottom: 1px solid #E8380D;
        background-color: #E7370D;
        color: #fff;
      }
    .leftKey {
      position: fixed;
      width: 127px;
      background: #fff;
      box-shadow:0px 3px 8px 1px rgba(12,12,12,0.06);
      font-size: 14px;
      color: #333;
      background-color: rgba(255,252,252,1);
      top: 115px;
      z-index: 999;
      a {
        display: block;
        padding: 16px 0px 16px 21px;
        border-top: 1px solid rgba(255,228,222,1);
        border-left: 1px solid rgba(255,228,222,1);
        border-right: 1px solid rgba(255,228,222,1);
      }
      a:last-child {
        border-bottom: 1px solid rgba(255,228,222,1);
      }
      img {
        padding-right: 2px;
      }
    }

</style>


main.js

import Vue from 'vue'
import ElementUI from 'element-ui'
// 全局注册导航栏组件
import shortcutKey from '@/components/ ShortcutKey'

Vue.component('ymShortcutKey', shortcutKey)

Vue.config.productionTip = false
Vue.use(ElementUI)

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

参考文章:Vue中实现锚点

涉及知识:

  1. vue使用$once清除定时器

    mounted () {
        const that = this
        // 设置定时器
        const testSetinterval = setInterval(() => {
          setTimeout(() => {
            console.log('test clearInterval')
          }, 0)
        }, 2000)
        // 通过$once来监听生命周期beforeDestroy钩子,在组件销毁前清除定时器。
        this.$once('hook:beforeDestroy', () => {
          clearInterval(testSetinterval)
        })
      },
    

    参考:
      vue使用$once清除定时器的问题
      vue中如何清除定时器

  2. scrollTop 网页被卷去的高
    clientHeight 网页可见区域高
    scrollHeight 网页正文全文高

    参考:
      搞清clientHeight、offsetHeight、scrollHeight、offsetTop、scrollTop
      DOM元素位置,滚动位置和鼠标事件位置相关属性函数总结
      获取页面滚动高度
      document.documentElement.scrollTop || document.body.scrollTop;

  3. this.$nextTick()

    this.$nextTick()这个方法作用是当数据被修改后使用这个方法会回调获取更新后的dom再render出来(调用render()函数,重新编译。将vue模版里的逻辑如v-if、v-for等这些内容是浏览器不能识别的,必须通过js去转换为html,才能够显示页面。)

    参考:
      对vue实现数据实时更新,render() 函数的一些理解
      个人理解this.$nextTick()的使用场景

      this.$nextTick

  4. ref$refs
    使用ref绑定DOM元素,通过this.$refs获取绑定的该元素。这样可以减少获取dom节点的消耗

    <input type="text" ref="input1"/>
    this.$refs.input1.value ="22";
    

    参考:$refs基本用法
    Vue基础4: ref 和 $refs

  5. setInterval()setTimeout()
    setInterval的特点:一直循环调用函数,不会自己停止;需要用window.clearInterval(setInt);这个函数去停止循环
    setTimeout的特点:只调用一次
    setTimeout(“showTime()”,5000); //延迟5秒刷新页面
    业务场景:
    setTimeout用于延迟执行某方法或功能
    setInterval则一般用于刷新表单,对于一些表单的假实时指定时间刷新同步

    参考:
    setInterval()与setTimeout()计时器
    setTimeout和setInterval的区别
    setInterval和setTimeout的区别

Logo

前往低代码交流专区

更多推荐