import { useRef, useState, useEffect, useCallback, useMemo } from "react"; import { useLockFn } from "ahooks"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { getConnections, providerHealthCheck, updateProxy, deleteConnection, getGroupProxyDelays, } from "@/services/api"; import { useProfiles } from "@/hooks/use-profiles"; import { useVerge } from "@/hooks/use-verge"; import { BaseEmpty } from "../base"; import { useRenderList } from "./use-render-list"; import { ProxyRender } from "./proxy-render"; import delayManager from "@/services/delay"; import { useTranslation } from "react-i18next"; import { ScrollTopButton } from "../layout/scroll-top-button"; import { Box, styled } from "@mui/material"; import { memo } from "react"; // 将选择器组件抽离出来,避免主组件重渲染时重复创建样式 const AlphabetSelector = styled(Box)(({ theme }) => ({ position: "fixed", right: 4, top: "50%", transform: "translateY(-50%)", display: "flex", flexDirection: "column", background: "transparent", zIndex: 1000, gap: "2px", padding: "8px 4px", willChange: "transform", // 优化动画性能 "& .letter": { padding: "2px 4px", fontSize: "12px", cursor: "pointer", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", color: theme.palette.text.secondary, position: "relative", width: "1.5em", height: "1.5em", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1)", // 稍微加快动画速度 transform: "scale(1) translateZ(0)", // 开启GPU加速 backfaceVisibility: "hidden", // 防止闪烁 borderRadius: "6px", "&:hover": { color: theme.palette.primary.main, transform: "scale(1.2) translateZ(0)", backgroundColor: theme.palette.action.hover, "& .tooltip": { opacity: 1, transform: "translateX(0) translateZ(0)", visibility: "visible", }, }, "&:hover ~ .letter": { transform: "translateY(2px) translateZ(0)", }, }, "& .tooltip": { position: "absolute", right: "calc(100% + 8px)", background: theme.palette.background.paper, padding: "4px 8px", borderRadius: "6px", boxShadow: theme.shadows[3], whiteSpace: "nowrap", opacity: 0, visibility: "hidden", transform: "translateX(4px) translateZ(0)", transition: "all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1)", fontSize: "12px", color: theme.palette.text.primary, pointerEvents: "none", backfaceVisibility: "hidden", "&::after": { content: '""', position: "absolute", right: "-4px", top: "50%", transform: "translateY(-50%)", width: 0, height: 0, borderTop: "4px solid transparent", borderBottom: "4px solid transparent", borderLeft: `4px solid ${theme.palette.background.paper}`, }, }, })); // 抽离字母选择器子组件 const LetterItem = memo( ({ name, onClick, getFirstChar, }: { name: string; onClick: (name: string) => void; getFirstChar: (str: string) => string; }) => (
onClick(name)}> {getFirstChar(name)}
{name}
), ); interface Props { mode: string; } export const ProxyGroups = (props: Props) => { const { t } = useTranslation(); const { mode } = props; const { renderList, onProxies, onHeadState } = useRenderList(mode); const { verge } = useVerge(); const { current, patchCurrent } = useProfiles(); const timeout = verge?.default_latency_timeout || 10000; const virtuosoRef = useRef(null); const scrollPositionRef = useRef>({}); const [showScrollTop, setShowScrollTop] = useState(false); const scrollerRef = useRef(null); // 使用useMemo缓存字母索引数据 const { groupFirstLetters, letterIndexMap } = useMemo(() => { const letters = new Set(); const indexMap: Record = {}; renderList.forEach((item, index) => { if (item.type === 0) { const fullName = item.group.name; letters.add(fullName); if (!(fullName in indexMap)) { indexMap[fullName] = index; } } }); return { groupFirstLetters: Array.from(letters), letterIndexMap: indexMap, }; }, [renderList]); // 缓存getFirstChar函数 const getFirstChar = useCallback((str: string) => { const regex = /\p{Extended_Pictographic}|\p{L}|\p{N}|./u; const match = str.match(regex); return match ? match[0] : str.charAt(0); }, []); // 从 localStorage 恢复滚动位置 useEffect(() => { if (renderList.length === 0) return; try { const savedPositions = localStorage.getItem("proxy-scroll-positions"); if (savedPositions) { const positions = JSON.parse(savedPositions); scrollPositionRef.current = positions; const savedPosition = positions[mode]; if (savedPosition !== undefined) { setTimeout(() => { virtuosoRef.current?.scrollTo({ top: savedPosition, behavior: "auto", }); }, 100); } } } catch (e) { console.error("Error restoring scroll position:", e); } }, [mode, renderList]); // 使用防抖保存滚动位置 const saveScrollPosition = useCallback( (scrollTop: number) => { try { scrollPositionRef.current[mode] = scrollTop; localStorage.setItem( "proxy-scroll-positions", JSON.stringify(scrollPositionRef.current), ); } catch (e) { console.error("Error saving scroll position:", e); } }, [mode], ); // 优化滚动处理函数,使用防抖 const handleScroll = useCallback( debounce((e: any) => { const scrollTop = e.target.scrollTop; setShowScrollTop(scrollTop > 100); saveScrollPosition(scrollTop); }, 16), [saveScrollPosition], ); // 添加和清理滚动事件监听器 useEffect(() => { const currentScroller = scrollerRef.current; if (currentScroller) { currentScroller.addEventListener("scroll", handleScroll, { passive: true, }); return () => { currentScroller.removeEventListener("scroll", handleScroll); }; } }, [handleScroll]); // 滚动到顶部 const scrollToTop = useCallback(() => { virtuosoRef.current?.scrollTo?.({ top: 0, behavior: "smooth", }); saveScrollPosition(0); }, [saveScrollPosition]); // 处理字母点击,使用useCallback const handleLetterClick = useCallback( (name: string) => { const index = letterIndexMap[name]; if (index !== undefined) { virtuosoRef.current?.scrollToIndex({ index, align: "start", behavior: "smooth", }); } }, [letterIndexMap], ); // 切换分组的节点代理 const handleChangeProxy = useLockFn( async (group: IProxyGroupItem, proxy: IProxyItem) => { if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return; const { name, now } = group; await updateProxy(name, proxy.name); onProxies(); // 断开连接 if (verge?.auto_close_connection) { getConnections().then(({ connections }) => { connections.forEach((conn) => { if (conn.chains.includes(now!)) { deleteConnection(conn.id); } }); }); } // 保存到selected中 if (!current) return; if (!current.selected) current.selected = []; const index = current.selected.findIndex( (item) => item.name === group.name, ); if (index < 0) { current.selected.push({ name, now: proxy.name }); } else { current.selected[index] = { name, now: proxy.name }; } await patchCurrent({ selected: current.selected }); }, ); // 测全部延迟 const handleCheckAll = useLockFn(async (groupName: string) => { const proxies = renderList .filter( (e) => e.group?.name === groupName && (e.type === 2 || e.type === 4), ) .flatMap((e) => e.proxyCol || e.proxy!) .filter(Boolean); const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean)); if (providers.size) { Promise.allSettled( [...providers].map((p) => providerHealthCheck(p)), ).then(() => onProxies()); } const names = proxies.filter((p) => !p!.provider).map((p) => p!.name); await Promise.race([ delayManager.checkListDelay(names, groupName, timeout), getGroupProxyDelays(groupName, delayManager.getUrl(groupName), timeout), // 查询group delays 将清除fixed(不关注调用结果) ]); onProxies(); }); // 滚到对应的节点 const handleLocation = (group: IProxyGroupItem) => { if (!group) return; const { name, now } = group; const index = renderList.findIndex( (e) => e.group?.name === name && ((e.type === 2 && e.proxy?.name === now) || (e.type === 4 && e.proxyCol?.some((p) => p.name === now))), ); if (index >= 0) { virtuosoRef.current?.scrollToIndex?.({ index, align: "center", behavior: "smooth", }); } }; if (mode === "direct") { return ; } return (
{ scrollerRef.current = ref; }} components={{ Footer: () =>
, }} itemContent={(index) => ( )} /> {groupFirstLetters.map((name) => ( ))}
); }; // 简单的防抖函数 function debounce any>( func: T, wait: number, ): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; return (...args: Parameters) => { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; }