import React, { useState, useMemo, useRef, useCallback } from 'react';
import { preinit } from 'react-dom';
const ITEM_HEIGHT = 40;
const VIEWPORT_HEIGHT = 200;
const TOTAL_ITEMS = 1000;
const TOTAL_HEIGHT = TOTAL_ITEMS * ITEM_HEIGHT;
const Item = ({ data }) => (
<div
className="h-full w-full rounded-md flex items-center justify-between px-4 font-bold text-sm text-white box-border bg-gradient-to-br from-green-500 to-green-600 dark:from-green-600 dark:to-green-700 border-2 border-green-700 dark:border-green-800"
>
<span>{data.name}</span>
<span className="text-xs opacity-80 font-normal">
{data.description}
</span>
</div>
);
const useVirtualList = ({
itemCount,
itemHeight,
containerHeight,
overscan = 3
}) => {
const [scrollTop, setScrollTop] = useState(0);
const [isScrolling, setIsScrolling] = useState(false);
const scrollElementRef = useRef(null);
const { startIndex, endIndex, totalHeight, visibleItems } = useMemo(() => {
if (itemCount === 0) {
return { startIndex: 0, endIndex: 0, totalHeight: 0, visibleItems: [] };
}
const startIndex = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(startIndex + visibleCount - 1, itemCount - 1);
const startWithOverscan = Math.max(0, startIndex - overscan);
const endWithOverscan = Math.min(itemCount - 1, endIndex + overscan);
const visibleItems = [];
for (let index = startWithOverscan; index <= endWithOverscan; index++) {
visibleItems.push({
index,
style: {
position: 'absolute',
top: index * itemHeight,
left: 0,
right: 0,
height: itemHeight,
}
});
}
return {
startIndex: startWithOverscan,
endIndex: endWithOverscan,
totalHeight: itemCount * itemHeight,
visibleItems
};
}, [scrollTop, itemHeight, containerHeight, itemCount, overscan]);
const handleScroll = useCallback((event) => {
const newScrollTop = event.currentTarget.scrollTop;
setScrollTop(newScrollTop);
setIsScrolling(true);
const timeoutId = setTimeout(() => setIsScrolling(false), 150);
return () => clearTimeout(timeoutId);
}, []);
return {
containerProps: {
ref: scrollElementRef,
onScroll: handleScroll,
style: {
height: containerHeight,
overflow: 'auto',
position: 'relative',
}
},
innerProps: {
style: {
height: totalHeight,
position: 'relative',
}
},
visibleItems,
isScrolling,
startIndex,
endIndex,
};
};
export default function App({ itemCount = 1000 }) {
preinit("https://cdn.tailwindcss.com", {as: "script"});
const items = useMemo(() =>
Array.from({ length: itemCount }, (_, i) => ({
id: i,
name: `Item ${i + 1}`,
description: `This is item number ${i + 1}`,
}))
, [itemCount]);
const {
containerProps,
innerProps,
visibleItems,
} = useVirtualList({
itemCount: items.length,
itemHeight: 60,
containerHeight: 400,
overscan: 3,
});
return (
<div className="min-h-screen p-4 sm:p-8 flex items-center justify-center bg-bg-primary">
<div className="w-full max-w-4xl mx-auto">
<div className="rounded-xl shadow-lg dark:shadow-slate-900/50 p-6 sm:p-8 border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900">
<div className="space-y-4">
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">虚拟列表</h1>
<div className="flex justify-center">
<div className="w-full border-2 border-blue-500 dark:border-blue-400 rounded-lg overflow-hidden shadow-inner bg-slate-50 dark:bg-slate-800">
<div {...containerProps} className="border-0 bg-transparent">
<div {...innerProps}>
{visibleItems.map(({ index, style }) => (
<div key={index} style={{...style, padding: '2px 8px'}}>
<Item data={items[index]} />
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};