Skip to content
本页目录

索引组件滚动高亮

JS、CSS 实现粘性布局 & 吸顶效果

前置知识点


一、 el.offsetParentel.offsetTop

  1. element.offsetParent 为包含 element 的祖先元素中,层级最近的定位元素。
  • 也就是说, offsetParent 必须满足三个条件:
    • element祖先元素
    • 最靠近 element
    • 是定位元素, 即 position 属性不为 static
  1. element.offsetTop 是当前元素顶部到 element.offsetParent 顶部的距离.

二、 sticky

sticky 不生效原因有:

  • 父元素设置了 overflow:hiddenoverflow:auto
  • 未指定 toprightbottomleft 4个值中的任意一个
  • 父元素高度小于 sticky 定位的元素高度
  • sticky 属性依赖于用户的滚动,在 position:relative position:fixed 定位之间切换。
  • 元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。

sticky 属性仅在以下几个条件都满足时有效:

  • 父元素不能 overflow:hidden 或者 overflow:auto 属性
  • 必须指定 topbottomleftright 4 个值之一,否则只会处于相对定位
  • 父元素的高度不能低于 sticky 元素的高度

实现索引组件滚动高亮

css 实现粘性布局
索引行 1
🎲 第1-1项目 🎲 第1-2项目 🎲 第1-3项目 🎲 第1-4项目 🎲 第1-5项目 🎲 第1-6项目 🎲 第1-7项目 🎲 第1-8项目 🎲 第1-9项目 🎲 第1-10项目 🎲 第1-11项目 🎲 第1-12项目 🎲 第1-13项目 🎲 第1-14项目 🎲 第1-15项目
索引行 2
🎲 第2-1项目 🎲 第2-2项目 🎲 第2-3项目 🎲 第2-4项目 🎲 第2-5项目 🎲 第2-6项目 🎲 第2-7项目 🎲 第2-8项目 🎲 第2-9项目 🎲 第2-10项目 🎲 第2-11项目 🎲 第2-12项目 🎲 第2-13项目 🎲 第2-14项目 🎲 第2-15项目
索引行 3
🎲 第3-1项目 🎲 第3-2项目 🎲 第3-3项目 🎲 第3-4项目 🎲 第3-5项目 🎲 第3-6项目 🎲 第3-7项目 🎲 第3-8项目 🎲 第3-9项目 🎲 第3-10项目 🎲 第3-11项目 🎲 第3-12项目 🎲 第3-13项目 🎲 第3-14项目 🎲 第3-15项目
索引行 4
🎲 第4-1项目 🎲 第4-2项目 🎲 第4-3项目 🎲 第4-4项目 🎲 第4-5项目 🎲 第4-6项目 🎲 第4-7项目 🎲 第4-8项目 🎲 第4-9项目 🎲 第4-10项目 🎲 第4-11项目 🎲 第4-12项目 🎲 第4-13项目 🎲 第4-14项目 🎲 第4-15项目
索引行 5
🎲 第5-1项目 🎲 第5-2项目 🎲 第5-3项目 🎲 第5-4项目 🎲 第5-5项目 🎲 第5-6项目 🎲 第5-7项目 🎲 第5-8项目 🎲 第5-9项目 🎲 第5-10项目 🎲 第5-11项目 🎲 第5-12项目 🎲 第5-13项目 🎲 第5-14项目 🎲 第5-15项目
索引行 6
🎲 第6-1项目 🎲 第6-2项目 🎲 第6-3项目 🎲 第6-4项目 🎲 第6-5项目 🎲 第6-6项目 🎲 第6-7项目 🎲 第6-8项目 🎲 第6-9项目 🎲 第6-10项目 🎲 第6-11项目 🎲 第6-12项目 🎲 第6-13项目 🎲 第6-14项目 🎲 第6-15项目
索引行 7
🎲 第7-1项目 🎲 第7-2项目 🎲 第7-3项目 🎲 第7-4项目 🎲 第7-5项目 🎲 第7-6项目 🎲 第7-7项目 🎲 第7-8项目 🎲 第7-9项目 🎲 第7-10项目 🎲 第7-11项目 🎲 第7-12项目 🎲 第7-13项目 🎲 第7-14项目 🎲 第7-15项目
索引行 8
🎲 第8-1项目 🎲 第8-2项目 🎲 第8-3项目 🎲 第8-4项目 🎲 第8-5项目 🎲 第8-6项目 🎲 第8-7项目 🎲 第8-8项目 🎲 第8-9项目 🎲 第8-10项目 🎲 第8-11项目 🎲 第8-12项目 🎲 第8-13项目 🎲 第8-14项目 🎲 第8-15项目
索引行 9
🎲 第9-1项目 🎲 第9-2项目 🎲 第9-3项目 🎲 第9-4项目 🎲 第9-5项目 🎲 第9-6项目 🎲 第9-7项目 🎲 第9-8项目 🎲 第9-9项目 🎲 第9-10项目 🎲 第9-11项目 🎲 第9-12项目 🎲 第9-13项目 🎲 第9-14项目 🎲 第9-15项目
索引行 10
🎲 第10-1项目 🎲 第10-2项目 🎲 第10-3项目 🎲 第10-4项目 🎲 第10-5项目 🎲 第10-6项目 🎲 第10-7项目 🎲 第10-8项目 🎲 第10-9项目 🎲 第10-10项目 🎲 第10-11项目 🎲 第10-12项目 🎲 第10-13项目 🎲 第10-14项目 🎲 第10-15项目
<template>
  <div class="h-300px overflow-auto">
    <div v-for="i in 10" :key="i" class="w-full relative">
      <h6 class="w-full bg-blue-500 px-2 py-2 rounded text-sm text-white" style="position: sticky;top: 0;">
        索引行 {{ i }}
      </h6>
      <span
        v-for="j in 15" :key="j"
        class="w-full flex flex-col text-base py-2 last:border-none border-b-2 border-b-red-400 border-b-dashed"
      >
        🎲 第{{ `${i}-${j}` }}项目
      </span>
    </div>
  </div>
</template>
优缺点
  • 优点: 实现简便

  • 缺点:

    1. 存在兼容性问题。sticky 基本只能兼容 2021 年开始版本的浏览器 can i use: sticky
    2. 吸顶元素与内容元素距离顶部有大概 1px 的间隙
