diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 3bc4b298..7f7b4930 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next"; import { Box, Typography, - Stack, Chip, Button, alpha, @@ -16,7 +15,6 @@ import { } from "@mui/material"; import { useEffect, useState } from "react"; import { - RouterOutlined, SignalWifi4Bar as SignalStrong, SignalWifi3Bar as SignalGood, SignalWifi2Bar as SignalMedium, @@ -28,8 +26,14 @@ import { import { useNavigate } from "react-router-dom"; import { useCurrentProxy } from "@/hooks/use-current-proxy"; 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 { useVerge } from "@/hooks/use-verge"; // 本地存储的键名 const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; @@ -121,6 +125,7 @@ export const CurrentProxyCard = () => { useCurrentProxy(); const navigate = useNavigate(); const theme = useTheme(); + const { verge } = useVerge(); // 判断模式 const isGlobalMode = mode === "global"; @@ -341,11 +346,23 @@ export const CurrentProxyCard = () => { const refreshProxyData = async () => { try { 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) { const globalNow = data.global.now || ""; - setSelectedProxy(globalNow); if (globalNow && data.records[globalNow]) { @@ -359,24 +376,48 @@ export const CurrentProxyCard = () => { setProxyOptions(options); } - // 更新直连代理信息 - if (isDirectMode && data.records["DIRECT"]) { + else if (isDirectMode && data.records["DIRECT"]) { setDirectProxy(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) { console.error("刷新代理信息失败", error); } }; - // 每隔一段时间刷新特殊模式下的代理信息 + // 每隔一段时间刷新代理信息 - 修改为在所有模式下都刷新 useEffect(() => { - if (!isGlobalMode && !isDirectMode) return; + // 初始刷新一次 + refreshProxyData(); - const refreshInterval = setInterval(refreshProxyData, 3000); + // 定期刷新所有模式下的代理信息 + const refreshInterval = setInterval(refreshProxyData, 2000); return () => clearInterval(refreshInterval); - }, [isGlobalMode, isDirectMode]); + }, [isGlobalMode, isDirectMode, selectedGroup]); // 依赖项添加selectedGroup以便在切换组时重新设置定时器 // 处理代理组变更 const handleGroupChange = (event: SelectChangeEvent) => { @@ -393,6 +434,8 @@ export const CurrentProxyCard = () => { if (isDirectMode) return; const newProxy = event.target.value; + const previousProxy = selectedProxy; // 保存变更前的代理节点名称 + setSelectedProxy(newProxy); // 更新显示的代理节点信息 @@ -403,6 +446,18 @@ export const CurrentProxyCard = () => { try { // 更新代理设置 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(() => { refreshProxy(); if (isGlobalMode || isDirectMode) { diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 8feadf3a..ed961884 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback, memo } from "react"; import { useTranslation } from "react-i18next"; import { 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 { clashInfo } = useClashInfo(); - const { verge } = useVerge(); - const trafficRef = useRef(null); - const pageVisible = useVisibility(); - const [isDebug, setIsDebug] = useState(false); - const [trafficStats, setTrafficStats] = useState({ - 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({ - up: 0, - down: 0, - }); - const [memoryData, setMemoryData] = useState({ 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 palette = theme.palette; @@ -224,15 +86,8 @@ export const EnhancedTrafficStats = () => { } return palette.primary.main; }; - - // 统计卡片组件 - const CompactStatCard = ({ - icon, - title, - value, - unit, - color, - }: StatCardProps) => ( + + return ( { borderRadius: 2, bgcolor: alpha(getColorFromPalette(color), 0.05), border: `1px solid ${alpha(getColorFromPalette(color), 0.15)}`, - //height: "80px", padding: "8px", transition: "all 0.2s ease-in-out", - cursor: "pointer", - "&:hover": { + cursor: onClick ? "pointer" : "default", + "&:hover": onClick ? { bgcolor: alpha(getColorFromPalette(color), 0.1), border: `1px solid ${alpha(getColorFromPalette(color), 0.3)}`, boxShadow: `0 4px 8px rgba(0,0,0,0.05)`, - }, + } : {}, }} + onClick={onClick} > {/* 图标容器 */} { ); +}); + +// 添加显示名称 +CompactStatCard.displayName = "CompactStatCard"; + +export const EnhancedTrafficStats = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { clashInfo } = useClashInfo(); + const { verge } = useVerge(); + const trafficRef = useRef(null); + const pageVisible = useVisibility(); + const [isDebug, setIsDebug] = useState(false); + + // 为流量数据和内存数据准备状态 + const [trafficData, setTrafficData] = useState({ + up: 0, + down: 0, + }); + const [memoryData, setMemoryData] = useState({ inuse: 0 }); + const [trafficStats, setTrafficStats] = useState({ + uploadTotal: 0, + downloadTotal: 0, + activeConnections: 0, + }); + + // 是否显示流量图表 + const trafficGraph = verge?.traffic_graph ?? true; + + // WebSocket引用 + const trafficSocketRef = useRef | null>(null); + const memorySocketRef = useRef | 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; return ( @@ -328,7 +380,7 @@ export const EnhancedTrafficStats = () => { ); - }; + }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); // 统计卡片配置 const statCards = [ @@ -373,7 +425,7 @@ export const EnhancedTrafficStats = () => { value: inuse, unit: inuseUnit, color: "error" as const, - onClick: isDebug ? async () => await gc() : undefined, + onClick: isDebug ? handleGarbageCollection : undefined, }, ]; @@ -385,7 +437,7 @@ export const EnhancedTrafficStats = () => { {/* 统计卡片区域 */} {statCards.map((card, index) => ( - + ))}