mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 03:23:57 +08:00
feat: add AppDataProvider for centralized app data management and optimized refresh logic
This commit is contained in:
parent
804fad6083
commit
5a0eb56f70
16
UPDATELOG.md
16
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)
|
||||
|
13
src/App.tsx
Normal file
13
src/App.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { AppDataProvider } from "./providers/app-data-provider";
|
||||
import React from "react";
|
||||
import Layout from "./pages/_layout";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AppDataProvider>
|
||||
<Layout />
|
||||
</AppDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -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<any[]>([]);
|
||||
|
||||
// 使用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 (
|
||||
<Stack spacing={1.5}>
|
||||
@ -76,7 +51,7 @@ export const ClashInfoCard = () => {
|
||||
{t("Mixed Port")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.mixed_port || "-"}
|
||||
{clashConfig["mixed-port"] || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
@ -85,7 +60,7 @@ export const ClashInfoCard = () => {
|
||||
{t("Uptime")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{uptime}
|
||||
{formattedUptime}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
@ -99,7 +74,7 @@ export const ClashInfoCard = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
||||
}, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import useSWR from "swr";
|
||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { patchClashMode } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import {
|
||||
@ -11,22 +10,12 @@ import {
|
||||
DirectionsRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useMemo } from "react";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
|
||||
export const ClashModeCard = () => {
|
||||
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);
|
||||
}
|
||||
|
@ -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<number>(0);
|
||||
const isRefreshingRef = useRef<boolean>(false);
|
||||
const pendingRefreshRef = useRef<boolean>(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 (
|
||||
<EnhancedCard
|
||||
title={t("Current Node")}
|
||||
icon={
|
||||
<Tooltip
|
||||
title={
|
||||
proxyToDisplay
|
||||
currentProxy
|
||||
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
||||
: "无代理节点"
|
||||
}
|
||||
>
|
||||
<Box sx={{ color: signalInfo.color }}>
|
||||
{proxyToDisplay ? signalInfo.icon : <SignalNone color="disabled" />}
|
||||
{currentProxy ? signalInfo.icon : <SignalNone color="disabled" />}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
iconColor={proxyToDisplay ? "primary" : undefined}
|
||||
iconColor={currentProxy ? "primary" : undefined}
|
||||
action={
|
||||
<Button
|
||||
variant="outlined"
|
||||
@ -482,7 +450,7 @@ export const CurrentProxyCard = () => {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{proxyToDisplay ? (
|
||||
{currentProxy ? (
|
||||
<Box>
|
||||
{/* 代理节点信息显示 */}
|
||||
<Box
|
||||
@ -499,7 +467,7 @@ export const CurrentProxyCard = () => {
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body1" fontWeight="medium">
|
||||
{proxyToDisplay.name}
|
||||
{currentProxy.name}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
@ -510,7 +478,7 @@ export const CurrentProxyCard = () => {
|
||||
color="text.secondary"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{proxyToDisplay.type}
|
||||
{currentProxy.type}
|
||||
</Typography>
|
||||
{isGlobalMode && (
|
||||
<Chip
|
||||
@ -529,26 +497,26 @@ export const CurrentProxyCard = () => {
|
||||
/>
|
||||
)}
|
||||
{/* 节点特性 */}
|
||||
{proxyToDisplay.udp && (
|
||||
{currentProxy.udp && (
|
||||
<Chip size="small" label="UDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.tfo && (
|
||||
{currentProxy.tfo && (
|
||||
<Chip size="small" label="TFO" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.xudp && (
|
||||
{currentProxy.xudp && (
|
||||
<Chip size="small" label="XUDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.mptcp && (
|
||||
{currentProxy.mptcp && (
|
||||
<Chip size="small" label="MPTCP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.smux && (
|
||||
{currentProxy.smux && (
|
||||
<Chip size="small" label="SMUX" variant="outlined" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 显示延迟 */}
|
||||
{proxyToDisplay && !isDirectMode && (
|
||||
{currentProxy && !isDirectMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(currentDelay)}
|
||||
|
@ -28,6 +28,7 @@ import { createAuthSockette } from "@/utils/websocket";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
||||
import { ReactNode } from "react";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
@ -157,11 +158,13 @@ export const EnhancedTrafficStats = () => {
|
||||
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<typeof createAuthSockette> | 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: <LinkRounded fontSize="small" />,
|
||||
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 (
|
||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||
|
@ -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 {
|
||||
|
@ -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配置中获取开关状态
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Proxy Provider")}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
Object.entries(data || {}).forEach(async ([key], index) => {
|
||||
await handleUpdate(key, index);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{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 (
|
||||
<>
|
||||
<ListItem
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: "10px",
|
||||
border: "solid 2px var(--divider-color)",
|
||||
mb: 1,
|
||||
}}
|
||||
key={key}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 1 }}
|
||||
primary={
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
noWrap
|
||||
title={key}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
||||
{item.proxies.length}
|
||||
</TypeBox>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<StyledTypeBox component="span">
|
||||
{item.vehicleType}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{t("Update At")} {time.fromNow()}
|
||||
</StyledTypeBox>
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
||||
<span title="Used / Total">
|
||||
{parseTraffic(upload + download)} /{" "}
|
||||
{parseTraffic(total)}
|
||||
</span>
|
||||
<span title="Expire Time">
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
style={{ opacity: total > 0 ? 1 : 0 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={`${t("Update")}${t("Proxy Provider")}`}
|
||||
onClick={() => handleUpdate(key, index)}
|
||||
sx={{
|
||||
animation: updating[index]
|
||||
? `1s linear infinite ${round}`
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
// 样式化组件 - 类型框
|
||||
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<Record<string, boolean>>({});
|
||||
|
||||
// 检查是否有提供者
|
||||
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<string, boolean>);
|
||||
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 (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<StorageOutlined />}
|
||||
onClick={() => setOpen(true)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{t("Proxy Provider")}
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">{t("Proxy Providers")}</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={updateAllProviders}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{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 (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s"
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
noWrap
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.proxies.length}
|
||||
</TypeBox>
|
||||
<TypeBox component="span">
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" noWrap>
|
||||
<small>{t("Update At")}: </small>{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
{/* 订阅信息 */}
|
||||
{hasSubInfo && (
|
||||
<>
|
||||
<Box sx={{
|
||||
mb: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<span title={t("Used / Total") as string}>
|
||||
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time") as string}>
|
||||
{parseExpire(expire)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
{/* 进度条 */}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
opacity: total > 0 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box sx={{
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={(e) => {
|
||||
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}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t("Close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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<HeadState>) => void;
|
||||
onChangeProxy: (group: IProxyGroupItem, proxy: IProxyItem) => void;
|
||||
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void;
|
||||
}
|
||||
|
||||
export const ProxyRender = (props: RenderProps) => {
|
||||
|
@ -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 = <T = any>(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,
|
||||
};
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Rule Provider")}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
||||
<Typography variant="h6">{t("Rule Provider")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
Object.entries(data || {}).forEach(async ([key], index) => {
|
||||
await handleUpdate(key, index);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ width: 400 }}
|
||||
disableOk
|
||||
cancelBtn={t("Close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(data || {}).map(([key, item], index) => {
|
||||
const time = dayjs(item.updatedAt);
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: "10px",
|
||||
border: "solid 2px var(--divider-color)",
|
||||
mb: 1,
|
||||
}}
|
||||
key={key}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 1 }}
|
||||
primary={
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
noWrap
|
||||
title={key}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
||||
{item.ruleCount}
|
||||
</TypeBox>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<StyledTypeBox component="span">
|
||||
{item.vehicleType}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{item.behavior}
|
||||
</StyledTypeBox>
|
||||
<StyledTypeBox component="span">
|
||||
{t("Update At")} {time.fromNow()}
|
||||
</StyledTypeBox>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={`${t("Update")}${t("Rule Provider")}`}
|
||||
onClick={() => handleUpdate(key, index)}
|
||||
sx={{
|
||||
animation: updating[index]
|
||||
? `1s linear infinite ${round}`
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
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<Record<string, boolean>>({});
|
||||
|
||||
// 检查是否有提供者
|
||||
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<string, boolean>);
|
||||
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 (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<StorageOutlined />}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t("Rule Provider")}
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">{t("Rule Providers")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={updateAllProviders}
|
||||
>
|
||||
{t("Update All")}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<List sx={{ py: 0, minHeight: 250 }}>
|
||||
{Object.entries(ruleProviders || {}).map(([key, item]) => {
|
||||
const provider = item as RuleProviderItem;
|
||||
const time = dayjs(provider.updatedAt);
|
||||
const isUpdating = updating[key];
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
sx={[
|
||||
{
|
||||
p: 0,
|
||||
mb: "8px",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s"
|
||||
},
|
||||
({ palette: { mode, primary } }) => {
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ px: 2, py: 1 }}
|
||||
primary={
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
noWrap
|
||||
title={key}
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||
<TypeBox component="span">
|
||||
{provider.ruleCount}
|
||||
</TypeBox>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" noWrap>
|
||||
<small>{t("Update At")}: </small>{time.fromNow()}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<TypeBox component="span">
|
||||
{provider.vehicleType}
|
||||
</TypeBox>
|
||||
<TypeBox component="span">
|
||||
{provider.behavior}
|
||||
</TypeBox>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box sx={{
|
||||
width: 40,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => 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}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t("Close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<ComposeContextProvider contexts={contexts}>
|
||||
<BaseErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
<AppDataProvider>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</AppDataProvider>
|
||||
</BaseErrorBoundary>
|
||||
</ComposeContextProvider>
|
||||
</React.StrictMode>
|
||||
|
@ -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<IConnections | null>(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 (
|
||||
<BasePage
|
||||
|
@ -203,7 +203,7 @@ const HomeSettingsDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
const HomePage = () => {
|
||||
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;
|
@ -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<VirtuosoHandle>(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 = () => {
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
</Box>
|
||||
|
||||
{rules.length > 0 ? (
|
||||
{filteredRules.length > 0 ? (
|
||||
<>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={rules}
|
||||
data={filteredRules}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
|
299
src/providers/app-data-provider.tsx
Normal file
299
src/providers/app-data-provider.tsx
Normal file
@ -0,0 +1,299 @@
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { getProxies, getConnections, getRules, getClashConfig, getProxyProviders, getRuleProviders } from "@/services/api";
|
||||
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { createAuthSockette } from "@/utils/websocket";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
|
||||
// 定义AppDataContext类型 - 使用宽松类型
|
||||
interface AppDataContextType {
|
||||
proxies: any;
|
||||
clashConfig: any;
|
||||
rules: any[];
|
||||
sysproxy: any;
|
||||
runningMode?: string;
|
||||
uptime: number;
|
||||
proxyProviders: any;
|
||||
ruleProviders: any;
|
||||
connections: {
|
||||
data: any[];
|
||||
count: number;
|
||||
uploadTotal: number;
|
||||
downloadTotal: number;
|
||||
};
|
||||
traffic: {up: number; down: number};
|
||||
memory: {inuse: number};
|
||||
refreshProxy: () => Promise<any>;
|
||||
refreshClashConfig: () => Promise<any>;
|
||||
refreshRules: () => Promise<any>;
|
||||
refreshSysproxy: () => Promise<any>;
|
||||
refreshProxyProviders: () => Promise<any>;
|
||||
refreshRuleProviders: () => Promise<any>;
|
||||
refreshAll: () => Promise<any>;
|
||||
}
|
||||
|
||||
// 创建上下文
|
||||
const AppDataContext = createContext<AppDataContextType | null>(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 (
|
||||
<AppDataContext.Provider value={value}>
|
||||
{children}
|
||||
</AppDataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// 自定义Hook访问全局数据
|
||||
export const useAppData = () => {
|
||||
const context = useContext(AppDataContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useAppData必须在AppDataProvider内使用");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user