重写ssh前端组件,通用化

This commit is contained in:
dengqichen 2025-12-06 22:48:26 +08:00
parent 004e1adcf5
commit 83c36866cf
7 changed files with 108 additions and 47 deletions

View File

@ -114,6 +114,23 @@ export const Terminal: React.FC<TerminalProps> = ({
}; };
}, [id]); }, [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(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
@ -132,38 +149,27 @@ export const Terminal: React.FC<TerminalProps> = ({
}; };
}, []); }, []);
// 显示审计警告(只显示一次) // 显示审计警告(委托给TerminalInstance确保只显示一次)
useEffect(() => { useEffect(() => {
if (connectionStatus === 'connected' && audit?.enabled && instanceRef.current && !auditShown) { if (connectionStatus === 'connected' && audit?.enabled && instanceRef.current) {
const instance = instanceRef.current; const instance = instanceRef.current;
const companyName = audit.companyName || ''; const companyName = audit.companyName || '';
const customMessage = audit.message; const customMessage = audit.message;
if (customMessage) { // 使用TerminalInstance的showAudit方法自动处理去重
instance.writeln(`\r\n\x1b[33m${customMessage}\x1b[0m\r\n`); const shown = instance.showAudit(companyName, customMessage);
} else { if (shown) {
instance.writeln('\r\n\x1b[33m┌─────────────────────────────────────────────────────────────\x1b[0m'); setAuditShown(true); // 更新组件状态可选仅用于UI反馈
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');
} }
setAuditShown(true);
setTimeout(() => {
instance.getFitAddon()?.fit();
}, 100);
} }
}, [connectionStatus, audit, auditShown]); }, [connectionStatus, audit]);
// 重连处理 // 重连处理
const handleReconnect = useCallback(() => { const handleReconnect = useCallback(() => {
if (instanceRef.current) { if (instanceRef.current) {
instanceRef.current.disconnect(); instanceRef.current.disconnect();
setAuditShown(false); // 重置审计警告标记,重连后重新显示 instanceRef.current.resetAudit(); // 重置审计警告标记
setAuditShown(false);
setTimeout(() => { setTimeout(() => {
instanceRef.current?.connect(); instanceRef.current?.connect();
}, 100); }, 100);

View File

@ -14,6 +14,7 @@ export interface TerminalSplitViewProps {
getAuditConfig: () => TerminalAuditConfig; getAuditConfig: () => TerminalAuditConfig;
getToolbarConfig: () => TerminalToolbarConfig; getToolbarConfig: () => TerminalToolbarConfig;
onCloseAllReady?: (closeAllFn: () => void) => void; // 暴露closeAll函数给外部 onCloseAllReady?: (closeAllFn: () => void) => void; // 暴露closeAll函数给外部
onWindowClose?: () => void; // 最后一个Tab关闭时通知关闭整个窗口
} }
/** /**
@ -30,6 +31,7 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
getAuditConfig, getAuditConfig,
getToolbarConfig, getToolbarConfig,
onCloseAllReady, onCloseAllReady,
onWindowClose,
}) => { }) => {
const { const {
layout, layout,
@ -44,7 +46,7 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
closeAll, closeAll,
resizeGroups, resizeGroups,
setActiveGroupId, setActiveGroupId,
} = useSplitView({ initialTab }); } = useSplitView({ initialTab, onWindowClose });
// 暴露closeAll方法给外部 // 暴露closeAll方法给外部
useEffect(() => { useEffect(() => {

View File

@ -54,6 +54,7 @@ export class TerminalInstance {
private stateListeners: Set<StateChangeCallback> = new Set(); private stateListeners: Set<StateChangeCallback> = new Set();
private unsubscribers: Array<() => void> = []; private unsubscribers: Array<() => void> = [];
private auditShown: boolean = false;
constructor(private config: TerminalInstanceConfig) { constructor(private config: TerminalInstanceConfig) {
// 初始化 XTerm // 初始化 XTerm
@ -86,25 +87,39 @@ export class TerminalInstance {
} }
/** /**
* DOM * DOM
*/ */
mount(container: HTMLElement): void { mount(container: HTMLElement): void {
if (this.mounted && this.currentContainer === container) { if (!container) {
console.log(`[TerminalInstance ${this.config.id}] Already mounted to this container`); console.warn(`[TerminalInstance ${this.config.id}] No container provided`);
return; return;
} }
if (this.mounted) { // 如果已经挂载到其他容器,先卸载
if (this.mounted && this.currentContainer !== container) {
this.unmount(); 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.currentContainer = container;
this.mounted = true; this.mounted = true;
// 自适应尺寸 // 自适应尺寸并通知后端
setTimeout(() => { setTimeout(() => {
this.fitAddon.fit(); 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); }, 100);
console.log(`[TerminalInstance ${this.config.id}] Mounted to DOM`); console.log(`[TerminalInstance ${this.config.id}] Mounted to DOM`);
@ -193,10 +208,10 @@ export class TerminalInstance {
} }
/** /**
* *
*/ */
sendResize(rows: number, cols: number): void { sendResize(cols: number, rows: number): void {
this.connectionStrategy.sendResize(rows, cols); this.connectionStrategy.resize(cols, rows);
} }
/** /**
@ -235,6 +250,45 @@ export class TerminalInstance {
this.xterm.writeln(data); 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`);
}
/** /**
* *
*/ */

View File

@ -40,14 +40,14 @@ export abstract class BaseConnectionStrategy {
abstract disconnect(): void; abstract disconnect(): void;
/** /**
* *
*/ */
abstract sendInput(data: string): void; abstract sendInput(data: string): void;
/** /**
* *
*/ */
abstract sendResize(rows: number, cols: number): void; abstract resize(cols: number, rows: number): void;
/** /**
* *

View File

@ -116,23 +116,15 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
* *
*/ */
sendInput(data: string): void { sendInput(data: string): void {
if (this.ws?.readyState === WebSocket.OPEN) { if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ this.ws.send(data);
type: 'input',
data: {
request: {
type: 'input',
command: data,
}
}
}));
} }
} }
/** /**
* *
*/ */
sendResize(rows: number, cols: number): void { resize(cols: number, rows: number): void {
if (this.ws?.readyState === WebSocket.OPEN) { if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ this.ws.send(JSON.stringify({
type: 'resize', type: 'resize',
@ -144,6 +136,7 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
} }
} }
})); }));
console.log(`[SSHConnectionStrategy] Resize sent: ${cols}x${rows}`);
} }
} }

