refactor: streamline app initialization and enhance WebSocket cleanup logic

This commit is contained in:
xmk23333
2025-10-20 13:15:51 +08:00
parent a0ef64cda8
commit b77cc012e1
16 changed files with 582 additions and 554 deletions

View File

@@ -408,6 +408,18 @@ pub fn run() {
/// Handle window destroyed events /// Handle window destroyed events
pub fn handle_window_destroyed() { pub fn handle_window_destroyed() {
AsyncHandler::spawn(|| async {
if let Err(e) = handle::Handle::mihomo()
.await
.clear_all_ws_connections()
.await
{
logging!(warn, Type::Window, "清理 WebSocket 连接失败: {}", e);
} else {
logging!(info, Type::Window, "WebSocket 连接已清理");
}
});
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
use crate::core::hotkey::SystemHotkey; use crate::core::hotkey::SystemHotkey;

35
src/hooks/use-cleanup.ts Normal file
View File

@@ -0,0 +1,35 @@
import { useEffect, useRef } from "react";
/**
* 资源清理 Hook
* 用于在组件卸载或窗口关闭时统一清理资源
*/
export const useCleanup = () => {
const cleanupFnsRef = useRef<Set<() => void>>(new Set());
const registerCleanup = (fn: () => void) => {
cleanupFnsRef.current.add(fn);
return () => {
cleanupFnsRef.current.delete(fn);
};
};
const cleanup = () => {
cleanupFnsRef.current.forEach((fn) => {
try {
fn();
} catch (error) {
console.error("[资源清理] 清理失败:", error);
}
});
cleanupFnsRef.current.clear();
};
useEffect(() => {
return () => {
cleanup();
};
}, []);
return { registerCleanup, cleanup };
};

View File

@@ -87,7 +87,12 @@ export const useConnectionData = () => {
} }
return () => { return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
ws.current?.close(); ws.current?.close();
ws.current = null;
}; };
}, },
{ {

View File

@@ -13,9 +13,26 @@ export const useListen = () => {
unlistenFns.current.push(unlisten); unlistenFns.current.push(unlisten);
return unlisten; return unlisten;
}; };
const removeAllListeners = () => { const removeAllListeners = () => {
unlistenFns.current.forEach((unlisten) => unlisten()); const errors: Error[] = [];
unlistenFns.current = [];
unlistenFns.current.forEach((unlisten) => {
try {
unlisten();
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
});
if (errors.length > 0) {
console.warn(
`[useListen] 清理监听器时发生 ${errors.length} 个错误`,
errors,
);
}
unlistenFns.current.length = 0;
}; };
const setupCloseListener = async function () { const setupCloseListener = async function () {
@@ -26,6 +43,7 @@ export const useListen = () => {
return { return {
addListener, addListener,
removeAllListeners,
setupCloseListener, setupCloseListener,
}; };
}; };

View File

@@ -120,7 +120,12 @@ export const useLogData = () => {
} }
return () => { return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
ws.current?.close(); ws.current?.close();
ws.current = null;
}; };
}, },
{ {

View File

@@ -63,7 +63,12 @@ export const useMemoryData = () => {
} }
return () => { return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
ws.current?.close(); ws.current?.close();
ws.current = null;
}; };
}, },
{ {

View File

@@ -69,7 +69,12 @@ export const useTrafficData = () => {
} }
return () => { return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
ws.current?.close(); ws.current?.close();
ws.current = null;
}; };
}, },
{ {

View File

@@ -47,41 +47,30 @@ document.addEventListener("keydown", (event) => {
} }
}); });
const initializeApp = async () => { const initializeApp = () => {
try { const contexts = [
await initializeLanguage("zh"); <ThemeModeProvider key="theme" />,
<LoadingCacheProvider key="loading" />,
<UpdateStateProvider key="update" />,
];
const contexts = [ const root = createRoot(container);
<ThemeModeProvider key="theme" />, root.render(
<LoadingCacheProvider key="loading" />, <React.StrictMode>
<UpdateStateProvider key="update" />, <ComposeContextProvider contexts={contexts}>
]; <BaseErrorBoundary>
<WindowProvider>
const root = createRoot(container); <AppDataProvider>
root.render( <RouterProvider router={router} />
<React.StrictMode> </AppDataProvider>
<ComposeContextProvider contexts={contexts}> </WindowProvider>
<BaseErrorBoundary> </BaseErrorBoundary>
<WindowProvider> </ComposeContextProvider>
<AppDataProvider> </React.StrictMode>,
<RouterProvider router={router} /> );
</AppDataProvider>
</WindowProvider>
</BaseErrorBoundary>
</ComposeContextProvider>
</React.StrictMode>,
);
} catch (error) {
console.error("[main.tsx] 应用初始化失败:", error);
const root = createRoot(container);
root.render(
<div style={{ padding: "20px", color: "red" }}>
: {error instanceof Error ? error.message : String(error)}
</div>,
);
}
}; };
initializeLanguage("zh").catch(console.error);
initializeApp(); initializeApp();
// 错误处理 // 错误处理
@@ -94,7 +83,7 @@ window.addEventListener("unhandledrejection", (event) => {
}); });
// 页面关闭/刷新事件 // 页面关闭/刷新事件
window.addEventListener("beforeunload", async () => { window.addEventListener("beforeunload", () => {
// 强制清理所有 WebSocket 实例, 防止内存泄漏 // 同步清理所有 WebSocket 实例, 防止内存泄漏
await MihomoWebSocket.cleanupAll(); MihomoWebSocket.cleanupAll();
}); });

