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
parent 3f1f53434c
commit bf4e1a3270

View File

@@ -1,6 +1,7 @@
import { import {
AccessTimeRounded, AccessTimeRounded,
ChevronRight, ChevronRight,
NetworkCheckRounded,
WifiOff as SignalError, WifiOff as SignalError,
SignalWifi3Bar as SignalGood, SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium, SignalWifi2Bar as SignalMedium,
@@ -25,13 +26,17 @@ import {
alpha, alpha,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks";
import React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card"; import { EnhancedCard } from "@/components/home/enhanced-card";
import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context"; import { useAppData } from "@/providers/app-data-context";
import { getGroupProxyDelays, providerHealthCheck } from "@/services/cmds";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
// 本地存储的键名 // 本地存储的键名
@@ -47,7 +52,9 @@ interface ProxyOption {
// 排序类型: 默认 | 按延迟 | 按字母 // 排序类型: 默认 | 按延迟 | 按字母
type ProxySortType = 0 | 1 | 2; type ProxySortType = 0 | 1 | 2;
function convertDelayColor(delayValue: number) { function convertDelayColor(
delayValue: number,
): "success" | "warning" | "error" | "primary" | "default" {
const colorStr = delayManager.formatDelayColor(delayValue); const colorStr = delayManager.formatDelayColor(delayValue);
if (!colorStr) return "default"; 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) if (delay < 0)
return { icon: <SignalNone />, text: "未测试", color: "text.secondary" }; return { icon: <SignalNone />, text: "未测试", color: "text.secondary" };
if (delay >= 10000) if (delay >= 10000)
@@ -81,20 +92,12 @@ function getSignalIcon(delay: number) {
return { icon: <SignalStrong />, text: "延迟极佳", color: "success.main" }; 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 = () => { export const CurrentProxyCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const { proxies, clashConfig, refreshProxy } = useAppData(); const { proxies, clashConfig, refreshProxy } = useAppData();
const { verge } = useVerge();
// 统一代理选择器 // 统一代理选择器
const { handleSelectChange } = useProxySelection({ const { handleSelectChange } = useProxySelection({
@@ -172,6 +175,7 @@ export const CurrentProxyCard = () => {
// 根据模式确定初始组 // 根据模式确定初始组
if (isGlobalMode) { if (isGlobalMode) {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
selection: { selection: {
@@ -180,6 +184,7 @@ export const CurrentProxyCard = () => {
}, },
})); }));
} else if (isDirectMode) { } else if (isDirectMode) {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
selection: { selection: {
@@ -189,6 +194,7 @@ export const CurrentProxyCard = () => {
})); }));
} else { } else {
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP); const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
selection: { selection: {
@@ -203,6 +209,7 @@ export const CurrentProxyCard = () => {
useEffect(() => { useEffect(() => {
if (!proxies) return; if (!proxies) return;
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => { setState((prev) => {
// 只保留 Selector 类型的组用于选择 // 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups const filteredGroups = proxies.groups
@@ -270,16 +277,23 @@ export const CurrentProxyCard = () => {
}, [proxies, isGlobalMode, isDirectMode]); }, [proxies, isGlobalMode, isDirectMode]);
// 使用防抖包装状态更新 // 使用防抖包装状态更新
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedSetState = useCallback( const debouncedSetState = useCallback(
debounce((updateFn: (prev: ProxyState) => ProxyState) => { (updateFn: (prev: ProxyState) => ProxyState) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setState(updateFn); setState(updateFn);
}, 300), }, 300);
[], },
[setState],
); );
// 处理代理组变更 // 处理代理组变更
const handleGroupChange = useCallback( const handleGroupChange = useCallback(
(event: SelectChangeEvent) => { (event: SelectChangeEvent<string>) => {
if (isGlobalMode || isDirectMode) return; if (isGlobalMode || isDirectMode) return;
const newGroup = event.target.value; const newGroup = event.target.value;
@@ -314,7 +328,7 @@ export const CurrentProxyCard = () => {
// 处理代理节点变更 // 处理代理节点变更
const handleProxyChange = useCallback( const handleProxyChange = useCallback(
(event: SelectChangeEvent) => { (event: SelectChangeEvent<string>) => {
if (isDirectMode) return; if (isDirectMode) return;
const newProxy = event.target.value; const newProxy = event.target.value;
@@ -399,6 +413,85 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_SORT_TYPE, newSortType.toString()); localStorage.setItem(STORAGE_KEY_SORT_TYPE, newSortType.toString());
}, [sortType]); }, [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( const sortProxies = useCallback(
(proxies: ProxyOption[]) => { (proxies: ProxyOption[]) => {
@@ -474,7 +567,7 @@ export const CurrentProxyCard = () => {
]); ]);
// 获取排序图标 // 获取排序图标
const getSortIcon = () => { const getSortIcon = (): React.ReactElement => {
switch (sortType) { switch (sortType) {
case 1: case 1:
return <AccessTimeRounded fontSize="small" />; return <AccessTimeRounded fontSize="small" />;
@@ -486,7 +579,7 @@ export const CurrentProxyCard = () => {
}; };
// 获取排序提示文本 // 获取排序提示文本
const getSortTooltip = () => { const getSortTooltip = (): string => {
switch (sortType) { switch (sortType) {
case 0: case 0:
return t("Sort by default"); return t("Sort by default");
@@ -517,13 +610,24 @@ export const CurrentProxyCard = () => {
} }
iconColor={currentProxy ? "primary" : undefined} iconColor={currentProxy ? "primary" : undefined}
action={ 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()}> <Tooltip title={getSortTooltip()}>
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
onClick={handleSortTypeChange} onClick={handleSortTypeChange}
sx={{ mr: 1 }}
> >
{getSortIcon()} {getSortIcon()}
</IconButton> </IconButton>
@@ -657,7 +761,7 @@ export const CurrentProxyCard = () => {
> >
{isDirectMode {isDirectMode
? null ? null
: proxyOptions.map((proxy, index) => { : proxyOptions.map((proxy) => {
const delayValue = const delayValue =
state.proxyData.records[proxy.name] && state.proxyData.records[proxy.name] &&
state.selection.group state.selection.group
@@ -668,7 +772,7 @@ export const CurrentProxyCard = () => {
: -1; : -1;
return ( return (
<MenuItem <MenuItem
key={`${proxy.name}-${index}`} key={proxy.name}
value={proxy.name} value={proxy.name}
sx={{ sx={{
display: "flex", display: "flex",