关于vue页面封装锚点功能(锚点列表与正文列表同时滚动)

项目场景

项目需要给页面模块设置锚点列表。锚点列表同时也具备滚动并随着列表滚动而一切滚动。


问题解决预览

测试锚点示例


问题描述

  1. 页面滚动,锚点位置高亮比较容易实现
  2. 页面滚动,要使高亮锚点始终处于视口中。即页面滚动,锚点列表也同时滚动。
  3. 要使滚动平滑且及时,以免影响用户体验。

原因分析:

给页面及锚点列表同时绑定滚动事件


解决方案:

代码仅仅是示例功能,样式请自行编写

父组件:

<template>
  <div class="home">
    <!-- <img alt="Vue logo" src="../assets/logo.png"> -->
    <div class="content">
      <ul @mousewheel="scrollChange" class="files-detail">
        <li class="item form-part" v-for="(item, index) in formTitle" :key ="index" :style="{'height': (index + 2) + '00px' }" :id="item.id">{{ item }}</li>
      </ul>
      <Anchor ref="anchor" :list="formTitle" />
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import Anchor from '@/components/Anchor.vue'

export default {
  name: 'Home',
  components: {
    Anchor
  },
  data() {
    return {
      formTitle: []
    }
  },
  mounted() {
    for(let i = 0; i < 10; i ++) {
      this.formTitle.push({
        title: i,
        id: 'anchor_' + i
      })
    }
  },
  methods: {
    // 处理锚点滚动事件
    scrollChange() {
      this.$refs.anchor.handleScrollChange()
    },
  }
}
</script>

<style lang="scss" scoped>
.content {
  padding: 20px;
  border: 1px solid red;
  position: relative;
}
ul {
  height: 500px;
  overflow-y: auto;
  width: 80%;
}
.item {
  border: 1px solid black;
  margin-top: 10px;
}
</style>


子组件: anchor.vue

<template>
  <div class="anchor-card">
    <transition name="el-zoom-in-bottom">
      <el-card>
        <div slot="header" class="card-header">
          <span>目录</span>
          <i class="el-icon-close close-btn" @click=
          "handleTitleClick" />
        </div>
        <el-timeline v-if="show" class="anchor-list">
          <el-timeline-item
            v-for="(item, index) in anchorList"
            class="anchor-item"
            :key="index"
            :size="item.checked ? 'large' : 'normal'"
            :type="item.checked ? 'primary' : ''"
            :icon="item.checked ? 'el-icon-circle-check' : ''">
            <p @click="handleAnchorClick(index)" :class="{'checked': item.checked}">{{item.title}}</p>
          </el-timeline-item>
        </el-timeline>
      </el-card> 
    </transition>
  </div>
</template>

<script>
export default {
  name: 'AnchorCard',
  props: [ 'list' ],
  data() {
    return {
      show: true,
      anchorList: []
    }
  },
  watch: {
    list: {
      handler(val) {
        this.anchorList = val
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    handleTitleClick() {
      this.show = !this.show
    },
    handleAnchorClick(index) {
      this.handleAnchorItemChecked(index)
      this.goAnchor(this.anchorList[index].id, index)
    },
    goAnchor(selector) {/*参数selector是id选择器(#anchor14)*/
      // console.log(selector)
      const el = document.getElementById(selector)
      if (el) {
        setTimeout(() => { // 页面没有渲染完成时是无法滚动的,因此设置延迟
          el.scrollIntoView({behavior: 'smooth'}) // js的内置方法,可滚动视图位置至元素位置
        }, 100)
      }
    },
    handleScrollChange() {
      const content = document.getElementsByClassName('files-detail')[0]
      this.handleRecursionListScroll(content, 'form-part')
    },
    handleAnchorItemChecked(index) {
      const item = this.anchorList[index]
      this.anchorList.forEach(ele => {
        ele.checked = false
      })
      let currentItem = {
        ...item,
        checked: true,
        hideTimestamp: true
      }
      this.anchorList.splice(index, 1, currentItem)
    },
    handleRecursionListScroll(content, className) {
      const items = document.getElementsByClassName(className)
      let tops = []
      for(let i = 0; i < items.length; i++) {
        tops.push(items[i].getBoundingClientRect().top)
      }
      let distances = tops.findIndex(item => {
        return item > 0
      })
      const list = document.getElementsByClassName('anchor-item')
      distances = distances === -1 ? tops.length - 1 : distances
      this.handleAnchorItemChecked(distances)
      document.getElementsByClassName('anchor-list')[0].scrollTo({
          behavior: 'smooth',
          top: list[distances].getBoundingClientRect().height * distances
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.anchor-card {
  width: 200px;
  position: absolute;
  bottom: 20px;
  right: 20px;
  z-index: 999;
  .el-card__header {
    background-color: #F4F9FF;
    .close-btn {
      color: #409eff;
      float: right; 
      padding: 3px 0
    }
  }
  ::v-deep .el-card__body {
    padding: 0;
    // max-height: 300px;
    .el-timeline {
      padding: 25px;
      padding-bottom: 0;
      position: relative;
      overflow-y: auto;
      max-height: 300px;
      .el-timeline-item {
        cursor: pointer;
        font-size: 14px;
        .checked {
          color: #409eff;
        }
      }
    }
  }
}
</style>

你可能感兴趣的:(前端-vue,vue.js,javascript,前端)