View File

@@ -1,13 +1,10 @@
import { List, Paper, SvgIcon, ThemeProvider } from "@mui/material"; import { List, Paper, SvgIcon, ThemeProvider } from "@mui/material";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { useCallback, useEffect, useMemo, useRef } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Outlet, useNavigate } from "react-router"; import { Outlet, useNavigate } from "react-router";
import { SWRConfig, mutate } from "swr"; import { SWRConfig } from "swr";
import iconDark from "@/assets/image/icon_dark.svg?react"; import iconDark from "@/assets/image/icon_dark.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react"; import iconLight from "@/assets/image/icon_light.svg?react";
@@ -21,229 +18,60 @@ import { UpdateButton } from "@/components/layout/update-button";
import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { useConnectionData } from "@/hooks/use-connection-data"; import { useConnectionData } from "@/hooks/use-connection-data";
import { useI18n } from "@/hooks/use-i18n"; import { useI18n } from "@/hooks/use-i18n";
import { useListen } from "@/hooks/use-listen";
import { useLogData } from "@/hooks/use-log-data"; import { useLogData } from "@/hooks/use-log-data";
import { useMemoryData } from "@/hooks/use-memory-data"; import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data"; import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useWindowDecorations } from "@/hooks/use-window"; import { useWindowDecorations } from "@/hooks/use-window";
import { getAxios } from "@/services/api";
import { showNotice } from "@/services/noticeService";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { handleNoticeMessage } from "./_layout/notificationHandlers";
import { useAppInitialization } from "./_layout/useAppInitialization";
import { useLayoutEvents } from "./_layout/useLayoutEvents";
import { useLoadingOverlay } from "./_layout/useLoadingOverlay";
import { navItems } from "./_routers"; import { navItems } from "./_routers";
import "dayjs/locale/ru"; import "dayjs/locale/ru";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
const appWindow = getCurrentWebviewWindow();
export const portableFlag = false; export const portableFlag = false;
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const OS = getSystem(); const OS = getSystem();
// 通知处理函数
const handleNoticeMessage = (
status: string,
msg: string,
t: (key: string) => string,
navigate: (path: string, options?: any) => void,
) => {
console.log("[通知监听 V2] 收到消息:", status, msg);
switch (status) {
case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } });
showNotice("success", t("Import Subscription Successful"));
break;
case "import_sub_url::error":
navigate("/profile");
showNotice("error", msg);
break;
case "set_config::error":
showNotice("error", msg);
break;
case "update_with_clash_proxy":
showNotice(
"success",
`${t("Update with Clash proxy successfully")} ${msg}`,
);
break;
case "update_retry_with_clash":
showNotice("info", t("Update failed, retrying with Clash proxy..."));
break;
case "update_failed_even_with_clash":
showNotice(
"error",
`${t("Update failed even with Clash proxy")}: ${msg}`,
);
break;
case "update_failed":
showNotice("error", msg);
break;
case "config_validate::boot_error":
showNotice("error", `${t("Boot Config Validation Failed")} ${msg}`);
break;
case "config_validate::core_change":
showNotice(
"error",
`${t("Core Change Config Validation Failed")} ${msg}`,
);
break;
case "config_validate::error":
showNotice("error", `${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::process_terminated":
showNotice("error", t("Config Validation Process Terminated"));
break;
case "config_validate::stdout_error":
showNotice("error", `${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::script_error":
showNotice("error", `${t("Script File Error")} ${msg}`);
break;
case "config_validate::script_syntax_error":
showNotice("error", `${t("Script Syntax Error")} ${msg}`);
break;
case "config_validate::script_missing_main":
showNotice("error", `${t("Script Missing Main")} ${msg}`);
break;
case "config_validate::file_not_found":
showNotice("error", `${t("File Not Found")} ${msg}`);
break;
case "config_validate::yaml_syntax_error":
showNotice("error", `${t("YAML Syntax Error")} ${msg}`);
break;
case "config_validate::yaml_read_error":
showNotice("error", `${t("YAML Read Error")} ${msg}`);
break;
case "config_validate::yaml_mapping_error":
showNotice("error", `${t("YAML Mapping Error")} ${msg}`);
break;
case "config_validate::yaml_key_error":
showNotice("error", `${t("YAML Key Error")} ${msg}`);
break;
case "config_validate::yaml_error":
showNotice("error", `${t("YAML Error")} ${msg}`);
break;
case "config_validate::merge_syntax_error":
showNotice("error", `${t("Merge File Syntax Error")} ${msg}`);
break;
case "config_validate::merge_mapping_error":
showNotice("error", `${t("Merge File Mapping Error")} ${msg}`);
break;
case "config_validate::merge_key_error":
showNotice("error", `${t("Merge File Key Error")} ${msg}`);
break;
case "config_validate::merge_error":
showNotice("error", `${t("Merge File Error")} ${msg}`);
break;
case "config_core::change_success":
showNotice("success", `${t("Core Changed Successfully")}: ${msg}`);
break;
case "config_core::change_error":
showNotice("error", `${t("Failed to Change Core")}: ${msg}`);
break;
default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
break;
}
};
const Layout = () => { const Layout = () => {
useTrafficData(); const trafficData = useTrafficData();
useMemoryData(); const memoryData = useMemoryData();
useConnectionData(); const connectionData = useConnectionData();
useLogData(); const logData = useLogData();
const mode = useThemeMode(); const mode = useThemeMode();
const isDark = mode === "light" ? false : true; const isDark = mode !== "light";
const { t } = useTranslation(); const { t } = useTranslation();
const { theme } = useCustomTheme(); const { theme } = useCustomTheme();
const { verge } = useVerge(); const { verge } = useVerge();
const { language } = verge ?? {}; const { language } = verge ?? {};
const { switchLanguage } = useI18n(); const { switchLanguage } = useI18n();
const navigate = useNavigate(); const navigate = useNavigate();
const { addListener } = useListen();
const initRef = useRef(false);
const overlayRemovedRef = useRef(false);
const themeReady = useMemo(() => Boolean(theme), [theme]); const themeReady = useMemo(() => Boolean(theme), [theme]);
const windowControls = useRef<any>(null); const windowControls = useRef<any>(null);
const { decorated } = useWindowDecorations(); const { decorated } = useWindowDecorations();
const customTitlebar = useMemo(() => { const customTitlebar = useMemo(
console.debug( () =>
"[Layout] Titlebar rendering - decorated:", !decorated ? (
decorated,
"| showing:",
!decorated,
"| theme mode:",
mode,
);
if (!decorated) {
return (
<div className="the_titlebar" data-tauri-drag-region="true"> <div className="the_titlebar" data-tauri-drag-region="true">
<WindowControls ref={windowControls} /> <WindowControls ref={windowControls} />
</div> </div>
); ) : null,
} [decorated],
return null; );
}, [decorated, mode]);
useEffect(() => { useLoadingOverlay(themeReady);
if (!themeReady || overlayRemovedRef.current) { useAppInitialization();
return;
}
let fadeTimer: number | null = null;
let retryTimer: number | null = null;
let attempts = 0;
const maxAttempts = 50;
let stopped = false;
const tryRemoveOverlay = () => {
if (stopped || overlayRemovedRef.current) {
return;
}
const overlay = document.getElementById("initial-loading-overlay");
if (overlay) {
overlayRemovedRef.current = true;
overlay.style.opacity = "0";
overlay.style.pointerEvents = "none";
fadeTimer = window.setTimeout(() => {
try {
overlay.remove();
} catch (error) {
console.warn("[Layout] Failed to remove loading overlay:", error);
}
}, 300);
return;
}
if (attempts < maxAttempts) {
attempts += 1;
retryTimer = window.setTimeout(tryRemoveOverlay, 100);
} else {
console.warn("[Layout] Loading overlay not found after retries");
}
};
tryRemoveOverlay();
return () => {
stopped = true;
if (fadeTimer) {
window.clearTimeout(fadeTimer);
}
if (retryTimer) {
window.clearTimeout(retryTimer);
}
};
}, [themeReady]);
const handleNotice = useCallback( const handleNotice = useCallback(
(payload: [string, string]) => { (payload: [string, string]) => {
@@ -251,275 +79,14 @@ const Layout = () => {
try { try {
handleNoticeMessage(status, msg, t, navigate); handleNoticeMessage(status, msg, t, navigate);
} catch (error) { } catch (error) {
console.error("[Layout] 处理通知消息失败:", error); console.error("[通知处理] 失败:", error);
} }
}, },
[t, navigate], [t, navigate],
); );
// 设置监听 useLayoutEvents(handleNotice);
useEffect(() => {
const unlisteners: Array<() => void> = [];
let disposed = false;
const register = (
maybeUnlisten: void | (() => void) | Promise<void | (() => void)>,
) => {
if (!maybeUnlisten) {
return;
}
if (typeof maybeUnlisten === "function") {
unlisteners.push(maybeUnlisten);
return;
}
maybeUnlisten
.then((unlisten) => {
if (!unlisten) {
return;
}
if (disposed) {
unlisten();
} else {
unlisteners.push(unlisten);
}
})
.catch((error) => {
console.error("[Layout] 注册事件监听失败", error);
});
};
register(
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
}),
);
register(
addListener("verge://refresh-verge-config", () => {
mutate("getVergeConfig");
mutate("getSystemProxy");
mutate("getAutotemProxy");
mutate("getRunningMode");
mutate("isServiceAvailable");
}),
);
register(
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
);
register(
(async () => {
const [hideUnlisten, showUnlisten] = await Promise.all([
listen("verge://hide-window", () => appWindow.hide()),
listen("verge://show-window", () => appWindow.show()),
]);
return () => {
hideUnlisten();
showUnlisten();
};
})(),
);
return () => {
disposed = true;
unlisteners.forEach((unlisten) => {
try {
unlisten();
} catch (error) {
console.error("[Layout] 清理事件监听器失败", error);
}
});
};
}, [addListener, handleNotice]);
useEffect(() => {
if (initRef.current) {
console.log("[Layout] 初始化代码已执行过,跳过");
return;
}
console.log("[Layout] 开始执行初始化代码");
initRef.current = true;
let isInitialized = false;
let initializationAttempts = 0;
const maxAttempts = 3;
const timers = new Set<number>();
const scheduleTimeout = (handler: () => void, delay: number) => {
/* eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout -- timeout is registered in the timers set and cleared during cleanup */
const id = window.setTimeout(handler, delay);
timers.add(id);
return id;
};
const notifyBackend = async (action: string, stage?: string) => {
try {
if (stage) {
console.log(`[Layout] 通知后端 ${action}: ${stage}`);
await invoke("update_ui_stage", { stage });
} else {
console.log(`[Layout] 通知后端 ${action}`);
await invoke("notify_ui_ready");
}
} catch (err) {
console.error(`[Layout] 通知失败 ${action}:`, err);
}
};
const removeLoadingOverlay = () => {
const initialOverlay = document.getElementById("initial-loading-overlay");
if (initialOverlay) {
console.log("[Layout] 移除加载指示器");
initialOverlay.style.opacity = "0";
scheduleTimeout(() => {
try {
initialOverlay.remove();
} catch {
console.log("[Layout] 加载指示器已被移除");
}
}, 300);
}
};
const performInitialization = async () => {
if (isInitialized) {
console.log("[Layout] 已经初始化过,跳过");
return;
}
initializationAttempts++;
console.log(`[Layout] 开始第 ${initializationAttempts} 次初始化尝试`);
try {
removeLoadingOverlay();
await notifyBackend("加载阶段", "Loading");
await new Promise<void>((resolve) => {
const checkReactMount = () => {
const rootElement = document.getElementById("root");
if (rootElement && rootElement.children.length > 0) {
console.log("[Layout] React组件已挂载");
resolve();
} else {
scheduleTimeout(checkReactMount, 50);
}
};
checkReactMount();
scheduleTimeout(() => {
console.log("[Layout] React组件挂载检查超时继续执行");
resolve();
}, 2000);
});
await notifyBackend("DOM就绪", "DomReady");
await new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve());
});
await notifyBackend("资源加载完成", "ResourcesLoaded");
await notifyBackend("UI就绪");
isInitialized = true;
console.log(`[Layout] 第 ${initializationAttempts} 次初始化完成`);
} catch (error) {
console.error(
`[Layout] 第 ${initializationAttempts} 次初始化失败:`,
error,
);
if (initializationAttempts < maxAttempts) {
console.log(
`[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`,
);
scheduleTimeout(performInitialization, 500);
} else {
console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化");
removeLoadingOverlay();
try {
await notifyBackend("UI就绪");
isInitialized = true;
} catch (e) {
console.error("[Layout] 紧急初始化也失败", e);
}
}
}
};
let hasEventTriggered = false;
const setupEventListener = async () => {
try {
console.log("[Layout] 开始监听启动完成事件");
// TODO: 监听启动完成事件的实现
} catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err);
}
};
void setupEventListener();
const checkImmediateInitialization = async () => {
try {
console.log("[Layout] 检查后端是否已就绪");
await invoke("update_ui_stage", { stage: "Loading" });
if (!hasEventTriggered && !isInitialized) {
console.log("[Layout] 后端已就绪,立即开始初始化");
hasEventTriggered = true;
performInitialization();
}
} catch {
console.log("[Layout] 后端尚未就绪,等待启动完成事件");
}
};
const backupInitialization = scheduleTimeout(() => {
if (!hasEventTriggered && !isInitialized) {
console.warn("[Layout] 备用初始化触发1.5秒内未开始初始化");
hasEventTriggered = true;
performInitialization();
}
}, 1500);
const emergencyInitialization = scheduleTimeout(() => {
if (!isInitialized) {
console.error("[Layout] 紧急初始化触发5秒内未完成初始化");
removeLoadingOverlay();
notifyBackend("UI就绪").catch(() => {});
isInitialized = true;
}
}, 5000);
const immediateInitTimer = scheduleTimeout(
checkImmediateInitialization,
100,
);
return () => {
window.clearTimeout(backupInitialization);
window.clearTimeout(emergencyInitialization);
window.clearTimeout(immediateInitTimer);
timers.delete(backupInitialization);
timers.delete(emergencyInitialization);
timers.delete(immediateInitTimer);
timers.forEach((timeoutId) => window.clearTimeout(timeoutId));
timers.clear();
};
}, []);
// 语言和起始页设置
useEffect(() => { useEffect(() => {
if (language) { if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language); dayjs.locale(language === "zh" ? "zh-cn" : language);

View File

@@ -0,0 +1,86 @@
import { showNotice } from "@/services/noticeService";
type NavigateFunction = (path: string, options?: any) => void;
type TranslateFunction = (key: string) => string;
export const handleNoticeMessage = (
status: string,
msg: string,
t: TranslateFunction,
navigate: NavigateFunction,
) => {
const handlers: Record<string, () => void> = {
"import_sub_url::ok": () => {
navigate("/profile", { state: { current: msg } });
showNotice("success", t("Import Subscription Successful"));
},
"import_sub_url::error": () => {
navigate("/profile");
showNotice("error", msg);
},
"set_config::error": () => showNotice("error", msg),
update_with_clash_proxy: () =>
showNotice(
"success",
`${t("Update with Clash proxy successfully")} ${msg}`,
),
update_retry_with_clash: () =>
showNotice("info", t("Update failed, retrying with Clash proxy...")),
update_failed_even_with_clash: () =>
showNotice(
"error",
`${t("Update failed even with Clash proxy")}: ${msg}`,
),
update_failed: () => showNotice("error", msg),
"config_validate::boot_error": () =>
showNotice("error", `${t("Boot Config Validation Failed")} ${msg}`),
"config_validate::core_change": () =>
showNotice(
"error",
`${t("Core Change Config Validation Failed")} ${msg}`,
),
"config_validate::error": () =>
showNotice("error", `${t("Config Validation Failed")} ${msg}`),
"config_validate::process_terminated": () =>
showNotice("error", t("Config Validation Process Terminated")),
"config_validate::stdout_error": () =>
showNotice("error", `${t("Config Validation Failed")} ${msg}`),
"config_validate::script_error": () =>
showNotice("error", `${t("Script File Error")} ${msg}`),
"config_validate::script_syntax_error": () =>
showNotice("error", `${t("Script Syntax Error")} ${msg}`),
"config_validate::script_missing_main": () =>
showNotice("error", `${t("Script Missing Main")} ${msg}`),
"config_validate::file_not_found": () =>
showNotice("error", `${t("File Not Found")} ${msg}`),
"config_validate::yaml_syntax_error": () =>
showNotice("error", `${t("YAML Syntax Error")} ${msg}`),
"config_validate::yaml_read_error": () =>
showNotice("error", `${t("YAML Read Error")} ${msg}`),
"config_validate::yaml_mapping_error": () =>
showNotice("error", `${t("YAML Mapping Error")} ${msg}`),
"config_validate::yaml_key_error": () =>
showNotice("error", `${t("YAML Key Error")} ${msg}`),
"config_validate::yaml_error": () =>
showNotice("error", `${t("YAML Error")} ${msg}`),
"config_validate::merge_syntax_error": () =>
showNotice("error", `${t("Merge File Syntax Error")} ${msg}`),
"config_validate::merge_mapping_error": () =>
showNotice("error", `${t("Merge File Mapping Error")} ${msg}`),
"config_validate::merge_key_error": () =>
showNotice("error", `${t("Merge File Key Error")} ${msg}`),
"config_validate::merge_error": () =>
showNotice("error", `${t("Merge File Error")} ${msg}`),
"config_core::change_success": () =>
showNotice("success", `${t("Core Changed Successfully")}: ${msg}`),
"config_core::change_error": () =>
showNotice("error", `${t("Failed to Change Core")}: ${msg}`),
};
const handler = handlers[status];
if (handler) {
handler();
} else {
console.warn(`未处理的通知状态: ${status}`);
}
};

View File

@@ -0,0 +1,108 @@
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useRef } from "react";
export const useAppInitialization = () => {
const initRef = useRef(false);
useEffect(() => {
if (initRef.current) return;
initRef.current = true;
let isInitialized = false;
let isCancelled = false;
const timers = new Set<number>();
const scheduleTimeout = (handler: () => void, delay: number) => {
if (isCancelled) return -1;
const id = window.setTimeout(() => {
if (!isCancelled) {
handler();
}
timers.delete(id);
}, delay);
timers.add(id);
return id;
};
const notifyBackend = async (stage?: string) => {
try {
if (stage) {
await invoke("update_ui_stage", { stage });
} else {
await invoke("notify_ui_ready");
}
} catch (err) {
console.error(`[初始化] 通知后端失败:`, err);
}
};
const removeLoadingOverlay = () => {
const overlay = document.getElementById("initial-loading-overlay");
if (overlay) {
overlay.style.opacity = "0";
scheduleTimeout(() => overlay.remove(), 300);
}
};
const performInitialization = async () => {
if (isInitialized) return;
isInitialized = true;
try {
removeLoadingOverlay();
await notifyBackend("Loading");
await new Promise<void>((resolve) => {
const check = () => {
const root = document.getElementById("root");
if (root && root.children.length > 0) {
resolve();
} else {
scheduleTimeout(check, 50);
}
};
check();
scheduleTimeout(resolve, 2000);
});
await notifyBackend("DomReady");
await new Promise((resolve) => requestAnimationFrame(resolve));
await notifyBackend("ResourcesLoaded");
await notifyBackend();
} catch (error) {
console.error("[初始化] 失败:", error);
removeLoadingOverlay();
notifyBackend().catch(console.error);
}
};
const checkBackendReady = async () => {
try {
await invoke("update_ui_stage", { stage: "Loading" });
performInitialization();
} catch {
scheduleTimeout(performInitialization, 1500);
}
};
scheduleTimeout(checkBackendReady, 100);
scheduleTimeout(() => {
if (!isInitialized) {
removeLoadingOverlay();
notifyBackend().catch(console.error);
}
}, 5000);
return () => {
isCancelled = true;
timers.forEach((id) => {
try {
window.clearTimeout(id);
} catch (error) {
console.warn("[初始化] 清理定时器失败:", error);
}
});
timers.clear();
};
}, []);
};

View File

@@ -0,0 +1,104 @@
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useEffect } from "react";
import { mutate } from "swr";
import { useListen } from "@/hooks/use-listen";
import { getAxios } from "@/services/api";
export const useLayoutEvents = (
handleNotice: (payload: [string, string]) => void,
) => {
const { addListener } = useListen();
useEffect(() => {
const unlisteners: Array<() => void> = [];
let disposed = false;
const register = (
maybeUnlisten: void | (() => void) | Promise<void | (() => void)>,
) => {
if (!maybeUnlisten) return;
if (typeof maybeUnlisten === "function") {
unlisteners.push(maybeUnlisten);
return;
}
maybeUnlisten
.then((unlisten) => {
if (!unlisten) return;
if (disposed) {
unlisten();
} else {
unlisteners.push(unlisten);
}
})
.catch((error) => console.error("[事件监听] 注册失败", error));
};
register(
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
}),
);
register(
addListener("verge://refresh-verge-config", () => {
mutate("getVergeConfig");
mutate("getSystemProxy");
mutate("getAutotemProxy");
mutate("getRunningMode");
mutate("isServiceAvailable");
}),
);
register(
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
);
const appWindow = getCurrentWebviewWindow();
register(
(async () => {
const [hideUnlisten, showUnlisten] = await Promise.all([
listen("verge://hide-window", () => appWindow.hide()),
listen("verge://show-window", () => appWindow.show()),
]);
return () => {
hideUnlisten();
showUnlisten();
};
})(),
);
return () => {
disposed = true;
const errors: Error[] = [];
unlisteners.forEach((unlisten) => {
try {
unlisten();
} catch (error) {
errors.push(
error instanceof Error ? error : new Error(String(error)),
);
}
});
if (errors.length > 0) {
console.error(
`[事件监听] 清理过程中发生 ${errors.length} 个错误:`,
errors,
);
}
unlisteners.length = 0;
};
}, [addListener, handleNotice]);
};

