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
This commit is contained in:
Sline
2025-10-07 16:39:22 +08:00
committed by GitHub
Unverified
parent 3f1f53434c
commit bf4e1a3270

View File

@@ -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: <SignalNone />, text: "未测试", color: "text.secondary" };
if (delay >= 10000)
@@ -81,20 +92,12 @@ function getSignalIcon(delay: number) {
return { icon: <SignalStrong />, text: "延迟极佳", color: "success.main" };
}
// 简单的防抖函数
function debounce(fn: Function, ms = 100) {
let timeoutId: ReturnType<typeof setTimeout>;
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<ReturnType<typeof setTimeout> | 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<string>) => {
if (isGlobalMode || isDirectMode) return;
const newGroup = event.target.value;
@@ -314,7 +328,7 @@ export const CurrentProxyCard = () => {
// 处理代理节点变更
const handleProxyChange = useCallback(
(event: SelectChangeEvent) => {
(event: SelectChangeEvent<string>) => {
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<string> = 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 <AccessTimeRounded fontSize="small" />;
@@ -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={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip title={t("Delay check")}>
<span>
<IconButton
size="small"
color="inherit"
onClick={handleCheckDelay}
disabled={isDirectMode}
>
<NetworkCheckRounded />
</IconButton>
</span>
</Tooltip>
<Tooltip title={getSortTooltip()}>
<IconButton
size="small"
color="inherit"
onClick={handleSortTypeChange}
sx={{ mr: 1 }}
>
{getSortIcon()}
</IconButton>
@@ -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 (
<MenuItem
key={`${proxy.name}-${index}`}
key={proxy.name}
value={proxy.name}
sx={{
display: "flex",