feat: refactor app data provider and context for improved data management and performance

This commit is contained in:
Tunglies
2025-10-04 21:20:31 +08:00
parent 90b98f695b
commit 1176f8c863
19 changed files with 403 additions and 353 deletions

View File

@@ -58,6 +58,32 @@ export default defineConfig([
"@eslint-react/no-forward-ref": "off",
// React performance and production quality rules
"@eslint-react/no-array-index-key": "warn",
"@eslint-react/no-children-count": "error",
"@eslint-react/no-children-for-each": "error",
"@eslint-react/no-children-map": "error",
"@eslint-react/no-children-only": "error",
"@eslint-react/no-children-prop": "error",
"@eslint-react/no-children-to-array": "error",
"@eslint-react/no-class-component": "error",
"@eslint-react/no-clone-element": "error",
"@eslint-react/no-create-ref": "error",
"@eslint-react/no-default-props": "error",
"@eslint-react/no-direct-mutation-state": "error",
"@eslint-react/no-implicit-key": "error",
"@eslint-react/no-prop-types": "error",
"@eslint-react/no-set-state-in-component-did-mount": "error",
"@eslint-react/no-set-state-in-component-did-update": "error",
"@eslint-react/no-set-state-in-component-will-update": "error",
"@eslint-react/no-string-refs": "error",
"@eslint-react/no-unstable-context-value": "warn",
"@eslint-react/no-unstable-default-props": "warn",
"@eslint-react/no-unused-class-component-members": "error",
"@eslint-react/no-unused-state": "error",
"@eslint-react/no-useless-fragment": "warn",
"@eslint-react/prefer-destructuring-assignment": "warn",
// TypeScript
"@typescript-eslint/no-explicit-any": "off",

View File

@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import { LoadingButton } from "@mui/lab";
import {
Button,

View File

@@ -1,10 +1,10 @@
import { DeveloperBoardOutlined } from "@mui/icons-material";
import { Typography, Stack, Divider } from "@mui/material";
import { Divider, Stack, Typography } from "@mui/material";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useClash } from "@/hooks/use-clash";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import { EnhancedCard } from "./enhanced-card";

View File

@@ -1,17 +1,16 @@
import {
DirectionsRounded,
LanguageRounded,
MultipleStopRounded,
DirectionsRounded,
} from "@mui/icons-material";
import { Box, Typography, Paper, Stack } from "@mui/material";
import { Box, Paper, Stack, Typography } from "@mui/material";
import { useLockFn } from "ahooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
import { useAppData } from "@/providers/app-data-context";
import { closeAllConnections, patchClashMode } from "@/services/cmds";
export const ClashModeCard = () => {
const { t } = useTranslation();

View File

@@ -1,37 +1,37 @@
import {
SignalWifi4Bar as SignalStrong,
AccessTimeRounded,
ChevronRight,
WifiOff as SignalError,
SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium,
SignalWifi1Bar as SignalWeak,
SignalWifi0Bar as SignalNone,
WifiOff as SignalError,
ChevronRight,
SortRounded,
AccessTimeRounded,
SignalWifi4Bar as SignalStrong,
SignalWifi1Bar as SignalWeak,
SortByAlphaRounded,
SortRounded,
} from "@mui/icons-material";
import {
Box,
Typography,
Chip,
Button,
alpha,
useTheme,
Select,
MenuItem,
Chip,
FormControl,
IconButton,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Tooltip,
IconButton,
Typography,
alpha,
useTheme,
} from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { EnhancedCard } from "@/components/home/enhanced-card";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import delayManager from "@/services/delay";
// 本地存储的键名

View File

@@ -1,22 +1,21 @@
import {
ArrowUpwardRounded,
ArrowDownwardRounded,
MemoryRounded,
LinkRounded,
CloudUploadRounded,
ArrowUpwardRounded,
CloudDownloadRounded,
CloudUploadRounded,
LinkRounded,
MemoryRounded,
} from "@mui/icons-material";
import {
Typography,
Box,
Grid,
PaletteColor,
Paper,
Typography,
alpha,
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import { useRef, useCallback, memo, useMemo } from "react";
import { ReactNode } from "react";
import { ReactNode, memo, useCallback, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
@@ -24,8 +23,8 @@ import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { isDebugEnabled, gc } from "@/services/cmds";
import { useAppData } from "@/providers/app-data-context";
import { gc, isDebugEnabled } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import {

View File

@@ -1,30 +1,30 @@
import {
CloudUploadOutlined,
StorageOutlined,
UpdateOutlined,
DnsOutlined,
SpeedOutlined,
EventOutlined,
LaunchOutlined,
SpeedOutlined,
StorageOutlined,
UpdateOutlined,
} from "@mui/icons-material";
import {
Box,
Typography,
Button,
Stack,
LinearProgress,
alpha,
useTheme,
Link,
Stack,
Typography,
alpha,
keyframes,
useTheme,
} from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useMemo, useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import { openWebUrl, updateProfile } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";

View File

@@ -1,18 +1,18 @@
import { StorageOutlined, RefreshRounded } from "@mui/icons-material";
import { RefreshRounded, StorageOutlined } from "@mui/icons-material";
import {
Button,
Box,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
DialogContent,
DialogTitle,
Divider,
IconButton,
LinearProgress,
List,
ListItem,
ListItemText,
Typography,
Divider,
LinearProgress,
alpha,
styled,
} from "@mui/material";
@@ -21,7 +21,7 @@ import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import { proxyProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";

View File

@@ -35,7 +35,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import {
closeAllConnections,
getProxies,

View File

@@ -1,28 +1,28 @@
import { ExpandMoreRounded } from "@mui/icons-material";
import {
Box,
Snackbar,
Alert,
Box,
Chip,
Typography,
IconButton,
Menu,
MenuItem,
Snackbar,
Typography,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import useSWR from "swr";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import {
providerHealthCheck,
getGroupProxyDelays,
updateProxyChainConfigInRuntime,
getRuntimeConfig,
providerHealthCheck,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
import delayManager from "@/services/delay";

View File

@@ -2,14 +2,14 @@ import { useEffect, useMemo } from "react";
import useSWR from "swr";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import { getRuntimeConfig } from "@/services/cmds";
import delayManager from "@/services/delay";
import { filterSort } from "./use-filter-sort";
import {
useHeadStateNew,
DEFAULT_STATE,
useHeadStateNew,
type HeadState,
} from "./use-head-state";
import { useWindowWidth } from "./use-window-width";

View File

@@ -1,17 +1,17 @@
import { StorageOutlined, RefreshRounded } from "@mui/icons-material";
import { RefreshRounded, StorageOutlined } from "@mui/icons-material";
import {
Button,
Box,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
DialogContent,
DialogTitle,
Divider,
IconButton,
List,
ListItem,
ListItemText,
Typography,
Divider,
alpha,
styled,
} from "@mui/material";
@@ -20,7 +20,7 @@ import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import { ruleProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";

View File

@@ -26,10 +26,10 @@ import { BaseFieldset } from "@/components/base/base-fieldset";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { getClashConfig } from "@/services/cmds";
import { useAppData } from "@/providers/app-data-context";
import {
getAutotemProxy,
getClashConfig,
getNetworkInterfacesInfo,
getSystemHostname,
getSystemProxy,
@@ -440,14 +440,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
</Typography>
</FlexBox>
{!value.pac && (
<>
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="value">
{getSystemProxyAddress}
</Typography>
</FlexBox>
</>
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="value">{getSystemProxyAddress}</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
@@ -582,39 +578,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)}
{value.pac && (
<>
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
<ListItemText
primary={t("PAC Script Content")}
sx={{ padding: "3px 0" }}
/>
<Button
startIcon={<EditRounded />}
variant="outlined"
onClick={() => {
setEditorOpen(true);
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
<ListItemText
primary={t("PAC Script Content")}
sx={{ padding: "3px 0" }}
/>
<Button
startIcon={<EditRounded />}
variant="outlined"
onClick={() => {
setEditorOpen(true);
}}
>
{t("Edit")} PAC
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
>
{t("Edit")} PAC
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
onClose={() => setEditorOpen(false)}
/>
)}
</ListItem>
</>
onClose={() => setEditorOpen(false)}
/>
)}
</ListItem>
)}
</List>
</BaseDialog>

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
// 定义代理组类型
interface ProxyGroup {

View File

@@ -1,9 +1,8 @@
import useSWR, { mutate } from "swr";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { getAutotemProxy } from "@/services/cmds";
import { closeAllConnections } from "@/services/cmds";
import { useAppData } from "@/providers/app-data-context";
import { closeAllConnections, getAutotemProxy } from "@/services/cmds";
// 系统代理状态检测统一逻辑
export const useSystemProxyState = () => {

View File

@@ -20,7 +20,7 @@ import {
import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
import { closeAllConnections } from "@/services/cmds";
import { useConnectionSetting } from "@/services/states";
import parseTraffic from "@/utils/parse-traffic";

View File

@@ -1,5 +1,5 @@
import { Box } from "@mui/material";
import { useState, useMemo, useRef, useEffect } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
@@ -9,7 +9,7 @@ import { ScrollTopButton } from "@/components/layout/scroll-top-button";
import { ProviderButton } from "@/components/rule/provider-button";
import RuleItem from "@/components/rule/rule-item";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { useAppData } from "@/providers/app-data-context";
const RulesPage = () => {
const { t } = useTranslation();

View File

@@ -0,0 +1,53 @@
import { createContext, use } from "react";
export 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>;
}
export interface ConnectionWithSpeed extends IConnectionsItem {
curUpload: number;
curDownload: number;
}
export interface ConnectionSpeedData {
id: string;
upload: number;
download: number;
timestamp: number;
}
export const AppDataContext = createContext<AppDataContextType | null>(null);
export const useAppData = () => {
const context = use(AppDataContext);
if (!context) {
throw new Error("useAppData必须在AppDataProvider内使用");
}
return context;
};

View File

@@ -1,5 +1,5 @@
import { listen } from "@tauri-apps/api/event";
import React, { createContext, use, useEffect, useMemo, useRef } from "react";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import useSWR from "swr";
import { useClashInfo } from "@/hooks/use-clash";
@@ -20,50 +20,11 @@ import {
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);
import {
AppDataContext,
type ConnectionSpeedData,
type ConnectionWithSpeed,
} from "./app-data-context";
// 全局数据提供者组件
export const AppDataProvider = ({
@@ -135,196 +96,227 @@ export const AppDataProvider = ({
// 监听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();
let isUnmounted = false;
const scheduledTimeouts = new Set<ReturnType<typeof setTimeout>>();
const cleanupFns: Array<() => void> = [];
const fallbackWindowListeners: Array<[string, EventListener]> = [];
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 registerCleanup = (fn: () => void) => {
if (isUnmounted) {
fn();
} else {
cleanupFns.push(fn);
}
};
const cleanupPromise = setupEventListeners();
const scheduleTimeout = (
callback: () => void | Promise<void>,
delay: number,
) => {
const timeoutId = window.setTimeout(() => {
scheduledTimeouts.delete(timeoutId);
void callback();
}, delay);
scheduledTimeouts.add(timeoutId);
return timeoutId;
};
const clearScheduledTimeout = (
timeoutId: ReturnType<typeof setTimeout>,
) => {
if (scheduledTimeouts.has(timeoutId)) {
clearTimeout(timeoutId);
scheduledTimeouts.delete(timeoutId);
}
};
const clearAllTimeouts = () => {
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
scheduledTimeouts.clear();
};
const withTimeout = async <T,>(
promise: Promise<T>,
timeoutMs: number,
label: string,
): Promise<T> => {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = scheduleTimeout(() => reject(new Error(label)), timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutId !== null) {
clearScheduledTimeout(timeoutId);
}
}
};
const handleProfileChanged = (event: { payload: string }) => {
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;
scheduleTimeout(() => {
void forceRefreshProxies()
.catch((error) => {
console.warn("[AppDataProvider] forceRefreshProxies 失败:", error);
})
.finally(() => {
scheduleTimeout(() => {
refreshProxy().catch((error) => {
console.warn("[AppDataProvider] 普通刷新也失败:", error);
});
}, 200);
});
}, 0);
};
const handleRefreshClash = () => {
const now = Date.now();
console.log("[AppDataProvider] Clash配置刷新事件");
if (now - lastUpdateTime <= refreshThrottle) {
return;
}
lastUpdateTime = now;
scheduleTimeout(async () => {
try {
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
await withTimeout(
forceRefreshProxies(),
8000,
"forceRefreshProxies timeout",
);
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) {
return;
}
lastUpdateTime = now;
scheduleTimeout(() => {
refreshProxy().catch((error) =>
console.warn("[AppDataProvider] 代理刷新失败:", error),
);
}, 100);
};
const handleForceRefreshProxies = () => {
console.log("[AppDataProvider] 强制代理刷新事件");
void forceRefreshProxies()
.then(() => {
console.log("[AppDataProvider] 强制刷新代理缓存完成");
return refreshProxy();
})
.then(() => {
console.log("[AppDataProvider] 前端代理数据刷新完成");
})
.catch((error) => {
console.warn("[AppDataProvider] 强制代理刷新失败:", error);
refreshProxy().catch((fallbackError) => {
console.warn(
"[AppDataProvider] 普通代理刷新也失败:",
fallbackError,
);
});
});
};
const initializeListeners = async () => {
try {
const unlistenProfile = await listen<string>(
"profile-changed",
handleProfileChanged,
);
registerCleanup(unlistenProfile);
} catch (error) {
console.error("[AppDataProvider] 监听 Profile 事件失败:", error);
}
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,
);
registerCleanup(() => {
unlistenClash();
unlistenProxy();
unlistenForceRefresh();
});
} catch (error) {
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
const fallbackHandlers: Array<[string, EventListener]> = [
["verge://refresh-clash-config", handleRefreshClash],
["verge://refresh-proxy-config", handleRefreshProxy],
["verge://force-refresh-proxies", handleForceRefreshProxies],
];
fallbackHandlers.forEach(([eventName, handler]) => {
window.addEventListener(eventName, handler);
fallbackWindowListeners.push([eventName, handler]);
});
}
};
void initializeListeners();
return () => {
profileUnlisten?.then((unlisten) => unlisten()).catch(console.error);
cleanupPromise.then((cleanup) => cleanup());
isUnmounted = true;
clearAllTimeouts();
fallbackWindowListeners.splice(0).forEach(([eventName, handler]) => {
window.removeEventListener(eventName, handler);
});
cleanupFns.splice(0).forEach((fn) => fn());
};
}, [refreshProxy]);
@@ -474,7 +466,7 @@ export const AppDataProvider = ({
);
// 提供统一的刷新方法
const refreshAll = React.useCallback(async () => {
const refreshAll = useCallback(async () => {
await Promise.all([
refreshProxy(),
refreshClashConfig(),
@@ -585,14 +577,3 @@ export const AppDataProvider = ({
return <AppDataContext value={value}>{children}</AppDataContext>;
};
// 自定义Hook访问全局数据
export const useAppData = () => {
const context = use(AppDataContext);
if (!context) {
throw new Error("useAppData必须在AppDataProvider内使用");
}
return context;
};