js 实现粘性布局 | 版本一
索引行1
  • 🎲 第1-1项目
  • 🎲 第1-2项目
  • 🎲 第1-3项目
  • 🎲 第1-4项目
  • 🎲 第1-5项目
  • 🎲 第1-6项目
  • 🎲 第1-7项目
  • 🎲 第1-8项目
  • 🎲 第1-9项目
  • 🎲 第1-10项目
  • 🎲 第1-11项目
  • 🎲 第1-12项目
  • 🎲 第1-13项目
  • 🎲 第1-14项目
  • 🎲 第1-15项目
索引行2
  • 🎲 第2-1项目
  • 🎲 第2-2项目
  • 🎲 第2-3项目
  • 🎲 第2-4项目
  • 🎲 第2-5项目
  • 🎲 第2-6项目
  • 🎲 第2-7项目
  • 🎲 第2-8项目
  • 🎲 第2-9项目
  • 🎲 第2-10项目
  • 🎲 第2-11项目
  • 🎲 第2-12项目
  • 🎲 第2-13项目
  • 🎲 第2-14项目
  • 🎲 第2-15项目
索引行3
  • 🎲 第3-1项目
  • 🎲 第3-2项目
  • 🎲 第3-3项目
  • 🎲 第3-4项目
  • 🎲 第3-5项目
  • 🎲 第3-6项目
  • 🎲 第3-7项目
  • 🎲 第3-8项目
  • 🎲 第3-9项目
  • 🎲 第3-10项目
  • 🎲 第3-11项目
  • 🎲 第3-12项目
  • 🎲 第3-13项目
  • 🎲 第3-14项目
  • 🎲 第3-15项目
索引行4
  • 🎲 第4-1项目
  • 🎲 第4-2项目
  • 🎲 第4-3项目
  • 🎲 第4-4项目
  • 🎲 第4-5项目
  • 🎲 第4-6项目
  • 🎲 第4-7项目
  • 🎲 第4-8项目
  • 🎲 第4-9项目
  • 🎲 第4-10项目
  • 🎲 第4-11项目
  • 🎲 第4-12项目
  • 🎲 第4-13项目
  • 🎲 第4-14项目
  • 🎲 第4-15项目
索引行5
  • 🎲 第5-1项目
  • 🎲 第5-2项目
  • 🎲 第5-3项目
  • 🎲 第5-4项目
  • 🎲 第5-5项目
  • 🎲 第5-6项目
  • 🎲 第5-7项目
  • 🎲 第5-8项目
  • 🎲 第5-9项目
  • 🎲 第5-10项目
  • 🎲 第5-11项目
  • 🎲 第5-12项目
  • 🎲 第5-13项目
  • 🎲 第5-14项目
  • 🎲 第5-15项目
索引行6
  • 🎲 第6-1项目
  • 🎲 第6-2项目
  • 🎲 第6-3项目
  • 🎲 第6-4项目
  • 🎲 第6-5项目
  • 🎲 第6-6项目
  • 🎲 第6-7项目
  • 🎲 第6-8项目
  • 🎲 第6-9项目
  • 🎲 第6-10项目
  • 🎲 第6-11项目
  • 🎲 第6-12项目
  • 🎲 第6-13项目
  • 🎲 第6-14项目
  • 🎲 第6-15项目
索引行7
  • 🎲 第7-1项目
  • 🎲 第7-2项目
  • 🎲 第7-3项目
  • 🎲 第7-4项目
  • 🎲 第7-5项目
  • 🎲 第7-6项目
  • 🎲 第7-7项目
  • 🎲 第7-8项目
  • 🎲 第7-9项目
  • 🎲 第7-10项目
  • 🎲 第7-11项目
  • 🎲 第7-12项目
  • 🎲 第7-13项目
  • 🎲 第7-14项目
  • 🎲 第7-15项目
索引行8
  • 🎲 第8-1项目
  • 🎲 第8-2项目
  • 🎲 第8-3项目
  • 🎲 第8-4项目
  • 🎲 第8-5项目
  • 🎲 第8-6项目
  • 🎲 第8-7项目
  • 🎲 第8-8项目
  • 🎲 第8-9项目
  • 🎲 第8-10项目
  • 🎲 第8-11项目
  • 🎲 第8-12项目
  • 🎲 第8-13项目
  • 🎲 第8-14项目
  • 🎲 第8-15项目
索引行9
  • 🎲 第9-1项目
  • 🎲 第9-2项目
  • 🎲 第9-3项目
  • 🎲 第9-4项目
  • 🎲 第9-5项目
  • 🎲 第9-6项目
  • 🎲 第9-7项目
  • 🎲 第9-8项目
  • 🎲 第9-9项目
  • 🎲 第9-10项目
  • 🎲 第9-11项目
  • 🎲 第9-12项目
  • 🎲 第9-13项目
  • 🎲 第9-14项目
  • 🎲 第9-15项目
索引行10
  • 🎲 第10-1项目
  • 🎲 第10-2项目
  • 🎲 第10-3项目
  • 🎲 第10-4项目
  • 🎲 第10-5项目
  • 🎲 第10-6项目
  • 🎲 第10-7项目
  • 🎲 第10-8项目
  • 🎲 第10-9项目
  • 🎲 第10-10项目
  • 🎲 第10-11项目
  • 🎲 第10-12项目
  • 🎲 第10-13项目
  • 🎲 第10-14项目
  • 🎲 第10-15项目
<script setup lang="ts">
import {  onMounted, ref } from 'vue'

const titleDomOffsetTopArr: number[] = []
const titleArrRef = ref<HTMLDivElement[]>([])
const stickyIndex = ref<number>(1)

const calcTitleDomOffsetTopArr = () => titleArrRef.value.forEach(titleDom => titleDomOffsetTopArr.push(titleDom.offsetTop))

onMounted(() => calcTitleDomOffsetTopArr())

function onScroll(evt: UIEvent) {
  const scrollDom = evt.target as HTMLElement
  if (!scrollDom) return
  const { scrollTop } = scrollDom
  stickyIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr) as number
}

