refactor: 重构前端
This commit is contained in:
@@ -1,232 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'; // Added watch
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n'; // 引入 useI18n
|
||||
import TerminalComponent from '../components/Terminal.vue'; // 引入终端组件
|
||||
import FileManagerComponent from '../components/FileManager.vue'; // 引入文件管理器组件
|
||||
import StatusMonitorComponent from '../components/StatusMonitor.vue'; // 引入状态监控组件
|
||||
import type { Terminal } from 'xterm'; // 引入 Terminal 类型
|
||||
import { useWebSocketConnection } from '../composables/useWebSocketConnection'; // 只导入 hook
|
||||
import { useSshTerminal } from '../composables/useSshTerminal'; // 导入 SSH 终端模块
|
||||
import { useStatusMonitor } from '../composables/useStatusMonitor'; // 导入状态监控模块
|
||||
import type { ServerStatus } from '../types/server.types'; // 从类型文件导入 ServerStatus
|
||||
// Removed duplicate/unused import: import type { WebSocketMessage, MessagePayload } from '../types/websocket.types';
|
||||
|
||||
// --- Interfaces ---
|
||||
// Updated interface to match StatusMonitor and backend
|
||||
interface ServerStatus {
|
||||
cpuPercent?: number;
|
||||
memPercent?: number;
|
||||
memUsed?: number; // MB
|
||||
memTotal?: number; // MB
|
||||
diskPercent?: number;
|
||||
diskUsed?: number; // KB
|
||||
diskTotal?: number; // KB
|
||||
cpuModel?: string;
|
||||
}
|
||||
// --- 接口定义 ---
|
||||
// ServerStatus 现在从 types/server.types.ts 导入
|
||||
|
||||
const { t } = useI18n(); // 获取 t 函数
|
||||
const route = useRoute();
|
||||
const connectionId = computed(() => route.params.connectionId as string); // 从路由获取 connectionId
|
||||
|
||||
const terminalInstance = ref<Terminal | null>(null); // 终端实例引用
|
||||
const ws = ref<WebSocket | null>(null); // WebSocket 实例引用
|
||||
const connectionStatus = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('connecting');
|
||||
const statusMessage = ref<string>(t('workspace.status.initializing')); // 使用 i18n
|
||||
const terminalOutputBuffer = ref<string[]>([]); // 缓冲 WebSocket 消息直到终端准备好
|
||||
const serverStatus = ref<ServerStatus | null>(null); // 存储服务器状态数据
|
||||
const statusError = ref<string | null>(null); // 存储状态获取错误
|
||||
// --- WebSocket 连接模块 ---
|
||||
const {
|
||||
isConnected,
|
||||
connectionStatus, // Get reactive status from composable
|
||||
statusMessage, // Get reactive status message from composable
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage,
|
||||
onMessage,
|
||||
} = useWebSocketConnection();
|
||||
|
||||
// 辅助函数:根据状态码获取 i18n 状态文本
|
||||
const getStatusText = (statusKey: string, params?: Record<string, any>): string => {
|
||||
return t(`workspace.status.${statusKey}`, params || {});
|
||||
};
|
||||
// --- SSH 终端模块 ---
|
||||
const {
|
||||
// terminalInstance, // 不再需要直接从这里访问
|
||||
handleTerminalReady,
|
||||
handleTerminalData,
|
||||
handleTerminalResize,
|
||||
registerSshHandlers,
|
||||
unregisterAllSshHandlers,
|
||||
} = useSshTerminal();
|
||||
|
||||
// 辅助函数:获取终端消息文本
|
||||
const getTerminalText = (key: string, params?: Record<string, any>): string => {
|
||||
return t(`workspace.terminal.${key}`, params || {});
|
||||
};
|
||||
// --- 状态监控模块 ---
|
||||
const {
|
||||
serverStatus, // 从 composable 获取状态
|
||||
statusError, // 从 composable 获取错误
|
||||
registerStatusHandlers, // 重命名以避免与 SSH 冲突
|
||||
unregisterAllStatusHandlers, // 重命名以避免与 SSH 冲突
|
||||
} = useStatusMonitor();
|
||||
|
||||
|
||||
// 处理终端准备就绪事件
|
||||
const onTerminalReady = (term: Terminal) => {
|
||||
terminalInstance.value = term;
|
||||
// 将缓冲区的输出写入终端
|
||||
terminalOutputBuffer.value.forEach(data => term.write(data));
|
||||
terminalOutputBuffer.value = []; // 清空缓冲区
|
||||
console.log('终端准备就绪');
|
||||
};
|
||||
|
||||
// 处理终端用户输入
|
||||
const onTerminalData = (data: string) => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
ws.value.send(JSON.stringify({ type: 'ssh:input', payload: { data } }));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理终端大小调整
|
||||
const onTerminalResize = (dimensions: { cols: number; rows: number }) => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
console.log('发送终端大小调整:', dimensions);
|
||||
ws.value.send(JSON.stringify({ type: 'ssh:resize', payload: dimensions }));
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化 WebSocket 连接
|
||||
const initializeWebSocketConnection = () => {
|
||||
// 使用当前页面的协议和主机,但端口固定为后端端口 (3001),路径为 /
|
||||
// 注意:这里假设后端 WebSocket 监听根路径,如果不是,需要修改路径
|
||||
// 并且假设前端和后端在同一主机上,只是端口不同
|
||||
const wsUrl = `ws://${window.location.hostname}:3001`; // 构建 WebSocket URL
|
||||
|
||||
console.log(`尝试连接 WebSocket: ${wsUrl}`);
|
||||
statusMessage.value = getStatusText('connectingWs', { url: wsUrl });
|
||||
connectionStatus.value = 'connecting';
|
||||
|
||||
ws.value = new WebSocket(wsUrl);
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket 连接已打开');
|
||||
statusMessage.value = getStatusText('wsConnected');
|
||||
// 连接打开后,发送 ssh:connect 消息
|
||||
if (ws.value) {
|
||||
ws.value.send(JSON.stringify({ type: 'ssh:connect', payload: { connectionId: connectionId.value } }));
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
// console.log('收到 WebSocket 消息:', message); // Debug log
|
||||
|
||||
switch (message.type) {
|
||||
case 'ssh:output':
|
||||
let outputData = message.payload;
|
||||
// 检查是否为 Base64 编码
|
||||
if (message.encoding === 'base64' && typeof outputData === 'string') {
|
||||
try {
|
||||
// 解码 Base64 并尝试用 UTF-8 解释
|
||||
// 注意:atob 在浏览器中可用,但在 Node.js 环境中可能需要 Buffer.from(..., 'base64').toString()
|
||||
outputData = atob(outputData);
|
||||
} catch (e) {
|
||||
console.error('Base64 解码失败:', e, '原始数据:', message.payload);
|
||||
outputData = `\r\n[解码错误: ${e}]\r\n`; // 在终端显示解码错误
|
||||
}
|
||||
}
|
||||
// 写入终端
|
||||
if (terminalInstance.value) {
|
||||
terminalInstance.value.write(outputData);
|
||||
} else {
|
||||
// 如果终端还没准备好,先缓冲输出 (缓冲解码后的数据)
|
||||
terminalOutputBuffer.value.push(message.payload);
|
||||
}
|
||||
break;
|
||||
case 'ssh:connected':
|
||||
console.log('SSH 会话已连接');
|
||||
connectionStatus.value = 'connected';
|
||||
statusMessage.value = getStatusText('connected');
|
||||
terminalInstance.value?.focus(); // 连接成功后聚焦终端
|
||||
break;
|
||||
case 'ssh:disconnected':
|
||||
const reasonDisconnect = message.payload || '未知原因';
|
||||
console.log('SSH 会话已断开:', reasonDisconnect);
|
||||
connectionStatus.value = 'disconnected';
|
||||
statusMessage.value = getStatusText('disconnected', { reason: reasonDisconnect });
|
||||
terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('disconnectMsg', { reason: reasonDisconnect })}\x1b[0m`);
|
||||
break;
|
||||
case 'ssh:error':
|
||||
const errorMsg = message.payload || '未知 SSH 错误';
|
||||
console.error('SSH 错误:', errorMsg);
|
||||
connectionStatus.value = 'error';
|
||||
// 尝试匹配特定的错误 key
|
||||
let errorKey = 'sshError';
|
||||
if (errorMsg.includes('解密')) errorKey = 'decryptError';
|
||||
else if (errorMsg.includes('未找到 ID')) errorKey = 'noConnInfo';
|
||||
else if (errorMsg.includes('缺少密码')) errorKey = 'noPassword';
|
||||
else if (errorMsg.includes('打开 Shell 失败')) errorKey = 'shellError';
|
||||
else if (errorMsg.includes('已存在活动的 SSH 连接')) errorKey = 'alreadyConnected';
|
||||
|
||||
statusMessage.value = getStatusText(errorKey, { message: errorMsg, id: connectionId.value });
|
||||
terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('genericErrorMsg', { message: errorMsg })}\x1b[0m`);
|
||||
break;
|
||||
case 'ssh:status':
|
||||
const statusKey = message.payload?.key || 'unknown'; // 假设后端会发送 key
|
||||
const statusParams = message.payload?.params || {};
|
||||
console.log('SSH 状态:', statusKey, statusParams);
|
||||
statusMessage.value = getStatusText(statusKey, statusParams); // 更新状态信息
|
||||
break;
|
||||
case 'info': // 处理后端发送的普通信息
|
||||
console.log('后端信息:', message.payload);
|
||||
terminalInstance.value?.writeln(`\r\n\x1b[34m${getTerminalText('infoPrefix')} ${message.payload}\x1b[0m`);
|
||||
break;
|
||||
case 'error': // 处理后端发送的通用错误
|
||||
console.error('后端错误:', message.payload);
|
||||
connectionStatus.value = 'error';
|
||||
statusMessage.value = getStatusText('error', { message: message.payload });
|
||||
terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('errorPrefix')} ${message.payload}\x1b[0m`);
|
||||
break;
|
||||
// --- Handle Status Updates ---
|
||||
case 'status_update': // Corrected message type
|
||||
// console.log('收到状态更新:', message.payload.status); // Debug log
|
||||
// Ensure payload and status exist before assigning
|
||||
if (message.payload && message.payload.status) {
|
||||
serverStatus.value = message.payload.status; // Assign the nested status object
|
||||
statusError.value = null; // Clear previous error on successful update
|
||||
} else {
|
||||
console.warn('WorkspaceView: Received status_update message with missing payload.status');
|
||||
}
|
||||
break;
|
||||
// Optional: Handle status errors if backend sends them
|
||||
// case 'ssh:status:error':
|
||||
// console.error('获取服务器状态时出错:', message.payload);
|
||||
// statusError.value = message.payload || '无法获取服务器状态';
|
||||
// serverStatus.value = null; // Clear status data on error
|
||||
// break;
|
||||
// default: // Removed default case to allow other components to handle messages
|
||||
// console.warn('WorkspaceView: 收到未处理的 WebSocket 消息类型:', message.type);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理 WebSocket 消息时出错:', e);
|
||||
// 如果收到的不是 JSON,直接写入终端
|
||||
if (terminalInstance.value && typeof event.data === 'string') {
|
||||
terminalInstance.value.write(event.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error);
|
||||
connectionStatus.value = 'error';
|
||||
statusMessage.value = getStatusText('wsError');
|
||||
terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('wsErrorMsg')}\x1b[0m`);
|
||||
};
|
||||
|
||||
ws.value.onclose = (event) => {
|
||||
console.log('WebSocket 连接已关闭:', event.code, event.reason);
|
||||
if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') {
|
||||
connectionStatus.value = 'disconnected';
|
||||
statusMessage.value = getStatusText('wsClosed', { code: event.code });
|
||||
terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('wsCloseMsg', { code: event.code })}\x1b[0m`);
|
||||
}
|
||||
ws.value = null; // 清理引用
|
||||
serverStatus.value = null; // Clear server status on disconnect
|
||||
statusError.value = null; // Clear status error on disconnect
|
||||
};
|
||||
};
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
if (connectionId.value) {
|
||||
initializeWebSocketConnection();
|
||||
const wsUrl = `ws://${window.location.hostname}:3001`; // 构建 WebSocket URL
|
||||
connect(wsUrl, connectionId.value); // 使用 WebSocket 模块的 connect
|
||||
// 不在此处立即注册,等待 isConnected 变为 true
|
||||
// registerSshHandlers();
|
||||
// registerStatusHandlers();
|
||||
} else {
|
||||
statusMessage.value = getStatusText('error', { message: '缺少连接 ID' });
|
||||
connectionStatus.value = 'error';
|
||||
console.error('WorkspaceView: 缺少 connectionId 路由参数。');
|
||||
console.error('[工作区视图] 缺少 connectionId 路由参数。');
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (ws.value) {
|
||||
console.log('组件卸载,关闭 WebSocket 连接...');
|
||||
ws.value.close();
|
||||
}
|
||||
disconnect(); // 使用 WebSocket 模块的 disconnect
|
||||
unregisterAllSshHandlers(); // 注销 SSH 终端处理器
|
||||
unregisterAllStatusHandlers(); // 使用状态监控模块的注销函数
|
||||
});
|
||||
|
||||
</script>
|
||||
// 监听 connectionId 变化 (例如,在工作区之间导航)
|
||||
watch(connectionId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
console.log(`[工作区视图] 连接 ID 从 ${oldId} 更改为 ${newId}。正在重新连接...`);
|
||||
// 断开现有连接,注销处理器,然后为新 ID 连接并注册
|
||||
disconnect();
|
||||
unregisterAllSshHandlers();
|
||||
unregisterAllStatusHandlers(); // 使用状态监控模块的注销函数
|
||||
// serverStatus 和 statusError 由 useStatusMonitor 自动管理,无需手动重置
|
||||
|
||||
// 重新连接
|
||||
const wsUrl = `ws://${window.location.hostname}:3001`;
|
||||
connect(wsUrl, newId);
|
||||
// registerSshHandlers(); // 注册移至 isConnected watch
|
||||
// registerStatusHandlers(); // 注册移至 isConnected watch
|
||||
} else if (!newId && oldId) {
|
||||
// 导航离开工作区视图
|
||||
disconnect(); // isConnected 会变为 false,自动触发清理
|
||||
// unregisterAllSshHandlers(); // 注销移至 isConnected watch
|
||||
// unregisterAllStatusHandlers(); // 注销移至 isConnected watch
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 WebSocket 连接状态变化来注册/注销处理器
|
||||
watch(isConnected, (connected) => {
|
||||
if (connected) {
|
||||
console.log('[工作区视图] WebSocket 已连接,注册 SSH 和状态处理器。');
|
||||
registerSshHandlers();
|
||||
registerStatusHandlers();
|
||||
} else {
|
||||
console.log('[工作区视图] WebSocket 已断开,注销 SSH 和状态处理器。');
|
||||
// isConnected 变为 false 时,确保清理
|
||||
unregisterAllSshHandlers();
|
||||
unregisterAllStatusHandlers();
|
||||
// 注意:disconnect() 应该在 connectionId 变化或组件卸载时调用,
|
||||
// isConnected 变为 false 是结果,而不是原因。
|
||||
}
|
||||
});
|
||||
|
||||
// 辅助函数:获取终端消息文本 (已移至 useSshTerminal)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workspace-view">
|
||||
@@ -234,20 +117,23 @@ onBeforeUnmount(() => {
|
||||
<!-- 使用 t 函数渲染状态栏文本 -->
|
||||
{{ t('workspace.statusBar', { status: statusMessage, id: connectionId }) }}
|
||||
<!-- 状态颜色仍然通过 class 绑定 -->
|
||||
<!-- 使用来自 useWebSocketConnection 的状态 -->
|
||||
<span :class="`status-${connectionStatus}`"></span>
|
||||
</div>
|
||||
<div class="main-content-area">
|
||||
<div class="left-pane">
|
||||
<div class="terminal-wrapper">
|
||||
<!-- 将事件绑定到 useSshTerminal 的处理函数 -->
|
||||
<TerminalComponent
|
||||
@ready="onTerminalReady"
|
||||
@data="onTerminalData"
|
||||
@resize="onTerminalResize"
|
||||
@ready="handleTerminalReady"
|
||||
@data="handleTerminalData"
|
||||
@resize="handleTerminalResize"
|
||||
/>
|
||||
</div>
|
||||
<!-- 文件管理器窗格 -->
|
||||
<div class="file-manager-wrapper">
|
||||
<FileManagerComponent :ws="ws" :is-connected="connectionStatus === 'connected'" />
|
||||
<!-- Removed :ws prop. Communication will be handled via composables -->
|
||||
<FileManagerComponent :is-connected="isConnected" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 状态监控窗格 -->
|
||||
|
||||
Reference in New Issue
Block a user