refactor: add dynamic virtual list
This commit is contained in:
parent
15c6290587
commit
01d81f3929
103
src/client/components/DynamicVirtualList.tsx
Normal file
103
src/client/components/DynamicVirtualList.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { useWatch } from '@/hooks/useWatch';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { Empty } from 'antd';
|
||||
import { last } from 'lodash-es';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
interface VirtualListProps<T = any> {
|
||||
allData: T[];
|
||||
hasNextPage: boolean | undefined;
|
||||
isFetchingNextPage: boolean;
|
||||
onFetchNextPage: () => void;
|
||||
estimateSize: number;
|
||||
renderItem: (item: T) => React.ReactElement;
|
||||
renderEmpty?: () => React.ReactElement;
|
||||
}
|
||||
export const DynamicVirtualList: React.FC<VirtualListProps> = React.memo(
|
||||
(props) => {
|
||||
const {
|
||||
allData,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
onFetchNextPage,
|
||||
estimateSize,
|
||||
renderItem,
|
||||
renderEmpty,
|
||||
} = props;
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: hasNextPage ? allData.length + 1 : allData.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useWatch([virtualItems], () => {
|
||||
const lastItem = last(virtualItems);
|
||||
|
||||
if (!lastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastItem.index >= allData.length - 1 &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
onFetchNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{
|
||||
contain: 'strict',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{virtualItems.length === 0 &&
|
||||
(renderEmpty ? renderEmpty() : <Empty />)}
|
||||
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const isLoaderRow = virtualRow.index > allData.length - 1;
|
||||
const data = allData[virtualRow.index];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
>
|
||||
{isLoaderRow
|
||||
? hasNextPage
|
||||
? t('Loading more...')
|
||||
: t('Nothing more to load')
|
||||
: renderItem(data)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
DynamicVirtualList.displayName = 'DynamicVirtualList';
|
@ -17,12 +17,12 @@ export const FeedEventItem: React.FC<{
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-muted flex items-center rounded-lg border px-4 py-2',
|
||||
'border-muted flex items-center overflow-hidden rounded-lg border px-2 py-2 sm:px-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 gap-2">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm">
|
||||
<div className="flex-1 gap-2 overflow-hidden">
|
||||
<div className="mb-2 flex w-full items-center gap-2 overflow-hidden text-sm">
|
||||
<div className="border-muted rounded-lg border p-2">
|
||||
<FeedIcon source={event.source} size={24} />
|
||||
</div>
|
||||
@ -31,25 +31,27 @@ export const FeedEventItem: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>{event.source}</Badge>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>{event.source}</Badge>
|
||||
|
||||
<Badge variant="secondary">{event.eventName}</Badge>
|
||||
<Badge variant="secondary">{event.eventName}</Badge>
|
||||
|
||||
{event.tags.map((tag) => (
|
||||
<Badge variant="outline">{tag}</Badge>
|
||||
))}
|
||||
{event.tags.map((tag) => (
|
||||
<Badge variant="outline">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-default self-end text-xs opacity-60">
|
||||
<div>{dayjs(event.createdAt).fromNow()}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-default self-end text-xs opacity-60">
|
||||
<div>{dayjs(event.createdAt).fromNow()}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -15,7 +15,7 @@ import { FeedIntegration } from '@/components/feed/FeedIntegration';
|
||||
import { DialogWrapper } from '@/components/DialogWrapper';
|
||||
import { useSocketSubscribeList } from '@/api/socketio';
|
||||
import { useMemo } from 'react';
|
||||
import { SimpleVirtualList } from '@/components/SimpleVirtualList';
|
||||
import { DynamicVirtualList } from '@/components/DynamicVirtualList';
|
||||
|
||||
export const Route = createFileRoute('/feed/$channelId/')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -112,7 +112,7 @@ function PageComponent() {
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full overflow-hidden p-4">
|
||||
<SimpleVirtualList
|
||||
<DynamicVirtualList
|
||||
allData={fullEvents}
|
||||
estimateSize={100}
|
||||
hasNextPage={hasNextPage}
|
||||
|
Loading…
Reference in New Issue
Block a user