View File

@ -8,9 +8,10 @@ import { TerminalInstanceManager } from './core/TerminalInstanceManager';
interface UseSplitViewOptions { interface UseSplitViewOptions {
initialTab: TerminalTab; initialTab: TerminalTab;
onWindowClose?: () => void; // 最后一个Tab关闭时的回调
} }
export const useSplitView = ({ initialTab }: UseSplitViewOptions) => { export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions) => {
const [layout, setLayout] = useState<SplitLayout>(() => ({ const [layout, setLayout] = useState<SplitLayout>(() => ({
root: { root: {
type: 'group', type: 'group',
@ -320,7 +321,11 @@ export const useSplitView = ({ initialTab }: UseSplitViewOptions) => {
const { parent } = result; const { parent } = result;
if (!parent) { if (!parent) {
// 不能关闭根节点 // 根节点 且 只有一个Tab关闭整个窗口
if (group && group.tabs.length === 1) {
console.log('[useSplitView] 最后一个Tab关闭整个窗口');
onWindowClose?.();
}
return prev; return prev;
} }

View File

@ -66,6 +66,7 @@ const SSHTerminalSplitViewWrapper: React.FC<SSHTerminalSplitViewWrapperProps> =
// 保存closeAll函数到ref // 保存closeAll函数到ref
closeAllRef.current = closeAllFn; closeAllRef.current = closeAllFn;
}} }}
onWindowClose={onCloseReady}
/> />
); );
}; };