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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex-1 gap-2">
|
<div className="flex-1 gap-2 overflow-hidden">
|
||||||
<div className="mb-2 flex items-center gap-2 text-sm">
|
<div className="mb-2 flex w-full items-center gap-2 overflow-hidden text-sm">
|
||||||
<div className="border-muted rounded-lg border p-2">
|
<div className="border-muted rounded-lg border p-2">
|
||||||
<FeedIcon source={event.source} size={24} />
|
<FeedIcon source={event.source} size={24} />
|
||||||
</div>
|
</div>
|
||||||
@ -31,25 +31,27 @@ export const FeedEventItem: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex justify-between">
|
||||||
<Badge>{event.source}</Badge>
|
<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) => (
|
{event.tags.map((tag) => (
|
||||||
<Badge variant="outline">{tag}</Badge>
|
<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>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@ import { FeedIntegration } from '@/components/feed/FeedIntegration';
|
|||||||
import { DialogWrapper } from '@/components/DialogWrapper';
|
import { DialogWrapper } from '@/components/DialogWrapper';
|
||||||
import { useSocketSubscribeList } from '@/api/socketio';
|
import { useSocketSubscribeList } from '@/api/socketio';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { SimpleVirtualList } from '@/components/SimpleVirtualList';
|
import { DynamicVirtualList } from '@/components/DynamicVirtualList';
|
||||||
|
|
||||||
export const Route = createFileRoute('/feed/$channelId/')({
|
export const Route = createFileRoute('/feed/$channelId/')({
|
||||||
beforeLoad: routeAuthBeforeLoad,
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
@ -112,7 +112,7 @@ function PageComponent() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="h-full w-full overflow-hidden p-4">
|
<div className="h-full w-full overflow-hidden p-4">
|
||||||
<SimpleVirtualList
|
<DynamicVirtualList
|
||||||
allData={fullEvents}
|
allData={fullEvents}
|
||||||
estimateSize={100}
|
estimateSize={100}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
|
Loading…
Reference in New Issue
Block a user