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 (
+ <>
+ }
+ onClick={() => setOpen(true)}
+ sx={{ mr: 1 }}
+ >
+ {t("Proxy Provider")}
+
+
+
+ }
+ 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 (
+ <>
+ }
+ onClick={() => setOpen(true)}
+ >
+ {t("Rule Provider")}
+
+
+
+ >
+ );
+};
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