重写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]);
// 监听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<TerminalProps> = ({
};
}, []);
// 显示审计警告(只显示一次)
// 显示审计警告(委托给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);

View File

@ -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<TerminalSplitViewProps> = ({
getAuditConfig,
getToolbarConfig,
onCloseAllReady,
onWindowClose,
}) => {
const {
layout,
@ -44,7 +46,7 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
closeAll,
resizeGroups,
setActiveGroupId,
} = useSplitView({ initialTab });
} = useSplitView({ initialTab, onWindowClose });
// 暴露closeAll方法给外部
useEffect(() => {

View File

@ -54,6 +54,7 @@ export class TerminalInstance {
private stateListeners: Set<StateChangeCallback> = 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`);
}
/**
*
*/

View File

@ -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;
/**
*

View File

@ -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}`);
}
}

View File

@ -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<SplitLayout>(() => ({
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;
}

View File

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