feat: add AppDataProvider for centralized app data management and optimized refresh logic

This commit is contained in:
wonfen
2025-03-26 13:26:32 +08:00
parent 804fad6083
commit 5a0eb56f70
18 changed files with 1142 additions and 784 deletions

View File

@@ -1,13 +1,10 @@
import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider } from "@mui/material";
import { DeveloperBoardOutlined } from "@mui/icons-material";
import { useClashInfo } from "@/hooks/use-clash";
import { useClash } from "@/hooks/use-clash";
import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr";
import { getRules } from "@/services/api";
import { getAppUptime, getSystemProxy } from "@/services/cmds";
import { useMemo, useState, useEffect } from "react";
import { useMemo } from "react";
import { useAppData } from "@/providers/app-data-provider";
// 将毫秒转换为时:分:秒格式的函数
const formatUptime = (uptimeMs: number) => {
@@ -19,37 +16,15 @@ const formatUptime = (uptimeMs: number) => {
export const ClashInfoCard = () => {
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const { version: clashVersion } = useClash();
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
const [rules, setRules] = useState<any[]>([]);
// 使用SWR获取应用运行时间降低更新频率
const { data: uptimeMs = 0 } = useSWR(
"appUptime",
getAppUptime,
{
refreshInterval: 1000,
revalidateOnFocus: false,
dedupingInterval: 1000,
},
);
// 在组件加载时获取系统代理信息和规则数据
useEffect(() => {
// 获取系统代理信息
getSystemProxy().then(setSysproxy);
// 获取规则数据
getRules().then(setRules).catch(() => setRules([]));
}, []);
const { clashConfig, sysproxy, rules, uptime } = useAppData();
// 使用useMemo缓存格式化后的uptime避免频繁计算
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
// 使用备忘录组件内容,减少重新渲染
const cardContent = useMemo(() => {
if (!clashInfo) return null;
if (!clashConfig) return null;
return (
<Stack spacing={1.5}>
@@ -76,7 +51,7 @@ export const ClashInfoCard = () => {
{t("Mixed Port")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashInfo.mixed_port || "-"}
{clashConfig["mixed-port"] || "-"}
</Typography>
</Stack>
<Divider />
@@ -85,7 +60,7 @@ export const ClashInfoCard = () => {
{t("Uptime")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{uptime}
{formattedUptime}
</Typography>
</Stack>
<Divider />
@@ -99,7 +74,7 @@ export const ClashInfoCard = () => {
</Stack>
</Stack>
);
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
}, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]);
return (
<EnhancedCard

View File

@@ -1,8 +1,7 @@
import { useTranslation } from "react-i18next";
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
import { useLockFn } from "ahooks";
import useSWR from "swr";
import { closeAllConnections, getClashConfig } from "@/services/api";
import { closeAllConnections } from "@/services/api";
import { patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import {
@@ -11,22 +10,12 @@ import {
DirectionsRounded,
} from "@mui/icons-material";
import { useMemo } from "react";
import { useAppData } from "@/providers/app-data-provider";
export const ClashModeCard = () => {
const { t } = useTranslation();
const { verge } = useVerge();
// 获取当前Clash配置
const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig,
{
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000
}
);
const { clashConfig, refreshProxy } = useAppData();
// 支持的模式列表
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
@@ -50,7 +39,8 @@ export const ClashModeCard = () => {
try {
await patchClashMode(mode);
mutateClash();
// 使用共享的刷新方法
refreshProxy();
} catch (error) {
console.error("Failed to change mode:", error);
}

View File

@@ -13,7 +13,7 @@ import {
SelectChangeEvent,
Tooltip,
} from "@mui/material";
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { useEffect, useState, useMemo, useCallback } from "react";
import {
SignalWifi4Bar as SignalStrong,
SignalWifi3Bar as SignalGood,
@@ -24,16 +24,11 @@ import {
ChevronRight,
} from "@mui/icons-material";
import { useNavigate } from "react-router-dom";
import { useCurrentProxy } from "@/hooks/use-current-proxy";
import { EnhancedCard } from "@/components/home/enhanced-card";
import {
getProxies,
updateProxy,
getConnections,
deleteConnection,
} from "@/services/api";
import { updateProxy, deleteConnection } from "@/services/api";
import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
// 本地存储的键名
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
@@ -92,21 +87,16 @@ function debounce(fn: Function, ms = 100) {
export const CurrentProxyCard = () => {
const { t } = useTranslation();
const { currentProxy, primaryGroupName, mode, refreshProxy } =
useCurrentProxy();
const navigate = useNavigate();
const theme = useTheme();
const { verge } = useVerge();
const { proxies, connections, clashConfig, refreshProxy } = useAppData();
// 判断模式
const mode = clashConfig?.mode?.toLowerCase() || "rule";
const isGlobalMode = mode === "global";
const isDirectMode = mode === "direct";
// 使用 useRef 存储最后一次刷新时间和是否正在刷新
const lastRefreshRef = useRef<number>(0);
const isRefreshingRef = useRef<boolean>(false);
const pendingRefreshRef = useRef<boolean>(false);
// 定义状态类型
type ProxyState = {
proxyData: {
@@ -139,6 +129,32 @@ export const CurrentProxyCard = () => {
// 初始化选择的组
useEffect(() => {
if (!proxies) return;
// 提取primaryGroupName
const getPrimaryGroupName = () => {
if (!proxies?.groups?.length) return "";
// 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组
const primaryKeywords = [
"auto",
"select",
"proxy",
"节点选择",
"自动选择",
];
const primaryGroup =
proxies.groups.find((group: { name: string }) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
return primaryGroup?.name || "";
};
const primaryGroupName = getPrimaryGroupName();
// 根据模式确定初始组
if (isGlobalMode) {
setState((prev) => ({
@@ -166,148 +182,79 @@ export const CurrentProxyCard = () => {
},
}));
}
}, [isGlobalMode, isDirectMode, primaryGroupName]);
}, [isGlobalMode, isDirectMode, proxies]);
// 带锁的代理数据获取函数,防止并发请求
const fetchProxyData = useCallback(
async (force = false) => {
// 防止重复请求
if (isRefreshingRef.current) {
pendingRefreshRef.current = true;
return;
}
// 监听代理数据变化,更新状态
useEffect(() => {
if (!proxies) return;
// 使用函数式更新确保状态更新的原子性
setState((prev) => {
// 过滤和格式化组
const filteredGroups = proxies.groups
.filter((g: { name: string }) => g.name !== "DIRECT" && g.name !== "REJECT")
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p: { name: string }) => p.name),
}));
let newProxy = "";
let newDisplayProxy = null;
let newGroup = prev.selection.group;
// 检查刷新间隔,强制增加最小间隔
const now = Date.now();
if (!force && now - lastRefreshRef.current < 1500) {
return;
}
// 根据模式确定新代理
if (isDirectMode) {
newGroup = "DIRECT";
newProxy = "DIRECT";
newDisplayProxy = proxies.records?.DIRECT || null;
} else if (isGlobalMode && proxies.global) {
newGroup = "GLOBAL";
newProxy = proxies.global.now || "";
newDisplayProxy = proxies.records?.[newProxy] || null;
} else {
// 普通模式 - 检查当前选择的组是否存在
const currentGroup = filteredGroups.find(
(g: { name: string }) => g.name === prev.selection.group,
);
isRefreshingRef.current = true;
lastRefreshRef.current = now;
// 如果当前组不存在或为空,自动选择第一个组
if (!currentGroup && filteredGroups.length > 0) {
newGroup = filteredGroups[0].name;
const firstGroup = filteredGroups[0];
newProxy = firstGroup.now;
newDisplayProxy = proxies.records?.[newProxy] || null;
try {
const data = await getProxies();
// 过滤和格式化组
const filteredGroups = data.groups
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
.map((g) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p) => p.name),
}));
// 使用函数式更新确保状态更新的原子性
setState((prev) => {
let newProxy = "";
let newDisplayProxy = null;
let newGroup = prev.selection.group;
// 根据模式确定新代理
if (isDirectMode) {
newGroup = "DIRECT";
newProxy = "DIRECT";
newDisplayProxy = data.records?.DIRECT || null;
} else if (isGlobalMode && data.global) {
newGroup = "GLOBAL";
newProxy = data.global.now || "";
newDisplayProxy = data.records?.[newProxy] || null;
} else {
// 普通模式 - 检查当前选择的组是否存在
const currentGroup = filteredGroups.find(
(g) => g.name === prev.selection.group,
);
// 如果当前组不存在或为空,自动选择第一个组
if (!currentGroup && filteredGroups.length > 0) {
newGroup = filteredGroups[0].name;
const firstGroup = filteredGroups[0];
newProxy = firstGroup.now;
newDisplayProxy = data.records?.[newProxy] || null;
// 保存到本地存储
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
}
} else if (currentGroup) {
// 使用当前组的代理
newProxy = currentGroup.now;
newDisplayProxy = data.records?.[newProxy] || null;
// 保存到本地存储
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
}
// 返回新状态
return {
proxyData: {
groups: filteredGroups,
records: data.records || {},
globalProxy: data.global?.now || "",
directProxy: data.records?.DIRECT || null,
},
selection: {
group: newGroup,
proxy: newProxy,
},
displayProxy: newDisplayProxy,
};
});
} catch (error) {
console.error("获取代理信息失败", error);
} finally {
isRefreshingRef.current = false;
// 处理待处理的刷新请求,但增加延迟
if (pendingRefreshRef.current) {
pendingRefreshRef.current = false;
setTimeout(() => fetchProxyData(), 500);
} else if (currentGroup) {
// 使用当前组的代理
newProxy = currentGroup.now;
newDisplayProxy = proxies.records?.[newProxy] || null;
}
}
},
[isGlobalMode, isDirectMode],
);
// 响应 currentProxy 变化,增加时间检查避免循环调用
useEffect(() => {
if (
currentProxy &&
(!state.displayProxy ||
(currentProxy.name !== state.displayProxy.name &&
Date.now() - lastRefreshRef.current > 1000))
) {
fetchProxyData(true);
}
}, [currentProxy, fetchProxyData]);
// 监听模式变化mode变化时刷新
useEffect(() => {
fetchProxyData(true);
}, [mode, fetchProxyData]);
// 计算要显示的代理选项 - 使用 useMemo 优化
const proxyOptions = useMemo(() => {
if (isDirectMode) {
return [{ name: "DIRECT" }];
}
if (isGlobalMode && state.proxyData.records) {
// 全局模式下的选项
return Object.keys(state.proxyData.records)
.filter((name) => name !== "DIRECT" && name !== "REJECT")
.map((name) => ({ name }));
}
// 普通模式
const group = state.proxyData.groups.find(
(g) => g.name === state.selection.group,
);
if (group) {
return group.all.map((name) => ({ name }));
}
return [];
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
// 返回新状态
return {
proxyData: {
groups: filteredGroups,
records: proxies.records || {},
globalProxy: proxies.global?.now || "",
directProxy: proxies.records?.DIRECT || null,
},
selection: {
group: newGroup,
proxy: newProxy,
},
displayProxy: newDisplayProxy,
};
});
}, [proxies, isGlobalMode, isDirectMode]);
// 使用防抖包装状态更新,避免快速连续更新,增加防抖时间
const debouncedSetState = useCallback(
@@ -329,7 +276,7 @@ export const CurrentProxyCard = () => {
// 获取该组当前选中的代理
setState((prev) => {
const group = prev.proxyData.groups.find((g) => g.name === newGroup);
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup);
if (group) {
return {
...prev,
@@ -382,20 +329,16 @@ export const CurrentProxyCard = () => {
// 自动关闭连接设置
if (verge?.auto_close_connection && previousProxy) {
getConnections().then(({ connections }) => {
connections.forEach((conn) => {
if (conn.chains.includes(previousProxy)) {
deleteConnection(conn.id);
}
});
connections.data.forEach((conn: any) => {
if (conn.chains.includes(previousProxy)) {
deleteConnection(conn.id);
}
});
}
// 延长刷新延迟时间
setTimeout(() => {
refreshProxy();
// 给refreshProxy一点时间完成再触发fetchProxyData
setTimeout(() => fetchProxyData(true), 300);
}, 500);
} catch (error) {
console.error("更新代理失败", error);
@@ -408,8 +351,8 @@ export const CurrentProxyCard = () => {
state.selection,
verge?.auto_close_connection,
refreshProxy,
fetchProxyData,
debouncedSetState,
connections.data,
],
);
@@ -419,11 +362,14 @@ export const CurrentProxyCard = () => {
}, [navigate]);
// 获取要显示的代理节点
const proxyToDisplay = state.displayProxy || currentProxy;
const currentProxy = useMemo(() => {
// 从state中获取当前代理信息
return state.displayProxy;
}, [state.displayProxy]);
// 获取当前节点的延迟
const currentDelay = proxyToDisplay
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
const currentDelay = currentProxy
? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
// 获取信号图标
@@ -453,23 +399,45 @@ export const CurrentProxyCard = () => {
[state.proxyData.records, state.selection.group],
);
// 计算要显示的代理选项 - 使用 useMemo 优化
const proxyOptions = useMemo(() => {
if (isDirectMode) {
return [{ name: "DIRECT" }];
}
if (isGlobalMode && state.proxyData.records) {
// 全局模式下的选项
return Object.keys(state.proxyData.records)
.filter((name) => name !== "DIRECT" && name !== "REJECT")
.map((name) => ({ name }));
}
// 普通模式
const group = state.proxyData.groups.find(
(g: { name: string }) => g.name === state.selection.group,
);
if (group) {
return group.all.map((name) => ({ name }));
}
return [];
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
return (
<EnhancedCard
title={t("Current Node")}
icon={
<Tooltip
title={
proxyToDisplay
currentProxy
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
: "无代理节点"
}
>
<Box sx={{ color: signalInfo.color }}>
{proxyToDisplay ? signalInfo.icon : <SignalNone color="disabled" />}
{currentProxy ? signalInfo.icon : <SignalNone color="disabled" />}
</Box>
</Tooltip>
}
iconColor={proxyToDisplay ? "primary" : undefined}
iconColor={currentProxy ? "primary" : undefined}
action={
<Button
variant="outlined"
@@ -482,7 +450,7 @@ export const CurrentProxyCard = () => {
</Button>
}
>
{proxyToDisplay ? (
{currentProxy ? (
<Box>
{/* 代理节点信息显示 */}
<Box
@@ -499,7 +467,7 @@ export const CurrentProxyCard = () => {
>
<Box>
<Typography variant="body1" fontWeight="medium">
{proxyToDisplay.name}
{currentProxy.name}
</Typography>
<Box
@@ -510,7 +478,7 @@ export const CurrentProxyCard = () => {
color="text.secondary"
sx={{ mr: 1 }}
>
{proxyToDisplay.type}
{currentProxy.type}
</Typography>
{isGlobalMode && (
<Chip
@@ -529,26 +497,26 @@ export const CurrentProxyCard = () => {
/>
)}
{/* 节点特性 */}
{proxyToDisplay.udp && (
{currentProxy.udp && (
<Chip size="small" label="UDP" variant="outlined" />
)}
{proxyToDisplay.tfo && (
{currentProxy.tfo && (
<Chip size="small" label="TFO" variant="outlined" />
)}
{proxyToDisplay.xudp && (
{currentProxy.xudp && (
<Chip size="small" label="XUDP" variant="outlined" />
)}
{proxyToDisplay.mptcp && (
{currentProxy.mptcp && (
<Chip size="small" label="MPTCP" variant="outlined" />
)}
{proxyToDisplay.smux && (
{currentProxy.smux && (
<Chip size="small" label="SMUX" variant="outlined" />
)}
</Box>
</Box>
{/* 显示延迟 */}
{proxyToDisplay && !isDirectMode && (
{currentProxy && !isDirectMode && (
<Chip
size="small"
label={delayManager.formatDelay(currentDelay)}

View File

@@ -28,6 +28,7 @@ import { createAuthSockette } from "@/utils/websocket";
import parseTraffic from "@/utils/parse-traffic";
import { getConnections, isDebugEnabled, gc } from "@/services/api";
import { ReactNode } from "react";
import { useAppData } from "@/providers/app-data-provider";
interface MemoryUsage {
inuse: number;
@@ -157,11 +158,13 @@ export const EnhancedTrafficStats = () => {
const pageVisible = useVisibility();
const [isDebug, setIsDebug] = useState(false);
// 使用AppDataProvider
const { connections, uptime } = useAppData();
// 使用单一状态对象减少状态更新次数
const [stats, setStats] = useState({
traffic: { up: 0, down: 0 },
memory: { inuse: 0, oslimit: undefined as number | undefined },
connections: { uploadTotal: 0, downloadTotal: 0, activeConnections: 0 },
});
// 创建一个标记来追踪最后更新时间,用于节流
@@ -176,36 +179,6 @@ export const EnhancedTrafficStats = () => {
memory: null as ReturnType<typeof createAuthSockette> | null,
});
// 获取连接数据
const fetchConnections = useCallback(async () => {
if (!pageVisible) return;
try {
const connections = await getConnections();
if (connections) {
setStats(prev => ({
...prev,
connections: {
uploadTotal: connections.uploadTotal || 0,
downloadTotal: connections.downloadTotal || 0,
activeConnections: connections.connections ? connections.connections.length : 0,
}
}));
}
} catch (err) {
console.error("Failed to fetch connections:", err);
}
}, [pageVisible]);
// 定期更新连接数据
useEffect(() => {
if (!pageVisible) return;
fetchConnections();
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
return () => clearInterval(intervalId);
}, [pageVisible, fetchConnections]);
// 检查是否支持调试
useEffect(() => {
isDebugEnabled().then((flag) => setIsDebug(flag));
@@ -328,14 +301,14 @@ export const EnhancedTrafficStats = () => {
const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(stats.connections.uploadTotal);
const [downloadTotal, downloadTotalUnit] = parseTraffic(stats.connections.downloadTotal);
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
return {
up, upUnit, down, downUnit, inuse, inuseUnit,
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
};
}, [stats]);
}, [stats, connections.uploadTotal, connections.downloadTotal]);
// 渲染流量图表 - 使用useMemo缓存渲染结果
const trafficGraphComponent = useMemo(() => {
@@ -398,7 +371,7 @@ export const EnhancedTrafficStats = () => {
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
value: stats.connections.activeConnections,
value: connections.count,
unit: "",
color: "success" as const,
},
@@ -424,7 +397,7 @@ export const EnhancedTrafficStats = () => {
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
},
], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]);
], [t, parsedData, connections.count, isDebug, handleGarbageCollection]);
return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>

View File

@@ -27,6 +27,7 @@ import { openWebUrl, updateProfile } from "@/services/cmds";
import { useLockFn } from "ahooks";
import { Notice } from "@/components/base";
import { EnhancedCard } from "./enhanced-card";
import { useAppData } from "@/providers/app-data-provider";
// 定义旋转动画
const round = keyframes`
@@ -270,6 +271,7 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { refreshAll } = useAppData();
// 更新当前订阅
const [updating, setUpdating] = useState(false);
@@ -282,6 +284,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
await updateProfile(current.uid);
Notice.success(t("Update subscription successfully"));
onProfileUpdated?.();
// 刷新首页数据
refreshAll();
} catch (err: any) {
Notice.error(err?.message || err.toString());
} finally {

View File

@@ -18,13 +18,8 @@ import {
HelpOutlineRounded,
SvgIconComponent,
} from "@mui/icons-material";
import useSWR from "swr";
import {
getSystemProxy,
getAutotemProxy,
getRunningMode,
} from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
@@ -150,8 +145,7 @@ export const ProxyTunCard: FC = () => {
);
// 获取代理状态信息
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
const { sysproxy, runningMode } = useAppData();
const { verge } = useVerge();
// 从verge配置中获取开关状态