From bf4e1a3270806ddfab96c5f4635debd0968bdb41 Mon Sep 17 00:00:00 2001 From: Sline Date: Tue, 7 Oct 2025 16:39:22 +0800 Subject: [PATCH] feat: url test button for proxy card and type safety (#4964) * feat: url test button for proxy card and type safety * fix: resolve ESLint hook dependency error in current-proxy-card.tsx --- src/components/home/current-proxy-card.tsx | 150 +++++++++++++++++---- 1 file changed, 127 insertions(+), 23 deletions(-) diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index b96dbb05..1e8d140f 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -1,6 +1,7 @@ import { AccessTimeRounded, ChevronRight, + NetworkCheckRounded, WifiOff as SignalError, SignalWifi3Bar as SignalGood, SignalWifi2Bar as SignalMedium, @@ -25,13 +26,17 @@ import { alpha, useTheme, } from "@mui/material"; +import { useLockFn } from "ahooks"; +import React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { EnhancedCard } from "@/components/home/enhanced-card"; import { useProxySelection } from "@/hooks/use-proxy-selection"; +import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-context"; +import { getGroupProxyDelays, providerHealthCheck } from "@/services/cmds"; import delayManager from "@/services/delay"; // 本地存储的键名 @@ -47,7 +52,9 @@ interface ProxyOption { // 排序类型: 默认 | 按延迟 | 按字母 type ProxySortType = 0 | 1 | 2; -function convertDelayColor(delayValue: number) { +function convertDelayColor( + delayValue: number, +): "success" | "warning" | "error" | "primary" | "default" { const colorStr = delayManager.formatDelayColor(delayValue); if (!colorStr) return "default"; @@ -67,7 +74,11 @@ function convertDelayColor(delayValue: number) { } } -function getSignalIcon(delay: number) { +function getSignalIcon(delay: number): { + icon: React.ReactElement; + text: string; + color: string; +} { if (delay < 0) return { icon: , text: "未测试", color: "text.secondary" }; if (delay >= 10000) @@ -81,20 +92,12 @@ function getSignalIcon(delay: number) { return { icon: , text: "延迟极佳", color: "success.main" }; } -// 简单的防抖函数 -function debounce(fn: Function, ms = 100) { - let timeoutId: ReturnType; - return function (this: any, ...args: any[]) { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => fn.apply(this, args), ms); - }; -} - export const CurrentProxyCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); const { proxies, clashConfig, refreshProxy } = useAppData(); + const { verge } = useVerge(); // 统一代理选择器 const { handleSelectChange } = useProxySelection({ @@ -172,6 +175,7 @@ export const CurrentProxyCard = () => { // 根据模式确定初始组 if (isGlobalMode) { + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => ({ ...prev, selection: { @@ -180,6 +184,7 @@ export const CurrentProxyCard = () => { }, })); } else if (isDirectMode) { + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => ({ ...prev, selection: { @@ -189,6 +194,7 @@ export const CurrentProxyCard = () => { })); } else { const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP); + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => ({ ...prev, selection: { @@ -203,6 +209,7 @@ export const CurrentProxyCard = () => { useEffect(() => { if (!proxies) return; + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => { // 只保留 Selector 类型的组用于选择 const filteredGroups = proxies.groups @@ -270,16 +277,23 @@ export const CurrentProxyCard = () => { }, [proxies, isGlobalMode, isDirectMode]); // 使用防抖包装状态更新 + const timeoutRef = React.useRef | null>(null); + const debouncedSetState = useCallback( - debounce((updateFn: (prev: ProxyState) => ProxyState) => { - setState(updateFn); - }, 300), - [], + (updateFn: (prev: ProxyState) => ProxyState) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setState(updateFn); + }, 300); + }, + [setState], ); // 处理代理组变更 const handleGroupChange = useCallback( - (event: SelectChangeEvent) => { + (event: SelectChangeEvent) => { if (isGlobalMode || isDirectMode) return; const newGroup = event.target.value; @@ -314,7 +328,7 @@ export const CurrentProxyCard = () => { // 处理代理节点变更 const handleProxyChange = useCallback( - (event: SelectChangeEvent) => { + (event: SelectChangeEvent) => { if (isDirectMode) return; const newProxy = event.target.value; @@ -399,6 +413,85 @@ export const CurrentProxyCard = () => { localStorage.setItem(STORAGE_KEY_SORT_TYPE, newSortType.toString()); }, [sortType]); + // 延迟测试 + const handleCheckDelay = useLockFn(async () => { + const groupName = state.selection.group; + if (!groupName || isDirectMode) return; + + console.log(`[CurrentProxyCard] 开始测试所有延迟,组: ${groupName}`); + + const timeout = verge?.default_latency_timeout || 10000; + + // 获取当前组的所有代理 + const proxyNames: string[] = []; + const providers: Set = new Set(); + + if (isGlobalMode && proxies?.global) { + // 全局模式 + const allProxies = proxies.global.all + .filter((p: any) => { + const name = typeof p === "string" ? p : p.name; + return name !== "DIRECT" && name !== "REJECT"; + }) + .map((p: any) => (typeof p === "string" ? p : p.name)); + + allProxies.forEach((name: string) => { + const proxy = state.proxyData.records[name]; + if (proxy?.provider) { + providers.add(proxy.provider); + } else { + proxyNames.push(name); + } + }); + } else { + // 规则模式 + const group = state.proxyData.groups.find((g) => g.name === groupName); + if (group) { + group.all.forEach((name: string) => { + const proxy = state.proxyData.records[name]; + if (proxy?.provider) { + providers.add(proxy.provider); + } else { + proxyNames.push(name); + } + }); + } + } + + console.log( + `[CurrentProxyCard] 找到代理数量: ${proxyNames.length}, 提供者数量: ${providers.size}`, + ); + + // 测试提供者的节点 + if (providers.size > 0) { + console.log(`[CurrentProxyCard] 开始测试提供者节点`); + await Promise.allSettled( + [...providers].map((p) => providerHealthCheck(p)), + ); + } + + // 测试非提供者的节点 + if (proxyNames.length > 0) { + const url = delayManager.getUrl(groupName); + console.log(`[CurrentProxyCard] 测试URL: ${url}, 超时: ${timeout}ms`); + + try { + await Promise.race([ + delayManager.checkListDelay(proxyNames, groupName, timeout), + getGroupProxyDelays(groupName, url, timeout), + ]); + console.log(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`); + } catch (error) { + console.error( + `[CurrentProxyCard] 延迟测试出错,组: ${groupName}`, + error, + ); + } + } + + refreshProxy(); + }); + // 排序代理函数(增加非空校验) const sortProxies = useCallback( (proxies: ProxyOption[]) => { @@ -474,7 +567,7 @@ export const CurrentProxyCard = () => { ]); // 获取排序图标 - const getSortIcon = () => { + const getSortIcon = (): React.ReactElement => { switch (sortType) { case 1: return ; @@ -486,7 +579,7 @@ export const CurrentProxyCard = () => { }; // 获取排序提示文本 - const getSortTooltip = () => { + const getSortTooltip = (): string => { switch (sortType) { case 0: return t("Sort by default"); @@ -517,13 +610,24 @@ export const CurrentProxyCard = () => { } iconColor={currentProxy ? "primary" : undefined} action={ - + + + + + + + + {getSortIcon()} @@ -657,7 +761,7 @@ export const CurrentProxyCard = () => { > {isDirectMode ? null - : proxyOptions.map((proxy, index) => { + : proxyOptions.map((proxy) => { const delayValue = state.proxyData.records[proxy.name] && state.selection.group @@ -668,7 +772,7 @@ export const CurrentProxyCard = () => { : -1; return (