mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-09 03:42:06 +08:00
Adds src/services/bus (the only listen/emit site: one handler per contract event plus the frontend-internal test-all event) and the React context/reducer app store. Migrates the five scattered subscriptions (layout events, pending failures, proxy refresh, profile update/timer, test-all), deletes the old glue (events.ts, use-listen, use-layout-events, LoadingCacheProvider), moves run state into the store, and adds an architecture gate script wired into CI. Behavior is unchanged; the run-state query's poll/focus safety net is preserved by the store provider.
217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
import {
|
|
DragDropProvider,
|
|
PointerSensor,
|
|
type DragEndEvent,
|
|
} from '@dnd-kit/react'
|
|
import { isSortable } from '@dnd-kit/react/sortable'
|
|
import { Add, NetworkCheck } from '@mui/icons-material'
|
|
import { Box, IconButton, Tooltip, alpha, styled } from '@mui/material'
|
|
import { nanoid } from 'nanoid'
|
|
import { useEffect, useRef, useMemo, useCallback } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
// test icons
|
|
import apple from '@/assets/image/test/apple.svg?raw'
|
|
import github from '@/assets/image/test/github.svg?raw'
|
|
import google from '@/assets/image/test/google.svg?raw'
|
|
import youtube from '@/assets/image/test/youtube.svg?raw'
|
|
import { SortableItem } from '@/components/base'
|
|
import { TestItem } from '@/components/test/test-item'
|
|
import { TestViewer, type TestViewerRef } from '@/components/test/test-viewer'
|
|
import { useVerge } from '@/hooks/use-verge'
|
|
import { emitTestAll } from '@/services/bus'
|
|
|
|
import { EnhancedCard } from './enhanced-card'
|
|
|
|
// 自定义滚动条样式
|
|
const ScrollBox = styled(Box)(({ theme }) => ({
|
|
maxHeight: '180px',
|
|
overflowY: 'auto',
|
|
overflowX: 'hidden',
|
|
'&::-webkit-scrollbar': {
|
|
width: '6px',
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: alpha(theme.palette.text.primary, 0.2),
|
|
borderRadius: '3px',
|
|
},
|
|
}))
|
|
|
|
// 默认测试列表,移到组件外部避免重复创建
|
|
const DEFAULT_TEST_LIST = [
|
|
{
|
|
uid: nanoid(),
|
|
name: 'Apple',
|
|
url: 'https://www.apple.com',
|
|
icon: apple,
|
|
},
|
|
{
|
|
uid: nanoid(),
|
|
name: 'GitHub',
|
|
url: 'https://www.github.com',
|
|
icon: github,
|
|
},
|
|
{
|
|
uid: nanoid(),
|
|
name: 'Google',
|
|
url: 'https://www.google.com',
|
|
icon: google,
|
|
},
|
|
{
|
|
uid: nanoid(),
|
|
name: 'YouTube',
|
|
url: 'https://www.youtube.com',
|
|
icon: youtube,
|
|
},
|
|
]
|
|
|
|
export const TestCard = () => {
|
|
const { t } = useTranslation()
|
|
const { verge, mutateVerge, patchVerge } = useVerge()
|
|
const viewerRef = useRef<TestViewerRef>(null)
|
|
|
|
// 使用useMemo优化测试列表,避免每次渲染重新计算
|
|
const testList = useMemo(() => {
|
|
return verge?.test_list ?? DEFAULT_TEST_LIST
|
|
}, [verge?.test_list])
|
|
|
|
// 使用useCallback优化函数引用,避免不必要的重新渲染
|
|
const onTestListItemChange = useCallback(
|
|
(uid: string, patch?: Partial<IVergeTestItem>) => {
|
|
if (!patch) {
|
|
mutateVerge()
|
|
return
|
|
}
|
|
|
|
const newList = testList.map((x) =>
|
|
x.uid === uid ? { ...x, ...patch } : x,
|
|
)
|
|
|
|
mutateVerge({ ...verge, test_list: newList }, false)
|
|
},
|
|
[testList, verge, mutateVerge],
|
|
)
|
|
|
|
const onDeleteTestListItem = useCallback(
|
|
(uid: string) => {
|
|
const newList = testList.filter((x) => x.uid !== uid)
|
|
patchVerge({ test_list: newList })
|
|
mutateVerge({ ...verge, test_list: newList }, false)
|
|
},
|
|
[testList, verge, patchVerge, mutateVerge],
|
|
)
|
|
|
|
const onDragEnd = useCallback(
|
|
async (event: DragEndEvent) => {
|
|
const { operation, canceled } = event
|
|
const { source, target } = operation
|
|
|
|
if (canceled || !target || !isSortable(source)) return
|
|
|
|
const { index: newIndex, initialIndex: oldIndex } = source.sortable
|
|
if (
|
|
oldIndex < 0 ||
|
|
newIndex < 0 ||
|
|
oldIndex >= testList.length ||
|
|
newIndex >= testList.length ||
|
|
oldIndex === newIndex
|
|
) {
|
|
return
|
|
}
|
|
|
|
const activeId = testList[oldIndex].uid
|
|
const overId = testList[newIndex].uid
|
|
|
|
if (activeId == null || overId == null || activeId === overId) return
|
|
|
|
const newList = [...testList]
|
|
const [removed] = newList.splice(oldIndex, 1)
|
|
newList.splice(newIndex, 0, removed)
|
|
|
|
// 优化:先本地更新,再异步 patch,避免UI卡死
|
|
mutateVerge({ ...verge, test_list: newList }, false)
|
|
const patchFn = () => {
|
|
try {
|
|
patchVerge({ test_list: newList })
|
|
} catch {}
|
|
}
|
|
if (window.requestIdleCallback) {
|
|
window.requestIdleCallback(patchFn)
|
|
} else {
|
|
setTimeout(patchFn, 0)
|
|
}
|
|
},
|
|
[testList, verge, mutateVerge, patchVerge],
|
|
)
|
|
|
|
// 仅在verge首次加载时初始化测试列表
|
|
useEffect(() => {
|
|
if (verge && !verge.test_list) {
|
|
patchVerge({ test_list: DEFAULT_TEST_LIST })
|
|
}
|
|
}, [verge, patchVerge])
|
|
|
|
// 使用useMemo优化UI内容,减少渲染计算
|
|
const renderTestItems = useMemo(
|
|
() => (
|
|
<Box
|
|
sx={{
|
|
mb: 1.5,
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))',
|
|
gap: 1,
|
|
px: 1,
|
|
}}
|
|
>
|
|
{testList.map((item, index) => (
|
|
<SortableItem key={item.uid} id={item.uid} index={index}>
|
|
<TestItem
|
|
itemData={item}
|
|
onEdit={() => viewerRef.current?.edit(item)}
|
|
onDelete={onDeleteTestListItem}
|
|
/>
|
|
</SortableItem>
|
|
))}
|
|
</Box>
|
|
),
|
|
[testList, onDeleteTestListItem],
|
|
)
|
|
|
|
const handleTestAll = useCallback(() => {
|
|
emitTestAll()
|
|
}, [])
|
|
|
|
const handleCreateTest = useCallback(() => {
|
|
viewerRef.current?.create()
|
|
}, [])
|
|
|
|
return (
|
|
<EnhancedCard
|
|
title={t('home.components.tests.title')}
|
|
icon={<NetworkCheck />}
|
|
action={
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
<Tooltip title={t('tests.page.actions.testAll')} arrow>
|
|
<IconButton size="small" onClick={handleTestAll}>
|
|
<NetworkCheck fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip title={t('tests.modals.test.title.create')} arrow>
|
|
<IconButton size="small" onClick={handleCreateTest}>
|
|
<Add fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
}
|
|
>
|
|
<ScrollBox>
|
|
<DragDropProvider sensors={[PointerSensor]} onDragEnd={onDragEnd}>
|
|
{renderTestItems}
|
|
</DragDropProvider>
|
|
</ScrollBox>
|
|
|
|
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
|
|
</EnhancedCard>
|
|
)
|
|
}
|