|
1 | 1 | <template> |
2 | | - <div class="nt-virtual-list"> |
| 2 | + <div class="nt-virtual-list" ref="$list"> |
3 | 3 | <!-- 占位元素, 用于撑开滚动条,达到滚动效果 --> |
4 | | - <div class="nt-virtual-placeholder"></div> |
| 4 | + <div class="nt-virtual-placeholder" ref="$placeholder"></div> |
5 | 5 | <!-- 内容元素, 用于显示列表项 --> |
6 | | - <div class="nt-virtual-content"> |
| 6 | + <div class="nt-virtual-content" ref="$itemContent"> |
7 | 7 | <!-- 列表项 --> |
8 | | - <div class="nt-virtual-item"></div> |
| 8 | + <div |
| 9 | + v-for="(item, index) in visibleData" |
| 10 | + :style="{ height: props.itemSize + 'px' }" |
| 11 | + :key="keyField != null ? (item as any)[keyField] : index" |
| 12 | + :class="['nt-virtual-item', itemClass]" |
| 13 | + > |
| 14 | + <slot :item="item"></slot> |
| 15 | + </div> |
9 | 16 | </div> |
10 | 17 | </div> |
11 | 18 | </template> |
12 | | -<script setup lang="ts"></script> |
| 19 | +<script setup lang="ts" generic="T"> |
| 20 | +import { onMounted, ref } from 'vue'; |
| 21 | +import { debounce } from 'ph-utils/web'; |
| 22 | +
|
| 23 | +const props = withDefaults( |
| 24 | + defineProps<{ |
| 25 | + /** 需要展示的数据 */ |
| 26 | + items: T[]; |
| 27 | + /** 列表项的高度,用于计算滚动大小和位置 */ |
| 28 | + itemSize: number; |
| 29 | + itemClass?: string; |
| 30 | + /** 选项 key 的字段名, 用于 v-for 的 key */ |
| 31 | + keyField?: string; |
| 32 | + }>(), |
| 33 | + { |
| 34 | + itemClass: '', |
| 35 | + }, |
| 36 | +); |
| 37 | +
|
| 38 | +/** 可视区域内能显示的数据总数 */ |
| 39 | +let visibleCount = 0; |
| 40 | +/** 是否正在处理数据 */ |
| 41 | +let loading = false; |
| 42 | +const $list = ref<HTMLDivElement>(); |
| 43 | +const $itemContent = ref<HTMLDivElement>(); |
| 44 | +const $placeholder = ref<HTMLDivElement>(); |
| 45 | +/** 实际显示的数据列表 */ |
| 46 | +const visibleData = ref<T[]>([]); |
| 47 | +
|
| 48 | +function renderData() { |
| 49 | + if ($list.value != null) { |
| 50 | + // 计算可视区域数据的开始索引 |
| 51 | + const startIndex = Math.floor($list.value.scrollTop / props.itemSize); |
| 52 | + if ($itemContent.value != null) { |
| 53 | + // 开始项距离容器顶部的距离, 保证在滚动时数据一直在可视区域内 |
| 54 | + const top = `${startIndex * props.itemSize}px`; |
| 55 | + $itemContent.value.style.top = top; |
| 56 | + } |
| 57 | + // 生成可视区域数据 |
| 58 | + visibleData.value = props.items.slice( |
| 59 | + startIndex, |
| 60 | + startIndex + visibleCount, |
| 61 | + ) as any[]; |
| 62 | + } |
| 63 | +} |
| 64 | +
|
| 65 | +onMounted(() => { |
| 66 | + if ($list.value != null) { |
| 67 | + visibleCount = Math.ceil($list.value.clientHeight / props.itemSize); |
| 68 | +
|
| 69 | + if ($placeholder.value != null) { |
| 70 | + const height = props.itemSize * props.items.length; |
| 71 | + $placeholder.value.style.height = `${height}px`; |
| 72 | + } |
| 73 | +
|
| 74 | + renderData(); // 初始化渲染数据 |
| 75 | + } |
| 76 | +}); |
| 77 | +</script> |
0 commit comments