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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user