diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index a9019a32..81cc065e 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -75,7 +75,7 @@ const CompactStatCard = memo(({ onClick, }: StatCardProps) => { const theme = useTheme(); - + // 获取调色板颜色 - 使用useMemo避免重复计算 const colorValue = useMemo(() => { const palette = theme.palette; @@ -88,7 +88,7 @@ const CompactStatCard = memo(({ } return palette.primary.main; }, [theme.palette, color]); - + return ( { const trafficRef = useRef(null); const pageVisible = useVisibility(); const [isDebug, setIsDebug] = useState(false); - + // 使用AppDataProvider const { connections, uptime } = useAppData(); - + // 使用单一状态对象减少状态更新次数 const [stats, setStats] = useState({ traffic: { up: 0, down: 0 }, @@ -169,7 +169,7 @@ export const EnhancedTrafficStats = () => { // 创建一个标记来追踪最后更新时间,用于节流 const lastUpdateRef = useRef({ traffic: 0 }); - + // 是否显示流量图表 const trafficGraph = verge?.traffic_graph ?? true; @@ -199,38 +199,31 @@ export const EnhancedTrafficStats = () => { // 使用节流控制更新频率 const now = Date.now(); if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) { - // 如果距离上次更新时间小于阈值,只更新图表不更新状态 - if (trafficRef.current) { - trafficRef.current.appendData({ + try { + trafficRef.current?.appendData({ up: data.up, down: data.down, timestamp: now, }); - } + } catch { } return; } - - // 更新最后更新时间 lastUpdateRef.current.traffic = now; - - // 验证数据有效性,防止NaN const safeUp = isNaN(data.up) ? 0 : data.up; const safeDown = isNaN(data.down) ? 0 : data.down; - - // 批量更新状态 - setStats(prev => ({ - ...prev, - traffic: { up: safeUp, down: safeDown } - })); - - // 更新图表数据 - if (trafficRef.current) { - trafficRef.current.appendData({ + try { + setStats(prev => ({ + ...prev, + traffic: { up: safeUp, down: safeDown } + })); + } catch { } + try { + trafficRef.current?.appendData({ up: safeUp, down: safeDown, timestamp: now, }); - } + } catch { } } } catch (err) { console.error("[Traffic] 解析数据错误:", err, event.data); @@ -294,7 +287,7 @@ export const EnhancedTrafficStats = () => { } }, }); - + console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`); socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, { onmessage: handleMemoryUpdate, @@ -317,6 +310,18 @@ export const EnhancedTrafficStats = () => { return cleanupSockets; }, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]); + // 组件卸载时清理所有定时器/引用 + useEffect(() => { + return () => { + try { + Object.values(socketRefs.current).forEach(socket => { + if (socket) socket.close(); + }); + socketRefs.current = { traffic: null, memory: null }; + } catch { } + }; + }, []); + // 执行垃圾回收 const handleGarbageCollection = useCallback(async () => { if (isDebug) { @@ -336,7 +341,7 @@ export const EnhancedTrafficStats = () => { const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse); const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal); const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal); - + return { up, upUnit, down, downUnit, inuse, inuseUnit, uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit, diff --git a/src/components/home/test-card.tsx b/src/components/home/test-card.tsx index bb64fe68..5139bb8f 100644 --- a/src/components/home/test-card.tsx +++ b/src/components/home/test-card.tsx @@ -85,11 +85,11 @@ export const TestCard = () => { mutateVerge(); return; } - - const newList = testList.map((x) => + + const newList = testList.map((x) => x.uid === uid ? { ...x, ...patch } : x ); - + mutateVerge({ ...verge, test_list: newList }, false); }, [testList, verge, mutateVerge] @@ -108,17 +108,27 @@ export const TestCard = () => { async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; - + const old_index = testList.findIndex((x) => x.uid === active.id); const new_index = testList.findIndex((x) => x.uid === over.id); - + if (old_index >= 0 && new_index >= 0) { const newList = [...testList]; const [removed] = newList.splice(old_index, 1); newList.splice(new_index, 0, removed); - await mutateVerge({ ...verge, test_list: newList }, false); - await patchVerge({ test_list: newList }); + // 优化:先本地更新,再异步 patch,避免UI卡死 + mutateVerge({ ...verge, test_list: newList }, false); + const patchFn = () => { + try { + patchVerge({ test_list: newList }); + } catch { } + }; + if (window.requestIdleCallback) { + window.requestIdleCallback(patchFn); + } else { + setTimeout(patchFn, 0); + } } }, [testList, verge, mutateVerge, patchVerge] diff --git a/src/components/profile/groups-editor-viewer.tsx b/src/components/profile/groups-editor-viewer.tsx index 4f6e028f..9f9136a4 100644 --- a/src/components/profile/groups-editor-viewer.tsx +++ b/src/components/profile/groups-editor-viewer.tsx @@ -180,16 +180,27 @@ export const GroupsEditorViewer = (props: Props) => { setDeleteSeq(obj?.delete || []); }, [visualization]); + // 优化:异步处理大数据yaml.dump,避免UI卡死 useEffect(() => { - if (prependSeq && appendSeq && deleteSeq) - setCurrData( - yaml.dump( - { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, - { - forceQuotes: true, - } - ) - ); + if (prependSeq && appendSeq && deleteSeq) { + const serialize = () => { + try { + setCurrData( + yaml.dump( + { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, + { forceQuotes: true } + ) + ); + } catch (e) { + // 防止异常导致UI卡死 + } + }; + if (window.requestIdleCallback) { + window.requestIdleCallback(serialize); + } else { + setTimeout(serialize, 0); + } + } }, [prependSeq, appendSeq, deleteSeq]); const fetchProxyPolicy = async () => { @@ -486,11 +497,11 @@ export const GroupsEditorViewer = (props: Props) => { }} slotProps={{ input: { - endAdornment: ( - - {t("seconds")} - - ), + endAdornment: ( + + {t("seconds")} + + ), } }} /> @@ -895,9 +906,8 @@ export const GroupsEditorViewer = (props: Props) => { padding: { top: 33, // 顶部padding防止遮挡snippets }, - fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ - getSystem() === "windows" ? ", twemoji mozilla" : "" - }`, + fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : "" + }`, fontLigatures: false, // 连字符 smoothScrolling: true, // 平滑滚动 }} diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index 60bc064c..3ad84f87 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -184,8 +184,10 @@ export const ProfileViewer = forwardRef( setTimeout(() => formIns.reset(), 500); fileDataRef.current = null; - // 只传递当前配置激活状态,让父组件决定是否需要触发配置重载 - props.onChange(isActivating); + // 优化:UI先关闭,异步通知父组件 + setTimeout(() => { + props.onChange(isActivating); + }, 0); } catch (err: any) { showNotice("error", err.message || err.toString()); } finally { @@ -195,9 +197,11 @@ export const ProfileViewer = forwardRef( ); const handleClose = () => { - setOpen(false); - fileDataRef.current = null; - setTimeout(() => formIns.reset(), 500); + try { + setOpen(false); + fileDataRef.current = null; + setTimeout(() => formIns.reset(), 500); + } catch { } }; const text = { diff --git a/src/components/profile/proxies-editor-viewer.tsx b/src/components/profile/proxies-editor-viewer.tsx index 7ce96829..669e8026 100644 --- a/src/components/profile/proxies-editor-viewer.tsx +++ b/src/components/profile/proxies-editor-viewer.tsx @@ -130,8 +130,9 @@ export const ProxiesEditorViewer = (props: Props) => { } } }; - const handleParse = () => { - let proxies = [] as IProxyConfig[]; + // 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState + const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => { + let proxies: IProxyConfig[] = []; let names: string[] = []; let uris = ""; try { @@ -139,10 +140,13 @@ export const ProxiesEditorViewer = (props: Props) => { } catch { uris = proxyUri; } - uris - .trim() - .split("\n") - .forEach((uri) => { + const lines = uris.trim().split("\n"); + let idx = 0; + const batchSize = 50; + function parseBatch() { + const end = Math.min(idx + batchSize, lines.length); + for (; idx < end; idx++) { + const uri = lines[idx]; try { let proxy = parseUri(uri.trim()); if (!names.includes(proxy.name)) { @@ -150,10 +154,16 @@ export const ProxiesEditorViewer = (props: Props) => { names.push(proxy.name); } } catch (err: any) { - showNotice('error', err.message || err.toString()); + // 不阻塞主流程 } - }); - return proxies; + } + if (idx < lines.length) { + setTimeout(parseBatch, 0); + } else { + cb(proxies); + } + } + parseBatch(); }; const fetchProfile = async () => { let data = await readProfileFile(profileUid); @@ -192,15 +202,25 @@ export const ProxiesEditorViewer = (props: Props) => { }, [visualization]); useEffect(() => { - if (prependSeq && appendSeq && deleteSeq) - setCurrData( - yaml.dump( - { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, - { - forceQuotes: true, - } - ) - ); + if (prependSeq && appendSeq && deleteSeq) { + const serialize = () => { + try { + setCurrData( + yaml.dump( + { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, + { forceQuotes: true } + ) + ); + } catch (e) { + // 防止异常导致UI卡死 + } + }; + if (window.requestIdleCallback) { + window.requestIdleCallback(serialize); + } else { + setTimeout(serialize, 0); + } + } }, [prependSeq, appendSeq, deleteSeq]); useEffect(() => { @@ -276,8 +296,9 @@ export const ProxiesEditorViewer = (props: Props) => { variant="contained" startIcon={} onClick={() => { - let proxies = handleParse(); - setPrependSeq([...proxies, ...prependSeq]); + handleParseAsync((proxies) => { + setPrependSeq((prev) => [...proxies, ...prev]); + }); }} > {t("Prepend Proxy")} @@ -289,8 +310,9 @@ export const ProxiesEditorViewer = (props: Props) => { variant="contained" startIcon={} onClick={() => { - let proxies = handleParse(); - setAppendSeq([...appendSeq, ...proxies]); + handleParseAsync((proxies) => { + setAppendSeq((prev) => [...prev, ...proxies]); + }); }} > {t("Append Proxy")} @@ -431,9 +453,8 @@ export const ProxiesEditorViewer = (props: Props) => { padding: { top: 33, // 顶部padding防止遮挡snippets }, - fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ - getSystem() === "windows" ? ", twemoji mozilla" : "" - }`, + fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : "" + }`, fontLigatures: false, // 连字符 smoothScrolling: true, // 平滑滚动 }} diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index 3b395419..c5c09d47 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -76,161 +76,161 @@ const rules: { noResolve?: boolean; validator?: (value: string) => boolean; }[] = [ - { - name: "DOMAIN", - example: "example.com", - }, - { - name: "DOMAIN-SUFFIX", - example: "example.com", - }, - { - name: "DOMAIN-KEYWORD", - example: "example", - }, - { - name: "DOMAIN-REGEX", - example: "example.*", - }, - { - name: "GEOSITE", - example: "youtube", - }, - { - name: "GEOIP", - example: "CN", - noResolve: true, - }, - { - name: "SRC-GEOIP", - example: "CN", - }, - { - name: "IP-ASN", - example: "13335", - noResolve: true, - validator: (value) => (+value ? true : false), - }, - { - name: "SRC-IP-ASN", - example: "9808", - validator: (value) => (+value ? true : false), - }, - { - name: "IP-CIDR", - example: "127.0.0.0/8", - noResolve: true, - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), - }, - { - name: "IP-CIDR6", - example: "2620:0:2d0:200::7/32", - noResolve: true, - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), - }, - { - name: "SRC-IP-CIDR", - example: "192.168.1.201/32", - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), - }, - { - name: "IP-SUFFIX", - example: "8.8.8.8/24", - noResolve: true, - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), - }, - { - name: "SRC-IP-SUFFIX", - example: "192.168.1.201/8", - validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), - }, - { - name: "SRC-PORT", - example: "7777", - validator: (value) => portValidator(value), - }, - { - name: "DST-PORT", - example: "80", - validator: (value) => portValidator(value), - }, - { - name: "IN-PORT", - example: "7890", - validator: (value) => portValidator(value), - }, - { - name: "DSCP", - example: "4", - }, - { - name: "PROCESS-NAME", - example: getSystem() === "windows" ? "chrome.exe" : "curl", - }, - { - name: "PROCESS-PATH", - example: - getSystem() === "windows" - ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" - : "/usr/bin/wget", - }, - { - name: "PROCESS-NAME-REGEX", - example: ".*telegram.*", - }, - { - name: "PROCESS-PATH-REGEX", - example: - getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget", - }, - { - name: "NETWORK", - example: "udp", - validator: (value) => ["tcp", "udp"].includes(value), - }, - { - name: "UID", - example: "1001", - validator: (value) => (+value ? true : false), - }, - { - name: "IN-TYPE", - example: "SOCKS/HTTP", - }, - { - name: "IN-USER", - example: "mihomo", - }, - { - name: "IN-NAME", - example: "ss", - }, - { - name: "SUB-RULE", - example: "(NETWORK,tcp)", - }, - { - name: "RULE-SET", - example: "providername", - noResolve: true, - }, - { - name: "AND", - example: "((DOMAIN,baidu.com),(NETWORK,UDP))", - }, - { - name: "OR", - example: "((NETWORK,UDP),(DOMAIN,baidu.com))", - }, - { - name: "NOT", - example: "((DOMAIN,baidu.com))", - }, - { - name: "MATCH", - required: false, - }, -]; + { + name: "DOMAIN", + example: "example.com", + }, + { + name: "DOMAIN-SUFFIX", + example: "example.com", + }, + { + name: "DOMAIN-KEYWORD", + example: "example", + }, + { + name: "DOMAIN-REGEX", + example: "example.*", + }, + { + name: "GEOSITE", + example: "youtube", + }, + { + name: "GEOIP", + example: "CN", + noResolve: true, + }, + { + name: "SRC-GEOIP", + example: "CN", + }, + { + name: "IP-ASN", + example: "13335", + noResolve: true, + validator: (value) => (+value ? true : false), + }, + { + name: "SRC-IP-ASN", + example: "9808", + validator: (value) => (+value ? true : false), + }, + { + name: "IP-CIDR", + example: "127.0.0.0/8", + noResolve: true, + validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), + }, + { + name: "IP-CIDR6", + example: "2620:0:2d0:200::7/32", + noResolve: true, + validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), + }, + { + name: "SRC-IP-CIDR", + example: "192.168.1.201/32", + validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), + }, + { + name: "IP-SUFFIX", + example: "8.8.8.8/24", + noResolve: true, + validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), + }, + { + name: "SRC-IP-SUFFIX", + example: "192.168.1.201/8", + validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value), + }, + { + name: "SRC-PORT", + example: "7777", + validator: (value) => portValidator(value), + }, + { + name: "DST-PORT", + example: "80", + validator: (value) => portValidator(value), + }, + { + name: "IN-PORT", + example: "7890", + validator: (value) => portValidator(value), + }, + { + name: "DSCP", + example: "4", + }, + { + name: "PROCESS-NAME", + example: getSystem() === "windows" ? "chrome.exe" : "curl", + }, + { + name: "PROCESS-PATH", + example: + getSystem() === "windows" + ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + : "/usr/bin/wget", + }, + { + name: "PROCESS-NAME-REGEX", + example: ".*telegram.*", + }, + { + name: "PROCESS-PATH-REGEX", + example: + getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget", + }, + { + name: "NETWORK", + example: "udp", + validator: (value) => ["tcp", "udp"].includes(value), + }, + { + name: "UID", + example: "1001", + validator: (value) => (+value ? true : false), + }, + { + name: "IN-TYPE", + example: "SOCKS/HTTP", + }, + { + name: "IN-USER", + example: "mihomo", + }, + { + name: "IN-NAME", + example: "ss", + }, + { + name: "SUB-RULE", + example: "(NETWORK,tcp)", + }, + { + name: "RULE-SET", + example: "providername", + noResolve: true, + }, + { + name: "AND", + example: "((DOMAIN,baidu.com),(NETWORK,UDP))", + }, + { + name: "OR", + example: "((NETWORK,UDP),(DOMAIN,baidu.com))", + }, + { + name: "NOT", + example: "((DOMAIN,baidu.com))", + }, + { + name: "MATCH", + required: false, + }, + ]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; @@ -325,16 +325,27 @@ export const RulesEditorViewer = (props: Props) => { setDeleteSeq(obj?.delete || []); }, [visualization]); + // 优化:异步处理大数据yaml.dump,避免UI卡死 useEffect(() => { - if (prependSeq && appendSeq && deleteSeq) - setCurrData( - yaml.dump( - { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, - { - forceQuotes: true, - } - ) - ); + if (prependSeq && appendSeq && deleteSeq) { + const serialize = () => { + try { + setCurrData( + yaml.dump( + { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, + { forceQuotes: true } + ) + ); + } catch (e: any) { + showNotice('error', e?.message || e?.toString() || 'YAML dump error'); + } + }; + if (window.requestIdleCallback) { + window.requestIdleCallback(serialize); + } else { + setTimeout(serialize, 0); + } + } }, [prependSeq, appendSeq, deleteSeq]); const fetchProfile = async () => { @@ -407,9 +418,8 @@ export const RulesEditorViewer = (props: Props) => { } const condition = ruleType.required ?? true ? ruleContent : ""; - return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ - ruleType.noResolve && noResolve ? ",no-resolve" : "" - }`; + return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : "" + }`; }; const handleSave = useLockFn(async () => { @@ -701,9 +711,8 @@ export const RulesEditorViewer = (props: Props) => { padding: { top: 33, // 顶部padding防止遮挡snippets }, - fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ - getSystem() === "windows" ? ", twemoji mozilla" : "" - }`, + fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : "" + }`, fontLigatures: false, // 连字符 smoothScrolling: true, // 平滑滚动 }} diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index b813cfd3..4bb9aadd 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -198,3 +198,5 @@ export const useRenderList = (mode: string) => { currentColumns: col, }; }; + +// 优化建议:如有大数据量,建议用虚拟滚动(已在 ProxyGroups 组件中实现),此处无需额外处理。 diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 377c5bb3..d2433a0e 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -132,8 +132,8 @@ const handleNoticeMessage = ( showNotice('error', `${t("Failed to Change Core")}: ${msg}`); break; default: // Optional: Log unhandled statuses - console.warn(`[通知监听 V2] 未处理的状态: ${status}`); - break; + console.warn(`[通知监听 V2] 未处理的状态: ${status}`); + break; } }; @@ -151,6 +151,11 @@ const Layout = () => { const routersEles = useRoutes(routers); const { addListener, setupCloseListener } = useListen(); const initRef = useRef(false); + const [themeReady, setThemeReady] = useState(false); + + useEffect(() => { + setThemeReady(true); + }, [theme]); const handleNotice = useCallback( (payload: [string, string]) => { @@ -271,7 +276,7 @@ const Layout = () => { return unlisten; } catch (err) { console.error("[Layout] 监听启动完成事件失败:", err); - return () => {}; + return () => { }; } }; @@ -288,7 +293,7 @@ const Layout = () => { }, 100); }, 100); }, 100); - + // 启动监听器 const unlistenPromise = listenStartupCompleted(); @@ -311,13 +316,39 @@ const Layout = () => { } }, [start_page]); + if (!themeReady) { + return ( +
+ ); + } + if (!routersEles) return null; return ( - +
+ { ({ palette }) => ({ bgcolor: palette.background.paper }), OS === "linux" ? { - borderRadius: "8px", - border: "1px solid var(--divider-color)", - width: "calc(100vw - 4px)", - height: "calc(100vh - 4px)", - } + borderRadius: "8px", + border: "1px solid var(--divider-color)", + width: "calc(100vw - 4px)", + height: "calc(100vh - 4px)", + } : {}, ]} > diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 7a467fb2..fc24bb85 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -252,9 +252,13 @@ export const HomePage = () => { setSettingsOpen(true); }; - // 新增:保存设置 + // 新增:保存设置时用requestIdleCallback/setTimeout const handleSaveSettings = (newCards: HomeCardsSettings) => { - setHomeCards(newCards); + if (window.requestIdleCallback) { + window.requestIdleCallback(() => setHomeCards(newCards)); + } else { + setTimeout(() => setHomeCards(newCards), 0); + } }; return ( diff --git a/src/pages/unlock.tsx b/src/pages/unlock.tsx index 6d476316..351b724f 100644 --- a/src/pages/unlock.tsx +++ b/src/pages/unlock.tsx @@ -24,6 +24,7 @@ import { RefreshRounded, AccessTimeOutlined, } from "@mui/icons-material"; +import { showNotice } from "@/services/noticeService"; // 定义流媒体检测项类型 interface UnlockItem { @@ -121,61 +122,67 @@ const UnlockPage = () => { } }; + // invoke加超时,防止后端卡死 + const invokeWithTimeout = async ( + cmd: string, + args?: any, + timeout = 15000, + ): Promise => { + return Promise.race([ + invoke(cmd, args), + new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)), + ]); + }; + // 执行全部项目检测 const checkAllMedia = useLockFn(async () => { try { setIsCheckingAll(true); - const result = await invoke("check_media_unlock"); + const result = await invokeWithTimeout("check_media_unlock"); const sortedItems = sortItemsByName(result); - // 更新UI setUnlockItems(sortedItems); const currentTime = new Date().toLocaleString(); setLastCheckTime(currentTime); - // 保存结果到本地存储 saveResultsToStorage(sortedItems, currentTime); setIsCheckingAll(false); } catch (err: any) { setIsCheckingAll(false); + showNotice('error', err?.message || err?.toString() || '检测超时或失败'); + alert("检测超时或失败: " + (err?.message || err)); console.error("Failed to check media unlock:", err); } }); - // 根据项目名称检测单个流媒体服务 + // 检测单个流媒体服务 const checkSingleMedia = useLockFn(async (name: string) => { try { - // 将该项目添加到加载状态 setLoadingItems((prev) => [...prev, name]); + const result = await invokeWithTimeout("check_media_unlock"); - // 执行检测 - const result = await invoke("check_media_unlock"); - - // 找到对应的检测结果 const targetItem = result.find((item: UnlockItem) => item.name === name); if (targetItem) { - // 更新单个检测项结果并按名称排序 const updatedItems = sortItemsByName( unlockItems.map((item: UnlockItem) => item.name === name ? targetItem : item, ), ); - // 更新UI setUnlockItems(updatedItems); const currentTime = new Date().toLocaleString(); setLastCheckTime(currentTime); - // 保存结果到本地存储 saveResultsToStorage(updatedItems, currentTime); } - // 移除加载状态 setLoadingItems((prev) => prev.filter((item) => item !== name)); } catch (err: any) { setLoadingItems((prev) => prev.filter((item) => item !== name)); + showNotice('error', err?.message || err?.toString() || `检测${name}失败`); + alert("检测超时或失败: " + (err?.message || err)); console.error(`Failed to check ${name}:`, err); } });