refactor: migrate react-virtuoso to @tanstack/react-virtual

This commit is contained in:
Tunglies
2026-04-03 17:13:13 +08:00
parent a73fafaf9f
commit d6d15652ca
11 changed files with 516 additions and 434 deletions
+91 -77
View File
@@ -9,10 +9,10 @@ import {
Snackbar,
Typography,
} from '@mui/material'
import { useVirtualizer } from '@tanstack/react-virtual'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
import { delayGroup, healthcheckProxyProvider } from 'tauri-plugin-mihomo-api'
import { BaseEmpty } from '@/components/base'
@@ -46,8 +46,6 @@ interface ProxyChainItem {
delay?: number
}
const VirtuosoFooter = () => <div style={{ height: '8px' }} />
export const ProxyGroups = (props: Props) => {
const { t } = useTranslation()
const { mode, isChainMode = false, chainConfigData } = props
@@ -129,10 +127,17 @@ export const ProxyGroups = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000
const virtuosoRef = useRef<VirtuosoHandle>(null)
const parentRef = useRef<HTMLDivElement>(null)
const scrollPositionRef = useRef<Record<string, number>>({})
const [showScrollTop, setShowScrollTop] = useState(false)
const scrollerRef = useRef<Element | null>(null)
const virtualizer = useVirtualizer({
count: renderList.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 56,
overscan: 15,
getItemKey: (index) => renderList[index]?.key ?? index,
})
// 从 localStorage 恢复滚动位置
useEffect(() => {
@@ -149,10 +154,9 @@ export const ProxyGroups = (props: Props) => {
if (savedPosition !== undefined) {
restoreTimer = setTimeout(() => {
virtuosoRef.current?.scrollTo({
top: savedPosition,
behavior: 'auto',
})
if (parentRef.current) {
parentRef.current.scrollTop = savedPosition
}
}, 100)
}
}
@@ -198,7 +202,7 @@ export const ProxyGroups = (props: Props) => {
// 添加和清理滚动事件监听器
useEffect(() => {
const node = scrollerRef.current
const node = parentRef.current
if (!node) return
const listener = handleScroll as EventListener
@@ -213,7 +217,7 @@ export const ProxyGroups = (props: Props) => {
// 滚动到顶部
const scrollToTop = useCallback(() => {
virtuosoRef.current?.scrollTo?.({
parentRef.current?.scrollTo?.({
top: 0,
behavior: 'smooth',
})
@@ -362,11 +366,7 @@ export const ProxyGroups = (props: Props) => {
)
if (index >= 0) {
virtuosoRef.current?.scrollToIndex?.({
index,
align: 'center',
behavior: 'smooth',
})
virtualizer.scrollToIndex(index, { align: 'center', behavior: 'smooth' })
}
}
@@ -378,14 +378,10 @@ export const ProxyGroups = (props: Props) => {
)
if (index >= 0) {
virtuosoRef.current?.scrollToIndex?.({
index,
align: 'start',
behavior: 'smooth',
})
virtualizer.scrollToIndex(index, { align: 'start', behavior: 'smooth' })
}
},
[renderList],
[renderList, virtualizer],
)
const proxyGroupNames = useMemo(() => {
@@ -475,39 +471,49 @@ export const ProxyGroups = (props: Props) => {
</Box>
)}
<Virtuoso
ref={virtuosoRef}
<div
ref={parentRef}
style={{
height:
mode === 'rule' && proxyGroups.length > 0
? 'calc(100% - 80px)' // 只有标题的高度
: 'calc(100% - 14px)',
overflow: 'auto',
}}
totalCount={renderList.length}
increaseViewportBy={{ top: 200, bottom: 200 }}
overscan={150}
defaultItemHeight={56}
scrollerRef={(ref) => {
scrollerRef.current = ref as Element
}}
components={{
Footer: VirtuosoFooter,
}}
initialScrollTop={scrollPositionRef.current[mode]}
computeItemKey={(index) => renderList[index].key}
itemContent={(index) => (
<ProxyRender
key={renderList[index].key}
item={renderList[index]}
indent={mode === 'rule' || mode === 'script'}
onLocation={handleLocation}
onCheckAll={handleCheckAll}
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
isChainMode={isChainMode}
/>
)}
/>
>
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ProxyRender
item={renderList[virtualItem.index]}
indent={mode === 'rule' || mode === 'script'}
onLocation={handleLocation}
onCheckAll={handleCheckAll}
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
isChainMode={isChainMode}
/>
</div>
))}
<div style={{ height: 8 }} />
</div>
</div>
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
</Box>
@@ -603,34 +609,42 @@ export const ProxyGroups = (props: Props) => {
/>
)}
<Virtuoso
ref={virtuosoRef}
style={{ height: 'calc(100% - 14px)' }}
totalCount={renderList.length}
increaseViewportBy={{ top: 200, bottom: 200 }}
overscan={150}
defaultItemHeight={56}
scrollerRef={(ref) => {
scrollerRef.current = ref as Element
}}
components={{
Footer: VirtuosoFooter,
}}
// 添加平滑滚动设置
initialScrollTop={scrollPositionRef.current[mode]}
computeItemKey={(index) => renderList[index].key}
itemContent={(index) => (
<ProxyRender
key={renderList[index].key}
item={renderList[index]}
indent={mode === 'rule' || mode === 'script'}
onLocation={handleLocation}
onCheckAll={handleCheckAll}
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
/>
)}
/>
<div
ref={parentRef}
style={{ height: 'calc(100% - 14px)', overflow: 'auto' }}
>
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ProxyRender
item={renderList[virtualItem.index]}
indent={mode === 'rule' || mode === 'script'}
onLocation={handleLocation}
onCheckAll={handleCheckAll}
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
/>
</div>
))}
<div style={{ height: 8 }} />
</div>
</div>
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
</div>
)