From 83c36866cf5e69cdc9741e0c3e0b7249d1763d19 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 6 Dec 2025 22:48:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99ssh=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=80=9A=E7=94=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Terminal/Terminal.tsx | 46 ++++++------ .../components/Terminal/TerminalSplitView.tsx | 4 +- .../Terminal/core/TerminalInstance.ts | 72 ++++++++++++++++--- .../strategies/BaseConnectionStrategy.ts | 6 +- .../strategies/SSHConnectionStrategy.ts | 17 ++--- .../src/components/Terminal/useSplitView.ts | 9 ++- .../List/components/SSHWindowManager.tsx | 1 + 7 files changed, 108 insertions(+), 47 deletions(-) diff --git a/frontend/src/components/Terminal/Terminal.tsx b/frontend/src/components/Terminal/Terminal.tsx index b750f578..25bd404d 100644 --- a/frontend/src/components/Terminal/Terminal.tsx +++ b/frontend/src/components/Terminal/Terminal.tsx @@ -114,6 +114,23 @@ export const Terminal: React.FC = ({ }; }, [id]); + // 监听Tab激活状态,激活时调整尺寸并通知后端 + useEffect(() => { + if (isActive && instanceRef.current) { + setTimeout(() => { + const fitAddon = instanceRef.current?.getFitAddon(); + if (fitAddon && instanceRef.current) { + fitAddon.fit(); + // 发送新的尺寸给后端 + const cols = instanceRef.current.getXTerm().cols; + const rows = instanceRef.current.getXTerm().rows; + instanceRef.current.sendResize(cols, rows); + console.log(`[Terminal ${id}] Tab activated, resized and notified: ${cols}x${rows}`); + } + }, 100); + } + }, [isActive, id]); + // 监听窗口大小变化,自动调整终端尺寸 useEffect(() => { const handleResize = () => { @@ -132,38 +149,27 @@ export const Terminal: React.FC = ({ }; }, []); - // 显示审计警告(只显示一次) + // 显示审计警告(委托给TerminalInstance,确保只显示一次) useEffect(() => { - if (connectionStatus === 'connected' && audit?.enabled && instanceRef.current && !auditShown) { + if (connectionStatus === 'connected' && audit?.enabled && instanceRef.current) { const instance = instanceRef.current; const companyName = audit.companyName || ''; const customMessage = audit.message; - if (customMessage) { - instance.writeln(`\r\n\x1b[33m${customMessage}\x1b[0m\r\n`); - } else { - instance.writeln('\r\n\x1b[33m┌─────────────────────────────────────────────────────────────\x1b[0m'); - instance.writeln(`\x1b[33m│ ⚠️ ${companyName} - 安全提示\x1b[0m`); - instance.writeln('\x1b[33m│ 本次会话将被全程审计记录\x1b[0m'); - instance.writeln('\x1b[33m│ • 所有操作命令、输入、输出都将被完整记录\x1b[0m'); - instance.writeln('\x1b[33m│ • 审计日志用于安全审查、故障排查和合规要求\x1b[0m'); - instance.writeln('\x1b[33m│ • 请规范操作,遵守企业信息安全管理制度\x1b[0m'); - instance.writeln('\x1b[33m└─────────────────────────────────────────────────────────────\x1b[0m\r\n'); + // 使用TerminalInstance的showAudit方法,自动处理去重 + const shown = instance.showAudit(companyName, customMessage); + if (shown) { + setAuditShown(true); // 更新组件状态(可选,仅用于UI反馈) } - - setAuditShown(true); - - setTimeout(() => { - instance.getFitAddon()?.fit(); - }, 100); } - }, [connectionStatus, audit, auditShown]); + }, [connectionStatus, audit]); // 重连处理 const handleReconnect = useCallback(() => { if (instanceRef.current) { instanceRef.current.disconnect(); - setAuditShown(false); // 重置审计警告标记,重连后重新显示 + instanceRef.current.resetAudit(); // 重置审计警告标记 + setAuditShown(false); setTimeout(() => { instanceRef.current?.connect(); }, 100); diff --git a/frontend/src/components/Terminal/TerminalSplitView.tsx b/frontend/src/components/Terminal/TerminalSplitView.tsx index 8a785054..e9897563 100644 --- a/frontend/src/components/Terminal/TerminalSplitView.tsx +++ b/frontend/src/components/Terminal/TerminalSplitView.tsx @@ -14,6 +14,7 @@ export interface TerminalSplitViewProps { getAuditConfig: () => TerminalAuditConfig; getToolbarConfig: () => TerminalToolbarConfig; onCloseAllReady?: (closeAllFn: () => void) => void; // 暴露closeAll函数给外部 + onWindowClose?: () => void; // 最后一个Tab关闭时,通知关闭整个窗口 } /** @@ -30,6 +31,7 @@ export const TerminalSplitView: React.FC = ({ getAuditConfig, getToolbarConfig, onCloseAllReady, + onWindowClose, }) => { const { layout, @@ -44,7 +46,7 @@ export const TerminalSplitView: React.FC = ({ closeAll, resizeGroups, setActiveGroupId, - } = useSplitView({ initialTab }); + } = useSplitView({ initialTab, onWindowClose }); // 暴露closeAll方法给外部 useEffect(() => { diff --git a/frontend/src/components/Terminal/core/TerminalInstance.ts b/frontend/src/components/Terminal/core/TerminalInstance.ts index f4ce000c..08123cfa 100644 --- a/frontend/src/components/Terminal/core/TerminalInstance.ts +++ b/frontend/src/components/Terminal/core/TerminalInstance.ts @@ -54,6 +54,7 @@ export class TerminalInstance { private stateListeners: Set = new Set(); private unsubscribers: Array<() => void> = []; + private auditShown: boolean = false; constructor(private config: TerminalInstanceConfig) { // 初始化 XTerm @@ -86,25 +87,39 @@ export class TerminalInstance { } /** - * 挂载到 DOM 容器 + * 挂载到 DOM */ mount(container: HTMLElement): void { - if (this.mounted && this.currentContainer === container) { - console.log(`[TerminalInstance ${this.config.id}] Already mounted to this container`); + if (!container) { + console.warn(`[TerminalInstance ${this.config.id}] No container provided`); return; } - if (this.mounted) { + // 如果已经挂载到其他容器,先卸载 + if (this.mounted && this.currentContainer !== container) { this.unmount(); } - this.xterm.open(container); + // 只在第一次或切换容器时调用 open + if (!this.mounted) { + this.xterm.open(container); + console.log(`[TerminalInstance ${this.config.id}] XTerm opened in container`); + } else if (this.currentContainer === container) { + // 已经挂载到同一个容器,只需要调整尺寸 + console.log(`[TerminalInstance ${this.config.id}] Already mounted to same container, skipping open`); + } + this.currentContainer = container; this.mounted = true; - // 自适应尺寸 + // 自适应尺寸并通知后端 setTimeout(() => { this.fitAddon.fit(); + // 发送终端尺寸给后端 + const cols = this.xterm.cols; + const rows = this.xterm.rows; + this.connectionStrategy.resize(cols, rows); + console.log(`[TerminalInstance ${this.config.id}] Initial resize sent: ${cols}x${rows}`); }, 100); console.log(`[TerminalInstance ${this.config.id}] Mounted to DOM`); @@ -193,10 +208,10 @@ export class TerminalInstance { } /** - * 发送尺寸变化 + * 发送尺寸变化到后端 */ - sendResize(rows: number, cols: number): void { - this.connectionStrategy.sendResize(rows, cols); + sendResize(cols: number, rows: number): void { + this.connectionStrategy.resize(cols, rows); } /** @@ -235,6 +250,45 @@ export class TerminalInstance { this.xterm.writeln(data); } + /** + * 显示审计警告(只显示一次) + */ + showAudit(companyName: string, customMessage?: string): boolean { + if (this.auditShown) { + console.log(`[TerminalInstance ${this.config.id}] Audit already shown, skipping`); + return false; + } + + if (customMessage) { + this.writeln(`\r\n\x1b[33m${customMessage}\x1b[0m\r\n`); + } else { + this.writeln('\r\n\x1b[33m┌─────────────────────────────────────────────────────────────\x1b[0m'); + this.writeln(`\x1b[33m│ ⚠️ ${companyName} - 安全提示\x1b[0m`); + this.writeln('\x1b[33m│ 本次会话将被全程审计记录\x1b[0m'); + this.writeln('\x1b[33m│ • 所有操作命令、输入、输出都将被完整记录\x1b[0m'); + this.writeln('\x1b[33m│ • 审计日志用于安全审查、故障排查和合规要求\x1b[0m'); + this.writeln('\x1b[33m│ • 请规范操作,遵守企业信息安全管理制度\x1b[0m'); + this.writeln('\x1b[33m└─────────────────────────────────────────────────────────────\x1b[0m\r\n'); + } + + this.auditShown = true; + console.log(`[TerminalInstance ${this.config.id}] Audit warning displayed`); + + setTimeout(() => { + this.fitAddon.fit(); + }, 100); + + return true; + } + + /** + * 重置审计显示状态(重连时使用) + */ + resetAudit(): void { + this.auditShown = false; + console.log(`[TerminalInstance ${this.config.id}] Audit state reset`); + } + /** * 获取选中内容 */ diff --git a/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts b/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts index 7303cd42..97a0bc83 100644 --- a/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts +++ b/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts @@ -40,14 +40,14 @@ export abstract class BaseConnectionStrategy { abstract disconnect(): void; /** - * 发送输入数据 + * 发送输入 */ abstract sendInput(data: string): void; /** - * 发送终端尺寸变化 + * 发送终端尺寸调整消息到后端 */ - abstract sendResize(rows: number, cols: number): void; + abstract resize(cols: number, rows: number): void; /** * 获取当前连接状态 diff --git a/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts b/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts index 8de41e40..b1829173 100644 --- a/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts +++ b/frontend/src/components/Terminal/strategies/SSHConnectionStrategy.ts @@ -116,23 +116,15 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy { * 发送输入命令 */ sendInput(data: string): void { - if (this.ws?.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify({ - type: 'input', - data: { - request: { - type: 'input', - command: data, - } - } - })); + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(data); } } /** - * 发送终端尺寸变化 + * 发送终端尺寸调整消息 */ - sendResize(rows: number, cols: number): void { + resize(cols: number, rows: number): void { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'resize', @@ -144,6 +136,7 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy { } } })); + console.log(`[SSHConnectionStrategy] Resize sent: ${cols}x${rows}`); } } diff --git a/frontend/src/components/Terminal/useSplitView.ts b/frontend/src/components/Terminal/useSplitView.ts index f4081df6..d101ca6e 100644 --- a/frontend/src/components/Terminal/useSplitView.ts +++ b/frontend/src/components/Terminal/useSplitView.ts @@ -8,9 +8,10 @@ import { TerminalInstanceManager } from './core/TerminalInstanceManager'; interface UseSplitViewOptions { initialTab: TerminalTab; + onWindowClose?: () => void; // 最后一个Tab关闭时的回调 } -export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { +export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions) => { const [layout, setLayout] = useState(() => ({ root: { type: 'group', @@ -320,7 +321,11 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { const { parent } = result; if (!parent) { - // 不能关闭根节点 + // 根节点 且 只有一个Tab,关闭整个窗口 + if (group && group.tabs.length === 1) { + console.log('[useSplitView] 最后一个Tab,关闭整个窗口'); + onWindowClose?.(); + } return prev; } diff --git a/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx b/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx index 30f40b99..1174fad3 100644 --- a/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx +++ b/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx @@ -66,6 +66,7 @@ const SSHTerminalSplitViewWrapper: React.FC = // 保存closeAll函数到ref closeAllRef.current = closeAllFn; }} + onWindowClose={onCloseReady} /> ); };