function findStickIndex(scrollTop: number, offsetTopArr: number[]) {
  /** 二分查找/折半查找 */
  if (scrollTop <= 0) return 1
  let start = 0
  let end = offsetTopArr.length
  let ret: number = end - 1
  while (start <= end) {
    const middle = Math.trunc((start + end) / 2)
    const middlePos = middle === 0 ? 0 : middle - 1
    const offsetTop = offsetTopArr[middlePos]
    if (offsetTop === scrollTop) {
      return middle
    }
    else if (scrollTop < offsetTop) {
      if (ret === undefined || ret > middle)
        ret = middle

      end = middle - 1
    }
    else if (scrollTop > offsetTop) {
      ret = middle
      start = middle + 1
    }
  }
  return ret
}
</script>

<template>
  <div class="relative">
    <div class="css-sticky" @scroll="onScroll">
      <template v-for="i in 10" :key="i">
        <h6
          ref="titleArrRef"
          :class="{ sticky: stickyIndex === i }"
          class="title"
        >
          索引行{{ i }}
        </h6>
        <ul>
          <li v-for="j in 15" :key="j" class="item">
            🎲 第{{ `${i}-${j}` }}项目
          </li>
        </ul>
      </template>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.css-sticky {
  @apply h-300px w-full overflow-auto;
   /*  24px 是第一个 title 元素的实际高度 */
   padding-top: 24px;

  .title {
    @apply w-full bg-blue-500 px-2 py-2 rounded text-sm text-white;
  }

  ul {
    list-style: none;
    padding: 0;
    margin: 0;
    .item:last-child {
      border-bottom: none;
    }
  }


  .item {
    @apply w-full flex flex-col text-base py-2;
    @apply border-b-2 border-b-red-400;
    border-bottom-style: dashed
  }

  .sticky {
    position: absolute;
    top: 0;
  }
}
</style>

要点

  • 最外层的盒子是相对定位, 为了让title元素能获取到距离滚动容器顶部的距离,用于在滚动的时候,能够判断当前应该让哪个title元素设置为绝对定位
  • js-sticky 是滚动容器

优点: 兼容性强

缺点:

  • 实现相对复杂
  • 吸顶元素与同类未吸顶元素宽度不一致
js 实现粘性布局 | 版本二
简单修复 `吸顶元素与同类未吸顶元素宽度不一致` 问题(通过计算出滚动条宽度方式修复
索引行1
  • 第1-1项目
  • 第1-2项目
  • 第1-3项目
索引行2
  • 第2-1项目
  • 第2-2项目
  • 第2-3项目
索引行3
  • 第3-1项目
  • 第3-2项目
  • 第3-3项目
索引行4
  • 第4-1项目
  • 第4-2项目
  • 第4-3项目
  • 第4-4项目
  • 第4-5项目
  • 第4-6项目
  • 第4-7项目
  • 第4-8项目
  • 第4-9项目
  • 第4-10项目
  • 第4-11项目
  • 第4-12项目
  • 第4-13项目
  • 第4-14项目
  • 第4-15项目
索引行5
  • 第5-1项目
  • 第5-2项目
  • 第5-3项目
  • 第5-4项目
  • 第5-5项目
  • 第5-6项目
  • 第5-7项目
  • 第5-8项目
  • 第5-9项目
  • 第5-10项目
  • 第5-11项目
  • 第5-12项目
  • 第5-13项目
  • 第5-14项目
  • 第5-15项目
索引行6
  • 第6-1项目
  • 第6-2项目
  • 第6-3项目
  • 第6-4项目
  • 第6-5项目
  • 第6-6项目
  • 第6-7项目
  • 第6-8项目
  • 第6-9项目
  • 第6-10项目
  • 第6-11项目
  • 第6-12项目
  • 第6-13项目
  • 第6-14项目
  • 第6-15项目
索引行7
  • 第7-1项目
  • 第7-2项目
索引行8
  • 第8-1项目
  • 第8-2项目
索引行9
  • 第9-1项目
  • 第9-2项目
索引行10
  • 第10-1项目
  • 第10-2项目

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'


const titleDomOffsetTopArr: number[] = []
const titleArrRef = ref<HTMLDivElement[]>([])
const stickyIndex = ref<number>(1)
// 竖向滚动条宽度
const scrollBarWidth = ref<number>(0)
const JsStickyRef = ref<HTMLElement>()

function calcTitleDomOffsetTopArr() {
  titleArrRef.value.forEach((titleDom) => {
    titleDomOffsetTopArr.push(titleDom.offsetTop)
  })
}

onMounted(() => {
  calcTitleDomOffsetTopArr()
  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return
  calcScrollBarWidth(JsStickyDom)
})

const scrollBarWidthStr = computed<string>(() => `${scrollBarWidth.value}px`)

/** 计算竖向滚动条宽度 */
function calcScrollBarWidth(scrollDom: HTMLElement) {
  const { offsetWidth, clientWidth } = scrollDom
  // 计算滚动条的宽度
  const tmp = offsetWidth - clientWidth
  if (tmp !== scrollBarWidth.value) scrollBarWidth.value = tmp
}

function onScroll(evt: UIEvent) {
  const scrollDom = evt.target as HTMLElement
  if (!scrollDom) return
  calcScrollBarWidth(scrollDom)
  const { scrollTop } = scrollDom
  stickyIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr) as number
}

function findStickIndex(scrollTop: number, offsetTopArr: number[]) {
  if (scrollTop <= 0) return 1

  let start = 0
  let end = offsetTopArr.length
  let ret: number = end - 1
  while (start <= end) {
    const middle = Math.trunc((start + end) / 2)
    const middlePos = middle === 0 ? 0 : middle - 1
    const offsetTop = offsetTopArr[middlePos]
    if (offsetTop === scrollTop) {
      return middle
    }
    else if (scrollTop < offsetTop) {
      if (ret === undefined || ret > middle)
        ret = middle

      end = middle - 1
    }
    else if (scrollTop > offsetTop) {
      ret = middle
      start = middle + 1
    }
  }
  return ret
}
</script>

