<template>
<div ref="containerRef" relative>
<DynamicScroller
:items="virtualRows"
:min-item-size="virtualMinRowHeight"
key-field="id"
page-mode
:emitUpdate="true"
@update="onUpdate"
@resize="syncWidth"
v-bind="attrs"
>
<template
#default="{
item: vRow,
index,
active,
}: {
item: { items: T[] }
index: number
active: boolean
}"
>
<DynamicScrollerItem :item="vRow" :active="active" :data-index="index">
<div class="flex flex-wrap" :style="{ gap: `${gap}px`, paddingBlockEnd: `${gap}px` }">
<div v-for="item in vRow.items" :key="item[keyField]" class="w-1 flex-1">
<slot :item="item" :index="index" :active="active" />
</div>
</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
</div>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { computed, ref, useAttrs } from 'vue'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const props = defineProps<{
items: T[]
minItemWidth: number
minItemHeight: number
keyField: keyof T
gap: number
}>()
const attrs = useAttrs()
console.log(attrs)
const emit = defineEmits<{
(e: 'viewItems', items: T[]): void
(e: 'resize'): void
}>()
const containerWidth = ref(0)
const containerRef = ref<HTMLElement | null>(null)
function syncWidth() {
containerWidth.value = containerRef.value?.clientWidth ?? 0
}
function onUpdate(startIndex: number, endIndex: number) {
const items = virtualRows.value.slice(startIndex, endIndex + 1).flatMap((row) => row.items)
emit('viewItems', items)
}
const columns = computed(() => {
if (!containerWidth.value) return 1
const potentialColumns = Math.floor(containerWidth.value / (props.minItemWidth + props.gap))
return Math.max(1, potentialColumns)
})
const virtualRows = computed(() => {
const rows: { id: string; items: T[] }[] = []
for (let i = 0; i < props.items.length; i += columns.value) {
rows.push({
id: `row_${Math.floor(i / columns.value)}`,
items: props.items.slice(i, i + columns.value),
})
}
return rows
})
const virtualMinRowHeight = computed(() => props.minItemHeight + props.gap)
</script>
<style scoped>
/** 防止边框或阴影被遮挡 */
:deep(.vue-recycle-scroller__item-wrapper) {
overflow: visible;
}
</style>