View File

@@ -0,0 +1,25 @@
import { useEffect, useRef } from "react";
export const useLazyDataLoad = (
callbacks: Array<() => void>,
delay: number = 1000,
) => {
const hasLoadedRef = useRef(false);
useEffect(() => {
if (hasLoadedRef.current) return;
const timer = window.setTimeout(() => {
hasLoadedRef.current = true;
callbacks.forEach((callback) => {
try {
callback();
} catch (error) {
console.error("[延迟加载] 执行失败:", error);
}
});
}, delay);
return () => window.clearTimeout(timer);
}, [callbacks, delay]);
};

View File

@@ -0,0 +1,50 @@
import { useEffect, useRef } from "react";
export const useLoadingOverlay = (themeReady: boolean) => {
const overlayRemovedRef = useRef(false);
useEffect(() => {
if (!themeReady || overlayRemovedRef.current) return;
let fadeTimer: number | null = null;
let retryTimer: number | null = null;
let attempts = 0;
const maxAttempts = 50;
let stopped = false;
const tryRemoveOverlay = () => {
if (stopped || overlayRemovedRef.current) return;
const overlay = document.getElementById("initial-loading-overlay");
if (overlay) {
overlayRemovedRef.current = true;
overlay.style.opacity = "0";
overlay.style.pointerEvents = "none";
fadeTimer = window.setTimeout(() => {
try {
overlay.remove();
} catch (error) {
console.warn("[加载遮罩] 移除失败:", error);
}
}, 300);
return;
}
if (attempts < maxAttempts) {
attempts += 1;
retryTimer = window.setTimeout(tryRemoveOverlay, 100);
} else {
console.warn("[加载遮罩] 未找到元素");
}
};
tryRemoveOverlay();
return () => {
stopped = true;
if (fadeTimer) window.clearTimeout(fadeTimer);
if (retryTimer) window.clearTimeout(retryTimer);
};
}, [themeReady]);
};

