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

View File

@ -65,7 +65,6 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
<div <div
className="h-full w-full flex flex-col bg-white dark:bg-gray-900" className="h-full w-full flex flex-col bg-white dark:bg-gray-900"
onClick={() => { onClick={() => {
console.log(`[Group Click] Activating group: ${node.id}`);
onFocus(node.id); onFocus(node.id);
}} }}
> >
@ -82,7 +81,6 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
}`} }`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
console.log(`[Tab Click] groupId: ${node.id}, tabId: ${tab.id}`);
onTabClick(node.id, 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" className="flex items-center justify-center w-8 h-8 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
console.log(`[+ Button Click] Creating tab in group: ${node.id}`);
onNewTab(node.id); // 直接传递groupId onNewTab(node.id); // 直接传递groupId
}} }}
title="新建终端 (Cmd+T)" 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" /> <div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<Select value={currentTheme} onValueChange={onThemeChange}> <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" /> <Palette className="h-3.5 w-3.5 mr-1" />
<SelectValue placeholder="选择主题" /> <SelectValue placeholder="主题" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[9999]"> <SelectContent className="z-[9999]">
{themes.map((theme) => ( {themes.map((theme) => {
// 紧凑模式:只显示英文名(去掉括号和中文)
const displayLabel = compact
? theme.label.split('')[0].split('(')[0].trim()
: theme.label;
return (
<SelectItem key={theme.name} value={theme.name}> <SelectItem key={theme.name} value={theme.name}>
{theme.label} {displayLabel}
</SelectItem> </SelectItem>
))} );
})}
</SelectContent> </SelectContent>
</Select> </Select>
</> </>

View File

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

View File

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

View File

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

View File

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