<template>
  <div class="relative">
    <div ref="JsStickyRef" class="js-sticky" @scroll="onScroll">
      <template v-for="i in 10" :key="i">
        <template v-if="i <= 3">
          <div ref="titleArrRef" :class="{ sticky: stickyIndex === i }" class="title">
            索引行{{ i }}
          </div>

          <ul>
            <li v-for="j in 3" :key="j" class="item">
              第{{ `${i}-${j}` }}项目
            </li>
          </ul>
        </template>

        <template v-else-if="i >= 7">
          <div ref="titleArrRef" :class="{ sticky: stickyIndex === i }" class="title">
            索引行{{ i }}
          </div>

          <ul>
            <li v-for="j in 2" :key="j" class="item">
              第{{ `${i}-${j}` }}项目
            </li>
          </ul>
        </template>

        <template v-else>
          <div ref="titleArrRef" :class="{ sticky: stickyIndex === i }" class="title">
            索引行{{ i }}
          </div>

          <ul>
            <li v-for="j in 15" :key="j" class="item">
              第{{ `${i}-${j}` }}项目
            </li>
          </ul>
        </template>
      </template>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.js-sticky {
  @apply h-300px w-full overflow-auto;
  /*  24px 是第一个 title 元素的实际高度 */
  padding-top: 24px;

  .title {
    @apply w-full bg-blue-500 px-2 py-2 rounded text-sm text-white;
  }
  ul {
    list-style: none;
    padding: 0;
    margin: 0;
    .item:last-child {
      border-bottom: none;
    }
  }
  .item {
    @apply w-full text-base py-2 last:border-none;
    @apply border-b-2 border-b-red-400;
    border-bottom-style: dashed
  }
  .sticky {
    position: absolute;
    top: 0;
    /* 吸顶元素的宽度为:100% - 滚动条宽度 */
    width: calc(100% - v-bind(scrollBarWidthStr));
  }
}
</style>

实现 HTML 结构与数据初始化

WARNING

