滚动空白
<template>
<div>
<div>
<button @click="rowVirtualizer.scrollToIndex(40)">
Scroll to index 40
</button>
<button @click="rowVirtualizer.scrollToIndex(20)">
Then scroll to index 20
</button>
</div>
<br>
<br>
<div
ref="parentRef"
class="List"
style="height: 200px; width: 400px; overflow: auto"
>
<table :style="{ height: `${totalSize}px`, width: '100%' }">
<thead ref="theadRef">
<tr>
<th>Index</th>
<th>Key</th>
</tr>
</thead>
<tbody>
<tr
v-for="virtualRow in virtualRows"
:key="virtualRow.index"
:class="virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}"
>
<td>#{{ virtualRow.index }}</td>
<td>{{ virtualRow.key }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useElementSize } from '@vueuse/core'
import { useVirtualizer } from '@tanstack/vue-virtual'
const parentRef = ref<HTMLElement | null>(null)
const theadRef = ref<HTMLElement | null>(null)
const { height } = useElementSize(theadRef)
const rowVirtualizerOptions = computed(() => {
return {
count: 10000,
getScrollElement: () => parentRef.value,
estimateSize: () => 35,
overscan: 5,
paddingStart: height.value ?? 0,
scrollPaddingStart: height.value ?? 0
}
})
const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
</script>
<style scoped>
.List table {
background-color: #fff;
border: 1px solid #e6e4dc;
max-width: 100%;
border-collapse: collapse;
display: flex;
flex-direction: column;
align-items: stretch;
position: relative;
}
.List thead {
display: flex;
flex-direction: column;
background-color: #fff;
position: sticky;
top: 0;
z-index: 1;
}
.List thead tr {
height: 70px;
}
.List tr {
display: flex;
flex-direction: row;
}
.List td,
.List th {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 180px;
}
.ListItemEven {
background-color: #e6e4dc;
}
</style>
Scroll Padding
Scroll Padding(滚动填充)是 TanStack Virtual (以及其他一些虚拟化库) 中一个非常有用的概念,它允许你在滚动到特定元素时,在滚动容器的起始或结束位置额外留出一些空间。
什么是 Scroll Padding?
简单来说,当你使用 scrollToIndex() 或 scrollToOffset() 方法让虚拟器滚动到某个项目时,Scroll Padding 会在目标项目和滚动容器边缘之间添加一个“空白区域”。
这个概念在两个选项中体现:
scrollPaddingStart: 在滚动容器的起始边缘(例如,垂直滚动时是顶部,水平滚动时是左侧)添加的填充。scrollPaddingEnd: 在滚动容器的结束边缘(例如,垂直滚动时是底部,水平滚动时是右侧)添加的填充。
为什么需要 Scroll Padding?
Scroll Padding 解决了一些常见的 UI/UX 问题,尤其是在具有固定头部或底部导航的页面中:
- 避免内容被遮挡:
- 固定头部/导航栏: 很多网页设计会在页面顶部有一个固定不动的导航栏或工具栏。如果你滚动到某个项目,而这个项目恰好位于视口的顶部,它可能会被固定头部遮挡一部分。
scrollPaddingStart就可以在项目上方留出足够的空间,确保它完全可见,不会被遮挡。 - 固定底部/工具栏: 类似地,如果页面底部有固定的元素,
scrollPaddingEnd可以防止滚动到的项目被底部元素遮挡。
- 改善用户体验:
- 居中或偏离: 有时你可能不希望项目正好紧贴着滚动容器的边缘显示,而是希望它在滚动后能稍微居中或至少距离边缘有一定距离,看起来更舒服。
- 视觉指示: 额外的填充可以为用户提供更好的视觉指示,让他们清楚地看到目标项目,而不会显得过于局促。
如何工作?
当你调用 scrollToIndex(index, { align: 'start' }) 时,虚拟器会计算使 index 处的项目对齐到滚动容器起始边缘所需的偏移量。如果设置了 scrollPaddingStart,虚拟器会在这个计算出的偏移量上额外加上 scrollPaddingStart 的值,从而在目标项目和容器边缘之间留出空白。
示例场景
假设你的页面有一个高度为 60px 的固定顶部导航栏:
const virtualizer = useVirtualizer({
// ...其他选项
getScrollElement: () => document.getElementById('my-scroll-container'),
scrollPaddingStart: 60, // 留出60px,防止被顶部导航遮挡
// ...
});
// 当你滚动到某个项目时
virtualizer.scrollToIndex(someIndex, { align: 'start' });
这样,当你通过 scrollToIndex 将某个项目滚动到可视区域顶部时,这个项目将不会紧贴着容器顶部,而是会向下偏移 60px,从而避免被你的固定导航栏遮挡。