From 1125dc0562da947fcd7c1db83cc305479498256e Mon Sep 17 00:00:00 2001 From: oomeow Date: Thu, 13 Nov 2025 19:50:23 +0800 Subject: [PATCH] feat: support closed connections (#5244) * feat: support closed connections * fix: clear closed connections * feat: show footer * feat: show closed connection detail * docs: update Changelog.md * chore: update --- Changelog.md | 1 + .../connection/connection-detail.tsx | 41 +++-- src/components/connection/connection-item.tsx | 23 +-- .../connection/connection-table.tsx | 1 - .../home/enhanced-traffic-stats.tsx | 2 +- src/hooks/use-connection-data.ts | 63 +++++-- src/locales/ar/connections.json | 2 + src/locales/de/connections.json | 2 + src/locales/en/connections.json | 2 + src/locales/es/connections.json | 2 + src/locales/fa/connections.json | 2 + src/locales/id/connections.json | 2 + src/locales/jp/connections.json | 2 + src/locales/ko/connections.json | 2 + src/locales/ru/connections.json | 2 + src/locales/tr/connections.json | 2 + src/locales/tt/connections.json | 2 + src/locales/zh/connections.json | 2 + src/locales/zhtw/connections.json | 2 + src/pages/connections.tsx | 154 +++++++++--------- src/types/generated/i18n-keys.ts | 2 + src/types/generated/i18n-resources.ts | 2 + 22 files changed, 195 insertions(+), 120 deletions(-) diff --git a/Changelog.md b/Changelog.md index 91ec1ed5e..082960a0b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ - **Mihomo(Meta) 内核升级至 v1.19.16** - 支持连接页面各个项目的排序 - 实现可选的自动备份 +- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接) diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx index 7e5bc8737..766f400be 100644 --- a/src/components/connection/connection-detail.tsx +++ b/src/components/connection/connection-detail.tsx @@ -8,19 +8,21 @@ import { closeConnections } from "tauri-plugin-mihomo-api"; import parseTraffic from "@/utils/parse-traffic"; export interface ConnectionDetailRef { - open: (detail: IConnectionsItem) => void; + open: (detail: IConnectionsItem, closed: boolean) => void; } export function ConnectionDetail({ ref }: { ref?: Ref }) { const [open, setOpen] = useState(false); const [detail, setDetail] = useState(null!); + const [closed, setClosed] = useState(false); const theme = useTheme(); useImperativeHandle(ref, () => ({ - open: (detail: IConnectionsItem) => { + open: (detail: IConnectionsItem, closed: boolean) => { if (open) return; setOpen(true); setDetail(detail); + setClosed(closed); }, })); @@ -42,7 +44,11 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) { }} message={ detail ? ( - + ) : null } /> @@ -51,10 +57,11 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) { interface InnerProps { data: IConnectionsItem; + closed: boolean; onClose?: () => void; } -const InnerConnectionDetail = ({ data, onClose }: InnerProps) => { +const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => { const { t } = useTranslation(); const { metadata, rulePayload } = data; const theme = useTheme(); @@ -134,18 +141,20 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => { ))} - - - + {!closed && ( + + + + )} ); }; diff --git a/src/components/connection/connection-item.tsx b/src/components/connection/connection-item.tsx index d978733da..7288fc257 100644 --- a/src/components/connection/connection-item.tsx +++ b/src/components/connection/connection-item.tsx @@ -27,11 +27,12 @@ const Tag = styled("span")(({ theme }) => ({ interface Props { value: IConnectionsItem; + closed: boolean; onShowDetail?: () => void; } export const ConnectionItem = (props: Props) => { - const { value, onShowDetail } = props; + const { value, closed, onShowDetail } = props; const { id, metadata, chains, start, curUpload, curDownload } = value; const { t } = useTranslation(); @@ -44,15 +45,17 @@ export const ConnectionItem = (props: Props) => { dense sx={{ borderBottom: "1px solid var(--divider-color)" }} secondaryAction={ - - - + !closed && ( + + + + ) } > { > onShowDetail(e.row.connectionData)} diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 589c287cc..a725b542a 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -187,7 +187,7 @@ export const EnhancedTrafficStats = () => { uploadTotalUnit, downloadTotal, downloadTotalUnit, - connectionsCount: connections?.connections.length, + connectionsCount: connections?.activeConnections.length, }; }, [traffic, memory, connections]); diff --git a/src/hooks/use-connection-data.ts b/src/hooks/use-connection-data.ts index f599882b5..a4a103a00 100644 --- a/src/hooks/use-connection-data.ts +++ b/src/hooks/use-connection-data.ts @@ -4,12 +4,22 @@ import { mutate } from "swr"; import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; -export const initConnData: IConnections = { +export const initConnData: ConnectionMonitorData = { uploadTotal: 0, downloadTotal: 0, - connections: [], + activeConnections: [], + closedConnections: [], }; +export interface ConnectionMonitorData { + uploadTotal: number; + downloadTotal: number; + activeConnections: IConnectionsItem[]; + closedConnections: IConnectionsItem[]; +} + +const MAX_CLOSED_CONNS_NUM = 500; + export const useConnectionData = () => { const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now()); const subscriptKey = `getClashConnection-${date}`; @@ -18,7 +28,11 @@ export const useConnectionData = () => { const wsFirstConnection = useRef(true); const timeoutRef = useRef>(null); - const response = useSWRSubscription( + const response = useSWRSubscription< + ConnectionMonitorData, + any, + string | null + >( subscriptKey, (_key, { next }) => { const reconnect = async () => { @@ -41,28 +55,44 @@ export const useConnectionData = () => { } else { const data = JSON.parse(msg.data) as IConnections; next(null, (old = initConnData) => { - const oldConn = old.connections; + const oldConn = old.activeConnections; const maxLen = data.connections?.length; - const connections: IConnectionsItem[] = []; + const activeConns: IConnectionsItem[] = []; const rest = (data.connections || []).filter((each) => { const index = oldConn.findIndex((o) => o.id === each.id); if (index >= 0 && index < maxLen) { const old = oldConn[index]; each.curUpload = each.upload - old.upload; each.curDownload = each.download - old.download; - connections[index] = each; + activeConns[index] = each; return false; } return true; }); for (let i = 0; i < maxLen; ++i) { - if (!connections[i] && rest.length > 0) { - connections[i] = rest.shift()!; - connections[i].curUpload = 0; - connections[i].curDownload = 0; + if (!activeConns[i] && rest.length > 0) { + activeConns[i] = rest.shift()!; + activeConns[i].curUpload = 0; + activeConns[i].curDownload = 0; } } - return { ...data, connections }; + const currentClosedConns = oldConn.filter((each) => { + const index = activeConns.findIndex( + (o) => o.id === each.id, + ); + return index < 0; + }); + let closedConns = + old.closedConnections.concat(currentClosedConns); + if (closedConns.length > 500) { + closedConns = closedConns.slice(-MAX_CLOSED_CONNS_NUM); + } + return { + uploadTotal: data.uploadTotal, + downloadTotal: data.downloadTotal, + activeConnections: activeConns, + closedConnections: closedConns, + }; }); } } @@ -109,5 +139,14 @@ export const useConnectionData = () => { setDate(Date.now()); }; - return { response, refreshGetClashConnection }; + const clearClosedConnections = () => { + mutate(`$sub$${subscriptKey}`, { + uploadTotal: response.data?.uploadTotal ?? 0, + downloadTotal: response.data?.downloadTotal ?? 0, + activeConnections: response.data?.activeConnections ?? [], + closedConnections: [], + }); + }; + + return { response, refreshGetClashConnection, clearClosedConnections }; }; diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json index be0db7d6e..c5bcd0057 100644 --- a/src/locales/ar/connections.json +++ b/src/locales/ar/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "سرعة التنزيل" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "إغلاق الاتصال" }, "columnManager": { diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json index 65d032fcf..57f8bc1bb 100644 --- a/src/locales/de/connections.json +++ b/src/locales/de/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "Download-Geschwindigkeit" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Verbindung schließen" }, "columnManager": { diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json index 713a8d114..82b3cebac 100644 --- a/src/locales/en/connections.json +++ b/src/locales/en/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "Download Speed" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Close Connection" }, "columnManager": { diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json index d398d03fe..1b49bc17c 100644 --- a/src/locales/es/connections.json +++ b/src/locales/es/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "Velocidad de descarga" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Cerrar conexión" }, "columnManager": { diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json index 3f7d62f18..a8c8687e7 100644 --- a/src/locales/fa/connections.json +++ b/src/locales/fa/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "سرعت دانلود" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "بستن اتصال" }, "columnManager": { diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json index 6b515227a..29748ccf9 100644 --- a/src/locales/id/connections.json +++ b/src/locales/id/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "Kecepatan Unduh" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Tutup Koneksi" }, "columnManager": { diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json index 6cde1afbd..25fb84e8a 100644 --- a/src/locales/jp/connections.json +++ b/src/locales/jp/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "ダウンロード速度" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "接続を閉じる" }, "columnManager": { diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json index 036fa5853..88173f6ac 100644 --- a/src/locales/ko/connections.json +++ b/src/locales/ko/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "다운로드 속도" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "연결 닫기" }, "columnManager": { diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json index 24c706fbe..6aadd293d 100644 --- a/src/locales/ru/connections.json +++ b/src/locales/ru/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "Скорость скачивания" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Закрыть соединение" }, "columnManager": { diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json index 040cae012..9bcd349de 100644 --- a/src/locales/tr/connections.json +++ b/src/locales/tr/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "İndirme Hızı" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Bağlantıyı Kapat" }, "columnManager": { diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json index 27a2476a9..48a28d4b8 100644 --- a/src/locales/tt/connections.json +++ b/src/locales/tt/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "Йөкләү тизлеге" }, "actions": { + "active": "Active", + "closed": "Closed", "closeConnection": "Тоташуны ябу" }, "columnManager": { diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json index 2e498493f..33a447813 100644 --- a/src/locales/zh/connections.json +++ b/src/locales/zh/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "下载速度" }, "actions": { + "active": "活跃", + "closed": "已关闭", "closeConnection": "关闭连接" }, "columnManager": { diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index e2dca70c1..520f3a855 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -22,6 +22,8 @@ "downloadSpeed": "下載速度" }, "actions": { + "active": "活躍", + "closed": "已關閉", "closeConnection": "關閉連線" }, "columnManager": { diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 4d2dce6ae..52475813e 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,10 +1,17 @@ import { - PauseCircleOutlineRounded, - PlayCircleOutlineRounded, + DeleteForeverRounded, TableChartRounded, TableRowsRounded, } from "@mui/icons-material"; -import { Box, Button, IconButton, MenuItem } from "@mui/material"; +import { + Box, + Button, + ButtonGroup, + Fab, + IconButton, + MenuItem, + Zoom, +} from "@mui/material"; import { useLockFn } from "ahooks"; import { useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -21,16 +28,9 @@ import { import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; import { useConnectionData } from "@/hooks/use-connection-data"; -import { useVisibility } from "@/hooks/use-visibility"; import { useConnectionSetting } from "@/services/states"; import parseTraffic from "@/utils/parse-traffic"; -const initConn: IConnections = { - uploadTotal: 0, - downloadTotal: 0, - connections: [], -}; - type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; const ORDER_OPTIONS = [ @@ -70,61 +70,42 @@ const orderFunctionMap = ORDER_OPTIONS.reduce>( const ConnectionsPage = () => { const { t } = useTranslation(); - const pageVisible = useVisibility(); const [match, setMatch] = useState<(input: string) => boolean>( () => () => true, ); const [curOrderOpt, setCurOrderOpt] = useState("default"); + const [connectionsType, setConnectionsType] = useState<"active" | "closed">( + "active", + ); const { response: { data: connections }, + clearClosedConnections, } = useConnectionData(); const [setting, setSetting] = useConnectionSetting(); const isTableLayout = setting.layout === "table"; - const [isPaused, setIsPaused] = useState(false); - const [frozenData, setFrozenData] = useState(null); const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false); - // 使用全局连接数据 - const displayData = useMemo(() => { - if (!pageVisible) return initConn; - - if (isPaused) { - return ( - frozenData ?? { - uploadTotal: connections?.uploadTotal, - downloadTotal: connections?.downloadTotal, - connections: connections?.connections, - } - ); - } - - return { - uploadTotal: connections?.uploadTotal, - downloadTotal: connections?.downloadTotal, - connections: connections?.connections, - }; - }, [isPaused, frozenData, connections, pageVisible]); - const [filterConn] = useMemo(() => { const orderFunc = orderFunctionMap[curOrderOpt]; - let conns: IConnectionsItem[] = (displayData.connections ?? []).filter( - (conn) => { - const { host, destinationIP, process } = conn.metadata; - return ( - match(host || "") || - match(destinationIP || "") || - match(process || "") - ); - }, - ); - if (orderFunc) conns = orderFunc(conns); + const conns = + (connectionsType === "active" + ? connections?.activeConnections + : connections?.closedConnections) ?? []; + let matchConns = conns.filter((conn) => { + const { host, destinationIP, process } = conn.metadata; + return ( + match(host || "") || match(destinationIP || "") || match(process || "") + ); + }); - return [conns]; - }, [displayData, match, curOrderOpt]); + if (orderFunc) matchConns = orderFunc(matchConns ?? []); + + return [matchConns]; + }, [connections, connectionsType, match, curOrderOpt]); const onCloseAll = useLockFn(closeAllConnections); @@ -134,21 +115,6 @@ const ConnectionsPage = () => { setMatch(() => match); }, []); - const handlePauseToggle = useCallback(() => { - setIsPaused((prev) => { - if (!prev) { - setFrozenData({ - uploadTotal: connections?.uploadTotal ?? 0, - downloadTotal: connections?.downloadTotal ?? 0, - connections: connections?.connections ?? [], - }); - } else { - setFrozenData(null); - } - return !prev; - }); - }, [connections]); - const hasTableData = filterConn.length > 0; return ( @@ -170,11 +136,11 @@ const ConnectionsPage = () => { {t("shared.labels.downloaded")}:{" "} - {parseTraffic(displayData.downloadTotal)} + {parseTraffic(connections?.downloadTotal)} {t("shared.labels.uploaded")}:{" "} - {parseTraffic(displayData.uploadTotal)} + {parseTraffic(connections?.uploadTotal)} { )} - - {isPaused ? ( - - ) : ( - - )} - + + {!isTableLayout && ( { ) : isTableLayout ? ( detailRef.current?.open(detail)} + onShowDetail={(detail) => + detailRef.current?.open(detail, connectionsType === "closed") + } columnManagerOpen={isTableLayout && isColumnManagerOpen} onOpenColumnManager={() => setIsColumnManagerOpen(true)} onCloseColumnManager={() => setIsColumnManagerOpen(false)} @@ -276,12 +248,34 @@ const ConnectionsPage = () => { itemContent={(_, item) => ( detailRef.current?.open(item)} + closed={connectionsType === "closed"} + onShowDetail={() => + detailRef.current?.open(item, connectionsType === "closed") + } /> )} /> )} + 0} + unmountOnExit + > + clearClosedConnections()} + > + + {t("shared.actions.clear")} + + ); }; diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 99abd3fe6..ed28c0e8a 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -17,6 +17,8 @@ export const translationKeys = [ "connections.components.order.default", "connections.components.order.uploadSpeed", "connections.components.order.downloadSpeed", + "connections.components.actions.active", + "connections.components.actions.closed", "connections.components.actions.closeConnection", "connections.components.columnManager.title", "connections.components.columnManager.dragHandle", diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 3c23476eb..e64659cea 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -6,7 +6,9 @@ export interface TranslationResources { connections: { components: { actions: { + active: string; closeConnection: string; + closed: string; }; columnManager: { dragHandle: string;