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