使用Virtua替换@tanstack/react-virtual来实现虚拟列表

This commit is contained in:
xintaofei
2026-03-24 22:35:52 +08:00
parent 7d1db72b3e
commit 1da1fd1e38
5 changed files with 70 additions and 131 deletions

View File

@@ -12,7 +12,7 @@ import {
} from "react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { useVirtualizer } from "@tanstack/react-virtual"
import { Virtualizer, type VirtualizerHandle } from "virtua"
import { CheckCheck, ChevronRight, Download, Loader2, Plus } from "lucide-react"
import { useFolderContext } from "@/contexts/folder-context"
import { useTabContext } from "@/contexts/tab-context"
@@ -71,7 +71,6 @@ type FlatItem =
| { type: "header"; status: ConversationStatus; count: number }
| { type: "conversation"; conversation: DbConversationSummary }
const HEADER_HEIGHT = 32
const CARD_HEIGHT = 62
const GroupHeader = memo(function GroupHeader({
@@ -209,8 +208,7 @@ export function SidebarConversationList({
const scrollToActiveRef = useRef<() => void>(() => {})
const pendingScrollRef = useRef(false)
const virtualizerRef =
useRef<ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>>(null)
const virtualizerRef = useRef<VirtualizerHandle>(null)
useImperativeHandle(ref, () => ({
scrollToActive() {
@@ -272,22 +270,6 @@ export function SidebarConversationList({
)
const reviewConversationCount = reviewConversations.length
const virtualizer = useVirtualizer({
count: flatItems.length,
getScrollElement: () => scrollContainerRef.current,
estimateSize: (index) =>
flatItems[index].type === "header" ? HEADER_HEIGHT : CARD_HEIGHT,
getItemKey: (index) => {
const item = flatItems[index]
return item.type === "header"
? `header-${item.status}`
: `conv-${item.conversation.id}`
},
overscan: 5,
})
virtualizerRef.current = virtualizer
useEffect(() => {
scrollToActiveRef.current = () => {
if (!selectedConversation) return
@@ -312,7 +294,7 @@ export function SidebarConversationList({
if (index >= 0) {
virtualizerRef.current?.scrollToIndex(index, {
align: "center",
behavior: "smooth",
smooth: true,
})
}
}
@@ -494,33 +476,20 @@ export function SidebarConversationList({
ref={scrollContainerRef}
className={cn(
"flex-1 min-h-0 overflow-y-auto px-1.5",
"[overflow-anchor:none]",
"[&::-webkit-scrollbar]:w-1.5",
"[&::-webkit-scrollbar-thumb]:rounded-full",
"[&::-webkit-scrollbar-thumb]:bg-border"
)}
>
<div
style={{
height: virtualizer.getTotalSize(),
position: "relative",
width: "100%",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = flatItems[virtualRow.index]
<Virtualizer ref={virtualizerRef} itemSize={CARD_HEIGHT}>
{flatItems.map((item) => {
const key =
item.type === "header"
? `header-${item.status}`
: `conv-${item.conversation.id}`
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div key={key}>
{item.type === "header" ? (
item.status === "pending_review" ? (
<PendingReviewHeader
@@ -563,7 +532,7 @@ export function SidebarConversationList({
</div>
)
})}
</div>
</Virtualizer>
</div>
</ContextMenuTrigger>
<ContextMenuContent>