From 6b5fc6a5c32f424f604263d26f4bf3c73ce0b121 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Sun, 26 Jul 2026 19:22:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8F=8C=E5=B1=82=E6=8D=95=E8=8E=B7?= =?UTF-8?q?=E5=B9=B6=E5=8E=BB=E9=87=8D=E8=BD=AC=E5=8F=91Escape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Radix与终端容器共同覆盖Escape事件路径 - 对同一次按键去重后仅发送一个控制字符 --- .../src/components/server/TerminalDialog.tsx | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/server/TerminalDialog.tsx b/frontend/src/components/server/TerminalDialog.tsx index 669784a..98e90ac 100644 --- a/frontend/src/components/server/TerminalDialog.tsx +++ b/frontend/src/components/server/TerminalDialog.tsx @@ -1,4 +1,10 @@ -import { useState, useRef, useEffect, useLayoutEffect } from "react"; +import { + useCallback, + useState, + useRef, + useEffect, + useLayoutEffect, +} from "react"; import { useTranslation } from "react-i18next"; import { Dialog, @@ -51,6 +57,7 @@ export function TerminalDialog({ const termRef = useRef(null); const wsRef = useRef(null); const fitRef = useRef(null); + const lastEscapeSentRef = useRef(0); const [connected, setConnected] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [hasOutput, setHasOutput] = useState(false); @@ -61,14 +68,22 @@ export function TerminalDialog({ const [assigning, setAssigning] = useState(false); const [authReady, setAuthReady] = useState(hasAuth); // 认证就绪标志 - const sendTerminalInput = (data: string) => { + const sendTerminalInput = useCallback((data: string) => { const ws = wsRef.current; if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ status: 0, data })); return true; } return false; - }; + }, []); + + const sendTerminalEscape = useCallback(() => { + const now = Date.now(); + // Radix 与终端容器都可能捕获同一次按键,100ms 内只发送一个 ESC。 // Logan.Li + if (now - lastEscapeSentRef.current < 100) return; + lastEscapeSentRef.current = now; + sendTerminalInput("\x1b"); + }, [sendTerminalInput]); // 加载凭据列表 useEffect(() => { @@ -201,7 +216,7 @@ export function TerminalDialog({ if (event.key !== "Escape") return; event.preventDefault(); event.stopPropagation(); - sendTerminalInput("\x1b"); + sendTerminalEscape(); }; container.addEventListener("keydown", forwardEscape, true); detachEscape = () => @@ -225,14 +240,17 @@ export function TerminalDialog({ detachEscape?.(); cleanup?.(); }; - }, [open, authReady, fullscreen]); + }, [open, authReady, fullscreen, sendTerminalEscape]); return ( event.preventDefault()} + // Radix 捕获层先转发 Escape,终端容器监听器提供兜底。 // Logan.Li + onEscapeKeyDown={(event) => { + event.preventDefault(); + sendTerminalEscape(); + }} className={cn( "min-w-0 overflow-hidden bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0", // 禁用 CSS 过渡:终端窗口应该即时缩放,不需要动画