mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 10:42:09 +08:00
refactor: migrate react-virtuoso to @tanstack/react-virtual
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user