# 滚动跳转左右联动

如果是Vue项目的Hash模式的路由,则不太建议使用锚元素<a>来实现。会导致hash路由丢失状态,导致刷新失败。如果是 history模式的话,则可以使用。

下面是一个Vue示例, 利用Element.scrollIntoView这个Api来实现的一种思路。

右侧高亮的思路就是从上到下寻找第一个可见的标题, 判断条件就是top + height / 2 - ScrollContainerTop >= 0

表示这个标题的top值加上标题本身一半的高度再减去父元素的top值。取第一个大于等于0的元素为当前高亮的内容。

WARNING

这个方法有一个缺点就是可滚动区域很小时,有时候可见区域内并没有任何一个标题元素。这时候的高亮显示就比较奇怪。

TIP

除了使用标题元素来判断,也可以使用整个内容区的中线来判断。或者判断哪块内容占据的可视区域比例更大,这个都可以根据实际业务来调整。

在线Demo

<template>
  <div class="container">
    <div class="left"
         ref="ScrollContainer"
         @scroll="onScroll"
         @scrollend="onScrollEnd">
      <div v-for="item in list"
           :key="item.id">
        <h4 :id="item.id"
            :data-id="item.id"
            class="item-header"
            style="background: #eee;">{{item.title}}</h4>
        <p :style="{height: item.childHeight + 'px'}">内容</p>
      </div>
    </div>
    <div class="right">
      <a v-for="item in list"
         :key="item.id"
         :class="{'title-item__a': true, 'active': activeId === item.id}"
         style="display: block;"
         @click="handleJump(item)">{{item.title}}</a>
    </div>
  </div>
</template>
<script>
/* eslint-disable */
export default {
  name: 'ScrollReference',
  // mixins: [],
  // components: {},
  // props: {},
  data() {
    return {
      list: [
        { id: 'A', title: '项目A', childHeight: 200 },
        { id: 'B', title: '项目B', childHeight: 300 },
        { id: 'C', title: '项目C', childHeight: 100 },
        { id: 'D', title: '项目D', childHeight: 500 },
        { id: 'E', title: '项目E', childHeight: 400 },
        { id: 'F', title: '项目F', childHeight: 200 },
        { id: 'G', title: '项目G', childHeight: 300 }
      ],
      activeId: 'A',
      //   是否通过点击右侧标题,用scrollIntoView滚动
      scrollByClickTitle: false
    };
  },
  // computed: {},
  // filters: {},
  // watch:{},
  // created() {},
  // mounted() {},
  // beforeRouteEnter(to, from, next) { next(vm => {}) },
  // beforeRouteUpdate(to, from, next) {},
  // beforeRouteLeave(to, from, next) {},
  // beforeDestroy() {},
  // destroyed() {},
  methods: {
    handleJump(item) {
      this.activeId = item.id;
      const headerEl = document.querySelector(`#${item.id}`);
      this.scrollByClickTitle = true;
      headerEl.scrollIntoView({
        behavior: 'smooth',
        // behavior: 'auto',
        block: 'start',
        inline: 'start'
      });
    },
    onScroll() {
      if (this.scrollByClickTitle) {
        return;
      }
      if (this.timer) {
        clearTimeout(this.timer);
        this.timer = null;
      }
      this.timer = setTimeout(() => {
        const ScrollContainer = this.$refs['ScrollContainer'];
        const { top: ScrollContainerTop } = ScrollContainer.getBoundingClientRect();
        console.log(ScrollContainerTop);
        const headerElList = this.$el.querySelectorAll('.item-header');
        console.log(headerElList);
        for (let i = 0, len = headerElList.length; i < len; i++) {
          const headerEl = headerElList[i];
          const { top, height } = headerEl.getBoundingClientRect();
          // 滚动过一半的高度即认为该模块不可见了
          if (top + height / 2 - ScrollContainerTop >= 0) {
            this.activeId = headerEl.dataset.id;
            break;
          }
        }
      }, 100);
    },
    onScrollEnd() {
      console.log('onScrollEnd');
      this.scrollByClickTitle = false;
    }
  }
};
</script>
<style lang="stylus" scoped>
.container {
  display: flex;
  height: 100%;

  .left {
    flex: 0 0 400px;
    height: 100%;
    overflow-y: auto;
  }

  .right {
    flex: 0 0 200px;
    margin-left: 20px;
  }

  .title-item__a {
    display: block;
    text-decoration: none;
    outline: none;
    line-height: 1;
    font-size: 14px;
    padding: 12px 20px;
    box-sizing: border-box;
    // border-left: 1px solid #ccc;
    cursor: pointer;
    position: relative;
    transition: 0.2s;

    &::after {
      content: ' ';
      position: absolute;
      left: 0px;
      top: 0;
      bottom: 0;
      width: 1px;
      background-color: #ccc;
      transition: 0.2s;
    }

    &:link {
      color: #666;
    }

    &:visited {
      color: #333;
    }

    &:hover {
      color: #333;
    }

    &:active {
      color: #409eff;
    }

    &.active {
      color: #409eff;

      &::after {
        width: 2px;
        background-color: #409eff;
      }
    }
  }
}
</style>

上次更新: 1/22/2025, 9:39:13 AM