mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
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 {
|
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) => {
|
||||||
setState(updateFn);
|
if (timeoutRef.current) {
|
||||||
}, 300),
|
clearTimeout(timeoutRef.current);
|
||||||
[],
|
}
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setState(updateFn);
|
||||||
|
}, 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",
|
||||||
|
|||||||
Reference in New Issue
Block a user