fix: 双层捕获并去重转发Escape

- Radix与终端容器共同覆盖Escape事件路径
- 对同一次按键去重后仅发送一个控制字符
This commit is contained in:
Logan.Li committed 2026-07-26 19:22:21 +08:00
1 parent 82d1b8de10
commit 6b5fc6a5c3
1 file changed
+25 -7
@@ -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<XTerm | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const fitRef = useRef<FitAddon | null>(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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
// Escape 仅阻止弹框关闭,控制字符由 xterm 键处理器发送。 // Logan.Li
onEscapeKeyDown={(event) => 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 过渡:终端窗口应该即时缩放,不需要动画