mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
refactor: streamline app initialization and enhance WebSocket cleanup logic
This commit is contained in:
@@ -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
35
src/hooks/use-cleanup.ts
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
59
src/main.tsx
59
src/main.tsx
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
86
src/pages/_layout/notificationHandlers.ts
Normal file
86
src/pages/_layout/notificationHandlers.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
108
src/pages/_layout/useAppInitialization.ts
Normal file
108
src/pages/_layout/useAppInitialization.ts
Normal 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();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
104
src/pages/_layout/useLayoutEvents.ts
Normal file
104
src/pages/_layout/useLayoutEvents.ts
Normal 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]);
|
||||||
|
};
|
||||||
25
src/pages/_layout/useLazyDataLoad.ts
Normal file
25
src/pages/_layout/useLazyDataLoad.ts
Normal 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]);
|
||||||
|
};
|
||||||
50
src/pages/_layout/useLoadingOverlay.ts
Normal file
50
src/pages/_layout/useLoadingOverlay.ts
Normal 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]);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 提供统一的刷新方法
|
// 提供统一的刷新方法
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user