diff --git a/UPDATELOG.md b/UPDATELOG.md index e8b3041c..d7208e19 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -4,8 +4,12 @@ - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 ### 2.2.3-alpha 相对于 2.2.2 +#### 修复了: + - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题 + #### 优化 - 重构了内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 + - 集中管理应用数据,优化数据获取和刷新逻辑 ## v2.2.2 @@ -18,10 +22,10 @@ #### 已知问题 - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 -### 2.2.2 相对于 2.2.1(已下架不在提供) +### 2.2.2 相对于 2.2.1(已下架不再提供) #### 修复了: - 弹黑框的问题(原因是服务崩溃触发重装机制) - - MacOS进入轻量模式以后影藏Dock图标 + - MacOS进入轻量模式以后隐藏Dock图标 - 增加轻量模式缺失的tray翻译 - Linux下的窗口边框被削掉的问题 @@ -31,7 +35,7 @@ - 增加服务模式下的僵尸进程清理机制 - 新增当服务模式多次尝试失败后自动回退至用户空间模式 -### 2.2.1 相对于 2.2.0(已下架不在提供) +### 2.2.1 相对于 2.2.0(已下架不再提供) #### 修复了: 1. **首页** - 修复 Direct 模式首页无法渲染 @@ -62,7 +66,7 @@ --- -## 2.2.0(已下架不在提供) +## 2.2.0(已下架不再提供) #### 新增功能 1. **首页** @@ -141,14 +145,14 @@ 感谢 Tychristine 对社区群组管理做出的重大贡献! -##### 2.1.2相对2.1.1(已下架不在提供)更新了: +##### 2.1.2相对2.1.1(已下架不再提供)更新了: - 无法更新和签名验证失败的问题(该死的CDN缓存) - 设置菜单区分Verge基本设置和高级设置 - 增加v2 Updater的更多功能和权限 - 退出Verge后Tun代理状态仍保留的问题 -##### 2.1.1相对2.1.0(已下架不在提供)更新了: +##### 2.1.1相对2.1.0(已下架不再提供)更新了: - 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限) - MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies) diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..e3524f25 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,13 @@ +import { AppDataProvider } from "./providers/app-data-provider"; +import React from "react"; +import Layout from "./pages/_layout"; + +function App() { + return ( + + + + ); +} + +export default App; \ No newline at end of file diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 28f50bf4..cefb4f2f 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -1,13 +1,10 @@ import { useTranslation } from "react-i18next"; import { Typography, Stack, Divider } from "@mui/material"; import { DeveloperBoardOutlined } from "@mui/icons-material"; -import { useClashInfo } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash"; import { EnhancedCard } from "./enhanced-card"; -import useSWR from "swr"; -import { getRules } from "@/services/api"; -import { getAppUptime, getSystemProxy } from "@/services/cmds"; -import { useMemo, useState, useEffect } from "react"; +import { useMemo } from "react"; +import { useAppData } from "@/providers/app-data-provider"; // 将毫秒转换为时:分:秒格式的函数 const formatUptime = (uptimeMs: number) => { @@ -19,37 +16,15 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation(); - const { clashInfo } = useClashInfo(); const { version: clashVersion } = useClash(); - const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null); - const [rules, setRules] = useState([]); - - // 使用SWR获取应用运行时间,降低更新频率 - const { data: uptimeMs = 0 } = useSWR( - "appUptime", - getAppUptime, - { - refreshInterval: 1000, - revalidateOnFocus: false, - dedupingInterval: 1000, - }, - ); - - // 在组件加载时获取系统代理信息和规则数据 - useEffect(() => { - // 获取系统代理信息 - getSystemProxy().then(setSysproxy); - - // 获取规则数据 - getRules().then(setRules).catch(() => setRules([])); - }, []); + const { clashConfig, sysproxy, rules, uptime } = useAppData(); // 使用useMemo缓存格式化后的uptime,避免频繁计算 - const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]); + const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]); // 使用备忘录组件内容,减少重新渲染 const cardContent = useMemo(() => { - if (!clashInfo) return null; + if (!clashConfig) return null; return ( @@ -76,7 +51,7 @@ export const ClashInfoCard = () => { {t("Mixed Port")} - {clashInfo.mixed_port || "-"} + {clashConfig["mixed-port"] || "-"} @@ -85,7 +60,7 @@ export const ClashInfoCard = () => { {t("Uptime")} - {uptime} + {formattedUptime} @@ -99,7 +74,7 @@ export const ClashInfoCard = () => { ); - }, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]); + }, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]); return ( { const { t } = useTranslation(); const { verge } = useVerge(); - - // 获取当前Clash配置 - const { data: clashConfig, mutate: mutateClash } = useSWR( - "getClashConfig", - getClashConfig, - { - revalidateOnFocus: false, - revalidateIfStale: true, - dedupingInterval: 1000, - errorRetryInterval: 5000 - } - ); + const { clashConfig, refreshProxy } = useAppData(); // 支持的模式列表 const modeList = useMemo(() => ["rule", "global", "direct"] as const, []); @@ -50,7 +39,8 @@ export const ClashModeCard = () => { try { await patchClashMode(mode); - mutateClash(); + // 使用共享的刷新方法 + refreshProxy(); } catch (error) { console.error("Failed to change mode:", error); } diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index f760fd9b..9a27098d 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -13,7 +13,7 @@ import { SelectChangeEvent, Tooltip, } from "@mui/material"; -import { useEffect, useState, useMemo, useCallback, useRef } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { SignalWifi4Bar as SignalStrong, SignalWifi3Bar as SignalGood, @@ -24,16 +24,11 @@ import { ChevronRight, } from "@mui/icons-material"; import { useNavigate } from "react-router-dom"; -import { useCurrentProxy } from "@/hooks/use-current-proxy"; import { EnhancedCard } from "@/components/home/enhanced-card"; -import { - getProxies, - updateProxy, - getConnections, - deleteConnection, -} from "@/services/api"; +import { updateProxy, deleteConnection } from "@/services/api"; import delayManager from "@/services/delay"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-provider"; // 本地存储的键名 const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; @@ -92,21 +87,16 @@ function debounce(fn: Function, ms = 100) { export const CurrentProxyCard = () => { const { t } = useTranslation(); - const { currentProxy, primaryGroupName, mode, refreshProxy } = - useCurrentProxy(); const navigate = useNavigate(); const theme = useTheme(); const { verge } = useVerge(); + const { proxies, connections, clashConfig, refreshProxy } = useAppData(); // 判断模式 + const mode = clashConfig?.mode?.toLowerCase() || "rule"; const isGlobalMode = mode === "global"; const isDirectMode = mode === "direct"; - - // 使用 useRef 存储最后一次刷新时间和是否正在刷新 - const lastRefreshRef = useRef(0); - const isRefreshingRef = useRef(false); - const pendingRefreshRef = useRef(false); - + // 定义状态类型 type ProxyState = { proxyData: { @@ -139,6 +129,32 @@ export const CurrentProxyCard = () => { // 初始化选择的组 useEffect(() => { + if (!proxies) return; + + // 提取primaryGroupName + const getPrimaryGroupName = () => { + if (!proxies?.groups?.length) return ""; + + // 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组) + const primaryKeywords = [ + "auto", + "select", + "proxy", + "节点选择", + "自动选择", + ]; + const primaryGroup = + proxies.groups.find((group: { name: string }) => + primaryKeywords.some((keyword) => + group.name.toLowerCase().includes(keyword.toLowerCase()), + ), + ) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0]; + + return primaryGroup?.name || ""; + }; + + const primaryGroupName = getPrimaryGroupName(); + // 根据模式确定初始组 if (isGlobalMode) { setState((prev) => ({ @@ -166,148 +182,79 @@ export const CurrentProxyCard = () => { }, })); } - }, [isGlobalMode, isDirectMode, primaryGroupName]); + }, [isGlobalMode, isDirectMode, proxies]); - // 带锁的代理数据获取函数,防止并发请求 - const fetchProxyData = useCallback( - async (force = false) => { - // 防止重复请求 - if (isRefreshingRef.current) { - pendingRefreshRef.current = true; - return; - } + // 监听代理数据变化,更新状态 + useEffect(() => { + if (!proxies) return; + + // 使用函数式更新确保状态更新的原子性 + setState((prev) => { + // 过滤和格式化组 + const filteredGroups = proxies.groups + .filter((g: { name: string }) => g.name !== "DIRECT" && g.name !== "REJECT") + .map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({ + name: g.name, + now: g.now || "", + all: g.all.map((p: { name: string }) => p.name), + })); + + let newProxy = ""; + let newDisplayProxy = null; + let newGroup = prev.selection.group; - // 检查刷新间隔,强制增加最小间隔 - const now = Date.now(); - if (!force && now - lastRefreshRef.current < 1500) { - return; - } + // 根据模式确定新代理 + if (isDirectMode) { + newGroup = "DIRECT"; + newProxy = "DIRECT"; + newDisplayProxy = proxies.records?.DIRECT || null; + } else if (isGlobalMode && proxies.global) { + newGroup = "GLOBAL"; + newProxy = proxies.global.now || ""; + newDisplayProxy = proxies.records?.[newProxy] || null; + } else { + // 普通模式 - 检查当前选择的组是否存在 + const currentGroup = filteredGroups.find( + (g: { name: string }) => g.name === prev.selection.group, + ); - isRefreshingRef.current = true; - lastRefreshRef.current = now; + // 如果当前组不存在或为空,自动选择第一个组 + if (!currentGroup && filteredGroups.length > 0) { + newGroup = filteredGroups[0].name; + const firstGroup = filteredGroups[0]; + newProxy = firstGroup.now; + newDisplayProxy = proxies.records?.[newProxy] || null; - try { - const data = await getProxies(); - - // 过滤和格式化组 - 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), - })); - - // 使用函数式更新确保状态更新的原子性 - setState((prev) => { - let newProxy = ""; - let newDisplayProxy = null; - let newGroup = prev.selection.group; - - // 根据模式确定新代理 - if (isDirectMode) { - newGroup = "DIRECT"; - newProxy = "DIRECT"; - newDisplayProxy = data.records?.DIRECT || null; - } else if (isGlobalMode && data.global) { - newGroup = "GLOBAL"; - newProxy = data.global.now || ""; - newDisplayProxy = data.records?.[newProxy] || null; - } else { - // 普通模式 - 检查当前选择的组是否存在 - const currentGroup = filteredGroups.find( - (g) => g.name === prev.selection.group, - ); - - // 如果当前组不存在或为空,自动选择第一个组 - if (!currentGroup && filteredGroups.length > 0) { - newGroup = filteredGroups[0].name; - const firstGroup = filteredGroups[0]; - newProxy = firstGroup.now; - newDisplayProxy = data.records?.[newProxy] || null; - - // 保存到本地存储 - if (!isGlobalMode && !isDirectMode) { - localStorage.setItem(STORAGE_KEY_GROUP, newGroup); - if (newProxy) { - localStorage.setItem(STORAGE_KEY_PROXY, newProxy); - } - } - } else if (currentGroup) { - // 使用当前组的代理 - newProxy = currentGroup.now; - newDisplayProxy = data.records?.[newProxy] || null; + // 保存到本地存储 + if (!isGlobalMode && !isDirectMode) { + localStorage.setItem(STORAGE_KEY_GROUP, newGroup); + if (newProxy) { + localStorage.setItem(STORAGE_KEY_PROXY, newProxy); } } - - // 返回新状态 - return { - proxyData: { - groups: filteredGroups, - records: data.records || {}, - globalProxy: data.global?.now || "", - directProxy: data.records?.DIRECT || null, - }, - selection: { - group: newGroup, - proxy: newProxy, - }, - displayProxy: newDisplayProxy, - }; - }); - } catch (error) { - console.error("获取代理信息失败", error); - } finally { - isRefreshingRef.current = false; - - // 处理待处理的刷新请求,但增加延迟 - if (pendingRefreshRef.current) { - pendingRefreshRef.current = false; - setTimeout(() => fetchProxyData(), 500); + } else if (currentGroup) { + // 使用当前组的代理 + newProxy = currentGroup.now; + newDisplayProxy = proxies.records?.[newProxy] || null; } } - }, - [isGlobalMode, isDirectMode], - ); - // 响应 currentProxy 变化,增加时间检查避免循环调用 - useEffect(() => { - if ( - currentProxy && - (!state.displayProxy || - (currentProxy.name !== state.displayProxy.name && - Date.now() - lastRefreshRef.current > 1000)) - ) { - fetchProxyData(true); - } - }, [currentProxy, fetchProxyData]); - - // 监听模式变化,mode变化时刷新 - useEffect(() => { - fetchProxyData(true); - }, [mode, fetchProxyData]); - - // 计算要显示的代理选项 - 使用 useMemo 优化 - const proxyOptions = useMemo(() => { - if (isDirectMode) { - return [{ name: "DIRECT" }]; - } - if (isGlobalMode && state.proxyData.records) { - // 全局模式下的选项 - return Object.keys(state.proxyData.records) - .filter((name) => name !== "DIRECT" && name !== "REJECT") - .map((name) => ({ name })); - } - - // 普通模式 - const group = state.proxyData.groups.find( - (g) => g.name === state.selection.group, - ); - if (group) { - return group.all.map((name) => ({ name })); - } - return []; - }, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]); + // 返回新状态 + return { + proxyData: { + groups: filteredGroups, + records: proxies.records || {}, + globalProxy: proxies.global?.now || "", + directProxy: proxies.records?.DIRECT || null, + }, + selection: { + group: newGroup, + proxy: newProxy, + }, + displayProxy: newDisplayProxy, + }; + }); + }, [proxies, isGlobalMode, isDirectMode]); // 使用防抖包装状态更新,避免快速连续更新,增加防抖时间 const debouncedSetState = useCallback( @@ -329,7 +276,7 @@ export const CurrentProxyCard = () => { // 获取该组当前选中的代理 setState((prev) => { - const group = prev.proxyData.groups.find((g) => g.name === newGroup); + const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup); if (group) { return { ...prev, @@ -382,20 +329,16 @@ export const CurrentProxyCard = () => { // 自动关闭连接设置 if (verge?.auto_close_connection && previousProxy) { - getConnections().then(({ connections }) => { - connections.forEach((conn) => { - if (conn.chains.includes(previousProxy)) { - deleteConnection(conn.id); - } - }); + connections.data.forEach((conn: any) => { + if (conn.chains.includes(previousProxy)) { + deleteConnection(conn.id); + } }); } // 延长刷新延迟时间 setTimeout(() => { refreshProxy(); - // 给refreshProxy一点时间完成,再触发fetchProxyData - setTimeout(() => fetchProxyData(true), 300); }, 500); } catch (error) { console.error("更新代理失败", error); @@ -408,8 +351,8 @@ export const CurrentProxyCard = () => { state.selection, verge?.auto_close_connection, refreshProxy, - fetchProxyData, debouncedSetState, + connections.data, ], ); @@ -419,11 +362,14 @@ export const CurrentProxyCard = () => { }, [navigate]); // 获取要显示的代理节点 - const proxyToDisplay = state.displayProxy || currentProxy; + const currentProxy = useMemo(() => { + // 从state中获取当前代理信息 + return state.displayProxy; + }, [state.displayProxy]); // 获取当前节点的延迟 - const currentDelay = proxyToDisplay - ? delayManager.getDelayFix(proxyToDisplay, state.selection.group) + const currentDelay = currentProxy + ? delayManager.getDelayFix(currentProxy, state.selection.group) : -1; // 获取信号图标 @@ -453,23 +399,45 @@ export const CurrentProxyCard = () => { [state.proxyData.records, state.selection.group], ); + // 计算要显示的代理选项 - 使用 useMemo 优化 + const proxyOptions = useMemo(() => { + if (isDirectMode) { + return [{ name: "DIRECT" }]; + } + if (isGlobalMode && state.proxyData.records) { + // 全局模式下的选项 + return Object.keys(state.proxyData.records) + .filter((name) => name !== "DIRECT" && name !== "REJECT") + .map((name) => ({ name })); + } + + // 普通模式 + const group = state.proxyData.groups.find( + (g: { name: string }) => g.name === state.selection.group, + ); + if (group) { + return group.all.map((name) => ({ name })); + } + return []; + }, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]); + return ( - {proxyToDisplay ? signalInfo.icon : } + {currentProxy ? signalInfo.icon : } } - iconColor={proxyToDisplay ? "primary" : undefined} + iconColor={currentProxy ? "primary" : undefined} action={ } > - {proxyToDisplay ? ( + {currentProxy ? ( {/* 代理节点信息显示 */} { > - {proxyToDisplay.name} + {currentProxy.name} { color="text.secondary" sx={{ mr: 1 }} > - {proxyToDisplay.type} + {currentProxy.type} {isGlobalMode && ( { /> )} {/* 节点特性 */} - {proxyToDisplay.udp && ( + {currentProxy.udp && ( )} - {proxyToDisplay.tfo && ( + {currentProxy.tfo && ( )} - {proxyToDisplay.xudp && ( + {currentProxy.xudp && ( )} - {proxyToDisplay.mptcp && ( + {currentProxy.mptcp && ( )} - {proxyToDisplay.smux && ( + {currentProxy.smux && ( )} {/* 显示延迟 */} - {proxyToDisplay && !isDirectMode && ( + {currentProxy && !isDirectMode && ( { const pageVisible = useVisibility(); const [isDebug, setIsDebug] = useState(false); + // 使用AppDataProvider + const { connections, uptime } = useAppData(); + // 使用单一状态对象减少状态更新次数 const [stats, setStats] = useState({ traffic: { up: 0, down: 0 }, memory: { inuse: 0, oslimit: undefined as number | undefined }, - connections: { uploadTotal: 0, downloadTotal: 0, activeConnections: 0 }, }); // 创建一个标记来追踪最后更新时间,用于节流 @@ -176,36 +179,6 @@ export const EnhancedTrafficStats = () => { memory: null as ReturnType | null, }); - // 获取连接数据 - const fetchConnections = useCallback(async () => { - if (!pageVisible) return; - - try { - const connections = await getConnections(); - if (connections) { - setStats(prev => ({ - ...prev, - connections: { - uploadTotal: connections.uploadTotal || 0, - downloadTotal: connections.downloadTotal || 0, - activeConnections: connections.connections ? connections.connections.length : 0, - } - })); - } - } catch (err) { - console.error("Failed to fetch connections:", err); - } - }, [pageVisible]); - - // 定期更新连接数据 - useEffect(() => { - if (!pageVisible) return; - - fetchConnections(); - const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL); - return () => clearInterval(intervalId); - }, [pageVisible, fetchConnections]); - // 检查是否支持调试 useEffect(() => { isDebugEnabled().then((flag) => setIsDebug(flag)); @@ -328,14 +301,14 @@ export const EnhancedTrafficStats = () => { const [up, upUnit] = parseTraffic(stats.traffic.up); const [down, downUnit] = parseTraffic(stats.traffic.down); const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse); - const [uploadTotal, uploadTotalUnit] = parseTraffic(stats.connections.uploadTotal); - const [downloadTotal, downloadTotalUnit] = parseTraffic(stats.connections.downloadTotal); + const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal); + const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal); return { up, upUnit, down, downUnit, inuse, inuseUnit, uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit }; - }, [stats]); + }, [stats, connections.uploadTotal, connections.downloadTotal]); // 渲染流量图表 - 使用useMemo缓存渲染结果 const trafficGraphComponent = useMemo(() => { @@ -398,7 +371,7 @@ export const EnhancedTrafficStats = () => { { icon: , title: t("Active Connections"), - value: stats.connections.activeConnections, + value: connections.count, unit: "", color: "success" as const, }, @@ -424,7 +397,7 @@ export const EnhancedTrafficStats = () => { color: "error" as const, onClick: isDebug ? handleGarbageCollection : undefined, }, - ], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]); + ], [t, parsedData, connections.count, isDebug, handleGarbageCollection]); return ( diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index 520913ab..52fb0409 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -27,6 +27,7 @@ import { openWebUrl, updateProfile } from "@/services/cmds"; import { useLockFn } from "ahooks"; import { Notice } from "@/components/base"; import { EnhancedCard } from "./enhanced-card"; +import { useAppData } from "@/providers/app-data-provider"; // 定义旋转动画 const round = keyframes` @@ -270,6 +271,7 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => { export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => { const { t } = useTranslation(); const navigate = useNavigate(); + const { refreshAll } = useAppData(); // 更新当前订阅 const [updating, setUpdating] = useState(false); @@ -282,6 +284,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr await updateProfile(current.uid); Notice.success(t("Update subscription successfully")); onProfileUpdated?.(); + + // 刷新首页数据 + refreshAll(); } catch (err: any) { Notice.error(err?.message || err.toString()); } finally { diff --git a/src/components/home/proxy-tun-card.tsx b/src/components/home/proxy-tun-card.tsx index 208ad962..038ab5a9 100644 --- a/src/components/home/proxy-tun-card.tsx +++ b/src/components/home/proxy-tun-card.tsx @@ -18,13 +18,8 @@ import { HelpOutlineRounded, SvgIconComponent, } from "@mui/icons-material"; -import useSWR from "swr"; -import { - getSystemProxy, - getAutotemProxy, - getRunningMode, -} from "@/services/cmds"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-provider"; const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab"; @@ -150,8 +145,7 @@ export const ProxyTunCard: FC = () => { ); // 获取代理状态信息 - const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy); - const { data: runningMode } = useSWR("getRunningMode", getRunningMode); + const { sysproxy, runningMode } = useAppData(); const { verge } = useVerge(); // 从verge配置中获取开关状态 diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 233e341c..69d61a63 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -1,195 +1,46 @@ -import dayjs from "dayjs"; -import useSWR, { mutate } from "swr"; import { useState } from "react"; -import { +import { Button, + Box, + Dialog, + DialogTitle, + DialogContent, + DialogActions, IconButton, List, ListItem, ListItemText, - styled, - Box, - alpha, Typography, Divider, LinearProgress, - keyframes, + alpha, + styled, + useTheme } from "@mui/material"; -import { RefreshRounded } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; -import { getProxyProviders, proxyProviderUpdate } from "@/services/api"; -import { BaseDialog } from "../base"; +import { useLockFn } from "ahooks"; +import { proxyProviderUpdate } from "@/services/api"; +import { useAppData } from "@/providers/app-data-provider"; +import { Notice } from "@/components/base"; +import { StorageOutlined, RefreshRounded } from "@mui/icons-material"; +import dayjs from "dayjs"; import parseTraffic from "@/utils/parse-traffic"; -const round = keyframes` - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -`; - -export const ProviderButton = () => { - const { t } = useTranslation(); - const { data } = useSWR("getProxyProviders", getProxyProviders); - - const [open, setOpen] = useState(false); - - const hasProvider = Object.keys(data || {}).length > 0; - const [updating, setUpdating] = useState( - Object.keys(data || {}).map(() => false), - ); - - const setUpdatingAt = (status: boolean, index: number) => { - setUpdating((prev) => { - const next = [...prev]; - next[index] = status; - return next; - }); - }; - const handleUpdate = async (key: string, index: number) => { - setUpdatingAt(true, index); - proxyProviderUpdate(key) - .then(async () => { - setUpdatingAt(false, index); - await mutate("getProxies"); - await mutate("getProxyProviders"); - }) - .catch(async () => { - setUpdatingAt(false, index); - await mutate("getProxies"); - await mutate("getProxyProviders"); - }); +// 定义代理提供者类型 +interface ProxyProviderItem { + name?: string; + proxies: any[]; + updatedAt: number; + vehicleType: string; + subscriptionInfo?: { + Upload: number; + Download: number; + Total: number; + Expire: number; }; +} - if (!hasProvider) return null; - - return ( - <> - - - - {t("Proxy Provider")} - - - } - contentSx={{ width: 400 }} - disableOk - cancelBtn={t("Close")} - onClose={() => setOpen(false)} - onCancel={() => setOpen(false)} - > - - {Object.entries(data || {}).map(([key, item], index) => { - const time = dayjs(item.updatedAt); - const sub = item.subscriptionInfo; - const hasSubInfo = !!sub; - const upload = sub?.Upload || 0; - const download = sub?.Download || 0; - const total = sub?.Total || 0; - const expire = sub?.Expire || 0; - const progress = Math.min( - Math.round(((download + upload) * 100) / (total + 0.01)) + 1, - 100, - ); - return ( - <> - - - - {key} - - - {item.proxies.length} - - - } - secondary={ - <> - - {item.vehicleType} - - - {t("Update At")} {time.fromNow()} - - {hasSubInfo && ( - <> - - - {parseTraffic(upload + download)} /{" "} - {parseTraffic(total)} - - - {parseExpire(expire)} - - - - 0 ? 1 : 0 }} - /> - - )} - - } - /> - - handleUpdate(key, index)} - sx={{ - animation: updating[index] - ? `1s linear infinite ${round}` - : "none", - }} - > - - - - - ); - })} - - - - ); -}; +// 样式化组件 - 类型框 const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ display: "inline-block", border: "1px solid #ccc", @@ -202,28 +53,272 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ lineHeight: 1.25, })); -const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>( - ({ theme }) => ({ - display: "inline-block", - border: "1px solid #ccc", - borderColor: alpha(theme.palette.primary.main, 0.5), - color: alpha(theme.palette.primary.main, 0.8), - borderRadius: 4, - fontSize: 10, - marginRight: "4px", - padding: "0 2px", - lineHeight: 1.25, - }), -); - -const boxStyle = { - height: 26, - display: "flex", - alignItems: "center", - justifyContent: "space-between", -}; - -function parseExpire(expire?: number) { +// 解析过期时间 +const parseExpire = (expire?: number) => { if (!expire) return "-"; return dayjs(expire * 1000).format("YYYY-MM-DD"); -} +}; + +export const ProviderButton = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const [open, setOpen] = useState(false); + const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData(); + const [updating, setUpdating] = useState>({}); + + // 检查是否有提供者 + const hasProviders = Object.keys(proxyProviders || {}).length > 0; + + // 更新单个代理提供者 + const updateProvider = useLockFn(async (name: string) => { + try { + // 设置更新状态 + setUpdating(prev => ({ ...prev, [name]: true })); + + await proxyProviderUpdate(name); + + // 刷新数据 + await refreshProxy(); + await refreshProxyProviders(); + + Notice.success(`${name} 更新成功`); + } catch (err: any) { + Notice.error(`${name} 更新失败: ${err?.message || err.toString()}`); + } finally { + // 清除更新状态 + setUpdating(prev => ({ ...prev, [name]: false })); + } + }); + + // 更新所有代理提供者 + const updateAllProviders = useLockFn(async () => { + try { + // 获取所有provider的名称 + const allProviders = Object.keys(proxyProviders || {}); + if (allProviders.length === 0) { + Notice.info("没有可更新的代理提供者"); + return; + } + + // 设置所有provider为更新中状态 + const newUpdating = allProviders.reduce((acc, key) => { + acc[key] = true; + return acc; + }, {} as Record); + setUpdating(newUpdating); + + // 改为串行逐个更新所有provider + for (const name of allProviders) { + try { + await proxyProviderUpdate(name); + // 每个更新完成后更新状态 + setUpdating(prev => ({ ...prev, [name]: false })); + } catch (err) { + console.error(`更新 ${name} 失败`, err); + // 继续执行下一个,不中断整体流程 + } + } + + // 刷新数据 + await refreshProxy(); + await refreshProxyProviders(); + + Notice.success("全部代理提供者更新成功"); + } catch (err: any) { + Notice.error(`更新失败: ${err?.message || err.toString()}`); + } finally { + // 清除所有更新状态 + setUpdating({}); + } + }); + + const handleClose = () => { + setOpen(false); + }; + + if (!hasProviders) return null; + + return ( + <> + + + + + + {t("Proxy Providers")} + + + + + + + + + {Object.entries(proxyProviders || {}).map(([key, item]) => { + const provider = item as ProxyProviderItem; + const time = dayjs(provider.updatedAt); + const isUpdating = updating[key]; + + // 订阅信息 + const sub = provider.subscriptionInfo; + const hasSubInfo = !!sub; + const upload = sub?.Upload || 0; + const download = sub?.Download || 0; + const total = sub?.Total || 0; + const expire = sub?.Expire || 0; + + // 流量使用进度 + const progress = total > 0 + ? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100) + : 0; + + return ( + { + const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; + const hoverColor = mode === "light" + ? alpha(primary.main, 0.1) + : alpha(primary.main, 0.2); + + return { + backgroundColor: bgcolor, + "&:hover": { + backgroundColor: hoverColor, + borderColor: alpha(primary.main, 0.3) + } + }; + } + ]} + > + + + {key} + + {provider.proxies.length} + + + {provider.vehicleType} + + + + + {t("Update At")}: {time.fromNow()} + + + } + secondary={ + <> + {/* 订阅信息 */} + {hasSubInfo && ( + <> + + + {parseTraffic(upload + download)} / {parseTraffic(total)} + + + {parseExpire(expire)} + + + + {/* 进度条 */} + 0 ? 1 : 0, + }} + /> + + )} + + } + /> + + + { + updateProvider(key); + }} + disabled={isUpdating} + sx={{ + animation: isUpdating ? "spin 1s linear infinite" : "none", + "@keyframes spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" } + } + }} + title={t("Update Provider") as string} + > + + + + + ); + })} + + + + + + + + + ); +}; diff --git a/src/components/proxy/proxy-render.tsx b/src/components/proxy/proxy-render.tsx index 7a18b01d..2c147246 100644 --- a/src/components/proxy/proxy-render.tsx +++ b/src/components/proxy/proxy-render.tsx @@ -25,10 +25,10 @@ import { downloadIconCache } from "@/services/cmds"; interface RenderProps { item: IRenderItem; indent: boolean; - onLocation: (group: IProxyGroupItem) => void; + onLocation: (group: IRenderItem["group"]) => void; onCheckAll: (groupName: string) => void; onHeadState: (groupName: string, patch: Partial) => void; - onChangeProxy: (group: IProxyGroupItem, proxy: IProxyItem) => void; + onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void; } export const ProxyRender = (props: RenderProps) => { diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index b875b3f2..bee151da 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -1,6 +1,4 @@ -import useSWR from "swr"; import { useEffect, useMemo, useCallback } from "react"; -import { getProxies } from "@/services/api"; import { useVerge } from "@/hooks/use-verge"; import { filterSort } from "./use-filter-sort"; import { useWindowWidth } from "./use-window-width"; @@ -9,12 +7,52 @@ import { DEFAULT_STATE, type HeadState, } from "./use-head-state"; +import { useAppData } from "@/providers/app-data-provider"; + +// 定义代理项接口 +interface IProxyItem { + name: string; + type: string; + udp: boolean; + xudp: boolean; + tfo: boolean; + mptcp: boolean; + smux: boolean; + history: { + time: string; + delay: number; + }[]; + provider?: string; + testUrl?: string; + [key: string]: any; // 添加索引签名以适应其他可能的属性 +} + +// 代理组类型 +type ProxyGroup = { + name: string; + type: string; + udp: boolean; + xudp: boolean; + tfo: boolean; + mptcp: boolean; + smux: boolean; + history: { + time: string; + delay: number; + }[]; + now: string; + all: IProxyItem[]; + hidden?: boolean; + icon?: string; + testUrl?: string; + provider?: string; +}; export interface IRenderItem { // 组 | head | item | empty | item col type: 0 | 1 | 2 | 3 | 4; key: string; - group: IProxyGroupItem; + group: ProxyGroup; proxy?: IProxyItem; col?: number; proxyCol?: IProxyItem[]; @@ -51,16 +89,8 @@ const groupProxies = (list: T[], size: number): T[][] => { }; export const useRenderList = (mode: string) => { - const { data: proxiesData, mutate: mutateProxies } = useSWR( - "getProxies", - getProxies, - { - refreshInterval: 2000, - revalidateOnFocus: false, - revalidateOnReconnect: true, - }, - ); - + // 使用全局数据提供者 + const { proxies: proxiesData, refreshProxy } = useAppData(); const { verge } = useVerge(); const { width } = useWindowWidth(); const [headStates, setHeadState] = useHeadStateNew(); @@ -80,9 +110,9 @@ export const useRenderList = (mode: string) => { (mode === "rule" && !groups.length) || (mode === "global" && proxies.length < 2) ) { - setTimeout(() => mutateProxies(), 500); + setTimeout(() => refreshProxy(), 500); } - }, [proxiesData, mode, mutateProxies]); + }, [proxiesData, mode, refreshProxy]); // 处理渲染列表 const renderList: IRenderItem[] = useMemo(() => { @@ -94,7 +124,7 @@ export const useRenderList = (mode: string) => { ? proxiesData.groups : [proxiesData.global!]; - const retList = renderGroups.flatMap((group) => { + const retList = renderGroups.flatMap((group: ProxyGroup) => { const headState = headStates[group.name] || DEFAULT_STATE; const ret: IRenderItem[] = [ { @@ -158,12 +188,12 @@ export const useRenderList = (mode: string) => { }); if (!useRule) return retList.slice(1); - return retList.filter((item) => !item.group.hidden); + return retList.filter((item: IRenderItem) => !item.group.hidden); }, [headStates, proxiesData, mode, col]); return { renderList, - onProxies: mutateProxies, + onProxies: refreshProxy, onHeadState: setHeadState, currentColumns: col, }; diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index 29cc1311..c4e758d0 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -1,170 +1,39 @@ -import dayjs from "dayjs"; -import useSWR, { mutate } from "swr"; import { useState } from "react"; -import { - Button, +import { + Button, + Box, + Dialog, + DialogTitle, + DialogContent, + DialogActions, IconButton, List, ListItem, ListItemText, Typography, - styled, - Box, - alpha, Divider, - keyframes, + alpha, + styled, + useTheme } from "@mui/material"; -import { RefreshRounded } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; -import { getRuleProviders, ruleProviderUpdate } from "@/services/api"; -import { BaseDialog } from "../base"; +import { useLockFn } from "ahooks"; +import { ruleProviderUpdate } from "@/services/api"; +import { Notice } from "@/components/base"; +import { StorageOutlined, RefreshRounded } from "@mui/icons-material"; +import { useAppData } from "@/providers/app-data-provider"; +import dayjs from "dayjs"; -const round = keyframes` - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -`; +// 定义规则提供者类型 +interface RuleProviderItem { + behavior: string; + ruleCount: number; + updatedAt: number; + vehicleType: string; +} -export const ProviderButton = () => { - const { t } = useTranslation(); - const { data } = useSWR("getRuleProviders", getRuleProviders); - - const [open, setOpen] = useState(false); - - const hasProvider = Object.keys(data || {}).length > 0; - const [updating, setUpdating] = useState( - Object.keys(data || {}).map(() => false), - ); - - const setUpdatingAt = (status: boolean, index: number) => { - setUpdating((prev) => { - const next = [...prev]; - next[index] = status; - return next; - }); - }; - const handleUpdate = async (key: string, index: number) => { - setUpdatingAt(true, index); - ruleProviderUpdate(key) - .then(async () => { - setUpdatingAt(false, index); - await mutate("getRules"); - await mutate("getRuleProviders"); - }) - .catch(async () => { - setUpdatingAt(false, index); - await mutate("getRules"); - await mutate("getRuleProviders"); - }); - }; - - if (!hasProvider) return null; - - return ( - <> - - - - {t("Rule Provider")} - - - } - contentSx={{ width: 400 }} - disableOk - cancelBtn={t("Close")} - onClose={() => setOpen(false)} - onCancel={() => setOpen(false)} - > - - {Object.entries(data || {}).map(([key, item], index) => { - const time = dayjs(item.updatedAt); - return ( - <> - - - - {key} - - - {item.ruleCount} - - - } - secondary={ - <> - - {item.vehicleType} - - - {item.behavior} - - - {t("Update At")} {time.fromNow()} - - - } - /> - - handleUpdate(key, index)} - sx={{ - animation: updating[index] - ? `1s linear infinite ${round}` - : "none", - }} - > - - - - - ); - })} - - - - ); -}; -const TypeBox = styled(Box, { - shouldForwardProp: (prop) => prop !== "component", -})<{ component?: React.ElementType }>(({ theme }) => ({ +// 辅助组件 - 类型框 +const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ display: "inline-block", border: "1px solid #ccc", borderColor: alpha(theme.palette.secondary.main, 0.5), @@ -176,16 +45,222 @@ const TypeBox = styled(Box, { lineHeight: 1.25, })); -const StyledTypeBox = styled(Box, { - shouldForwardProp: (prop) => prop !== "component", -})<{ component?: React.ElementType }>(({ theme }) => ({ - display: "inline-block", - border: "1px solid #ccc", - borderColor: alpha(theme.palette.primary.main, 0.5), - color: alpha(theme.palette.primary.main, 0.8), - borderRadius: 4, - fontSize: 10, - marginRight: "4px", - padding: "0 2px", - lineHeight: 1.25, -})); +export const ProviderButton = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const [open, setOpen] = useState(false); + const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData(); + const [updating, setUpdating] = useState>({}); + + // 检查是否有提供者 + const hasProviders = Object.keys(ruleProviders || {}).length > 0; + + // 更新单个规则提供者 + const updateProvider = useLockFn(async (name: string) => { + try { + // 设置更新状态 + setUpdating(prev => ({ ...prev, [name]: true })); + + await ruleProviderUpdate(name); + + // 刷新数据 + await refreshRules(); + await refreshRuleProviders(); + + Notice.success(`${name} 更新成功`); + } catch (err: any) { + Notice.error(`${name} 更新失败: ${err?.message || err.toString()}`); + } finally { + // 清除更新状态 + setUpdating(prev => ({ ...prev, [name]: false })); + } + }); + + // 更新所有规则提供者 + const updateAllProviders = useLockFn(async () => { + try { + // 获取所有provider的名称 + const allProviders = Object.keys(ruleProviders || {}); + if (allProviders.length === 0) { + Notice.info("没有可更新的规则提供者"); + return; + } + + // 设置所有provider为更新中状态 + const newUpdating = allProviders.reduce((acc, key) => { + acc[key] = true; + return acc; + }, {} as Record); + setUpdating(newUpdating); + + // 改为串行逐个更新所有provider + for (const name of allProviders) { + try { + await ruleProviderUpdate(name); + // 每个更新完成后更新状态 + setUpdating(prev => ({ ...prev, [name]: false })); + } catch (err) { + console.error(`更新 ${name} 失败`, err); + // 继续执行下一个,不中断整体流程 + } + } + + // 刷新数据 + await refreshRules(); + await refreshRuleProviders(); + + Notice.success("全部规则提供者更新成功"); + } catch (err: any) { + Notice.error(`更新失败: ${err?.message || err.toString()}`); + } finally { + // 清除所有更新状态 + setUpdating({}); + } + }); + + const handleClose = () => { + setOpen(false); + }; + + if (!hasProviders) return null; + + return ( + <> + + + + + + {t("Rule Providers")} + + + + + + + {Object.entries(ruleProviders || {}).map(([key, item]) => { + const provider = item as RuleProviderItem; + const time = dayjs(provider.updatedAt); + const isUpdating = updating[key]; + + return ( + { + const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; + const hoverColor = mode === "light" + ? alpha(primary.main, 0.1) + : alpha(primary.main, 0.2); + + return { + backgroundColor: bgcolor, + "&:hover": { + backgroundColor: hoverColor, + borderColor: alpha(primary.main, 0.3) + } + }; + } + ]} + > + + + {key} + + {provider.ruleCount} + + + + + {t("Update At")}: {time.fromNow()} + + + } + secondary={ + + + {provider.vehicleType} + + + {provider.behavior} + + + } + /> + + + updateProvider(key)} + disabled={isUpdating} + sx={{ + animation: isUpdating ? "spin 1s linear infinite" : "none", + "@keyframes spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" } + } + }} + title={t("Update Provider") as string} + > + + + + + ); + })} + + + + + + + + + ); +}; diff --git a/src/hooks/use-current-proxy.ts b/src/hooks/use-current-proxy.ts index 5d5f381c..84da26e5 100644 --- a/src/hooks/use-current-proxy.ts +++ b/src/hooks/use-current-proxy.ts @@ -1,32 +1,25 @@ -import useSWR from "swr"; import { useMemo } from "react"; -import { getProxies } from "@/services/api"; -import { getClashConfig } from "@/services/api"; +import { useAppData } from "@/providers/app-data-provider"; + +// 定义代理组类型 +interface ProxyGroup { + name: string; + now: string; +} // 获取当前代理节点信息的自定义Hook export const useCurrentProxy = () => { - // 获取代理信息 - const { data: proxiesData, mutate: mutateProxies } = useSWR( - "getProxies", - getProxies, - { - refreshInterval: 2000, - revalidateOnFocus: false, - revalidateOnReconnect: true, - }, - ); - - // 获取当前Clash配置(包含模式信息) - const { data: clashConfig } = useSWR("getClashConfig", getClashConfig); + // 从AppDataProvider获取数据 + const { proxies, clashConfig, refreshProxy } = useAppData(); // 获取当前模式 const currentMode = clashConfig?.mode?.toLowerCase() || "rule"; // 获取当前代理节点信息 const currentProxyInfo = useMemo(() => { - if (!proxiesData) return { currentProxy: null, primaryGroupName: null }; + if (!proxies) return { currentProxy: null, primaryGroupName: null }; - const { global, groups, records } = proxiesData; + const { global, groups, records } = proxies; // 默认信息 let primaryGroupName = "GLOBAL"; @@ -43,11 +36,11 @@ export const useCurrentProxy = () => { "自动选择", ]; const primaryGroup = - groups.find((group) => + groups.find((group: ProxyGroup) => primaryKeywords.some((keyword) => group.name.toLowerCase().includes(keyword.toLowerCase()), ), - ) || groups.filter((g) => g.name !== "GLOBAL")[0]; + ) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0]; if (primaryGroup) { primaryGroupName = primaryGroup.name; @@ -71,12 +64,12 @@ export const useCurrentProxy = () => { }; return { currentProxy, primaryGroupName }; - }, [proxiesData, currentMode]); + }, [proxies, currentMode]); return { currentProxy: currentProxyInfo.currentProxy, primaryGroupName: currentProxyInfo.primaryGroupName, mode: currentMode, - refreshProxy: mutateProxies, + refreshProxy, }; }; diff --git a/src/main.tsx b/src/main.tsx index 7d233688..b72e735b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -19,6 +19,7 @@ import { ThemeModeProvider, UpdateStateProvider, } from "./services/states"; +import { AppDataProvider } from "./providers/app-data-provider"; const mainElementId = "root"; const container = document.getElementById(mainElementId); @@ -51,9 +52,11 @@ createRoot(container).render( - - - + + + + + diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index c81c74e2..ec9d6cb7 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -11,7 +11,6 @@ import { } from "@mui/icons-material"; import { closeAllConnections } from "@/services/api"; import { useConnectionSetting } from "@/services/states"; -import { useClashInfo } from "@/hooks/use-clash"; import { BaseEmpty, BasePage } from "@/components/base"; import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; @@ -25,10 +24,9 @@ import { type SearchState, } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; -import useSWRSubscription from "swr/subscription"; -import { createSockette, createAuthSockette } from "@/utils/websocket"; import { useTheme } from "@mui/material/styles"; import { useVisibility } from "@/hooks/use-visibility"; +import { useAppData } from "@/providers/app-data-provider"; const initConn: IConnections = { uploadTotal: 0, @@ -40,12 +38,14 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; const ConnectionsPage = () => { const { t } = useTranslation(); - const { clashInfo } = useClashInfo(); const pageVisible = useVisibility(); const theme = useTheme(); const isDark = theme.palette.mode === "dark"; const [match, setMatch] = useState(() => (_: string) => true); const [curOrderOpt, setOrderOpt] = useState("Default"); + + // 使用全局数据 + const { connections } = useAppData(); const [setting, setSetting] = useConnectionSetting(); @@ -66,99 +66,37 @@ const ConnectionsPage = () => { const [isPaused, setIsPaused] = useState(false); const [frozenData, setFrozenData] = useState(null); - const { data: connData = initConn } = useSWRSubscription< - IConnections, - any, - "getClashConnections" | null - >( - clashInfo && pageVisible ? "getClashConnections" : null, - (_key, { next }) => { - const { server = "", secret = "" } = clashInfo!; - - if (!server) { - console.warn("[Connections] 服务器地址为空,无法建立连接"); - next(null, initConn); - return () => {}; - } - - console.log(`[Connections] 正在连接: ${server}/connections`); - - // 设置较长的超时时间,确保连接可以建立 - const s = createAuthSockette(`${server}/connections`, secret, { - timeout: 8000, // 8秒超时 - onmessage(event) { - const data = JSON.parse(event.data) as IConnections; - next(null, (old = initConn) => { - const oldConn = old.connections; - const maxLen = data.connections?.length; - - const connections: IConnectionsItem[] = []; - - const rest = (data.connections || []).filter((each) => { - const index = oldConn.findIndex((o) => o.id === each.id); - - if (index >= 0 && index < maxLen) { - const old = oldConn[index]; - each.curUpload = each.upload - old.upload; - each.curDownload = each.download - old.download; - - connections[index] = each; - return false; - } - return true; - }); - - for (let i = 0; i < maxLen; ++i) { - if (!connections[i] && rest.length > 0) { - connections[i] = rest.shift()!; - connections[i].curUpload = 0; - connections[i].curDownload = 0; - } - } - - return { ...data, connections }; - }); - }, - onerror(event) { - console.error("[Connections] WebSocket 连接错误", event); - // 报告错误但提供空数据,避免UI崩溃 - next(null, initConn); - }, - onclose(event) { - console.log("[Connections] WebSocket 连接关闭", event); - }, - onopen(event) { - console.log("[Connections] WebSocket 连接已建立"); - }, - }); - - return () => { - console.log("[Connections] 清理WebSocket连接"); - try { - s.close(); - } catch (e) { - console.error("[Connections] 关闭连接时出错", e); - } - }; - }, - ); - + // 使用全局连接数据 const displayData = useMemo(() => { - return isPaused ? (frozenData ?? connData) : connData; - }, [isPaused, frozenData, connData]); + if (!pageVisible) return initConn; + + if (isPaused) { + return frozenData ?? { + uploadTotal: connections.uploadTotal, + downloadTotal: connections.downloadTotal, + connections: connections.data + }; + } + + return { + uploadTotal: connections.uploadTotal, + downloadTotal: connections.downloadTotal, + connections: connections.data + }; + }, [isPaused, frozenData, connections, pageVisible]); const [filterConn] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; - let connections = displayData.connections.filter((conn) => { + let conns = displayData.connections.filter((conn) => { const { host, destinationIP, process } = conn.metadata; return ( match(host || "") || match(destinationIP || "") || match(process || "") ); }); - if (orderFunc) connections = orderFunc(connections); + if (orderFunc) conns = orderFunc(conns); - return [connections]; + return [conns]; }, [displayData, match, curOrderOpt]); const onCloseAll = useLockFn(closeAllConnections); @@ -172,13 +110,17 @@ const ConnectionsPage = () => { const handlePauseToggle = useCallback(() => { setIsPaused((prev) => { if (!prev) { - setFrozenData(connData); + setFrozenData({ + uploadTotal: connections.uploadTotal, + downloadTotal: connections.downloadTotal, + connections: connections.data + }); } else { setFrozenData(null); } return !prev; }); - }, [connData]); + }, [connections]); return ( { +export const HomePage = () => { const { t } = useTranslation(); const { verge } = useVerge(); const { current, mutateProfiles } = useProfiles(); @@ -395,4 +395,4 @@ const ClashModeEnhancedCard = () => { ); }; -export default HomePage; +export default HomePage; \ No newline at end of file diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 2486b2c8..a2b93e26 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -1,28 +1,27 @@ -import useSWR from "swr"; import { useState, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; import { Box } from "@mui/material"; -import { getRules } from "@/services/api"; import { BaseEmpty, BasePage } from "@/components/base"; import RuleItem from "@/components/rule/rule-item"; import { ProviderButton } from "@/components/rule/provider-button"; import { BaseSearchBox } from "@/components/base/base-search-box"; import { useTheme } from "@mui/material/styles"; import { ScrollTopButton } from "@/components/layout/scroll-top-button"; +import { useAppData } from "@/providers/app-data-provider"; const RulesPage = () => { const { t } = useTranslation(); - const { data = [] } = useSWR("getRules", getRules); + const { rules = [] } = useAppData(); const theme = useTheme(); const isDark = theme.palette.mode === "dark"; const [match, setMatch] = useState(() => (_: string) => true); const virtuosoRef = useRef(null); const [showScrollTop, setShowScrollTop] = useState(false); - const rules = useMemo(() => { - return data.filter((item) => match(item.payload)); - }, [data, match]); + const filteredRules = useMemo(() => { + return rules.filter((item) => match(item.payload)); + }, [rules, match]); const scrollToTop = () => { virtuosoRef.current?.scrollTo({ @@ -64,11 +63,11 @@ const RulesPage = () => { setMatch(() => match)} /> - {rules.length > 0 ? ( + {filteredRules.length > 0 ? ( <> Promise; + refreshClashConfig: () => Promise; + refreshRules: () => Promise; + refreshSysproxy: () => Promise; + refreshProxyProviders: () => Promise; + refreshRuleProviders: () => Promise; + refreshAll: () => Promise; +} + +// 创建上下文 +const AppDataContext = createContext(null); + +// 全局数据提供者组件 +export const AppDataProvider = ({ children }: { children: React.ReactNode }) => { + const { clashInfo } = useClashInfo(); + const pageVisible = useVisibility(); + + // 基础数据 - 中频率更新 (5秒) + const { data: proxiesData, mutate: refreshProxy } = useSWR( + "getProxies", + getProxies, + { + refreshInterval: 5000, + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + const { data: clashConfig, mutate: refreshClashConfig } = useSWR( + "getClashConfig", + getClashConfig, + { + refreshInterval: 5000, + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + // 提供者数据 + const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR( + "getProxyProviders", + getProxyProviders, + { + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR( + "getRuleProviders", + getRuleProviders, + { + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + // 低频率更新数据 + const { data: rulesData, mutate: refreshRules } = useSWR( + "getRules", + getRules, + { + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + const { data: sysproxy, mutate: refreshSysproxy } = useSWR( + "getSystemProxy", + getSystemProxy, + { + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + const { data: runningMode } = useSWR( + "getRunningMode", + getRunningMode, + { + revalidateOnFocus: false, + suspense: false, + errorRetryCount: 3 + } + ); + + // 高频率更新数据 (1秒) + const { data: uptimeData } = useSWR( + "appUptime", + getAppUptime, + { + refreshInterval: 1000, + revalidateOnFocus: false, + suspense: false + } + ); + + // 连接数据 - 使用WebSocket实时更新 + const { data: connectionsData = { connections: [], uploadTotal: 0, downloadTotal: 0 } } = + useSWRSubscription( + clashInfo && pageVisible ? "connections" : null, + (_key, { next }) => { + if (!clashInfo || !pageVisible) return () => {}; + + const { server = "", secret = "" } = clashInfo; + if (!server) return () => {}; + + const socket = createAuthSockette(`${server}/connections`, secret, { + timeout: 5000, + onmessage(event) { + try { + const data = JSON.parse(event.data); + // 处理连接数据,计算当前上传下载速度 + next(null, (prev: any = { connections: [], uploadTotal: 0, downloadTotal: 0 }) => { + const oldConns = prev.connections || []; + const newConns = data.connections || []; + + // 计算当前速度 + const processedConns = newConns.map((conn: any) => { + const oldConn = oldConns.find((old: any) => old.id === conn.id); + if (oldConn) { + return { + ...conn, + curUpload: conn.upload - oldConn.upload, + curDownload: conn.download - oldConn.download + }; + } + return { ...conn, curUpload: 0, curDownload: 0 }; + }); + + return { + ...data, + connections: processedConns + }; + }); + } catch (err) { + console.error("[Connections] 解析数据错误:", err); + } + }, + onerror() { + next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 }); + } + }); + + return () => socket.close(); + } + ); + + // 流量和内存数据 - 通过WebSocket获取实时流量数据 + const { data: trafficData = { up: 0, down: 0 } } = useSWRSubscription( + clashInfo && pageVisible ? "traffic" : null, + (_key, { next }) => { + 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); + next(null, data); + } catch (err) { + console.error("[Traffic] 解析数据错误:", err); + } + } + }); + + return () => socket.close(); + } + ); + + const { data: memoryData = { inuse: 0 } } = useSWRSubscription( + clashInfo && pageVisible ? "memory" : null, + (_key, { next }) => { + 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); + next(null, data); + } catch (err) { + console.error("[Memory] 解析数据错误:", err); + } + } + }); + + return () => socket.close(); + } + ); + + // 提供统一的刷新方法 + const refreshAll = async () => { + await Promise.all([ + refreshProxy(), + refreshClashConfig(), + refreshRules(), + refreshSysproxy(), + refreshProxyProviders(), + refreshRuleProviders() + ]); + }; + + // 聚合所有数据 + const value = useMemo(() => ({ + // 数据 + proxies: proxiesData, + clashConfig, + rules: rulesData || [], + sysproxy, + runningMode, + uptime: uptimeData || 0, + + // 提供者数据 + proxyProviders: proxyProviders || {}, + ruleProviders: ruleProviders || {}, + + // 连接数据 + connections: { + data: connectionsData.connections || [], + count: connectionsData.connections?.length || 0, + uploadTotal: connectionsData.uploadTotal || 0, + downloadTotal: connectionsData.downloadTotal || 0 + }, + + // 实时流量数据 + traffic: trafficData, + memory: memoryData, + + // 刷新方法 + refreshProxy, + refreshClashConfig, + refreshRules, + refreshSysproxy, + refreshProxyProviders, + refreshRuleProviders, + refreshAll + }), [ + proxiesData, clashConfig, rulesData, sysproxy, + runningMode, uptimeData, connectionsData, + trafficData, memoryData, proxyProviders, ruleProviders, + refreshProxy, refreshClashConfig, refreshRules, refreshSysproxy, + refreshProxyProviders, refreshRuleProviders + ]); + + return ( + + {children} + + ); +}; + +// 自定义Hook访问全局数据 +export const useAppData = () => { + const context = useContext(AppDataContext); + + if (!context) { + throw new Error("useAppData必须在AppDataProvider内使用"); + } + + return context; +}; \ No newline at end of file