mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 06:43:44 +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发行版可用,将在未来做进一步适配和调优
|
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||||
|
|
||||||
### 2.2.3-alpha 相对于 2.2.2
|
### 2.2.3-alpha 相对于 2.2.2
|
||||||
|
#### 修复了:
|
||||||
|
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||||
|
|
||||||
#### 优化
|
#### 优化
|
||||||
- 重构了内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
- 重构了内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||||
|
- 集中管理应用数据,优化数据获取和刷新逻辑
|
||||||
|
|
||||||
## v2.2.2
|
## v2.2.2
|
||||||
|
|
||||||
@ -18,10 +22,10 @@
|
|||||||
#### 已知问题
|
#### 已知问题
|
||||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
- 仅在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翻译
|
- 增加轻量模式缺失的tray翻译
|
||||||
- Linux下的窗口边框被削掉的问题
|
- Linux下的窗口边框被削掉的问题
|
||||||
|
|
||||||
@ -31,7 +35,7 @@
|
|||||||
- 增加服务模式下的僵尸进程清理机制
|
- 增加服务模式下的僵尸进程清理机制
|
||||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||||
|
|
||||||
### 2.2.1 相对于 2.2.0(已下架不在提供)
|
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
||||||
#### 修复了:
|
#### 修复了:
|
||||||
1. **首页**
|
1. **首页**
|
||||||
- 修复 Direct 模式首页无法渲染
|
- 修复 Direct 模式首页无法渲染
|
||||||
@ -62,7 +66,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.2.0(已下架不在提供)
|
## 2.2.0(已下架不再提供)
|
||||||
|
|
||||||
#### 新增功能
|
#### 新增功能
|
||||||
1. **首页**
|
1. **首页**
|
||||||
@ -141,14 +145,14 @@
|
|||||||
|
|
||||||
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
||||||
|
|
||||||
##### 2.1.2相对2.1.1(已下架不在提供)更新了:
|
##### 2.1.2相对2.1.1(已下架不再提供)更新了:
|
||||||
|
|
||||||
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
||||||
- 设置菜单区分Verge基本设置和高级设置
|
- 设置菜单区分Verge基本设置和高级设置
|
||||||
- 增加v2 Updater的更多功能和权限
|
- 增加v2 Updater的更多功能和权限
|
||||||
- 退出Verge后Tun代理状态仍保留的问题
|
- 退出Verge后Tun代理状态仍保留的问题
|
||||||
|
|
||||||
##### 2.1.1相对2.1.0(已下架不在提供)更新了:
|
##### 2.1.1相对2.1.0(已下架不再提供)更新了:
|
||||||
|
|
||||||
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
||||||
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
- 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 { useTranslation } from "react-i18next";
|
||||||
import { Typography, Stack, Divider } from "@mui/material";
|
import { Typography, Stack, Divider } from "@mui/material";
|
||||||
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
|
||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from "@/hooks/use-clash";
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
import useSWR from "swr";
|
import { useMemo } from "react";
|
||||||
import { getRules } from "@/services/api";
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
import { getAppUptime, getSystemProxy } from "@/services/cmds";
|
|
||||||
import { useMemo, useState, useEffect } from "react";
|
|
||||||
|
|
||||||
// 将毫秒转换为时:分:秒格式的函数
|
// 将毫秒转换为时:分:秒格式的函数
|
||||||
const formatUptime = (uptimeMs: number) => {
|
const formatUptime = (uptimeMs: number) => {
|
||||||
@ -19,37 +16,15 @@ const formatUptime = (uptimeMs: number) => {
|
|||||||
|
|
||||||
export const ClashInfoCard = () => {
|
export const ClashInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash();
|
||||||
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
|
const { clashConfig, sysproxy, rules, uptime } = useAppData();
|
||||||
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([]));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
||||||
|
|
||||||
// 使用备忘录组件内容,减少重新渲染
|
// 使用备忘录组件内容,减少重新渲染
|
||||||
const cardContent = useMemo(() => {
|
const cardContent = useMemo(() => {
|
||||||
if (!clashInfo) return null;
|
if (!clashConfig) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
@ -76,7 +51,7 @@ export const ClashInfoCard = () => {
|
|||||||
{t("Mixed Port")}
|
{t("Mixed Port")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashInfo.mixed_port || "-"}
|
{clashConfig["mixed-port"] || "-"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -85,7 +60,7 @@ export const ClashInfoCard = () => {
|
|||||||
{t("Uptime")}
|
{t("Uptime")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{uptime}
|
{formattedUptime}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -99,7 +74,7 @@ export const ClashInfoCard = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
}, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import useSWR from "swr";
|
import { closeAllConnections } from "@/services/api";
|
||||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
|
||||||
import { patchClashMode } from "@/services/cmds";
|
import { patchClashMode } from "@/services/cmds";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import {
|
import {
|
||||||
@ -11,22 +10,12 @@ import {
|
|||||||
DirectionsRounded,
|
DirectionsRounded,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
const { clashConfig, refreshProxy } = useAppData();
|
||||||
// 获取当前Clash配置
|
|
||||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
|
||||||
"getClashConfig",
|
|
||||||
getClashConfig,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateIfStale: true,
|
|
||||||
dedupingInterval: 1000,
|
|
||||||
errorRetryInterval: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||||
@ -50,7 +39,8 @@ export const ClashModeCard = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await patchClashMode(mode);
|
await patchClashMode(mode);
|
||||||
mutateClash();
|
// 使用共享的刷新方法
|
||||||
|
refreshProxy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to change mode:", error);
|
console.error("Failed to change mode:", error);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
SignalWifi4Bar as SignalStrong,
|
SignalWifi4Bar as SignalStrong,
|
||||||
SignalWifi3Bar as SignalGood,
|
SignalWifi3Bar as SignalGood,
|
||||||
@ -24,16 +24,11 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useCurrentProxy } from "@/hooks/use-current-proxy";
|
|
||||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||||
import {
|
import { updateProxy, deleteConnection } from "@/services/api";
|
||||||
getProxies,
|
|
||||||
updateProxy,
|
|
||||||
getConnections,
|
|
||||||
deleteConnection,
|
|
||||||
} from "@/services/api";
|
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
// 本地存储的键名
|
// 本地存储的键名
|
||||||
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
||||||
@ -92,21 +87,16 @@ function debounce(fn: Function, ms = 100) {
|
|||||||
|
|
||||||
export const CurrentProxyCard = () => {
|
export const CurrentProxyCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
|
||||||
useCurrentProxy();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
const { proxies, connections, clashConfig, refreshProxy } = useAppData();
|
||||||
|
|
||||||
// 判断模式
|
// 判断模式
|
||||||
|
const mode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||||
const isGlobalMode = mode === "global";
|
const isGlobalMode = mode === "global";
|
||||||
const isDirectMode = mode === "direct";
|
const isDirectMode = mode === "direct";
|
||||||
|
|
||||||
// 使用 useRef 存储最后一次刷新时间和是否正在刷新
|
|
||||||
const lastRefreshRef = useRef<number>(0);
|
|
||||||
const isRefreshingRef = useRef<boolean>(false);
|
|
||||||
const pendingRefreshRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
// 定义状态类型
|
// 定义状态类型
|
||||||
type ProxyState = {
|
type ProxyState = {
|
||||||
proxyData: {
|
proxyData: {
|
||||||
@ -139,6 +129,32 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 初始化选择的组
|
// 初始化选择的组
|
||||||
useEffect(() => {
|
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) {
|
if (isGlobalMode) {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
@ -166,40 +182,23 @@ export const CurrentProxyCard = () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
}, [isGlobalMode, isDirectMode, proxies]);
|
||||||
|
|
||||||
// 带锁的代理数据获取函数,防止并发请求
|
// 监听代理数据变化,更新状态
|
||||||
const fetchProxyData = useCallback(
|
useEffect(() => {
|
||||||
async (force = false) => {
|
if (!proxies) return;
|
||||||
// 防止重复请求
|
|
||||||
if (isRefreshingRef.current) {
|
|
||||||
pendingRefreshRef.current = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查刷新间隔,强制增加最小间隔
|
|
||||||
const now = Date.now();
|
|
||||||
if (!force && now - lastRefreshRef.current < 1500) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRefreshingRef.current = true;
|
|
||||||
lastRefreshRef.current = now;
|
|
||||||
|
|
||||||
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) => {
|
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 newProxy = "";
|
||||||
let newDisplayProxy = null;
|
let newDisplayProxy = null;
|
||||||
let newGroup = prev.selection.group;
|
let newGroup = prev.selection.group;
|
||||||
@ -208,15 +207,15 @@ export const CurrentProxyCard = () => {
|
|||||||
if (isDirectMode) {
|
if (isDirectMode) {
|
||||||
newGroup = "DIRECT";
|
newGroup = "DIRECT";
|
||||||
newProxy = "DIRECT";
|
newProxy = "DIRECT";
|
||||||
newDisplayProxy = data.records?.DIRECT || null;
|
newDisplayProxy = proxies.records?.DIRECT || null;
|
||||||
} else if (isGlobalMode && data.global) {
|
} else if (isGlobalMode && proxies.global) {
|
||||||
newGroup = "GLOBAL";
|
newGroup = "GLOBAL";
|
||||||
newProxy = data.global.now || "";
|
newProxy = proxies.global.now || "";
|
||||||
newDisplayProxy = data.records?.[newProxy] || null;
|
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||||
} else {
|
} else {
|
||||||
// 普通模式 - 检查当前选择的组是否存在
|
// 普通模式 - 检查当前选择的组是否存在
|
||||||
const currentGroup = filteredGroups.find(
|
const currentGroup = filteredGroups.find(
|
||||||
(g) => g.name === prev.selection.group,
|
(g: { name: string }) => g.name === prev.selection.group,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果当前组不存在或为空,自动选择第一个组
|
// 如果当前组不存在或为空,自动选择第一个组
|
||||||
@ -224,7 +223,7 @@ export const CurrentProxyCard = () => {
|
|||||||
newGroup = filteredGroups[0].name;
|
newGroup = filteredGroups[0].name;
|
||||||
const firstGroup = filteredGroups[0];
|
const firstGroup = filteredGroups[0];
|
||||||
newProxy = firstGroup.now;
|
newProxy = firstGroup.now;
|
||||||
newDisplayProxy = data.records?.[newProxy] || null;
|
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||||
|
|
||||||
// 保存到本地存储
|
// 保存到本地存储
|
||||||
if (!isGlobalMode && !isDirectMode) {
|
if (!isGlobalMode && !isDirectMode) {
|
||||||
@ -236,7 +235,7 @@ export const CurrentProxyCard = () => {
|
|||||||
} else if (currentGroup) {
|
} else if (currentGroup) {
|
||||||
// 使用当前组的代理
|
// 使用当前组的代理
|
||||||
newProxy = currentGroup.now;
|
newProxy = currentGroup.now;
|
||||||
newDisplayProxy = data.records?.[newProxy] || null;
|
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,9 +243,9 @@ export const CurrentProxyCard = () => {
|
|||||||
return {
|
return {
|
||||||
proxyData: {
|
proxyData: {
|
||||||
groups: filteredGroups,
|
groups: filteredGroups,
|
||||||
records: data.records || {},
|
records: proxies.records || {},
|
||||||
globalProxy: data.global?.now || "",
|
globalProxy: proxies.global?.now || "",
|
||||||
directProxy: data.records?.DIRECT || null,
|
directProxy: proxies.records?.DIRECT || null,
|
||||||
},
|
},
|
||||||
selection: {
|
selection: {
|
||||||
group: newGroup,
|
group: newGroup,
|
||||||
@ -255,59 +254,7 @@ export const CurrentProxyCard = () => {
|
|||||||
displayProxy: newDisplayProxy,
|
displayProxy: newDisplayProxy,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
}, [proxies, isGlobalMode, isDirectMode]);
|
||||||
console.error("获取代理信息失败", error);
|
|
||||||
} finally {
|
|
||||||
isRefreshingRef.current = false;
|
|
||||||
|
|
||||||
// 处理待处理的刷新请求,但增加延迟
|
|
||||||
if (pendingRefreshRef.current) {
|
|
||||||
pendingRefreshRef.current = false;
|
|
||||||
setTimeout(() => fetchProxyData(), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[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]);
|
|
||||||
|
|
||||||
// 使用防抖包装状态更新,避免快速连续更新,增加防抖时间
|
// 使用防抖包装状态更新,避免快速连续更新,增加防抖时间
|
||||||
const debouncedSetState = useCallback(
|
const debouncedSetState = useCallback(
|
||||||
@ -329,7 +276,7 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 获取该组当前选中的代理
|
// 获取该组当前选中的代理
|
||||||
setState((prev) => {
|
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) {
|
if (group) {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@ -382,20 +329,16 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 自动关闭连接设置
|
// 自动关闭连接设置
|
||||||
if (verge?.auto_close_connection && previousProxy) {
|
if (verge?.auto_close_connection && previousProxy) {
|
||||||
getConnections().then(({ connections }) => {
|
connections.data.forEach((conn: any) => {
|
||||||
connections.forEach((conn) => {
|
|
||||||
if (conn.chains.includes(previousProxy)) {
|
if (conn.chains.includes(previousProxy)) {
|
||||||
deleteConnection(conn.id);
|
deleteConnection(conn.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 延长刷新延迟时间
|
// 延长刷新延迟时间
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshProxy();
|
refreshProxy();
|
||||||
// 给refreshProxy一点时间完成,再触发fetchProxyData
|
|
||||||
setTimeout(() => fetchProxyData(true), 300);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("更新代理失败", error);
|
console.error("更新代理失败", error);
|
||||||
@ -408,8 +351,8 @@ export const CurrentProxyCard = () => {
|
|||||||
state.selection,
|
state.selection,
|
||||||
verge?.auto_close_connection,
|
verge?.auto_close_connection,
|
||||||
refreshProxy,
|
refreshProxy,
|
||||||
fetchProxyData,
|
|
||||||
debouncedSetState,
|
debouncedSetState,
|
||||||
|
connections.data,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -419,11 +362,14 @@ export const CurrentProxyCard = () => {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// 获取要显示的代理节点
|
// 获取要显示的代理节点
|
||||||
const proxyToDisplay = state.displayProxy || currentProxy;
|
const currentProxy = useMemo(() => {
|
||||||
|
// 从state中获取当前代理信息
|
||||||
|
return state.displayProxy;
|
||||||
|
}, [state.displayProxy]);
|
||||||
|
|
||||||
// 获取当前节点的延迟
|
// 获取当前节点的延迟
|
||||||
const currentDelay = proxyToDisplay
|
const currentDelay = currentProxy
|
||||||
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
? delayManager.getDelayFix(currentProxy, state.selection.group)
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
// 获取信号图标
|
// 获取信号图标
|
||||||
@ -453,23 +399,45 @@ export const CurrentProxyCard = () => {
|
|||||||
[state.proxyData.records, state.selection.group],
|
[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 (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("Current Node")}
|
title={t("Current Node")}
|
||||||
icon={
|
icon={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
proxyToDisplay
|
currentProxy
|
||||||
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
||||||
: "无代理节点"
|
: "无代理节点"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box sx={{ color: signalInfo.color }}>
|
<Box sx={{ color: signalInfo.color }}>
|
||||||
{proxyToDisplay ? signalInfo.icon : <SignalNone color="disabled" />}
|
{currentProxy ? signalInfo.icon : <SignalNone color="disabled" />}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
iconColor={proxyToDisplay ? "primary" : undefined}
|
iconColor={currentProxy ? "primary" : undefined}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -482,7 +450,7 @@ export const CurrentProxyCard = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{proxyToDisplay ? (
|
{currentProxy ? (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 代理节点信息显示 */}
|
{/* 代理节点信息显示 */}
|
||||||
<Box
|
<Box
|
||||||
@ -499,7 +467,7 @@ export const CurrentProxyCard = () => {
|
|||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body1" fontWeight="medium">
|
<Typography variant="body1" fontWeight="medium">
|
||||||
{proxyToDisplay.name}
|
{currentProxy.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
@ -510,7 +478,7 @@ export const CurrentProxyCard = () => {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
>
|
>
|
||||||
{proxyToDisplay.type}
|
{currentProxy.type}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isGlobalMode && (
|
{isGlobalMode && (
|
||||||
<Chip
|
<Chip
|
||||||
@ -529,26 +497,26 @@ export const CurrentProxyCard = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* 节点特性 */}
|
{/* 节点特性 */}
|
||||||
{proxyToDisplay.udp && (
|
{currentProxy.udp && (
|
||||||
<Chip size="small" label="UDP" variant="outlined" />
|
<Chip size="small" label="UDP" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.tfo && (
|
{currentProxy.tfo && (
|
||||||
<Chip size="small" label="TFO" variant="outlined" />
|
<Chip size="small" label="TFO" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.xudp && (
|
{currentProxy.xudp && (
|
||||||
<Chip size="small" label="XUDP" variant="outlined" />
|
<Chip size="small" label="XUDP" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.mptcp && (
|
{currentProxy.mptcp && (
|
||||||
<Chip size="small" label="MPTCP" variant="outlined" />
|
<Chip size="small" label="MPTCP" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.smux && (
|
{currentProxy.smux && (
|
||||||
<Chip size="small" label="SMUX" variant="outlined" />
|
<Chip size="small" label="SMUX" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 显示延迟 */}
|
{/* 显示延迟 */}
|
||||||
{proxyToDisplay && !isDirectMode && (
|
{currentProxy && !isDirectMode && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={delayManager.formatDelay(currentDelay)}
|
label={delayManager.formatDelay(currentDelay)}
|
||||||
|
@ -28,6 +28,7 @@ import { createAuthSockette } from "@/utils/websocket";
|
|||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
interface MemoryUsage {
|
interface MemoryUsage {
|
||||||
inuse: number;
|
inuse: number;
|
||||||
@ -157,11 +158,13 @@ export const EnhancedTrafficStats = () => {
|
|||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
const [isDebug, setIsDebug] = useState(false);
|
const [isDebug, setIsDebug] = useState(false);
|
||||||
|
|
||||||
|
// 使用AppDataProvider
|
||||||
|
const { connections, uptime } = useAppData();
|
||||||
|
|
||||||
// 使用单一状态对象减少状态更新次数
|
// 使用单一状态对象减少状态更新次数
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
traffic: { up: 0, down: 0 },
|
traffic: { up: 0, down: 0 },
|
||||||
memory: { inuse: 0, oslimit: undefined as number | undefined },
|
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,
|
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(() => {
|
useEffect(() => {
|
||||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||||
@ -328,14 +301,14 @@ export const EnhancedTrafficStats = () => {
|
|||||||
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
||||||
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
||||||
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
||||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(stats.connections.uploadTotal);
|
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
|
||||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(stats.connections.downloadTotal);
|
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
up, upUnit, down, downUnit, inuse, inuseUnit,
|
up, upUnit, down, downUnit, inuse, inuseUnit,
|
||||||
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
|
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
|
||||||
};
|
};
|
||||||
}, [stats]);
|
}, [stats, connections.uploadTotal, connections.downloadTotal]);
|
||||||
|
|
||||||
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
||||||
const trafficGraphComponent = useMemo(() => {
|
const trafficGraphComponent = useMemo(() => {
|
||||||
@ -398,7 +371,7 @@ export const EnhancedTrafficStats = () => {
|
|||||||
{
|
{
|
||||||
icon: <LinkRounded fontSize="small" />,
|
icon: <LinkRounded fontSize="small" />,
|
||||||
title: t("Active Connections"),
|
title: t("Active Connections"),
|
||||||
value: stats.connections.activeConnections,
|
value: connections.count,
|
||||||
unit: "",
|
unit: "",
|
||||||
color: "success" as const,
|
color: "success" as const,
|
||||||
},
|
},
|
||||||
@ -424,7 +397,7 @@ export const EnhancedTrafficStats = () => {
|
|||||||
color: "error" as const,
|
color: "error" as const,
|
||||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||||
},
|
},
|
||||||
], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]);
|
], [t, parsedData, connections.count, isDebug, handleGarbageCollection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
<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 { useLockFn } from "ahooks";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
// 定义旋转动画
|
// 定义旋转动画
|
||||||
const round = keyframes`
|
const round = keyframes`
|
||||||
@ -270,6 +271,7 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
|||||||
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { refreshAll } = useAppData();
|
||||||
|
|
||||||
// 更新当前订阅
|
// 更新当前订阅
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
@ -282,6 +284,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
|||||||
await updateProfile(current.uid);
|
await updateProfile(current.uid);
|
||||||
Notice.success(t("Update subscription successfully"));
|
Notice.success(t("Update subscription successfully"));
|
||||||
onProfileUpdated?.();
|
onProfileUpdated?.();
|
||||||
|
|
||||||
|
// 刷新首页数据
|
||||||
|
refreshAll();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err?.message || err.toString());
|
Notice.error(err?.message || err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -18,13 +18,8 @@ import {
|
|||||||
HelpOutlineRounded,
|
HelpOutlineRounded,
|
||||||
SvgIconComponent,
|
SvgIconComponent,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import useSWR from "swr";
|
|
||||||
import {
|
|
||||||
getSystemProxy,
|
|
||||||
getAutotemProxy,
|
|
||||||
getRunningMode,
|
|
||||||
} from "@/services/cmds";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
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 { sysproxy, runningMode } = useAppData();
|
||||||
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
|
||||||
// 从verge配置中获取开关状态
|
// 从verge配置中获取开关状态
|
||||||
|
@ -1,195 +1,46 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
styled,
|
|
||||||
Box,
|
|
||||||
alpha,
|
|
||||||
Typography,
|
Typography,
|
||||||
Divider,
|
Divider,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
keyframes,
|
alpha,
|
||||||
|
styled,
|
||||||
|
useTheme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RefreshRounded } from "@mui/icons-material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
|
import { useLockFn } from "ahooks";
|
||||||
import { BaseDialog } from "../base";
|
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";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
const round = keyframes`
|
// 定义代理提供者类型
|
||||||
from { transform: rotate(0deg); }
|
interface ProxyProviderItem {
|
||||||
to { transform: rotate(360deg); }
|
name?: string;
|
||||||
`;
|
proxies: any[];
|
||||||
|
updatedAt: number;
|
||||||
export const ProviderButton = () => {
|
vehicleType: string;
|
||||||
const { t } = useTranslation();
|
subscriptionInfo?: {
|
||||||
const { data } = useSWR("getProxyProviders", getProxyProviders);
|
Upload: number;
|
||||||
|
Download: number;
|
||||||
const [open, setOpen] = useState(false);
|
Total: number;
|
||||||
|
Expire: number;
|
||||||
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");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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 }) => ({
|
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
border: "1px solid #ccc",
|
border: "1px solid #ccc",
|
||||||
@ -202,28 +53,272 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
|||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
|
// 解析过期时间
|
||||||
({ theme }) => ({
|
const parseExpire = (expire?: number) => {
|
||||||
display: "inline-block",
|
if (!expire) return "-";
|
||||||
border: "1px solid #ccc",
|
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
||||||
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 = {
|
export const ProviderButton = () => {
|
||||||
height: 26,
|
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",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
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>
|
||||||
|
|
||||||
function parseExpire(expire?: number) {
|
{/* 进度条 */}
|
||||||
if (!expire) return "-";
|
<LinearProgress
|
||||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
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 {
|
interface RenderProps {
|
||||||
item: IRenderItem;
|
item: IRenderItem;
|
||||||
indent: boolean;
|
indent: boolean;
|
||||||
onLocation: (group: IProxyGroupItem) => void;
|
onLocation: (group: IRenderItem["group"]) => void;
|
||||||
onCheckAll: (groupName: string) => void;
|
onCheckAll: (groupName: string) => void;
|
||||||
onHeadState: (groupName: string, patch: Partial<HeadState>) => 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) => {
|
export const ProxyRender = (props: RenderProps) => {
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { useEffect, useMemo, useCallback } from "react";
|
import { useEffect, useMemo, useCallback } from "react";
|
||||||
import { getProxies } from "@/services/api";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { filterSort } from "./use-filter-sort";
|
import { filterSort } from "./use-filter-sort";
|
||||||
import { useWindowWidth } from "./use-window-width";
|
import { useWindowWidth } from "./use-window-width";
|
||||||
@ -9,12 +7,52 @@ import {
|
|||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
type HeadState,
|
type HeadState,
|
||||||
} from "./use-head-state";
|
} 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 {
|
export interface IRenderItem {
|
||||||
// 组 | head | item | empty | item col
|
// 组 | head | item | empty | item col
|
||||||
type: 0 | 1 | 2 | 3 | 4;
|
type: 0 | 1 | 2 | 3 | 4;
|
||||||
key: string;
|
key: string;
|
||||||
group: IProxyGroupItem;
|
group: ProxyGroup;
|
||||||
proxy?: IProxyItem;
|
proxy?: IProxyItem;
|
||||||
col?: number;
|
col?: number;
|
||||||
proxyCol?: IProxyItem[];
|
proxyCol?: IProxyItem[];
|
||||||
@ -51,16 +89,8 @@ const groupProxies = <T = any>(list: T[], size: number): T[][] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useRenderList = (mode: string) => {
|
export const useRenderList = (mode: string) => {
|
||||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
// 使用全局数据提供者
|
||||||
"getProxies",
|
const { proxies: proxiesData, refreshProxy } = useAppData();
|
||||||
getProxies,
|
|
||||||
{
|
|
||||||
refreshInterval: 2000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { width } = useWindowWidth();
|
const { width } = useWindowWidth();
|
||||||
const [headStates, setHeadState] = useHeadStateNew();
|
const [headStates, setHeadState] = useHeadStateNew();
|
||||||
@ -80,9 +110,9 @@ export const useRenderList = (mode: string) => {
|
|||||||
(mode === "rule" && !groups.length) ||
|
(mode === "rule" && !groups.length) ||
|
||||||
(mode === "global" && proxies.length < 2)
|
(mode === "global" && proxies.length < 2)
|
||||||
) {
|
) {
|
||||||
setTimeout(() => mutateProxies(), 500);
|
setTimeout(() => refreshProxy(), 500);
|
||||||
}
|
}
|
||||||
}, [proxiesData, mode, mutateProxies]);
|
}, [proxiesData, mode, refreshProxy]);
|
||||||
|
|
||||||
// 处理渲染列表
|
// 处理渲染列表
|
||||||
const renderList: IRenderItem[] = useMemo(() => {
|
const renderList: IRenderItem[] = useMemo(() => {
|
||||||
@ -94,7 +124,7 @@ export const useRenderList = (mode: string) => {
|
|||||||
? proxiesData.groups
|
? proxiesData.groups
|
||||||
: [proxiesData.global!];
|
: [proxiesData.global!];
|
||||||
|
|
||||||
const retList = renderGroups.flatMap((group) => {
|
const retList = renderGroups.flatMap((group: ProxyGroup) => {
|
||||||
const headState = headStates[group.name] || DEFAULT_STATE;
|
const headState = headStates[group.name] || DEFAULT_STATE;
|
||||||
const ret: IRenderItem[] = [
|
const ret: IRenderItem[] = [
|
||||||
{
|
{
|
||||||
@ -158,12 +188,12 @@ export const useRenderList = (mode: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!useRule) return retList.slice(1);
|
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]);
|
}, [headStates, proxiesData, mode, col]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderList,
|
renderList,
|
||||||
onProxies: mutateProxies,
|
onProxies: refreshProxy,
|
||||||
onHeadState: setHeadState,
|
onHeadState: setHeadState,
|
||||||
currentColumns: col,
|
currentColumns: col,
|
||||||
};
|
};
|
||||||
|
@ -1,170 +1,39 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Typography,
|
Typography,
|
||||||
styled,
|
|
||||||
Box,
|
|
||||||
alpha,
|
|
||||||
Divider,
|
Divider,
|
||||||
keyframes,
|
alpha,
|
||||||
|
styled,
|
||||||
|
useTheme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RefreshRounded } from "@mui/icons-material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
|
import { useLockFn } from "ahooks";
|
||||||
import { BaseDialog } from "../base";
|
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); }
|
interface RuleProviderItem {
|
||||||
to { transform: rotate(360deg); }
|
behavior: string;
|
||||||
`;
|
ruleCount: number;
|
||||||
|
updatedAt: number;
|
||||||
export const ProviderButton = () => {
|
vehicleType: string;
|
||||||
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")}
|
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||||
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 }) => ({
|
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
border: "1px solid #ccc",
|
border: "1px solid #ccc",
|
||||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||||
@ -176,16 +45,222 @@ const TypeBox = styled(Box, {
|
|||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypeBox = styled(Box, {
|
export const ProviderButton = () => {
|
||||||
shouldForwardProp: (prop) => prop !== "component",
|
const { t } = useTranslation();
|
||||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
const theme = useTheme();
|
||||||
display: "inline-block",
|
const [open, setOpen] = useState(false);
|
||||||
border: "1px solid #ccc",
|
const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData();
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
|
||||||
borderRadius: 4,
|
// 检查是否有提供者
|
||||||
fontSize: 10,
|
const hasProviders = Object.keys(ruleProviders || {}).length > 0;
|
||||||
marginRight: "4px",
|
|
||||||
padding: "0 2px",
|
// 更新单个规则提供者
|
||||||
lineHeight: 1.25,
|
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 { useMemo } from "react";
|
||||||
import { getProxies } from "@/services/api";
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
import { getClashConfig } from "@/services/api";
|
|
||||||
|
// 定义代理组类型
|
||||||
|
interface ProxyGroup {
|
||||||
|
name: string;
|
||||||
|
now: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前代理节点信息的自定义Hook
|
// 获取当前代理节点信息的自定义Hook
|
||||||
export const useCurrentProxy = () => {
|
export const useCurrentProxy = () => {
|
||||||
// 获取代理信息
|
// 从AppDataProvider获取数据
|
||||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
const { proxies, clashConfig, refreshProxy } = useAppData();
|
||||||
"getProxies",
|
|
||||||
getProxies,
|
|
||||||
{
|
|
||||||
refreshInterval: 2000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取当前Clash配置(包含模式信息)
|
|
||||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
|
||||||
|
|
||||||
// 获取当前模式
|
// 获取当前模式
|
||||||
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
|
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||||
|
|
||||||
// 获取当前代理节点信息
|
// 获取当前代理节点信息
|
||||||
const currentProxyInfo = useMemo(() => {
|
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";
|
let primaryGroupName = "GLOBAL";
|
||||||
@ -43,11 +36,11 @@ export const useCurrentProxy = () => {
|
|||||||
"自动选择",
|
"自动选择",
|
||||||
];
|
];
|
||||||
const primaryGroup =
|
const primaryGroup =
|
||||||
groups.find((group) =>
|
groups.find((group: ProxyGroup) =>
|
||||||
primaryKeywords.some((keyword) =>
|
primaryKeywords.some((keyword) =>
|
||||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
),
|
),
|
||||||
) || groups.filter((g) => g.name !== "GLOBAL")[0];
|
) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0];
|
||||||
|
|
||||||
if (primaryGroup) {
|
if (primaryGroup) {
|
||||||
primaryGroupName = primaryGroup.name;
|
primaryGroupName = primaryGroup.name;
|
||||||
@ -71,12 +64,12 @@ export const useCurrentProxy = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return { currentProxy, primaryGroupName };
|
return { currentProxy, primaryGroupName };
|
||||||
}, [proxiesData, currentMode]);
|
}, [proxies, currentMode]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentProxy: currentProxyInfo.currentProxy,
|
currentProxy: currentProxyInfo.currentProxy,
|
||||||
primaryGroupName: currentProxyInfo.primaryGroupName,
|
primaryGroupName: currentProxyInfo.primaryGroupName,
|
||||||
mode: currentMode,
|
mode: currentMode,
|
||||||
refreshProxy: mutateProxies,
|
refreshProxy,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
ThemeModeProvider,
|
ThemeModeProvider,
|
||||||
UpdateStateProvider,
|
UpdateStateProvider,
|
||||||
} from "./services/states";
|
} from "./services/states";
|
||||||
|
import { AppDataProvider } from "./providers/app-data-provider";
|
||||||
|
|
||||||
const mainElementId = "root";
|
const mainElementId = "root";
|
||||||
const container = document.getElementById(mainElementId);
|
const container = document.getElementById(mainElementId);
|
||||||
@ -51,9 +52,11 @@ createRoot(container).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ComposeContextProvider contexts={contexts}>
|
<ComposeContextProvider contexts={contexts}>
|
||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
|
<AppDataProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Layout />
|
<Layout />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</AppDataProvider>
|
||||||
</BaseErrorBoundary>
|
</BaseErrorBoundary>
|
||||||
</ComposeContextProvider>
|
</ComposeContextProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { closeAllConnections } from "@/services/api";
|
import { closeAllConnections } from "@/services/api";
|
||||||
import { useConnectionSetting } from "@/services/states";
|
import { useConnectionSetting } from "@/services/states";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||||
@ -25,10 +24,9 @@ import {
|
|||||||
type SearchState,
|
type SearchState,
|
||||||
} from "@/components/base/base-search-box";
|
} from "@/components/base/base-search-box";
|
||||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
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 { useTheme } from "@mui/material/styles";
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from "@/hooks/use-visibility";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
const initConn: IConnections = {
|
const initConn: IConnections = {
|
||||||
uploadTotal: 0,
|
uploadTotal: 0,
|
||||||
@ -40,13 +38,15 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
|||||||
|
|
||||||
const ConnectionsPage = () => {
|
const ConnectionsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true);
|
||||||
const [curOrderOpt, setOrderOpt] = useState("Default");
|
const [curOrderOpt, setOrderOpt] = useState("Default");
|
||||||
|
|
||||||
|
// 使用全局数据
|
||||||
|
const { connections } = useAppData();
|
||||||
|
|
||||||
const [setting, setSetting] = useConnectionSetting();
|
const [setting, setSetting] = useConnectionSetting();
|
||||||
|
|
||||||
const isTableLayout = setting.layout === "table";
|
const isTableLayout = setting.layout === "table";
|
||||||
@ -66,99 +66,37 @@ const ConnectionsPage = () => {
|
|||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
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(() => {
|
const displayData = useMemo(() => {
|
||||||
return isPaused ? (frozenData ?? connData) : connData;
|
if (!pageVisible) return initConn;
|
||||||
}, [isPaused, frozenData, connData]);
|
|
||||||
|
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 [filterConn] = useMemo(() => {
|
||||||
const orderFunc = orderOpts[curOrderOpt];
|
const orderFunc = orderOpts[curOrderOpt];
|
||||||
let connections = displayData.connections.filter((conn) => {
|
let conns = displayData.connections.filter((conn) => {
|
||||||
const { host, destinationIP, process } = conn.metadata;
|
const { host, destinationIP, process } = conn.metadata;
|
||||||
return (
|
return (
|
||||||
match(host || "") || match(destinationIP || "") || match(process || "")
|
match(host || "") || match(destinationIP || "") || match(process || "")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orderFunc) connections = orderFunc(connections);
|
if (orderFunc) conns = orderFunc(conns);
|
||||||
|
|
||||||
return [connections];
|
return [conns];
|
||||||
}, [displayData, match, curOrderOpt]);
|
}, [displayData, match, curOrderOpt]);
|
||||||
|
|
||||||
const onCloseAll = useLockFn(closeAllConnections);
|
const onCloseAll = useLockFn(closeAllConnections);
|
||||||
@ -172,13 +110,17 @@ const ConnectionsPage = () => {
|
|||||||
const handlePauseToggle = useCallback(() => {
|
const handlePauseToggle = useCallback(() => {
|
||||||
setIsPaused((prev) => {
|
setIsPaused((prev) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
setFrozenData(connData);
|
setFrozenData({
|
||||||
|
uploadTotal: connections.uploadTotal,
|
||||||
|
downloadTotal: connections.downloadTotal,
|
||||||
|
connections: connections.data
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setFrozenData(null);
|
setFrozenData(null);
|
||||||
}
|
}
|
||||||
return !prev;
|
return !prev;
|
||||||
});
|
});
|
||||||
}, [connData]);
|
}, [connections]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
@ -203,7 +203,7 @@ const HomeSettingsDialog = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { current, mutateProfiles } = useProfiles();
|
const { current, mutateProfiles } = useProfiles();
|
||||||
|
@ -1,28 +1,27 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { useState, useMemo, useRef } from "react";
|
import { useState, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { getRules } from "@/services/api";
|
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import RuleItem from "@/components/rule/rule-item";
|
import RuleItem from "@/components/rule/rule-item";
|
||||||
import { ProviderButton } from "@/components/rule/provider-button";
|
import { ProviderButton } from "@/components/rule/provider-button";
|
||||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
import { BaseSearchBox } from "@/components/base/base-search-box";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
const RulesPage = () => {
|
const RulesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data = [] } = useSWR("getRules", getRules);
|
const { rules = [] } = useAppData();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true);
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||||
|
|
||||||
const rules = useMemo(() => {
|
const filteredRules = useMemo(() => {
|
||||||
return data.filter((item) => match(item.payload));
|
return rules.filter((item) => match(item.payload));
|
||||||
}, [data, match]);
|
}, [rules, match]);
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
virtuosoRef.current?.scrollTo({
|
virtuosoRef.current?.scrollTo({
|
||||||
@ -64,11 +63,11 @@ const RulesPage = () => {
|
|||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{rules.length > 0 ? (
|
{filteredRules.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={rules}
|
data={filteredRules}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
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