From 1176f8c8638b8f7a5cd711f861e24579d7031ac7 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 4 Oct 2025 21:20:31 +0800 Subject: [PATCH] feat: refactor app data provider and context for improved data management and performance --- eslint.config.ts | 26 + src/components/base/base-dialog.tsx | 1 - src/components/home/clash-info-card.tsx | 4 +- src/components/home/clash-mode-card.tsx | 9 +- src/components/home/current-proxy-card.tsx | 30 +- .../home/enhanced-traffic-stats.tsx | 23 +- src/components/home/home-profile-card.tsx | 18 +- src/components/proxy/provider-button.tsx | 14 +- src/components/proxy/proxy-chain.tsx | 2 +- src/components/proxy/proxy-groups.tsx | 14 +- src/components/proxy/use-render-list.ts | 4 +- src/components/rule/provider-button.tsx | 12 +- .../setting/mods/sysproxy-viewer.tsx | 78 ++- src/hooks/use-current-proxy.ts | 2 +- src/hooks/use-system-proxy-state.ts | 5 +- src/pages/connections.tsx | 2 +- src/pages/rules.tsx | 4 +- src/providers/app-data-context.ts | 53 ++ src/providers/app-data-provider.tsx | 455 +++++++++--------- 19 files changed, 403 insertions(+), 353 deletions(-) create mode 100644 src/providers/app-data-context.ts diff --git a/eslint.config.ts b/eslint.config.ts index 4d4f160c..b8cf71f1 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -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", diff --git a/src/components/base/base-dialog.tsx b/src/components/base/base-dialog.tsx index 305d6026..219b5108 100644 --- a/src/components/base/base-dialog.tsx +++ b/src/components/base/base-dialog.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ import { LoadingButton } from "@mui/lab"; import { Button, diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 7f548eff..a3ca1f75 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -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"; diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index ac3a8136..bceaadb4 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -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(); diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 855b5257..b96dbb05 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -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"; // 本地存储的键名 diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index e049e06a..9bf97200 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -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 { diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index eeb6fc5d..14f77192 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -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"; diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 4b292109..522a62cd 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -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"; diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index 57e87243..285a6889 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -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, diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index 33e6b553..2622dfb1 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -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"; diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index e5c17424..172d2915 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -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"; diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index 622b143a..9059f3e2 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -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"; diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index 329d857e..b8bc792b 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -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((props, ref) => { {!value.pac && ( - <> - - {t("Server Addr")} - - {getSystemProxyAddress} - - - + + {t("Server Addr")} + {getSystemProxyAddress} + )} {value.pac && ( @@ -582,39 +578,37 @@ export const SysproxyViewer = forwardRef((props, ref) => { )} {value.pac && ( - <> - - - + {editorOpen && ( + { + let pac = DEFAULT_PAC; + if (curr && curr.trim().length > 0) { + pac = curr; + } + setValue((v) => ({ ...v, pac_content: pac })); }} - > - {t("Edit")} PAC - - {editorOpen && ( - { - let pac = DEFAULT_PAC; - if (curr && curr.trim().length > 0) { - pac = curr; - } - setValue((v) => ({ ...v, pac_content: pac })); - }} - onClose={() => setEditorOpen(false)} - /> - )} - - + onClose={() => setEditorOpen(false)} + /> + )} + )} diff --git a/src/hooks/use-current-proxy.ts b/src/hooks/use-current-proxy.ts index 2826a793..7d352326 100644 --- a/src/hooks/use-current-proxy.ts +++ b/src/hooks/use-current-proxy.ts @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { useAppData } from "@/providers/app-data-provider"; +import { useAppData } from "@/providers/app-data-context"; // 定义代理组类型 interface ProxyGroup { diff --git a/src/hooks/use-system-proxy-state.ts b/src/hooks/use-system-proxy-state.ts index 9d04c3eb..75c6fde8 100644 --- a/src/hooks/use-system-proxy-state.ts +++ b/src/hooks/use-system-proxy-state.ts @@ -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 = () => { diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index c8f98723..e2416d9d 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -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"; diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 5fe7e85c..c800e13c 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -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(); diff --git a/src/providers/app-data-context.ts b/src/providers/app-data-context.ts new file mode 100644 index 00000000..3b97c92c --- /dev/null +++ b/src/providers/app-data-context.ts @@ -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; + refreshClashConfig: () => Promise; + refreshRules: () => Promise; + refreshSysproxy: () => Promise; + refreshProxyProviders: () => Promise; + refreshRuleProviders: () => Promise; + refreshAll: () => Promise; +} + +export interface ConnectionWithSpeed extends IConnectionsItem { + curUpload: number; + curDownload: number; +} + +export interface ConnectionSpeedData { + id: string; + upload: number; + download: number; + timestamp: number; +} + +export const AppDataContext = createContext(null); + +export const useAppData = () => { + const context = use(AppDataContext); + + if (!context) { + throw new Error("useAppData必须在AppDataProvider内使用"); + } + + return context; +}; diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 8abd1380..6f9972d5 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -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; - refreshClashConfig: () => Promise; - refreshRules: () => Promise; - refreshSysproxy: () => Promise; - refreshProxyProviders: () => Promise; - refreshRuleProviders: () => Promise; - refreshAll: () => Promise; -} - -// 创建上下文 -const AppDataContext = createContext(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("profile-changed", (event) => { - const newProfileId = event.payload; - const now = Date.now(); + let isUnmounted = false; + const scheduledTimeouts = new Set>(); + 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, + delay: number, + ) => { + const timeoutId = window.setTimeout(() => { + scheduledTimeouts.delete(timeoutId); + void callback(); + }, delay); + + scheduledTimeouts.add(timeoutId); + return timeoutId; + }; + + const clearScheduledTimeout = ( + timeoutId: ReturnType, + ) => { + if (scheduledTimeouts.has(timeoutId)) { + clearTimeout(timeoutId); + scheduledTimeouts.delete(timeoutId); + } + }; + + const clearAllTimeouts = () => { + scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); + scheduledTimeouts.clear(); + }; + + const withTimeout = async ( + promise: Promise, + timeoutMs: number, + label: string, + ): Promise => { + let timeoutId: ReturnType | null = null; + + const timeoutPromise = new Promise((_, 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( + "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 {children}; }; - -// 自定义Hook访问全局数据 -export const useAppData = () => { - const context = use(AppDataContext); - - if (!context) { - throw new Error("useAppData必须在AppDataProvider内使用"); - } - - return context; -};