不要通过css positionsticky 实现,因为 sticky 定位会导致offsetTop` 为 0

索引行1
索引行2
索引行3
索引行4
索引行5
索引行6
索引行7
索引行8
索引行9
索引行10
索引行11
索引行12
索引行13
索引行14
索引行15
索引行16
索引行17
索引行18
索引行19
索引行20
索引行21
索引行22
索引行23
索引行24
索引行25
索引行26
索引行1
  • 第1-1项目
  • 第1-2项目
  • 第1-3项目
索引行2
  • 第2-1项目
  • 第2-2项目
  • 第2-3项目
索引行3
  • 第3-1项目
  • 第3-2项目
  • 第3-3项目
索引行4
  • 第4-1项目
  • 第4-2项目
  • 第4-3项目
  • 第4-4项目
  • 第4-5项目
  • 第4-6项目
  • 第4-7项目
  • 第4-8项目
  • 第4-9项目
  • 第4-10项目
  • 第4-11项目
  • 第4-12项目
  • 第4-13项目
  • 第4-14项目
  • 第4-15项目
索引行5
  • 第5-1项目
  • 第5-2项目
  • 第5-3项目
  • 第5-4项目
  • 第5-5项目
  • 第5-6项目
  • 第5-7项目
  • 第5-8项目
  • 第5-9项目
  • 第5-10项目
  • 第5-11项目
  • 第5-12项目
  • 第5-13项目
  • 第5-14项目
  • 第5-15项目
索引行6
  • 第6-1项目
  • 第6-2项目
  • 第6-3项目
  • 第6-4项目
  • 第6-5项目
  • 第6-6项目
  • 第6-7项目
  • 第6-8项目
  • 第6-9项目
  • 第6-10项目
  • 第6-11项目
  • 第6-12项目
  • 第6-13项目
  • 第6-14项目
  • 第6-15项目
索引行7
  • 第7-1项目
  • 第7-2项目
索引行8
  • 第8-1项目
  • 第8-2项目
索引行9
  • 第9-1项目
  • 第9-2项目
索引行10
  • 第10-1项目
  • 第10-2项目
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'

const titleDomOffsetTopArr: number[] = []
const titleArrRef = ref<HTMLDivElement[]>([])
const JsStickyRef = ref<HTMLElement>()

/** 粘性元素索引号 */
const stickyIndex = ref<number>(1)
/** 竖向滚动条宽度 */
const scrollBarWidth = ref<number>(0)

onMounted(() => {
  /** calcTitleDomOffsetTopArr */
  titleArrRef.value.forEach(titleDom => titleDomOffsetTopArr.push(titleDom.offsetTop))
  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return
  calcScrollBarWidth(JsStickyDom)
})


const scrollBarWidthStr = computed<string>(() => `${scrollBarWidth.value}px`)

/** 计算竖向滚动条宽度 */
function calcScrollBarWidth(scrollDom: HTMLElement) {
  const { offsetWidth, clientWidth } = scrollDom
  // 计算滚动条的宽度
  const tmp = offsetWidth - clientWidth
  if (tmp !== scrollBarWidth.value)
    scrollBarWidth.value = tmp
}
function onScroll(evt: UIEvent) {
  const scrollDom = evt.target as HTMLElement
  if (!scrollDom) return
  calcScrollBarWidth(scrollDom)
  const { scrollTop } = scrollDom
  stickyIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr) as number
}

function findStickIndex(scrollTop: number, offsetTopArr: number[]) {
  // 二分查找/折半查找
  if (scrollTop <= 0) return 1
  let start = 0
  let end = offsetTopArr.length
  let ret: number = end - 1
  while (start <= end) {
    const middle = Math.trunc((start + end) / 2)
    const middlePos = middle === 0 ? 0 : middle - 1
    const offsetTop = offsetTopArr[middlePos]
    if (offsetTop === scrollTop) {
      return middle
    }
    else if (scrollTop < offsetTop) {
      if (ret === undefined || ret > middle)
        ret = middle

      end = middle - 1
    }
    else if (scrollTop > offsetTop) {
      ret = middle
      start = middle + 1
    }
  }
  return ret
}
</script>

<template>
  <div class="flex h-300px">
    <div class="index_list">
      <div v-for="i in 26" :key="i" class="index_item">
        索引行{{ i }}
      </div>
    </div>
    <div class="w-[0] flex-1 relative">
      <div ref="JsStickyRef" class="js-sticky" @scroll="onScroll">
        <template v-for="i in 10" :key="i">
          <template v-if="i <= 3">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <li v-for="j in 3" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </li>
            </ul>
          </template>
          <template v-else-if="i >= 7">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul class="projects">
              <li v-for="j in 2" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </li>
            </ul>
          </template>
          <template v-else>
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul class="projects">
              <li v-for="j in 15" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </li>
            </ul>
          </template>
        </template>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.index_list {
  @apply relative w-90px flex-shrink-0 overflow-auto flex flex-col text-center;
}

.index_item {
  @apply py-10px border border-b-red-400 last:border-none;
  border-bottom-style: dashed;
}

.js-sticky {
  @apply h-300px w-full overflow-auto;
  /*  24px 是第一个 title 元素的实际高度 */
  padding-top: 24px;

  .title {
    @apply w-full bg-gray-300 px-2 py-1 rounded text-sm;
  }

  ul {
    list-style: none;
    padding: 0;
    margin: 0;
    .item:last-child {
      border-bottom: none;
    }
  }

  .item {
    @apply w-full text-base py-2 mt-0;
    @apply border-b-1 border-b-red-400;
    border-bottom-style: dashed;
  }

  .sticky {
    position: absolute;
    top: 0;
    /* 吸顶元素的宽度为:100% - 滚动条宽度 */
    width: calc(100% - v-bind(scrollBarWidthStr));
  }
}
</style>

实现滚动高亮

索引行1
索引行2
索引行3
索引行4
索引行5
索引行6
索引行7
索引行8
索引行9
索引行10
索引行11
索引行12
索引行13
索引行14
索引行15
索引行16
索引行17
索引行18
索引行19
索引行20
索引行21
索引行22
索引行23
索引行24
索引行25
索引行26
索引行1
    第1-1项目
    第1-2项目
    第1-3项目
索引行2
    第2-1项目
    第2-2项目
    第2-3项目
索引行3
    第3-1项目
    第3-2项目
    第3-3项目
索引行4
    第4-1项目
    第4-2项目
    第4-3项目
    第4-4项目
    第4-5项目
    第4-6项目
    第4-7项目
    第4-8项目
    第4-9项目
    第4-10项目
    第4-11项目
    第4-12项目
    第4-13项目
    第4-14项目
    第4-15项目
索引行5
    第5-1项目
    第5-2项目
    第5-3项目
    第5-4项目
    第5-5项目
    第5-6项目
    第5-7项目
    第5-8项目
    第5-9项目
    第5-10项目
    第5-11项目
    第5-12项目
    第5-13项目
    第5-14项目
    第5-15项目
索引行6
    第6-1项目
    第6-2项目
    第6-3项目
    第6-4项目
    第6-5项目
    第6-6项目
    第6-7项目
    第6-8项目
    第6-9项目
    第6-10项目
    第6-11项目
    第6-12项目
    第6-13项目
    第6-14项目
    第6-15项目
索引行7
    第7-1项目
    第7-2项目
索引行8
    第8-1项目
    第8-2项目
索引行9
    第9-1项目
    第9-2项目
索引行10
    第10-1项目
    第10-2项目
索引行11
    第11-1项目
    第11-2项目
索引行12
    第12-1项目
    第12-2项目
索引行13
    第13-1项目
    第13-2项目
索引行14
    第14-1项目
    第14-2项目
索引行15
    第15-1项目
    第15-2项目
索引行16
    第16-1项目
    第16-2项目
索引行17
    第17-1项目
    第17-2项目
索引行18
    第18-1项目
    第18-2项目
索引行19
    第19-1项目
    第19-2项目
索引行20
    第20-1项目
    第20-2项目
索引行21
    第21-1项目
    第21-2项目
索引行22
    第22-1项目
    第22-2项目
索引行23
    第23-1项目
    第23-2项目
索引行24
    第24-1项目
    第24-2项目
索引行25
    第25-1项目
    第25-2项目
索引行26
    第26-1项目
    第26-2项目
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'

const titleDomOffsetTopArr: number[] = []
const titleArrRef = ref<HTMLDivElement[]>([])
const JsStickyRef = ref<HTMLElement>()

/** 粘性元素索引号 */
const stickyIndex = ref<number>(1)
/** 竖向滚动条宽度 */
const scrollBarWidth = ref<number>(0)
/** 高亮元素索引号 */
const highlightIndex = ref<number>(1)

onMounted(() => {
  /** calcTitleDomOffsetTopArr */
  titleArrRef.value.forEach(titleDom =>  titleDomOffsetTopArr.push(titleDom.offsetTop))
  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return
  calcScrollBarWidth(JsStickyDom)
})

const scrollBarWidthStr = computed<string>(() => `${scrollBarWidth.value}px`)

/** 计算竖向滚动条宽度 */
function calcScrollBarWidth(scrollDom: HTMLElement) {
  const { offsetWidth, clientWidth } = scrollDom
  /* 计算滚动条的宽度 */
  const tmp = offsetWidth - clientWidth
  if (tmp !== scrollBarWidth.value) scrollBarWidth.value = tmp
}

function onScroll(evt: UIEvent) {
  const scrollDom = evt.target as HTMLElement
  if (!scrollDom) return
  calcScrollBarWidth(scrollDom)
  const { scrollTop } = scrollDom
  /* 粘性元素索引号 */
  stickyIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr)
  /* 高亮元素索引号 */
  highlightIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr) as number
}

/* MARK: 当前高亮索引号和粘性元素索引号查找实现逻辑是一致的 */
function findStickIndex(scrollTop: number, offsetTopArr: number[]) {
  // 二分查找/折半查找
  if (scrollTop <= 0) return 1
  let start = 0
  let end = offsetTopArr.length
  let ret: number = end - 1
  while (start <= end) {
    const middle = Math.trunc((start + end) / 2)
    const middlePos = middle === 0 ? 0 : middle - 1
    const offsetTop = offsetTopArr[middlePos]
    if (offsetTop === scrollTop) {
      return middle
    }
    else if (scrollTop < offsetTop) {
      if (ret === undefined || ret > middle)
        ret = middle
      end = middle - 1
    }
    else if (scrollTop > offsetTop) {
      ret = middle
      start = middle + 1
    }
  }
  return ret
}
</script>

<template>
  <div class="flex h-300px">
    <div class="index_list">
      <div
        v-for="i in 26" :key="i"
        class="index_item"
        :class="{ 'text-green': highlightIndex === i }"
      >
        索引行{{ i }}
      </div>
    </div>
    <div class="w-[0] flex-1 relative">
      <div ref="JsStickyRef" class="js-sticky" @scroll="onScroll">
        <template v-for="i in 26" :key="i">
          <template v-if="i <= 3">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <div v-for="j in 3" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
          <template v-else-if="i >= 7">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>

            <ul>
              <div v-for="j in 2" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
          <template v-else>
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>

            <ul>
              <div v-for="j in 15" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
        </template>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.index_list {
  @apply relative w-90px flex-shrink-0 overflow-auto flex flex-col text-center;
}

.index_item {
  @apply py-10px border border-b-red-400;
  border-bottom-style: dashed;
}

.js-sticky {
  @apply h-300px w-full overflow-auto;
  /*  24px 是第一个 title 元素的实际高度 */
  padding-top: 24px;

  .title {
    @apply w-full bg-gray-300 px-2 py-1 rounded text-sm ;
  }

  ul {
    list-style: none;
    padding: 0;
    margin: 0;
    .item:last-child {
      border-bottom: none;
    }
  }


  .item {
    @apply w-full text-base py-2;
    @apply border-b-1 border-b-red-400;
    border-bottom-style: dashed;
  }

  &:last-of-type{
    .index_item{
      border-bottom: none;
    }
  }

  .sticky {
    position: absolute;
    top: 0;
    /* 吸顶元素的宽度为:100% - 滚动条宽度 */
    width: calc(100% - v-bind(scrollBarWidthStr));
  }
}
</style>

点击左侧索引,右侧自动跳转到对应位置

索引行1
索引行2
索引行3
索引行4
索引行5
索引行6
索引行7
索引行8
索引行9
索引行10
索引行11
索引行12
索引行13
索引行14
索引行15
索引行16
索引行17
索引行18
索引行19
索引行20
索引行21
索引行22
索引行23
索引行24
索引行25
索引行26
索引行1
    第1-1项目
    第1-2项目
    第1-3项目
索引行2
    第2-1项目
    第2-2项目
    第2-3项目
索引行3
    第3-1项目
    第3-2项目
    第3-3项目
索引行4
    第4-1项目
    第4-2项目
    第4-3项目
    第4-4项目
    第4-5项目
    第4-6项目
    第4-7项目
    第4-8项目
    第4-9项目
    第4-10项目
    第4-11项目
    第4-12项目
    第4-13项目
    第4-14项目
    第4-15项目
索引行5
    第5-1项目
    第5-2项目
    第5-3项目
    第5-4项目
    第5-5项目
    第5-6项目
    第5-7项目
    第5-8项目
    第5-9项目
    第5-10项目
    第5-11项目
    第5-12项目
    第5-13项目
    第5-14项目
    第5-15项目
索引行6
    第6-1项目
    第6-2项目
    第6-3项目
    第6-4项目
    第6-5项目
    第6-6项目
    第6-7项目
    第6-8项目
    第6-9项目
    第6-10项目
    第6-11项目
    第6-12项目
    第6-13项目
    第6-14项目
    第6-15项目
索引行7
    第7-1项目
    第7-2项目
索引行8
    第8-1项目
    第8-2项目
索引行9
    第9-1项目
    第9-2项目
索引行10
    第10-1项目
    第10-2项目
索引行11
    第11-1项目
    第11-2项目
索引行12
    第12-1项目
    第12-2项目
索引行13
    第13-1项目
    第13-2项目
索引行14
    第14-1项目
    第14-2项目
索引行15
    第15-1项目
    第15-2项目
索引行16
    第16-1项目
    第16-2项目
索引行17
    第17-1项目
    第17-2项目
索引行18
    第18-1项目
    第18-2项目
索引行19
    第19-1项目
    第19-2项目
索引行20
    第20-1项目
    第20-2项目
索引行21
    第21-1项目
    第21-2项目
索引行22
    第22-1项目
    第22-2项目
索引行23
    第23-1项目
    第23-2项目
索引行24
    第24-1项目
    第24-2项目
索引行25
    第25-1项目
    第25-2项目
索引行26
    第26-1项目
    第26-2项目
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { debounce, throttle } from 'lodash-es'

const titleDomOffsetTopArr: number[] = []
const titleArrRef = ref<HTMLDivElement[]>([])
const JsStickyRef = ref<HTMLElement>()

/** 粘性元素索引号 */
const stickyIndex = ref<number>(1)
/** 竖向滚动条宽度 */
const scrollBarWidth = ref<number>(0)
/** 高亮元素索引号 */
const highlightIndex = ref<number>(1)
/** 滚动事件函数是否需要根据滚动位置更新高亮索引号 */
let scrollNeedUpdateHighlightIndex = true

onMounted(() => {
  /**  calcTitleDomOffsetTopArr  */
  titleArrRef.value.forEach(titleDom => titleDomOffsetTopArr.push(titleDom.offsetTop))

  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return
  calcScrollBarWidth(JsStickyDom)
})

const scrollBarWidthStr = computed<string>(() => `${scrollBarWidth.value}px`)

/** 计算竖向滚动条宽度 */
function calcScrollBarWidth(scrollDom: HTMLElement) {
  const { offsetWidth, clientWidth } = scrollDom
  /* 计算滚动条的宽度 */
  const tmp = offsetWidth - clientWidth
  if (tmp !== scrollBarWidth.value) scrollBarWidth.value = tmp
}

const updateScrollNeedUpdateHighlightIndexToTrue = () => scrollNeedUpdateHighlightIndex = true

/**
 * 用于在滚动结束后,将scrollNeedUpdateHighlightIndex设置为true
 * 这两个参数是用于指定回调函数updateScrollNeedUpdateHighlightIndexToTrue在timeout结束时才调用
 * trailing: true,
   leading: false,
 */
const updateHighlightIndexDebounce = debounce(updateScrollNeedUpdateHighlightIndexToTrue, 100, {
  trailing: true,
  leading: false,
})

function updateHighlightIndex(scrollTop: number, offsetTopArr: number[]) {
  highlightIndex.value = findStickIndex(scrollTop, offsetTopArr) as number
}

/** 这里是用于实现无论滚动多块,都按照每300毫秒更新一次高亮索引号 */
const updateHighlightIndexThrottle = throttle(updateHighlightIndex, 300, {
  trailing: true,
  leading: false,
})

function onScroll(evt: UIEvent) {
  const scrollDom = evt.target as HTMLElement
  if (!scrollDom) return
  calcScrollBarWidth(scrollDom)

  const { scrollTop } = scrollDom
  stickyIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr)

  if (scrollNeedUpdateHighlightIndex)
    updateHighlightIndexThrottle(scrollTop, titleDomOffsetTopArr)
  else
    updateHighlightIndexDebounce()
}

/* 当前高亮索引号和粘性元素索引号查找实现逻辑是一致的 */
function findStickIndex(scrollTop: number, offsetTopArr: number[]) {
  // 二分查找/折半查找
  if (scrollTop <= 0) return 1
  let start = 0
  let end = offsetTopArr.length
  let ret: number = end - 1
  while (start <= end) {
    const middle = Math.trunc((start + end) / 2)
    const middlePos = middle === 0 ? 0 : middle - 1
    const offsetTop = offsetTopArr[middlePos]
    if (offsetTop === scrollTop) {
      return middle
    }
    else if (scrollTop < offsetTop) {
      if (ret === undefined || ret > middle)
        ret = middle
      end = middle - 1
    }
    else if (scrollTop > offsetTop) {
      ret = middle
      start = middle + 1
    }
  }
  return ret
}

function onClick(idx: number) {
  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return
  const offsetTop = titleDomOffsetTopArr[idx]
  if (titleDomOffsetTopArr[idx] !== undefined) {
    scrollNeedUpdateHighlightIndex = false
    highlightIndex.value = idx + 1
    JsStickyDom.scrollTo({ top: offsetTop, behavior: 'smooth' })
  }
}
</script>

<template>
  <div class="flex h-300px">
    <div class="index_list">
      <div
        v-for="i in 26" :key="i" class="index_item" :class="{ 'text-green': highlightIndex === i }"
        @click="onClick(i - 1)"
      >
        索引行{{ i }}
      </div>
    </div>
    <div class="w-[0] flex-1 relative">
      <div ref="JsStickyRef" class="js-sticky" @scroll="onScroll">
        <template v-for="i in 26" :key="i">
          <template v-if="i <= 3">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <div v-for="j in 3" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
          <template v-else-if="i >= 7">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>

            <ul>
              <div v-for="j in 2" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
          <template v-else>
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <div v-for="j in 15" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
        </template>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.index_list {
  @apply relative w-90px flex-shrink-0 overflow-auto flex flex-col text-center;
}

.index_item {
  @apply py-10px border border-b-red-400;
  border-bottom-style: dashed;
}

.js-sticky {
  @apply h-300px w-full overflow-auto;
  /*  24px 是第一个 title 元素的实际高度 */
  padding-top: 24px;

  .title {
    @apply w-full bg-gray-300 px-2 py-1 rounded text-sm;
  }

  ul {
    list-style: none;
    padding: 0;
    margin: 0;

    .item:last-child {
      border-bottom: none;
    }
  }

  .item {
    @apply w-full text-base py-2 last:border-none;
    @apply border-b-1 border-b-red-400;
    border-bottom-style: dashed;
  }

  .sticky {
    position: absolute;
    top: 0;
    /* 吸顶元素的宽度为:100% - 滚动条宽度 */
    width: calc(100% - v-bind(scrollBarWidthStr));
  }
}
</style>

实现左侧高亮索引,始终显示在视口中

索引行1
索引行2
索引行3
索引行4
索引行5
索引行6
索引行7
索引行8
索引行9
索引行10
索引行11
索引行12
索引行13
索引行14
索引行15
索引行16
索引行17
索引行18
索引行19
索引行20
索引行21
索引行22
索引行23
索引行24
索引行25
索引行26
索引行1
  • 第1-1项目
  • 第1-2项目
  • 第1-3项目
索引行2
  • 第2-1项目
  • 第2-2项目
  • 第2-3项目
索引行3
  • 第3-1项目
  • 第3-2项目
  • 第3-3项目
索引行4
    第4-1项目
    第4-2项目
    第4-3项目
    第4-4项目
    第4-5项目
    第4-6项目
    第4-7项目
    第4-8项目
    第4-9项目
    第4-10项目
    第4-11项目
    第4-12项目
    第4-13项目
    第4-14项目
    第4-15项目
索引行5
    第5-1项目
    第5-2项目
    第5-3项目
    第5-4项目
    第5-5项目
    第5-6项目
    第5-7项目
    第5-8项目
    第5-9项目
    第5-10项目
    第5-11项目
    第5-12项目
    第5-13项目
    第5-14项目
    第5-15项目
索引行6
    第6-1项目
    第6-2项目
    第6-3项目
    第6-4项目
    第6-5项目
    第6-6项目
    第6-7项目
    第6-8项目
    第6-9项目
    第6-10项目
    第6-11项目
    第6-12项目
    第6-13项目
    第6-14项目
    第6-15项目
索引行7
    第7-1项目
    第7-2项目
索引行8
    第8-1项目
    第8-2项目
索引行9
    第9-1项目
    第9-2项目
索引行10
    第10-1项目
    第10-2项目
索引行11
    第11-1项目
    第11-2项目
索引行12
    第12-1项目
    第12-2项目
索引行13
    第13-1项目
    第13-2项目
索引行14
    第14-1项目
    第14-2项目
索引行15
    第15-1项目
    第15-2项目
索引行16
    第16-1项目
    第16-2项目
索引行17
    第17-1项目
    第17-2项目
索引行18
    第18-1项目
    第18-2项目
索引行19
    第19-1项目
    第19-2项目
索引行20
    第20-1项目
    第20-2项目
索引行21
    第21-1项目
    第21-2项目
索引行22
    第22-1项目
    第22-2项目
索引行23
    第23-1项目
    第23-2项目
索引行24
    第24-1项目
    第24-2项目
索引行25
    第25-1项目
    第25-2项目
索引行26
    第26-1项目
    第26-2项目
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { debounce, throttle } from 'lodash-es'

const titleDomOffsetTopArr: number[] = []
const titleArrRef = ref<HTMLDivElement[]>([])
const JsStickyRef = ref<HTMLElement>()

/** 粘性元素索引号 */
const stickyIndex = ref<number>(1)
/** 竖向滚动条宽度 */
const scrollBarWidth = ref<number>(0)
/** 高亮元素索引号 */
const highlightIndex = ref<number>(1)

const indexRef = ref<HTMLElement>()
const indexArrRef = ref<HTMLDivElement[]>([])
const indexDomOffsetTopArr: number[] = []

/** 滚动事件函数是否需要根据滚动位置更新高亮索引号 */
let scrollNeedUpdateHighlightIndex = true

onMounted(() => {
  /** calcTitleDomOffsetTopArr  */
  indexArrRef.value.forEach(indexDom => indexDomOffsetTopArr.push(indexDom.offsetTop))
  /** calcIndexDomOffsetTopArr  */
  titleArrRef.value.forEach(titleDom => titleDomOffsetTopArr.push(titleDom.offsetTop))

  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return
  calcScrollBarWidth(JsStickyDom)
})

const scrollBarWidthStr = computed<string>(() => `${scrollBarWidth.value}px`)

/** 计算竖向滚动条宽度 */
function calcScrollBarWidth(scrollDom: HTMLElement) {
  const { offsetWidth, clientWidth } = scrollDom
  /* 计算滚动条的宽度 */
  const tmp = offsetWidth - clientWidth
  if (tmp !== scrollBarWidth.value) scrollBarWidth.value = tmp
}

const updateScrollNeedUpdateHighlightIndexToTrue = () => scrollNeedUpdateHighlightIndex = true

/**
 * 用于在滚动结束后,将 scrollNeedUpdateHighlightIndex 设置为true
 * 这两个参数是用于指定回调函数updateScrollNeedUpdateHighlightIndexToTrue在timeout结束时才调用
 * trailing: true,
   leading: false,
 */
const updateHighlightIndexDebounce = debounce(updateScrollNeedUpdateHighlightIndexToTrue, 100, {
  trailing: true,
  leading: false,
})

function updateHighlightIndex(scrollTop: number, offsetTopArr: number[]) {

  highlightIndex.value = findStickIndex(scrollTop, offsetTopArr) as number
  // 这里减一是因为界面数据的索引号是从1开始,而这里要取数组中的数据,数组下标从0开始,因此要减1
  let indexDomOffsetTop = indexDomOffsetTopArr[highlightIndex.value - 1]

  if (indexDomOffsetTop !== undefined) {
    indexDomOffsetTop = indexDomOffsetTop - 60
    indexDomOffsetTop = indexDomOffsetTop < 0 ? 0 : indexDomOffsetTop
    indexRef.value?.scrollTo({ top: indexDomOffsetTop, behavior: 'smooth' })
  }
}

/** 这里是用于实现无论滚动多块,都按照每300毫秒更新一次高亮索引号 */
const updateHighlightIndexThrottle = throttle(updateHighlightIndex, 300, {
  trailing: true,
  leading: false,
})

function onScroll(evt: UIEvent) {
  const scrollDom = evt.target as HTMLElement
  if (!scrollDom) return
  calcScrollBarWidth(scrollDom)

  const { scrollTop } = scrollDom
  stickyIndex.value = findStickIndex(scrollTop, titleDomOffsetTopArr)

  if (scrollNeedUpdateHighlightIndex) updateHighlightIndexThrottle(scrollTop, titleDomOffsetTopArr)
  else updateHighlightIndexDebounce()
}

/** 当前高亮索引号和粘性元素索引号查找实现逻辑是一致的 */
function findStickIndex(scrollTop: number, offsetTopArr: number[]) {
  // 二分查找/折半查找
  if (scrollTop <= 0) return 1

  let start = 0
  let end = offsetTopArr.length
  let ret: number = end - 1

  while (start <= end) {
    const middle = Math.trunc((start + end) / 2)
    const middlePos = middle === 0 ? 0 : middle - 1
    const offsetTop = offsetTopArr[middlePos]
    if (offsetTop === scrollTop) {
      return middle
    }
    else if (scrollTop < offsetTop) {
      if (ret === undefined || ret > middle)
        ret = middle
      end = middle - 1
    }
    else if (scrollTop > offsetTop) {
      ret = middle
      start = middle + 1
    }
  }
  return ret
}

function onClick(idx: number) {
  const JsStickyDom = JsStickyRef.value
  if (!JsStickyDom) return

  const offsetTop = titleDomOffsetTopArr[idx]
  if (titleDomOffsetTopArr[idx] !== undefined) {
    scrollNeedUpdateHighlightIndex = false
    highlightIndex.value = idx + 1
    JsStickyDom.scrollTo({ top: offsetTop, behavior: 'smooth' })
  }
}
</script>

<template>
  <div class="flex h-300px">
    <div ref="indexRef" class="index_list">
      <div
        v-for="i in 26" :key="i" ref="indexArrRef" class="index_item" :class="{ 'text-green': highlightIndex === i }"
        @click="onClick(i - 1)"
      >
        索引行{{ i }}
      </div>
    </div>
    <div class="w-[0] flex-1 relative">
      <div ref="JsStickyRef" class="js-sticky" @scroll="onScroll">
        <template v-for="i in 26" :key="i">
          <template v-if="i <= 3">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <li v-for="j in 3" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </li>
            </ul>
          </template>
          <template v-else-if="i >= 7">
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <div v-for="j in 2" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
          <template v-else>
            <div ref="titleArrRef" class="title" :class="{ sticky: stickyIndex === i }">
              索引行{{ i }}
            </div>
            <ul>
              <div v-for="j in 15" :key="j" class="item">
                第{{ `${i}-${j}` }}项目
              </div>
            </ul>
          </template>
        </template>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.index_list {
  @apply relative w-90px flex-shrink-0 overflow-auto flex flex-col text-center;
}

.index_item {
  @apply py-10px border border-b-red-400 last:border-none;
  border-bottom-style: dashed;
}

.js-sticky {
  @apply h-300px w-full overflow-auto;
  /*  24px 是第一个 title 元素的实际高度 */
  padding-top: 24px;

  .title {
    @apply w-full bg-gray-300 px-2 py-1 rounded text-sm;
  }

  ul {
    list-style: none;
    padding: 0;
    margin: 0;

    .item:last-child {
      border-bottom: none;
    }
  }


  .item {
    @apply w-full text-base py-2 last:border-none;
    @apply border-b-1 border-b-red-400;
    border-bottom-style: dashed;
  }

  .sticky {
    position: absolute;
    top: 0;
    /* 吸顶元素的宽度为:100% - 滚动条宽度 */
    width: calc(100% - v-bind(scrollBarWidthStr));
  }
}
</style>