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 过渡:终端窗口应该即时缩放,不需要动画