View File

@@ -26,15 +26,15 @@ export const AppDataProvider = ({
}) => { }) => {
const { verge } = useVerge(); const { verge } = useVerge();
// 基础数据 - 中频率更新 (5秒)
const { data: proxiesData, mutate: refreshProxy } = useSWR( const { data: proxiesData, mutate: refreshProxy } = useSWR(
"getProxies", "getProxies",
calcuProxies, calcuProxies,
{ {
refreshInterval: 5000, refreshInterval: 8000,
revalidateOnFocus: true, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
dedupingInterval: 3000,
}, },
); );
@@ -42,23 +42,23 @@ export const AppDataProvider = ({
"getClashConfig", "getClashConfig",
getBaseConfig, getBaseConfig,
{ {
refreshInterval: 60000, // 60秒刷新间隔减少频繁请求 refreshInterval: 60000,
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
dedupingInterval: 5000,
}, },
); );
// 提供者数据
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR( const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders", "getProxyProviders",
calcuProxyProviders, calcuProxyProviders,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
dedupingInterval: 3000, dedupingInterval: 5000,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
}, },
); );
@@ -68,26 +68,26 @@ export const AppDataProvider = ({
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
dedupingInterval: 5000,
}, },
); );
// 低频率更新数据
const { data: rulesData, mutate: refreshRules } = useSWR( const { data: rulesData, mutate: refreshRules } = useSWR(
"getRules", "getRules",
getRules, getRules,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
dedupingInterval: 5000,
}, },
); );
// 监听profile和clash配置变更事件
useEffect(() => { useEffect(() => {
let lastProfileId: string | null = null; let lastProfileId: string | null = null;
let lastUpdateTime = 0; let lastUpdateTime = 0;
const refreshThrottle = 500; const refreshThrottle = 800;
let isUnmounted = false; let isUnmounted = false;
const scheduledTimeouts = new Set<number>(); const scheduledTimeouts = new Set<number>();
@@ -95,14 +95,17 @@ export const AppDataProvider = ({
const registerCleanup = (fn: () => void) => { const registerCleanup = (fn: () => void) => {
if (isUnmounted) { if (isUnmounted) {
fn(); try {
fn();
} catch (error) {
console.error("[数据提供者] 立即清理失败:", error);
}
} else { } else {
cleanupFns.push(fn); cleanupFns.push(fn);
} }
}; };
const addWindowListener = (eventName: string, handler: EventListener) => { const addWindowListener = (eventName: string, handler: EventListener) => {
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener -- cleanup is returned by this helper
window.addEventListener(eventName, handler); window.addEventListener(eventName, handler);
return () => window.removeEventListener(eventName, handler); return () => window.removeEventListener(eventName, handler);
}; };
@@ -111,9 +114,13 @@ export const AppDataProvider = ({
callback: () => void | Promise<void>, callback: () => void | Promise<void>,
delay: number, delay: number,
) => { ) => {
if (isUnmounted) return -1;
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
scheduledTimeouts.delete(timeoutId); scheduledTimeouts.delete(timeoutId);
void callback(); if (!isUnmounted) {
void callback();
}
}, delay); }, delay);
scheduledTimeouts.add(timeoutId); scheduledTimeouts.add(timeoutId);
@@ -129,66 +136,48 @@ export const AppDataProvider = ({
const newProfileId = event.payload; const newProfileId = event.payload;
const now = Date.now(); const now = Date.now();
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
if ( if (
lastProfileId === newProfileId && lastProfileId === newProfileId &&
now - lastUpdateTime < refreshThrottle now - lastUpdateTime < refreshThrottle
) { ) {
console.log("[AppDataProvider] 重复事件被防抖,跳过");
return; return;
} }
lastProfileId = newProfileId; lastProfileId = newProfileId;
lastUpdateTime = now; lastUpdateTime = now;
// 刷新规则数据 scheduleTimeout(() => {
refreshRules().catch((error) => refreshRules().catch((error) =>
console.warn("[AppDataProvider] 规则刷新失败:", error), console.warn("[数据提供者] 规则刷新失败:", error),
); );
refreshRuleProviders().catch((error) => refreshRuleProviders().catch((error) =>
console.warn("[AppDataProvider] 规则提供者刷新失败:", error), console.warn("[数据提供者] 规则提供者刷新失败:", error),
); );
}, 200);
}; };
const handleRefreshClash = () => { const handleRefreshClash = () => {
const now = Date.now(); const now = Date.now();
console.log("[AppDataProvider] Clash配置刷新事件"); if (now - lastUpdateTime <= refreshThrottle) return;
if (now - lastUpdateTime <= refreshThrottle) {
return;
}
lastUpdateTime = now; lastUpdateTime = now;
scheduleTimeout(() => {
scheduleTimeout(async () => { refreshProxy().catch((error) =>
try { console.error("[数据提供者] 代理刷新失败:", error),
console.log("[AppDataProvider] Clash刷新 - 刷新代理数据"); );
await refreshProxy(); }, 200);
} catch (error) {
console.error(
"[AppDataProvider] Clash刷新时刷新代理数据失败:",
error,
);
}
}, 0);
}; };
const handleRefreshProxy = () => { const handleRefreshProxy = () => {
const now = Date.now(); const now = Date.now();
console.log("[AppDataProvider] 代理配置刷新事件"); if (now - lastUpdateTime <= refreshThrottle) return;
if (now - lastUpdateTime <= refreshThrottle) {
return;
}
lastUpdateTime = now; lastUpdateTime = now;
scheduleTimeout(() => { scheduleTimeout(() => {
refreshProxy().catch((error) => refreshProxy().catch((error) =>
console.warn("[AppDataProvider] 代理刷新失败:", error), console.warn("[数据提供者] 代理刷新失败:", error),
); );
}, 100); }, 200);
}; };
const initializeListeners = async () => { const initializeListeners = async () => {
@@ -235,7 +224,24 @@ export const AppDataProvider = ({
return () => { return () => {
isUnmounted = true; isUnmounted = true;
clearAllTimeouts(); clearAllTimeouts();
cleanupFns.splice(0).forEach((fn) => fn());
const errors: Error[] = [];
cleanupFns.splice(0).forEach((fn) => {
try {
fn();
} catch (error) {
errors.push(
error instanceof Error ? error : new Error(String(error)),
);
}
});
if (errors.length > 0) {
console.error(
`[数据提供者] 清理过程中发生 ${errors.length} 个错误:`,
errors,
);
}
}; };
}, [refreshProxy, refreshRules, refreshRuleProviders]); }, [refreshProxy, refreshRules, refreshRuleProviders]);
@@ -243,25 +249,26 @@ export const AppDataProvider = ({
"getSystemProxy", "getSystemProxy",
getSystemProxy, getSystemProxy,
{ {
revalidateOnFocus: true, revalidateOnFocus: false,
revalidateOnReconnect: true, revalidateOnReconnect: false,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
dedupingInterval: 5000,
}, },
); );
const { data: runningMode } = useSWR("getRunningMode", getRunningMode, { const { data: runningMode } = useSWR("getRunningMode", getRunningMode, {
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 3, errorRetryCount: 2,
dedupingInterval: 5000,
}); });
// 高频率更新数据 (2秒)
const { data: uptimeData } = useSWR("appUptime", getAppUptime, { const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
// TODO: 运行时间 refreshInterval: 3000,
refreshInterval: 2000,
revalidateOnFocus: false, revalidateOnFocus: false,
suspense: false, suspense: false,
errorRetryCount: 1,
}); });
// 提供统一的刷新方法 // 提供统一的刷新方法

View File

@@ -16,15 +16,22 @@ export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]); const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]);
useEffect(() => { useEffect(() => {
let isUnmounted = false;
const checkMaximized = debounce(async () => { const checkMaximized = debounce(async () => {
const value = await currentWindow.isMaximized(); if (!isUnmounted) {
setMaximized(value); const value = await currentWindow.isMaximized();
setMaximized(value);
}
}, 300); }, 300);
const unlistenPromise = currentWindow.onResized(checkMaximized); const unlistenPromise = currentWindow.onResized(checkMaximized);
return () => { return () => {
unlistenPromise.then((unlisten) => unlisten()); isUnmounted = true;
unlistenPromise
.then((unlisten) => unlisten())
.catch((err) => console.warn("[WindowProvider] 清理监听器失败:", err));
}; };
}, [currentWindow]); }, [currentWindow]);