diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 69795f38b..7bea75d22 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -147,18 +147,19 @@ export const EnhancedTrafficStats = () => { // 是否显示流量图表 const trafficGraph = verge?.traffic_graph ?? true + const displayMemory = verge?.enable_memory_usage ?? true const { response: { data: traffic }, - } = useTrafficData({ enabled: trafficGraph && pageVisible }) + } = useTrafficData({ enabled: pageVisible }) const { response: { data: memory }, - } = useMemoryData() + } = useMemoryData({ enabled: displayMemory && pageVisible }) const { response: { data: connectionSummary }, - } = useConnectionSummaryData() + } = useConnectionSummaryData({ enabled: pageVisible }) // Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点 @@ -211,8 +212,8 @@ export const EnhancedTrafficStats = () => { }, [trafficGraph, pageVisible, theme.palette.divider]) // 使用useMemo计算统计卡片配置 - const statCards = useMemo( - () => [ + const statCards = useMemo(() => { + const cards: StatCardProps[] = [ { icon: , title: t('home.components.traffic.metrics.uploadSpeed'), @@ -248,17 +249,20 @@ export const EnhancedTrafficStats = () => { unit: parsedData.downloadTotalUnit, color: 'primary' as const, }, - { + ] + + if (displayMemory) { + cards.push({ icon: , title: t('home.components.traffic.metrics.memoryUsage'), value: parsedData.inuse, unit: parsedData.inuseUnit, color: 'error' as const, - onClick: undefined, - }, - ], - [t, parsedData], - ) + }) + } + + return cards + }, [t, parsedData, displayMemory]) return ( { {/* 统计卡片区域 */} {statCards.map((card) => ( - + ))} diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index ce2fa31e8..90869b246 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -24,16 +24,17 @@ export const LayoutTraffic = () => { // whether hide traffic graph const trafficGraph = verge?.traffic_graph ?? true + const displayMemory = verge?.enable_memory_usage ?? true const trafficRef = useRef(null) const pageVisible = useVisibility() const { response: { data: traffic }, - } = useTrafficData({ enabled: trafficGraph && pageVisible }) + } = useTrafficData({ enabled: pageVisible }) const { response: { data: memory }, - } = useMemoryData() + } = useMemoryData({ enabled: displayMemory && pageVisible }) // 监听数据变化,为图表添加数据点 useEffect(() => { @@ -47,9 +48,6 @@ export const LayoutTraffic = () => { } }, [traffic]) - // 显示内存使用情况的设置 - const displayMemory = verge?.enable_memory_usage ?? true - // 使用parseTraffic统一处理转换,保持与首页一致的显示格式 const [up, upUnit] = parseTraffic(traffic?.up || 0) const [down, downUnit] = parseTraffic(traffic?.down || 0) diff --git a/src/hooks/use-connection-data.ts b/src/hooks/use-connection-data.ts index 2140a5e03..710e158a9 100644 --- a/src/hooks/use-connection-data.ts +++ b/src/hooks/use-connection-data.ts @@ -35,7 +35,6 @@ export const initConnSummaryData: ConnectionSummaryData = { let connectionData: ConnectionMonitorData = initConnData let connectionSummary: ConnectionSummaryData = initConnSummaryData let connectionSocket: MihomoWebSocket | null = null -let connectionStarted = false let connectionConnecting = false let reconnectTimer: ReturnType | null = null let flushTimer: ReturnType | null = null @@ -53,6 +52,9 @@ const notifySummaryListeners = () => { summaryListeners.forEach((listener) => listener()) } +const hasConnectionSubscribers = () => + connectionListeners.size > 0 || summaryListeners.size > 0 + const sameMetadata = (left: ConnectionMetadata, right: ConnectionMetadata) => metadataValue(left.network) === metadataValue(right.network) && metadataValue(left.type) === metadataValue(right.type) && @@ -215,7 +217,7 @@ const flushPendingMessage = () => { flushTimer = null const messageData = pendingMessageData pendingMessageData = null - if (!messageData) return + if (!messageData || !hasConnectionSubscribers()) return let payload: IConnections try { @@ -270,6 +272,7 @@ const closeConnectionSocket = async () => { } const scheduleReconnect = () => { + if (!hasConnectionSubscribers()) return if (reconnectTimer) return reconnectTimer = window.setTimeout(() => { reconnectTimer = null @@ -278,20 +281,27 @@ const scheduleReconnect = () => { } async function reconnectConnectionSocket() { + if (!hasConnectionSubscribers()) return await closeConnectionSocket() scheduleReconnect() } async function connectConnectionSocket() { if (connectionSocket || connectionConnecting) return + if (!hasConnectionSubscribers()) return clearReconnectTimer() connectionConnecting = true try { const socket = await MihomoWebSocket.connect_connections() + if (!hasConnectionSubscribers()) { + await socket.close() + return + } connectionSocket = socket socket.addListener((message) => { + if (connectionSocket !== socket) return if (message.type !== 'Text') return if (message.data.startsWith('Websocket error')) { void reconnectConnectionSocket() @@ -308,27 +318,39 @@ async function connectConnectionSocket() { } const startConnectionMonitor = () => { - if (connectionStarted) return - connectionStarted = true void connectConnectionSocket() } +const stopConnectionMonitorIfIdle = () => { + if (hasConnectionSubscribers()) return + + clearReconnectTimer() + pendingMessageData = null + if (flushTimer) { + window.clearTimeout(flushTimer) + flushTimer = null + } + void closeConnectionSocket() +} + const getConnectionSnapshot = () => connectionData const getConnectionSummarySnapshot = () => connectionSummary const subscribeConnectionData = (listener: ConnectionListener) => { - startConnectionMonitor() connectionListeners.add(listener) + startConnectionMonitor() return () => { connectionListeners.delete(listener) + stopConnectionMonitorIfIdle() } } const subscribeConnectionSummary = (listener: ConnectionListener) => { - startConnectionMonitor() summaryListeners.add(listener) + startConnectionMonitor() return () => { summaryListeners.delete(listener) + stopConnectionMonitorIfIdle() } } @@ -351,9 +373,15 @@ const clearClosedConnectionData = () => { notifyConnectionListeners() } -export const useConnectionData = () => { +export const useConnectionData = (options?: { enabled?: boolean }) => { + const enabled = options?.enabled ?? true + const subscribe = useCallback( + (listener: ConnectionListener) => + enabled ? subscribeConnectionData(listener) : () => {}, + [enabled], + ) const data = useSyncExternalStore( - subscribeConnectionData, + subscribe, getConnectionSnapshot, getConnectionSnapshot, ) @@ -372,9 +400,15 @@ export const useConnectionData = () => { } } -export const useConnectionSummaryData = () => { +export const useConnectionSummaryData = (options?: { enabled?: boolean }) => { + const enabled = options?.enabled ?? true + const subscribe = useCallback( + (listener: ConnectionListener) => + enabled ? subscribeConnectionSummary(listener) : () => {}, + [enabled], + ) const data = useSyncExternalStore( - subscribeConnectionSummary, + subscribe, getConnectionSummarySnapshot, getConnectionSummarySnapshot, ) diff --git a/src/hooks/use-memory-data.ts b/src/hooks/use-memory-data.ts index e629ce9e4..db6ea6cb9 100644 --- a/src/hooks/use-memory-data.ts +++ b/src/hooks/use-memory-data.ts @@ -29,10 +29,12 @@ const shouldSkipDuplicateMemory = (memory: IMemoryUsageItem) => { return false } -export const useMemoryData = () => { +export const useMemoryData = (options?: { enabled?: boolean }) => { + const enabled = options?.enabled ?? true + const { response, refresh } = useMihomoWsSubscription({ storageKey: 'mihomo_memory_date', - buildSubscriptKey: (date) => `getClashMemory-${date}`, + buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null), fallbackData: FALLBACK_MEMORY_USAGE, connect: () => MihomoWebSocket.connect_memory(), throttleMs: 500, diff --git a/src/hooks/use-traffic-data.ts b/src/hooks/use-traffic-data.ts index 7ec750dc7..bd48d09a7 100644 --- a/src/hooks/use-traffic-data.ts +++ b/src/hooks/use-traffic-data.ts @@ -33,7 +33,7 @@ export const useTrafficData = (options?: { enabled?: boolean }) => { } = useTrafficMonitorEnhanced({ subscribe: false, enabled }) const { response, refresh } = useMihomoWsSubscription({ storageKey: 'mihomo_traffic_date', - buildSubscriptKey: (date) => `getClashTraffic-${date}`, + buildSubscriptKey: (date) => (enabled ? `getClashTraffic-${date}` : null), fallbackData: FALLBACK_TRAFFIC, connect: () => MihomoWebSocket.connect_traffic(), throttleMs: 200, diff --git a/src/hooks/use-visibility.ts b/src/hooks/use-visibility.ts index 31589c862..fc1285bfb 100644 --- a/src/hooks/use-visibility.ts +++ b/src/hooks/use-visibility.ts @@ -1,28 +1,68 @@ +import { TauriEvent } from '@tauri-apps/api/event' +import { getCurrentWindow } from '@tauri-apps/api/window' import { useEffect, useState } from 'react' +const isDocumentVisible = () => + typeof document === 'undefined' || document.visibilityState === 'visible' + export const useVisibility = () => { - const [visible, setVisible] = useState(() => - typeof document === 'undefined' - ? true - : document.visibilityState === 'visible', - ) + const [visible, setVisible] = useState(isDocumentVisible) useEffect(() => { - const handleVisibilityChange = () => { - setVisible(document.visibilityState === 'visible') + let mounted = true + let visibilityTimer: ReturnType | null = null + const appWindow = getCurrentWindow() + + const updateVisible = async () => { + const windowVisible = await appWindow.isVisible().catch(() => true) + if (mounted) { + setVisible(isDocumentVisible() && windowVisible) + } + } + + const updateVisibleSoon = () => { + if (visibilityTimer) { + window.clearTimeout(visibilityTimer) + } + visibilityTimer = window.setTimeout(() => { + visibilityTimer = null + void updateVisible() + }, 50) + } + + const handleVisibleEvent = () => { + void updateVisible() } - const handleFocus = () => setVisible(true) const handlePointerDown = () => setVisible(true) - document.addEventListener('focus', handleFocus) + document.addEventListener('focus', handleVisibleEvent) document.addEventListener('pointerdown', handlePointerDown) - document.addEventListener('visibilitychange', handleVisibilityChange) + document.addEventListener('visibilitychange', handleVisibleEvent) + window.addEventListener('focus', handleVisibleEvent) + + const unlistenFocusChanged = appWindow.onFocusChanged(updateVisibleSoon) + const unlistenCloseRequested = appWindow.listen( + TauriEvent.WINDOW_CLOSE_REQUESTED, + () => { + setVisible(false) + updateVisibleSoon() + }, + ) + void updateVisible() return () => { - document.removeEventListener('focus', handleFocus) + mounted = false + if (visibilityTimer) { + window.clearTimeout(visibilityTimer) + visibilityTimer = null + } + document.removeEventListener('focus', handleVisibleEvent) document.removeEventListener('pointerdown', handlePointerDown) - document.removeEventListener('visibilitychange', handleVisibilityChange) + document.removeEventListener('visibilitychange', handleVisibleEvent) + window.removeEventListener('focus', handleVisibleEvent) + void unlistenFocusChanged.then((unlisten) => unlisten()) + void unlistenCloseRequested.then((unlisten) => unlisten()) } }, []) diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 8dd39e48a..650ee3630 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -40,6 +40,7 @@ import { ConnectionTable } from '@/components/connection/connection-table' import { useConnectionData } from '@/hooks/use-connection-data' import { useConnectionSetting } from '@/hooks/use-connection-setting' import { useTrafficData } from '@/hooks/use-traffic-data' +import { useVisibility } from '@/hooks/use-visibility' import parseTraffic from '@/utils/parse-traffic' type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[] @@ -80,6 +81,7 @@ const orderFunctionMap = ORDER_OPTIONS.reduce>( const EMPTY_CONNECTIONS: IConnectionsItem[] = [] const ConnectionsPage = () => { const { t } = useTranslation() + const pageVisible = useVisibility() const [match, setMatch] = useState<(input: string) => boolean>( () => () => true, ) @@ -92,10 +94,10 @@ const ConnectionsPage = () => { const { response: { data: connections }, clearClosedConnections, - } = useConnectionData() + } = useConnectionData({ enabled: pageVisible }) const { response: { data: traffic }, - } = useTrafficData() + } = useTrafficData({ enabled: pageVisible }) const [setting, setSetting] = useConnectionSetting()