fix: sync proxy node selection

This commit is contained in:
wonfen 2025-03-16 14:24:58 +08:00
parent f4cb978118
commit 79bb0f29f9
2 changed files with 286 additions and 179 deletions

View File

@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
import { import {
Box, Box,
Typography, Typography,
Stack,
Chip, Chip,
Button, Button,
alpha, alpha,
@ -16,7 +15,6 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
RouterOutlined,
SignalWifi4Bar as SignalStrong, SignalWifi4Bar as SignalStrong,
SignalWifi3Bar as SignalGood, SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium, SignalWifi2Bar as SignalMedium,
@ -28,8 +26,14 @@ import {
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useCurrentProxy } from "@/hooks/use-current-proxy"; import { useCurrentProxy } from "@/hooks/use-current-proxy";
import { EnhancedCard } from "@/components/home/enhanced-card"; import { EnhancedCard } from "@/components/home/enhanced-card";
import { getProxies, updateProxy } from "@/services/api"; import {
getProxies,
updateProxy,
getConnections,
deleteConnection,
} from "@/services/api";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
// 本地存储的键名 // 本地存储的键名
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
@ -121,6 +125,7 @@ export const CurrentProxyCard = () => {
useCurrentProxy(); useCurrentProxy();
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const { verge } = useVerge();
// 判断模式 // 判断模式
const isGlobalMode = mode === "global"; const isGlobalMode = mode === "global";
@ -341,11 +346,23 @@ export const CurrentProxyCard = () => {
const refreshProxyData = async () => { const refreshProxyData = async () => {
try { try {
const data = await getProxies(); const data = await getProxies();
// 更新所有代理记录
setRecords(data.records);
// 更新代理组信息
const filteredGroups = data.groups
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
.map((g) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p) => p.name),
}));
setGroups(filteredGroups);
// 检查并更新全局代理信息 // 检查并更新全局代理信息
if (isGlobalMode && data.global) { if (isGlobalMode && data.global) {
const globalNow = data.global.now || ""; const globalNow = data.global.now || "";
setSelectedProxy(globalNow); setSelectedProxy(globalNow);
if (globalNow && data.records[globalNow]) { if (globalNow && data.records[globalNow]) {
@ -359,24 +376,48 @@ export const CurrentProxyCard = () => {
setProxyOptions(options); setProxyOptions(options);
} }
// 更新直连代理信息 // 更新直连代理信息
if (isDirectMode && data.records["DIRECT"]) { else if (isDirectMode && data.records["DIRECT"]) {
setDirectProxy(data.records["DIRECT"]); setDirectProxy(data.records["DIRECT"]);
setDisplayProxy(data.records["DIRECT"]); setDisplayProxy(data.records["DIRECT"]);
} }
// 更新普通模式下当前选中组的信息
else {
const currentGroup = filteredGroups.find(
(g) => g.name === selectedGroup,
);
if (currentGroup) {
// 如果当前选中的代理节点与组中的now不一致则需要更新
if (currentGroup.now !== selectedProxy) {
setSelectedProxy(currentGroup.now);
if (data.records[currentGroup.now]) {
setDisplayProxy(data.records[currentGroup.now]);
}
}
// 更新代理选项
const options = currentGroup.all.map((proxyName) => ({
name: proxyName,
}));
setProxyOptions(options);
}
}
} catch (error) { } catch (error) {
console.error("刷新代理信息失败", error); console.error("刷新代理信息失败", error);
} }
}; };
// 每隔一段时间刷新特殊模式下的代理信息 // 每隔一段时间刷新代理信息 - 修改为在所有模式下都刷新
useEffect(() => { useEffect(() => {
if (!isGlobalMode && !isDirectMode) return; // 初始刷新一次
refreshProxyData();
const refreshInterval = setInterval(refreshProxyData, 3000); // 定期刷新所有模式下的代理信息
const refreshInterval = setInterval(refreshProxyData, 2000);
return () => clearInterval(refreshInterval); return () => clearInterval(refreshInterval);
}, [isGlobalMode, isDirectMode]); }, [isGlobalMode, isDirectMode, selectedGroup]); // 依赖项添加selectedGroup以便在切换组时重新设置定时器
// 处理代理组变更 // 处理代理组变更
const handleGroupChange = (event: SelectChangeEvent) => { const handleGroupChange = (event: SelectChangeEvent) => {
@ -393,6 +434,8 @@ export const CurrentProxyCard = () => {
if (isDirectMode) return; if (isDirectMode) return;
const newProxy = event.target.value; const newProxy = event.target.value;
const previousProxy = selectedProxy; // 保存变更前的代理节点名称
setSelectedProxy(newProxy); setSelectedProxy(newProxy);
// 更新显示的代理节点信息 // 更新显示的代理节点信息
@ -403,6 +446,18 @@ export const CurrentProxyCard = () => {
try { try {
// 更新代理设置 // 更新代理设置
await updateProxy(selectedGroup, newProxy); await updateProxy(selectedGroup, newProxy);
// 添加断开连接逻辑 - 与proxy-groups.tsx中的逻辑相同
if (verge?.auto_close_connection && previousProxy) {
getConnections().then(({ connections }) => {
connections.forEach((conn) => {
if (conn.chains.includes(previousProxy)) {
deleteConnection(conn.id);
}
});
});
}
setTimeout(() => { setTimeout(() => {
refreshProxy(); refreshProxy();
if (isGlobalMode || isDirectMode) { if (isGlobalMode || isDirectMode) {

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef, useCallback, memo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Typography, Typography,
@ -60,158 +60,20 @@ declare global {
} }
} }
export const EnhancedTrafficStats = () => { // 控制更新频率
const { t } = useTranslation(); const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
// 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(({
icon,
title,
value,
unit,
color,
onClick,
}: StatCardProps) => {
const theme = useTheme(); const theme = useTheme();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
const trafficRef = useRef<EnhancedTrafficGraphRef>(null);
const pageVisible = useVisibility();
const [isDebug, setIsDebug] = useState(false);
const [trafficStats, setTrafficStats] = useState<TrafficStatData>({
uploadTotal: 0,
downloadTotal: 0,
activeConnections: 0,
});
// 是否显示流量图表
const trafficGraph = verge?.traffic_graph ?? true;
// 获取连接数据
const fetchConnections = async () => {
try {
const connections = await getConnections();
if (connections && connections.connections) {
const uploadTotal = connections.connections.reduce(
(sum, conn) => sum + conn.upload,
0,
);
const downloadTotal = connections.connections.reduce(
(sum, conn) => sum + conn.download,
0,
);
setTrafficStats({
uploadTotal,
downloadTotal,
activeConnections: connections.connections.length,
});
}
} catch (err) {
console.error("Failed to fetch connections:", err);
}
};
// 定期更新连接数据
useEffect(() => {
if (pageVisible) {
fetchConnections();
const intervalId = setInterval(fetchConnections, 5000);
return () => clearInterval(intervalId);
}
}, [pageVisible]);
// 检查是否支持调试
useEffect(() => {
isDebugEnabled().then((flag) => setIsDebug(flag));
}, []);
// 为流量数据和内存数据准备状态
const [trafficData, setTrafficData] = useState<ITrafficItem>({
up: 0,
down: 0,
});
const [memoryData, setMemoryData] = useState<MemoryUsage>({ inuse: 0 });
// 使用 WebSocket 连接获取流量数据
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server, secret = "" } = clashInfo;
if (!server) return;
const socket = createAuthSockette(`${server}/traffic`, secret, {
onmessage(event) {
try {
const data = JSON.parse(event.data) as ITrafficItem;
if (
data &&
typeof data.up === "number" &&
typeof data.down === "number"
) {
setTrafficData({
up: isNaN(data.up) ? 0 : data.up,
down: isNaN(data.down) ? 0 : data.down,
});
if (trafficRef.current) {
const lastData = {
up: isNaN(data.up) ? 0 : data.up,
down: isNaN(data.down) ? 0 : data.down,
};
if (!window.lastTrafficData) {
window.lastTrafficData = { ...lastData };
}
trafficRef.current.appendData({
up: lastData.up,
down: lastData.down,
timestamp: Date.now(),
});
window.lastTrafficData = { ...lastData };
if (window.animationFrameId) {
cancelAnimationFrame(window.animationFrameId);
window.animationFrameId = undefined;
}
}
}
} catch (err) {
console.error("[Traffic] 解析数据错误:", err);
}
},
});
return () => socket.close();
}, [clashInfo, pageVisible]);
// 使用 WebSocket 连接获取内存数据
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server, secret = "" } = clashInfo;
if (!server) return;
const socket = createAuthSockette(`${server}/memory`, secret, {
onmessage(event) {
try {
const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") {
setMemoryData({
inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit,
});
}
} catch (err) {
console.error("[Memory] 解析数据错误:", err);
}
},
});
return () => socket.close();
}, [clashInfo, pageVisible]);
// 解析流量数据
const [up, upUnit] = parseTraffic(trafficData.up);
const [down, downUnit] = parseTraffic(trafficData.down);
const [inuse, inuseUnit] = parseTraffic(memoryData.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(trafficStats.uploadTotal);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
trafficStats.downloadTotal,
);
// 获取调色板颜色 // 获取调色板颜色
const getColorFromPalette = (colorName: string) => { const getColorFromPalette = (colorName: string) => {
const palette = theme.palette; const palette = theme.palette;
@ -224,15 +86,8 @@ export const EnhancedTrafficStats = () => {
} }
return palette.primary.main; return palette.primary.main;
}; };
// 统计卡片组件 return (
const CompactStatCard = ({
icon,
title,
value,
unit,
color,
}: StatCardProps) => (
<Paper <Paper
elevation={0} elevation={0}
sx={{ sx={{
@ -241,16 +96,16 @@ export const EnhancedTrafficStats = () => {
borderRadius: 2, borderRadius: 2,
bgcolor: alpha(getColorFromPalette(color), 0.05), bgcolor: alpha(getColorFromPalette(color), 0.05),
border: `1px solid ${alpha(getColorFromPalette(color), 0.15)}`, border: `1px solid ${alpha(getColorFromPalette(color), 0.15)}`,
//height: "80px",
padding: "8px", padding: "8px",
transition: "all 0.2s ease-in-out", transition: "all 0.2s ease-in-out",
cursor: "pointer", cursor: onClick ? "pointer" : "default",
"&:hover": { "&:hover": onClick ? {
bgcolor: alpha(getColorFromPalette(color), 0.1), bgcolor: alpha(getColorFromPalette(color), 0.1),
border: `1px solid ${alpha(getColorFromPalette(color), 0.3)}`, border: `1px solid ${alpha(getColorFromPalette(color), 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`, boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
}, } : {},
}} }}
onClick={onClick}
> >
{/* 图标容器 */} {/* 图标容器 */}
<Grid <Grid
@ -287,9 +142,206 @@ export const EnhancedTrafficStats = () => {
</Grid> </Grid>
</Paper> </Paper>
); );
});
// 添加显示名称
CompactStatCard.displayName = "CompactStatCard";
export const EnhancedTrafficStats = () => {
const { t } = useTranslation();
const theme = useTheme();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
const trafficRef = useRef<EnhancedTrafficGraphRef>(null);
const pageVisible = useVisibility();
const [isDebug, setIsDebug] = useState(false);
// 为流量数据和内存数据准备状态
const [trafficData, setTrafficData] = useState<ITrafficItem>({
up: 0,
down: 0,
});
const [memoryData, setMemoryData] = useState<MemoryUsage>({ inuse: 0 });
const [trafficStats, setTrafficStats] = useState<TrafficStatData>({
uploadTotal: 0,
downloadTotal: 0,
activeConnections: 0,
});
// 是否显示流量图表
const trafficGraph = verge?.traffic_graph ?? true;
// WebSocket引用
const trafficSocketRef = useRef<ReturnType<typeof createAuthSockette> | null>(null);
const memorySocketRef = useRef<ReturnType<typeof createAuthSockette> | null>(null);
// 获取连接数据
const fetchConnections = useCallback(async () => {
if (!pageVisible) return;
try {
const connections = await getConnections();
if (connections && connections.connections) {
const uploadTotal = connections.connections.reduce(
(sum, conn) => sum + conn.upload,
0,
);
const downloadTotal = connections.connections.reduce(
(sum, conn) => sum + conn.download,
0,
);
setTrafficStats({
uploadTotal,
downloadTotal,
activeConnections: connections.connections.length,
});
}
} catch (err) {
console.error("Failed to fetch connections:", err);
}
}, [pageVisible]);
// 定期更新连接数据
useEffect(() => {
if (pageVisible) {
fetchConnections();
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
return () => clearInterval(intervalId);
}
}, [pageVisible, fetchConnections]);
// 检查是否支持调试
useEffect(() => {
isDebugEnabled().then((flag) => setIsDebug(flag));
}, []);
// 处理流量数据更新
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
try {
const data = JSON.parse(event.data) as ITrafficItem;
if (
data &&
typeof data.up === "number" &&
typeof data.down === "number"
) {
// 验证数据有效性防止NaN
const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down;
setTrafficData({
up: safeUp,
down: safeDown,
});
// 更新图表数据
if (trafficRef.current) {
trafficRef.current.appendData({
up: safeUp,
down: safeDown,
timestamp: Date.now(),
});
// 清除之前可能存在的动画帧
if (window.animationFrameId) {
cancelAnimationFrame(window.animationFrameId);
window.animationFrameId = undefined;
}
}
}
} catch (err) {
console.error("[Traffic] 解析数据错误:", err);
}
}, []);
// 处理内存数据更新
const handleMemoryUpdate = useCallback((event: MessageEvent) => {
try {
const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") {
setMemoryData({
inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit,
});
}
} catch (err) {
console.error("[Memory] 解析数据错误:", err);
}
}, []);
// 使用 WebSocket 连接获取流量数据
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server, secret = "" } = clashInfo;
if (!server) return;
// 关闭现有连接
if (trafficSocketRef.current) {
trafficSocketRef.current.close();
}
// 创建新连接
trafficSocketRef.current = createAuthSockette(`${server}/traffic`, secret, {
onmessage: handleTrafficUpdate,
});
return () => {
if (trafficSocketRef.current) {
trafficSocketRef.current.close();
trafficSocketRef.current = null;
}
};
}, [clashInfo, pageVisible, handleTrafficUpdate]);
// 使用 WebSocket 连接获取内存数据
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server, secret = "" } = clashInfo;
if (!server) return;
// 关闭现有连接
if (memorySocketRef.current) {
memorySocketRef.current.close();
}
// 创建新连接
memorySocketRef.current = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate,
});
return () => {
if (memorySocketRef.current) {
memorySocketRef.current.close();
memorySocketRef.current = null;
}
};
}, [clashInfo, pageVisible, handleMemoryUpdate]);
// 解析流量数据
const [up, upUnit] = parseTraffic(trafficData.up);
const [down, downUnit] = parseTraffic(trafficData.down);
const [inuse, inuseUnit] = parseTraffic(memoryData.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(trafficStats.uploadTotal);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
trafficStats.downloadTotal,
);
// 执行垃圾回收
const handleGarbageCollection = useCallback(async () => {
if (isDebug) {
try {
await gc();
console.log("[Debug] 垃圾回收已执行");
} catch (err) {
console.error("[Debug] 垃圾回收失败:", err);
}
}
}, [isDebug]);
// 渲染流量图表 // 渲染流量图表
const renderTrafficGraph = () => { const renderTrafficGraph = useCallback(() => {
if (!trafficGraph || !pageVisible) return null; if (!trafficGraph || !pageVisible) return null;
return ( return (
@ -328,7 +380,7 @@ export const EnhancedTrafficStats = () => {
</div> </div>
</Paper> </Paper>
); );
}; }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
// 统计卡片配置 // 统计卡片配置
const statCards = [ const statCards = [
@ -373,7 +425,7 @@ export const EnhancedTrafficStats = () => {
value: inuse, value: inuse,
unit: inuseUnit, unit: inuseUnit,
color: "error" as const, color: "error" as const,
onClick: isDebug ? async () => await gc() : undefined, onClick: isDebug ? handleGarbageCollection : undefined,
}, },
]; ];
@ -385,7 +437,7 @@ export const EnhancedTrafficStats = () => {
</Grid> </Grid>
{/* 统计卡片区域 */} {/* 统计卡片区域 */}
{statCards.map((card, index) => ( {statCards.map((card, index) => (
<Grid size={4}> <Grid key={index} size={4}>
<CompactStatCard {...card} /> <CompactStatCard {...card} />
</Grid> </Grid>
))} ))}