索引组件滚动高亮
JS、CSS 实现粘性布局 & 吸顶效果
前置知识点
一、 el.offsetParent
与 el.offsetTop
element.offsetParent
为包含element
的祖先元素中,层级最近的定位元素。
- 也就是说,
offsetParent
必须满足三个条件:- 是
element
的祖先
元素 - 最靠近
element
- 是定位元素, 即
position
属性不为static
- 是
element.offsetTop
是当前元素顶部到element.offsetParent
顶部的距离.
二、 sticky
sticky 不生效原因有:
- 父元素设置了
overflow:hidden
或overflow:auto
- 未指定
top
、right
、bottom
、left
4个值中的任意一个 - 父元素高度小于
sticky
定位的元素高度 sticky
属性依赖于用户的滚动,在position:relative
与position:fixed
定位之间切换。- 元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。
sticky 属性仅在以下几个条件都满足时有效:
- 父元素不能
overflow:hidden
或者overflow:auto
属性 - 必须指定
top
、bottom
、left
、right
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>
优缺点
优点: 实现简便
缺点:
- 存在兼容性问题。sticky 基本只能兼容 2021 年开始版本的浏览器 can i use: sticky
- 吸顶元素与内容元素距离顶部有大概
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 position
的sticky
实现,因为
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>