- Updated multiple components to remove the use of forwardRef, simplifying the props structure. - Adjusted imports and component definitions accordingly. - Ensured consistent handling of refs and props across various viewer components. - Improved readability and maintainability of the codebase.
599 lines
17 KiB
TypeScript
599 lines
17 KiB
TypeScript
import { listen } from "@tauri-apps/api/event";
|
||
import React, { createContext, use, useEffect, useMemo, useRef } from "react";
|
||
import useSWR from "swr";
|
||
|
||
import { useClashInfo } from "@/hooks/use-clash";
|
||
import { useVerge } from "@/hooks/use-verge";
|
||
import { useVisibility } from "@/hooks/use-visibility";
|
||
import {
|
||
forceRefreshProxies,
|
||
getAppUptime,
|
||
getClashConfig,
|
||
getConnections,
|
||
getMemoryData,
|
||
getProxies,
|
||
getProxyProviders,
|
||
getRuleProviders,
|
||
getRules,
|
||
getRunningMode,
|
||
getSystemProxy,
|
||
getTrafficData,
|
||
} from "@/services/cmds";
|
||
|
||
// 连接速度计算接口
|
||
interface ConnectionSpeedData {
|
||
id: string;
|
||
upload: number;
|
||
download: number;
|
||
timestamp: number;
|
||
}
|
||
|
||
interface ConnectionWithSpeed extends IConnectionsItem {
|
||
curUpload: number;
|
||
curDownload: number;
|
||
}
|
||
|
||
// 定义AppDataContext类型 - 使用宽松类型
|
||
interface AppDataContextType {
|
||
proxies: any;
|
||
clashConfig: any;
|
||
rules: any[];
|
||
sysproxy: any;
|
||
runningMode?: string;
|
||
uptime: number;
|
||
proxyProviders: any;
|
||
ruleProviders: any;
|
||
connections: {
|
||
data: ConnectionWithSpeed[];
|
||
count: number;
|
||
uploadTotal: number;
|
||
downloadTotal: number;
|
||
};
|
||
traffic: { up: number; down: number };
|
||
memory: { inuse: number };
|
||
systemProxyAddress: string;
|
||
|
||
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 pageVisible = useVisibility();
|
||
const { clashInfo } = useClashInfo();
|
||
const { verge } = useVerge();
|
||
|
||
// 存储上一次连接数据用于速度计算
|
||
const previousConnectionsRef = useRef<Map<string, ConnectionSpeedData>>(
|
||
new Map(),
|
||
);
|
||
|
||
// 计算连接速度的函数
|
||
const calculateConnectionSpeeds = (
|
||
currentConnections: IConnectionsItem[],
|
||
): ConnectionWithSpeed[] => {
|
||
const now = Date.now();
|
||
const currentMap = new Map<string, ConnectionSpeedData>();
|
||
|
||
return currentConnections.map((conn) => {
|
||
const connWithSpeed: ConnectionWithSpeed = {
|
||
...conn,
|
||
curUpload: 0,
|
||
curDownload: 0,
|
||
};
|
||
|
||
const currentData: ConnectionSpeedData = {
|
||
id: conn.id,
|
||
upload: conn.upload,
|
||
download: conn.download,
|
||
timestamp: now,
|
||
};
|
||
|
||
currentMap.set(conn.id, currentData);
|
||
|
||
const previousData = previousConnectionsRef.current.get(conn.id);
|
||
if (previousData) {
|
||
const timeDiff = (now - previousData.timestamp) / 1000; // 转换为秒
|
||
|
||
if (timeDiff > 0) {
|
||
const uploadDiff = conn.upload - previousData.upload;
|
||
const downloadDiff = conn.download - previousData.download;
|
||
|
||
// 计算每秒速度 (字节/秒)
|
||
connWithSpeed.curUpload = Math.max(0, uploadDiff / timeDiff);
|
||
connWithSpeed.curDownload = Math.max(0, downloadDiff / timeDiff);
|
||
}
|
||
}
|
||
|
||
return connWithSpeed;
|
||
});
|
||
};
|
||
|
||
// 基础数据 - 中频率更新 (5秒)
|
||
const { data: proxiesData, mutate: refreshProxy } = useSWR(
|
||
"getProxies",
|
||
getProxies,
|
||
{
|
||
refreshInterval: 5000,
|
||
revalidateOnFocus: true,
|
||
suspense: false,
|
||
errorRetryCount: 3,
|
||
},
|
||
);
|
||
|
||
// 监听profile和clash配置变更事件
|
||
useEffect(() => {
|
||
let profileUnlisten: Promise<() => void> | undefined;
|
||
let lastProfileId: string | null = null;
|
||
let lastUpdateTime = 0;
|
||
const refreshThrottle = 500;
|
||
|
||
const setupEventListeners = async () => {
|
||
try {
|
||
// 监听profile切换事件
|
||
profileUnlisten = listen<string>("profile-changed", (event) => {
|
||
const newProfileId = event.payload;
|
||
const now = Date.now();
|
||
|
||
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
|
||
|
||
if (
|
||
lastProfileId === newProfileId &&
|
||
now - lastUpdateTime < refreshThrottle
|
||
) {
|
||
console.log("[AppDataProvider] 重复事件被防抖,跳过");
|
||
return;
|
||
}
|
||
|
||
lastProfileId = newProfileId;
|
||
lastUpdateTime = now;
|
||
|
||
setTimeout(() => {
|
||
// 先执行 forceRefreshProxies,完成后稍延迟再刷新前端数据,避免页面一直 loading
|
||
forceRefreshProxies()
|
||
.catch((e) =>
|
||
console.warn("[AppDataProvider] forceRefreshProxies 失败:", e),
|
||
)
|
||
.finally(() => {
|
||
setTimeout(() => {
|
||
refreshProxy().catch((e) =>
|
||
console.warn("[AppDataProvider] 普通刷新也失败:", e),
|
||
);
|
||
}, 200); // 200ms 延迟,保证后端缓存已清理
|
||
});
|
||
}, 0);
|
||
});
|
||
|
||
// 监听Clash配置刷新事件(enhance操作等)
|
||
const handleRefreshClash = () => {
|
||
const now = Date.now();
|
||
console.log("[AppDataProvider] Clash配置刷新事件");
|
||
|
||
if (now - lastUpdateTime > refreshThrottle) {
|
||
lastUpdateTime = now;
|
||
|
||
setTimeout(async () => {
|
||
try {
|
||
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
|
||
|
||
// 添加超时保护
|
||
const refreshPromise = Promise.race([
|
||
forceRefreshProxies(),
|
||
new Promise((_, reject) =>
|
||
setTimeout(
|
||
() => reject(new Error("forceRefreshProxies timeout")),
|
||
8000,
|
||
),
|
||
),
|
||
]);
|
||
|
||
await refreshPromise;
|
||
await refreshProxy();
|
||
} catch (error) {
|
||
console.error(
|
||
"[AppDataProvider] Clash刷新时强制刷新代理缓存失败:",
|
||
error,
|
||
);
|
||
refreshProxy().catch((e) =>
|
||
console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e),
|
||
);
|
||
}
|
||
}, 0);
|
||
}
|
||
};
|
||
|
||
// 监听代理配置刷新事件(托盘代理切换等)
|
||
const handleRefreshProxy = () => {
|
||
const now = Date.now();
|
||
console.log("[AppDataProvider] 代理配置刷新事件");
|
||
|
||
if (now - lastUpdateTime > refreshThrottle) {
|
||
lastUpdateTime = now;
|
||
|
||
setTimeout(() => {
|
||
refreshProxy().catch((e) =>
|
||
console.warn("[AppDataProvider] 代理刷新失败:", e),
|
||
);
|
||
}, 100);
|
||
}
|
||
};
|
||
|
||
// 监听强制代理刷新事件(托盘代理切换立即刷新)
|
||
const handleForceRefreshProxies = () => {
|
||
console.log("[AppDataProvider] 强制代理刷新事件");
|
||
|
||
// 立即刷新,无延迟,无防抖
|
||
forceRefreshProxies()
|
||
.then(() => {
|
||
console.log("[AppDataProvider] 强制刷新代理缓存完成");
|
||
// 强制刷新完成后,立即刷新前端显示
|
||
return refreshProxy();
|
||
})
|
||
.then(() => {
|
||
console.log("[AppDataProvider] 前端代理数据刷新完成");
|
||
})
|
||
.catch((e) => {
|
||
console.warn("[AppDataProvider] 强制代理刷新失败:", e);
|
||
// 如果强制刷新失败,尝试普通刷新
|
||
refreshProxy().catch((e2) =>
|
||
console.warn("[AppDataProvider] 普通代理刷新也失败:", e2),
|
||
);
|
||
});
|
||
};
|
||
|
||
// 使用 Tauri 事件监听器替代 window 事件监听器
|
||
const setupTauriListeners = async () => {
|
||
try {
|
||
const unlistenClash = await listen(
|
||
"verge://refresh-clash-config",
|
||
handleRefreshClash,
|
||
);
|
||
const unlistenProxy = await listen(
|
||
"verge://refresh-proxy-config",
|
||
handleRefreshProxy,
|
||
);
|
||
const unlistenForceRefresh = await listen(
|
||
"verge://force-refresh-proxies",
|
||
handleForceRefreshProxies,
|
||
);
|
||
|
||
return () => {
|
||
unlistenClash();
|
||
unlistenProxy();
|
||
unlistenForceRefresh();
|
||
};
|
||
} catch (error) {
|
||
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
|
||
|
||
// 降级到 window 事件监听器
|
||
window.addEventListener(
|
||
"verge://refresh-clash-config",
|
||
handleRefreshClash,
|
||
);
|
||
window.addEventListener(
|
||
"verge://refresh-proxy-config",
|
||
handleRefreshProxy,
|
||
);
|
||
window.addEventListener(
|
||
"verge://force-refresh-proxies",
|
||
handleForceRefreshProxies,
|
||
);
|
||
|
||
return () => {
|
||
window.removeEventListener(
|
||
"verge://refresh-clash-config",
|
||
handleRefreshClash,
|
||
);
|
||
window.removeEventListener(
|
||
"verge://refresh-proxy-config",
|
||
handleRefreshProxy,
|
||
);
|
||
window.removeEventListener(
|
||
"verge://force-refresh-proxies",
|
||
handleForceRefreshProxies,
|
||
);
|
||
};
|
||
}
|
||
};
|
||
|
||
const cleanupTauriListeners = setupTauriListeners();
|
||
|
||
return async () => {
|
||
const cleanup = await cleanupTauriListeners;
|
||
cleanup();
|
||
};
|
||
} catch (error) {
|
||
console.error("[AppDataProvider] 事件监听器设置失败:", error);
|
||
return () => {};
|
||
}
|
||
};
|
||
|
||
const cleanupPromise = setupEventListeners();
|
||
|
||
return () => {
|
||
profileUnlisten?.then((unlisten) => unlisten()).catch(console.error);
|
||
cleanupPromise.then((cleanup) => cleanup());
|
||
};
|
||
}, [refreshProxy]);
|
||
|
||
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||
"getClashConfig",
|
||
getClashConfig,
|
||
{
|
||
refreshInterval: 60000, // 60秒刷新间隔,减少频繁请求
|
||
revalidateOnFocus: false,
|
||
suspense: false,
|
||
errorRetryCount: 3,
|
||
},
|
||
);
|
||
|
||
// 提供者数据
|
||
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
||
"getProxyProviders",
|
||
getProxyProviders,
|
||
{
|
||
revalidateOnFocus: false,
|
||
revalidateOnReconnect: false,
|
||
dedupingInterval: 3000,
|
||
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: true,
|
||
revalidateOnReconnect: true,
|
||
suspense: false,
|
||
errorRetryCount: 3,
|
||
},
|
||
);
|
||
|
||
const { data: runningMode } = useSWR("getRunningMode", getRunningMode, {
|
||
revalidateOnFocus: false,
|
||
suspense: false,
|
||
errorRetryCount: 3,
|
||
});
|
||
|
||
// 高频率更新数据 (2秒)
|
||
const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
|
||
refreshInterval: 2000,
|
||
revalidateOnFocus: false,
|
||
suspense: false,
|
||
});
|
||
|
||
// 连接数据 - 使用IPC轮询更新并计算速度
|
||
const {
|
||
data: connectionsData = {
|
||
connections: [],
|
||
uploadTotal: 0,
|
||
downloadTotal: 0,
|
||
},
|
||
} = useSWR(
|
||
clashInfo && pageVisible ? "getConnections" : null,
|
||
async () => {
|
||
const data = await getConnections();
|
||
const rawConnections: IConnectionsItem[] = data.connections || [];
|
||
|
||
// 计算带速度的连接数据
|
||
const connectionsWithSpeed = calculateConnectionSpeeds(rawConnections);
|
||
|
||
// 更新上一次数据的引用
|
||
const currentMap = new Map<string, ConnectionSpeedData>();
|
||
const now = Date.now();
|
||
rawConnections.forEach((conn) => {
|
||
currentMap.set(conn.id, {
|
||
id: conn.id,
|
||
upload: conn.upload,
|
||
download: conn.download,
|
||
timestamp: now,
|
||
});
|
||
});
|
||
previousConnectionsRef.current = currentMap;
|
||
|
||
return {
|
||
connections: connectionsWithSpeed,
|
||
uploadTotal: data.uploadTotal || 0,
|
||
downloadTotal: data.downloadTotal || 0,
|
||
};
|
||
},
|
||
{
|
||
refreshInterval: 1000, // 1秒刷新一次
|
||
fallbackData: { connections: [], uploadTotal: 0, downloadTotal: 0 },
|
||
keepPreviousData: true,
|
||
onError: (error) => {
|
||
console.error("[Connections] IPC 获取数据错误:", error);
|
||
},
|
||
},
|
||
);
|
||
|
||
// 流量数据 - 使用IPC轮询更新
|
||
const { data: trafficData = { up: 0, down: 0 } } = useSWR(
|
||
clashInfo && pageVisible ? "getTrafficData" : null,
|
||
getTrafficData,
|
||
{
|
||
refreshInterval: 1000, // 1秒刷新一次
|
||
fallbackData: { up: 0, down: 0 },
|
||
keepPreviousData: true,
|
||
onSuccess: () => {
|
||
// console.log("[Traffic][AppDataProvider] IPC 获取到流量数据:", data);
|
||
},
|
||
onError: (error) => {
|
||
console.error("[Traffic][AppDataProvider] IPC 获取数据错误:", error);
|
||
},
|
||
},
|
||
);
|
||
|
||
// 内存数据 - 使用IPC轮询更新
|
||
const { data: memoryData = { inuse: 0 } } = useSWR(
|
||
clashInfo && pageVisible ? "getMemoryData" : null,
|
||
getMemoryData,
|
||
{
|
||
refreshInterval: 2000, // 2秒刷新一次
|
||
fallbackData: { inuse: 0 },
|
||
keepPreviousData: true,
|
||
onError: (error) => {
|
||
console.error("[Memory] IPC 获取数据错误:", error);
|
||
},
|
||
},
|
||
);
|
||
|
||
// 提供统一的刷新方法
|
||
const refreshAll = React.useCallback(async () => {
|
||
await Promise.all([
|
||
refreshProxy(),
|
||
refreshClashConfig(),
|
||
refreshRules(),
|
||
refreshSysproxy(),
|
||
refreshProxyProviders(),
|
||
refreshRuleProviders(),
|
||
]);
|
||
}, [
|
||
refreshProxy,
|
||
refreshClashConfig,
|
||
refreshRules,
|
||
refreshSysproxy,
|
||
refreshProxyProviders,
|
||
refreshRuleProviders,
|
||
]);
|
||
|
||
// 聚合所有数据
|
||
const value = useMemo(() => {
|
||
// 计算系统代理地址
|
||
const calculateSystemProxyAddress = () => {
|
||
if (!verge || !clashConfig) return "-";
|
||
|
||
const isPacMode = verge.proxy_auto_config ?? false;
|
||
|
||
if (isPacMode) {
|
||
// PAC模式:显示我们期望设置的代理地址
|
||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||
const proxyPort =
|
||
verge.verge_mixed_port || clashConfig["mixed-port"] || 7897;
|
||
return `${proxyHost}:${proxyPort}`;
|
||
} else {
|
||
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
|
||
const systemServer = sysproxy?.server;
|
||
if (
|
||
systemServer &&
|
||
systemServer !== "-" &&
|
||
!systemServer.startsWith(":")
|
||
) {
|
||
return systemServer;
|
||
} else {
|
||
// 系统地址无效,返回期望的代理地址
|
||
const proxyHost = verge.proxy_host || "127.0.0.1";
|
||
const proxyPort =
|
||
verge.verge_mixed_port || clashConfig["mixed-port"] || 7897;
|
||
return `${proxyHost}:${proxyPort}`;
|
||
}
|
||
}
|
||
};
|
||
|
||
return {
|
||
// 数据
|
||
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,
|
||
|
||
systemProxyAddress: calculateSystemProxyAddress(),
|
||
|
||
// 刷新方法
|
||
refreshProxy,
|
||
refreshClashConfig,
|
||
refreshRules,
|
||
refreshSysproxy,
|
||
refreshProxyProviders,
|
||
refreshRuleProviders,
|
||
refreshAll,
|
||
};
|
||
}, [
|
||
proxiesData,
|
||
clashConfig,
|
||
rulesData,
|
||
sysproxy,
|
||
runningMode,
|
||
uptimeData,
|
||
connectionsData,
|
||
trafficData,
|
||
memoryData,
|
||
proxyProviders,
|
||
ruleProviders,
|
||
verge,
|
||
refreshProxy,
|
||
refreshClashConfig,
|
||
refreshRules,
|
||
refreshSysproxy,
|
||
refreshProxyProviders,
|
||
refreshRuleProviders,
|
||
refreshAll,
|
||
]);
|
||
|
||
return <AppDataContext value={value}>{children}</AppDataContext>;
|
||
};
|
||
|
||
// 自定义Hook访问全局数据
|
||
export const useAppData = () => {
|
||
const context = use(AppDataContext);
|
||
|
||
if (!context) {
|
||
throw new Error("useAppData必须在AppDataProvider内使用");
|
||
}
|
||
|
||
return context;
|
||
};
|