重写ssh前端组件,通用化
This commit is contained in:
parent
004e1adcf5
commit
83c36866cf
@ -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);
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
// 只在第一次或切换容器时调用 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`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中内容
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 获取当前连接状态
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ const SSHTerminalSplitViewWrapper: React.FC<SSHTerminalSplitViewWrapperProps> =
|
||||
// 保存closeAll函数到ref
|
||||
closeAllRef.current = closeAllFn;
|
||||
}}
|
||||
onWindowClose={onCloseReady}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user