重写ssh前端组件,通用化

This commit is contained in:
dengqichen 2025-12-07 00:17:11 +08:00
parent 1cddee8d42
commit d83a87b259
7 changed files with 19 additions and 89 deletions

View File

@ -57,8 +57,6 @@ export const Terminal: React.FC<TerminalProps> = ({
// 初始化 Terminal 实例
useEffect(() => {
console.log(`[Terminal ${id}] 组件挂载 - 使用实例管理器`);
const manager = TerminalInstanceManager.getInstance();
// 获取或创建实例Manager 内部会处理连接策略的创建和复用)
@ -100,15 +98,11 @@ export const Terminal: React.FC<TerminalProps> = ({
const timer = setTimeout(() => {
const currentState = instance.getState();
if (currentState.status === 'disconnected' || currentState.status === 'error') {
console.log(`[Terminal ${id}] 开始连接`);
instance.connect();
} else {
console.log(`[Terminal ${id}] 实例已连接跳过connect调用当前状态: ${currentState.status}`);
}
}, 300);
return () => {
console.log(`[Terminal ${id}] 组件卸载 - 只unmount不销毁实例`);
clearTimeout(timer);
unsubscribe();
instance.unmount();
@ -122,23 +116,14 @@ export const Terminal: React.FC<TerminalProps> = ({
setTimeout(() => {
const fitAddon = instanceRef.current?.getFitAddon();
if (fitAddon && instanceRef.current) {
// 检查各层容器尺寸
if (wrapperRef.current && terminalRef.current) {
console.log(`[Terminal ${id}] 📦 Wrapper尺寸: ${wrapperRef.current.clientWidth}px × ${wrapperRef.current.clientHeight}px`);
console.log(`[Terminal ${id}] 📦 Content尺寸: ${terminalRef.current.clientWidth}px × ${terminalRef.current.clientHeight}px`);
}
fitAddon.fit();
// 发送新的尺寸给后端
const xterm = instanceRef.current.getXTerm();
const cols = xterm.cols;
const rows = xterm.rows;
console.log(`[Terminal ${id}] FitAddon计算结果: ${cols} cols x ${rows} rows`);
instanceRef.current.sendResize(cols, rows);
// 重要让XTerm获取焦点否则无法输入
xterm.focus();
const hasFocus = xterm.textarea?.matches(':focus');
console.log(`[Terminal ${id}] Tab activated, resized: ${cols}x${rows}, focused: ${hasFocus}`);
}
}, 100);
}
@ -147,20 +132,10 @@ export const Terminal: React.FC<TerminalProps> = ({
// 监听窗口大小变化,自动调整终端尺寸
useEffect(() => {
const handleResize = () => {
console.log(`[Terminal ${id}] 🔄 Window resize event`);
if (wrapperRef.current && terminalRef.current) {
console.log(`[Terminal ${id}] 📦 Resize - Wrapper: ${wrapperRef.current.clientWidth}px × ${wrapperRef.current.clientHeight}px`);
console.log(`[Terminal ${id}] 📦 Resize - Content: ${terminalRef.current.clientWidth}px × ${terminalRef.current.clientHeight}px`);
}
const fitAddon = instanceRef.current?.getFitAddon();
if (fitAddon) {
setTimeout(() => {
fitAddon.fit();
if (instanceRef.current) {
const xterm = instanceRef.current.getXTerm();
console.log(`[Terminal ${id}] 🔄 Resize后: ${xterm.cols} cols × ${xterm.rows} rows`);
}
}, 100);
}
};

View File

@ -65,7 +65,6 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
<div
className="h-full w-full flex flex-col bg-white dark:bg-gray-900"
onClick={() => {
console.log(`[Group Click] Activating group: ${node.id}`);
onFocus(node.id);
}}
>
@ -82,7 +81,6 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
}`}
onClick={(e) => {
e.stopPropagation();
console.log(`[Tab Click] groupId: ${node.id}, tabId: ${tab.id}`);
onTabClick(node.id, tab.id);
}}
>
@ -104,7 +102,6 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
className="flex items-center justify-center w-8 h-8 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
onClick={(e) => {
e.stopPropagation();
console.log(`[+ Button Click] Creating tab in group: ${node.id}`);
onNewTab(node.id); // 直接传递groupId
}}
title="新建终端 (Cmd+T)"

View File

@ -168,16 +168,22 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
<>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<Select value={currentTheme} onValueChange={onThemeChange}>
<SelectTrigger className={compact ? "h-7 w-[160px] text-xs" : "h-7 w-[200px] text-xs"}>
<SelectTrigger className={compact ? "h-7 w-[120px] text-xs" : "h-7 w-[200px] text-xs"}>
<Palette className="h-3.5 w-3.5 mr-1" />
<SelectValue placeholder="选择主题" />
<SelectValue placeholder="主题" />
</SelectTrigger>
<SelectContent className="z-[9999]">
{themes.map((theme) => (
<SelectItem key={theme.name} value={theme.name}>
{theme.label}
</SelectItem>
))}
{themes.map((theme) => {
// 紧凑模式:只显示英文名(去掉括号和中文)
const displayLabel = compact
? theme.label.split('')[0].split('(')[0].trim()
: theme.label;
return (
<SelectItem key={theme.name} value={theme.name}>
{displayLabel}
</SelectItem>
);
})}
</SelectContent>
</Select>
</>

View File

@ -82,8 +82,6 @@ export class TerminalInstance {
this.connectionStrategy = config.connection;
this.setupConnectionListeners();
this.setupTerminalListeners();
console.log(`[TerminalInstance ${config.id}] Created`);
}
/**
@ -103,10 +101,6 @@ export class TerminalInstance {
// 只在第一次或切换容器时调用 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;
@ -119,10 +113,8 @@ export class TerminalInstance {
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`);
}
/**
@ -135,14 +127,12 @@ export class TerminalInstance {
this.currentContainer = null;
this.mounted = false;
console.log(`[TerminalInstance ${this.config.id}] Unmounted from DOM`);
}
/**
*
*/
async connect(): Promise<void> {
console.log(`[TerminalInstance ${this.config.id}] Connecting...`);
await this.connectionStrategy.connect();
}
@ -150,7 +140,6 @@ export class TerminalInstance {
*
*/
disconnect(): void {
console.log(`[TerminalInstance ${this.config.id}] Disconnecting...`);
this.connectionStrategy.disconnect();
}
@ -255,7 +244,6 @@ export class TerminalInstance {
*/
showAudit(companyName: string, customMessage?: string): boolean {
if (this.auditShown) {
console.log(`[TerminalInstance ${this.config.id}] Audit already shown, skipping`);
return false;
}
@ -272,7 +260,6 @@ export class TerminalInstance {
}
this.auditShown = true;
console.log(`[TerminalInstance ${this.config.id}] Audit warning displayed`);
setTimeout(() => {
this.fitAddon.fit();
@ -286,7 +273,6 @@ export class TerminalInstance {
*/
resetAudit(): void {
this.auditShown = false;
console.log(`[TerminalInstance ${this.config.id}] Audit state reset`);
}
/**
@ -300,7 +286,6 @@ export class TerminalInstance {
*
*/
dispose(): void {
console.log(`[TerminalInstance ${this.config.id}] Disposing...`);
// 清理连接
this.disconnect();

View File

@ -18,7 +18,6 @@ export class TerminalInstanceManager {
private connectionStrategies: Map<string, BaseConnectionStrategy> = new Map();
private constructor() {
console.log('[TerminalInstanceManager] Initialized');
}
/**
@ -40,17 +39,14 @@ export class TerminalInstanceManager {
getOrCreate(tabId: string, config: TerminalInstanceCreateConfig): TerminalInstance {
// 如果实例已存在,直接返回
if (this.instances.has(tabId)) {
console.log(`[TerminalInstanceManager] Reusing instance for tab: ${tabId}`);
return this.instances.get(tabId)!;
}
// 创建连接策略(每个 Tab 独立的策略)
console.log(`[TerminalInstanceManager] Creating connection strategy for tab: ${tabId}`);
const connectionStrategy = this.createConnectionStrategy(config.connectionConfig);
this.connectionStrategies.set(tabId, connectionStrategy);
// 创建新实例
console.log(`[TerminalInstanceManager] Creating new instance for tab: ${tabId}`);
const instance = new TerminalInstance({
id: config.id,
connection: connectionStrategy,
@ -106,15 +102,13 @@ export class TerminalInstanceManager {
destroy(tabId: string): void {
const instance = this.instances.get(tabId);
if (instance) {
console.log(`[TerminalInstanceManager] Destroying instance for tab: ${tabId}`);
instance.dispose();
instance.dispose();
this.instances.delete(tabId);
}
// 同时销毁连接策略
const strategy = this.connectionStrategies.get(tabId);
if (strategy && 'dispose' in strategy) {
console.log(`[TerminalInstanceManager] Destroying connection strategy for tab: ${tabId}`);
(strategy as any).dispose();
}
this.connectionStrategies.delete(tabId);
@ -124,17 +118,14 @@ export class TerminalInstanceManager {
*
*/
destroyAll(): void {
console.log('[TerminalInstanceManager] Destroying all instances');
this.instances.forEach((instance, tabId) => {
console.log(`[TerminalInstanceManager] Destroying instance: ${tabId}`);
this.instances.forEach((instance) => {
instance.dispose();
});
this.instances.clear();
// 同时销毁所有连接策略
this.connectionStrategies.forEach((strategy, tabId) => {
// 销毁所有连接策略
this.connectionStrategies.forEach((strategy) => {
if ('dispose' in strategy) {
console.log(`[TerminalInstanceManager] Destroying connection strategy: ${tabId}`);
(strategy as any).dispose();
}
});

View File

@ -34,7 +34,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
*/
async connect(): Promise<void> {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log('[SSHConnectionStrategy] Already connected');
return;
}
@ -45,7 +44,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/v1/server-ssh/connect/${this.sshConfig.serverId}?token=${this.sshConfig.token}`;
console.log('[SSHConnectionStrategy] 连接WebSocket:', wsUrl);
await this.connectWebSocket(wsUrl);
}
@ -59,8 +57,7 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
this.ws = ws;
ws.onopen = () => {
console.log('🔗 SSH WebSocket connected:', wsUrl);
this.notifyStatusChange('connected');
this.notifyStatusChange('connected');
this.reconnectAttempts = 0;
resolve();
};
@ -77,7 +74,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
};
ws.onclose = () => {
console.log('[SSHConnectionStrategy] WebSocket closed');
// 只有在已连接状态下关闭才尝试重连
if (this.status === 'connected' && this.config.autoReconnect) {
@ -109,7 +105,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
}
this.notifyStatusChange('disconnected');
console.log('[SSHConnectionStrategy] Disconnected');
}
/**
@ -144,7 +139,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
}
}
}));
console.log(`[SSHConnectionStrategy] Resize sent: ${cols}x${rows}`);
}
}
@ -189,7 +183,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
const interval = this.config.reconnectInterval ?? 3000;
if (this.reconnectAttempts >= maxAttempts) {
console.log('[SSHConnectionStrategy] Max reconnect attempts reached');
this.notifyStatusChange('error');
this.notifyError('连接断开,已达到最大重连次数');
return;
@ -198,8 +191,6 @@ export class SSHConnectionStrategy extends BaseConnectionStrategy {
this.reconnectAttempts++;
this.notifyStatusChange('reconnecting');
console.log(`[SSHConnectionStrategy] Reconnecting... (${this.reconnectAttempts}/${maxAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.connect().catch(error => {
console.error('[SSHConnectionStrategy] Reconnect failed:', error);

View File

@ -206,23 +206,18 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
const splitInGroup = useCallback((groupId?: string) => {
// 如果提供了groupId就用它否则用activeGroupId
const targetGroupId = groupId || activeGroupId;
console.log(`[splitInGroup] Target groupId: ${targetGroupId}`);
const targetGroup = findGroup(layout.root, targetGroupId);
if (!targetGroup) {
console.log('[splitInGroup] Target group not found');
return;
}
console.log(`[splitInGroup] Target group: ${targetGroup.id}, tabs count: ${targetGroup.tabs.length}`);
const activeTab = targetGroup.tabs.find(t => t.id === targetGroup.activeTabId);
if (!activeTab) {
console.log('[splitInGroup] No active tab in target group');
return;
}
console.log(`[splitInGroup] Creating new tab based on: ${activeTab.serverName}`);
const newTab: TerminalTab = {
id: `tab-${Date.now()}`,
@ -243,20 +238,16 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
// 更新activeGroupId
setActiveGroupId(targetGroupId);
console.log(`[splitInGroup] New tab created: ${newTab.id} in group: ${targetGroup.id}`);
}, [activeGroupId, findGroup, layout.root]);
// 切换标签页
const switchTab = useCallback((groupId: string, tabId: string) => {
console.log(`[switchTab] Switching to group: ${groupId}, tab: ${tabId}`);
// 先设置activeGroupId确保后续操作使用正确的组
setActiveGroupId(groupId);
setLayout(prev => {
const group = findGroup(prev.root, groupId);
if (!group) {
console.log(`[switchTab] Group not found: ${groupId}`);
return prev;
}
@ -275,7 +266,6 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
// 销毁 Terminal 实例
const manager = TerminalInstanceManager.getInstance();
manager.destroy(tabId);
console.log(`[useSplitView] 销毁 Terminal 实例: ${tabId}`);
setLayout(prev => {
const group = findGroup(prev.root, groupId);
@ -313,7 +303,6 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
const manager = TerminalInstanceManager.getInstance();
group.tabs.forEach(tab => {
manager.destroy(tab.id);
console.log(`[useSplitView] 关闭组,销毁 Terminal 实例: ${tab.id}`);
});
}
@ -324,7 +313,6 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
if (!parent) {
// 根节点 且 只有一个Tab关闭整个窗口
if (group && group.tabs.length === 1) {
console.log('[useSplitView] 最后一个Tab关闭整个窗口');
onWindowClose?.();
}
return prev;
@ -413,7 +401,6 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
// 关闭所有Tab窗口关闭时调用
const closeAll = useCallback(() => {
console.log('[useSplitView] 关闭所有Tab断开所有连接');
const manager = TerminalInstanceManager.getInstance();
// 收集所有Tab ID
@ -430,10 +417,8 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
// 销毁所有Terminal实例
allTabIds.forEach(tabId => {
manager.destroy(tabId);
console.log(`[useSplitView] 已销毁Terminal实例: ${tabId}`);
});
console.log(`[useSplitView] 共关闭 ${allTabIds.length} 个Tab`);
}, [layout.root]);
return {