diff --git a/packages/backend/src/command-history/command-history.controller.ts b/packages/backend/src/command-history/command-history.controller.ts new file mode 100644 index 0000000..78ac998 --- /dev/null +++ b/packages/backend/src/command-history/command-history.controller.ts @@ -0,0 +1,73 @@ +import { Request, Response } from 'express'; +import * as CommandHistoryService from '../services/command-history.service'; + +/** + * 处理添加新命令历史记录的请求 + */ +export const addCommand = async (req: Request, res: Response): Promise => { + const { command } = req.body; + + if (!command || typeof command !== 'string' || command.trim().length === 0) { + res.status(400).json({ message: '命令不能为空' }); + return; + } + + try { + const newId = await CommandHistoryService.addCommandHistory(command); + res.status(201).json({ id: newId, message: '命令已添加到历史记录' }); + } catch (error: any) { + console.error('添加命令历史记录控制器出错:', error); + res.status(500).json({ message: error.message || '无法添加命令历史记录' }); + } +}; + +/** + * 处理获取所有命令历史记录的请求 + */ +export const getAllCommands = async (req: Request, res: Response): Promise => { + try { + const history = await CommandHistoryService.getAllCommandHistory(); + // 注意:前端要求最新在下,最旧在上。Repository 返回的是升序(旧->新),符合要求。 + res.status(200).json(history); + } catch (error: any) { + console.error('获取命令历史记录控制器出错:', error); + res.status(500).json({ message: error.message || '无法获取命令历史记录' }); + } +}; + +/** + * 处理根据 ID 删除命令历史记录的请求 + */ +export const deleteCommand = async (req: Request, res: Response): Promise => { + const id = parseInt(req.params.id, 10); + + if (isNaN(id)) { + res.status(400).json({ message: '无效的 ID' }); + return; + } + + try { + const success = await CommandHistoryService.deleteCommandHistoryById(id); + if (success) { + res.status(200).json({ message: '命令历史记录已删除' }); + } else { + res.status(404).json({ message: '未找到要删除的命令历史记录' }); + } + } catch (error: any) { + console.error('删除命令历史记录控制器出错:', error); + res.status(500).json({ message: error.message || '无法删除命令历史记录' }); + } +}; + +/** + * 处理清空所有命令历史记录的请求 + */ +export const clearAllCommands = async (req: Request, res: Response): Promise => { + try { + const count = await CommandHistoryService.clearAllCommandHistory(); + res.status(200).json({ count, message: `已清空 ${count} 条命令历史记录` }); + } catch (error: any) { + console.error('清空命令历史记录控制器出错:', error); + res.status(500).json({ message: error.message || '无法清空命令历史记录' }); + } +}; diff --git a/packages/backend/src/command-history/command-history.routes.ts b/packages/backend/src/command-history/command-history.routes.ts new file mode 100644 index 0000000..a0beb5b --- /dev/null +++ b/packages/backend/src/command-history/command-history.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import * as CommandHistoryController from './command-history.controller'; +import { isAuthenticated } from '../auth/auth.middleware'; // 使用正确的认证中间件 + +const router = Router(); + +// 应用认证中间件到所有命令历史记录相关的路由 +router.use(isAuthenticated); + +// 定义路由 +router.post('/', CommandHistoryController.addCommand); // POST /api/command-history +router.get('/', CommandHistoryController.getAllCommands); // GET /api/command-history +router.delete('/:id', CommandHistoryController.deleteCommand); // DELETE /api/command-history/:id +router.delete('/', CommandHistoryController.clearAllCommands); // DELETE /api/command-history (用于清空) + +export default router; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5aa7d52..6ce6498 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -16,6 +16,7 @@ import tagsRouter from './tags/tags.routes'; // 导入标签路由 import settingsRoutes from './settings/settings.routes'; // 导入设置路由 import notificationRoutes from './notifications/notification.routes'; // 导入通知路由 import auditRoutes from './audit/audit.routes'; // 导入审计路由 +import commandHistoryRoutes from './command-history/command-history.routes'; // 导入命令历史记录路由 import { initializeWebSocket } from './websocket'; import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件 @@ -102,6 +103,7 @@ app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由 app.use('/api/v1/settings', settingsRoutes); // 挂载设置相关的路由 app.use('/api/v1/notifications', notificationRoutes); // 挂载通知相关的路由 app.use('/api/v1/audit-logs', auditRoutes); // 挂载审计日志相关的路由 +app.use('/api/v1/command-history', commandHistoryRoutes); // 挂载命令历史记录相关的路由 // 状态检查接口 app.get('/api/v1/status', (req: Request, res: Response) => { diff --git a/packages/backend/src/migrations.ts b/packages/backend/src/migrations.ts index 215f25a..b7d0e7d 100644 --- a/packages/backend/src/migrations.ts +++ b/packages/backend/src/migrations.ts @@ -131,6 +131,14 @@ CREATE TABLE IF NOT EXISTS ip_blacklist ( blocked_until INTEGER NULL -- 封禁截止时间戳 (秒),NULL 表示未封禁或永久封禁 (根据逻辑决定) ); `; + +const createCommandHistoryTableSQL = ` +CREATE TABLE IF NOT EXISTS command_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT NOT NULL, + timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); +`; // --- 结束新增表结构定义 --- @@ -254,6 +262,15 @@ export const runMigrations = async (db: Database): Promise => { }); }); + // 创建 command_history 表 + await new Promise((resolve, reject) => { + db.run(createCommandHistoryTableSQL, (err: Error | null) => { + if (err) return reject(new Error(`创建 command_history 表时出错: ${err.message}`)); + console.log('Command_History 表已检查/创建。'); + resolve(); + }); + }); + // --- 结束新增表创建逻辑 --- diff --git a/packages/backend/src/repositories/command-history.repository.ts b/packages/backend/src/repositories/command-history.repository.ts new file mode 100644 index 0000000..46709fa --- /dev/null +++ b/packages/backend/src/repositories/command-history.repository.ts @@ -0,0 +1,115 @@ +import { getDb } from '../database'; + +// 定义命令历史记录的接口 +export interface CommandHistoryEntry { + id: number; + command: string; + timestamp: number; // Unix 时间戳 (秒) +} + +/** + * 插入或更新一条命令历史记录。 + * 如果命令已存在,则更新其时间戳;否则,插入新记录。 + * @param command - 要添加或更新的命令字符串 + * @returns 返回插入或更新记录的 ID + */ +export const upsertCommand = (command: string): Promise => { + const db = getDb(); + // 使用 INSERT ... ON CONFLICT DO UPDATE 语法 (SQLite 3.24.0+) + // 如果 command 列冲突 (假设我们为 command 列添加了 UNIQUE 约束,或者手动检查) + // 这里我们先不加 UNIQUE 约束,而是先尝试 UPDATE,再尝试 INSERT + const now = Math.floor(Date.now() / 1000); // 获取当前时间戳 + + return new Promise((resolve, reject) => { + // 1. 尝试更新现有记录的时间戳 + const updateSql = `UPDATE command_history SET timestamp = ? WHERE command = ?`; + db.run(updateSql, [now, command], function (updateErr) { + if (updateErr) { + console.error('更新命令历史记录时间戳时出错:', updateErr); + return reject(new Error('无法更新命令历史记录')); + } + + if (this.changes > 0) { + // 更新成功,需要获取被更新记录的 ID + const selectSql = `SELECT id FROM command_history WHERE command = ? ORDER BY timestamp DESC LIMIT 1`; + db.get(selectSql, [command], (selectErr, row: { id: number } | undefined) => { + if (selectErr) { + console.error('获取更新后记录 ID 时出错:', selectErr); + return reject(new Error('无法获取更新后的记录 ID')); + } + if (row) { + resolve(row.id); + } else { + // 理论上不应该发生,因为我们刚更新了它 + reject(new Error('更新成功但无法找到记录 ID')); + } + }); + } else { + // 2. 没有记录被更新,说明命令不存在,执行插入 + const insertSql = `INSERT INTO command_history (command, timestamp) VALUES (?, ?)`; + db.run(insertSql, [command, now], function (insertErr) { + if (insertErr) { + console.error('插入新命令历史记录时出错:', insertErr); + return reject(new Error('无法插入新命令历史记录')); + } + resolve(this.lastID); // 返回新插入的行 ID + }); + } + }); + }); +}; + +/** + * 获取所有命令历史记录,按时间戳升序排列(最旧的在前) + * @returns 返回包含所有历史记录条目的数组 + */ +export const getAllCommands = (): Promise => { + const db = getDb(); + const sql = `SELECT id, command, timestamp FROM command_history ORDER BY timestamp ASC`; + return new Promise((resolve, reject) => { + db.all(sql, [], (err, rows: CommandHistoryEntry[]) => { + if (err) { + console.error('获取命令历史记录时出错:', err); + return reject(new Error('无法获取命令历史记录')); + } + resolve(rows); + }); + }); +}; + +/** + * 根据 ID 删除指定的命令历史记录 + * @param id - 要删除的记录 ID + * @returns 返回删除的行数 (通常是 1 或 0) + */ +export const deleteCommandById = (id: number): Promise => { + const db = getDb(); + const sql = `DELETE FROM command_history WHERE id = ?`; + return new Promise((resolve, reject) => { + db.run(sql, [id], function (err) { + if (err) { + console.error('删除命令历史记录时出错:', err); + return reject(new Error('无法删除命令历史记录')); + } + resolve(this.changes); // 返回受影响的行数 + }); + }); +}; + +/** + * 清空所有命令历史记录 + * @returns 返回删除的行数 + */ +export const clearAllCommands = (): Promise => { + const db = getDb(); + const sql = `DELETE FROM command_history`; + return new Promise((resolve, reject) => { + db.run(sql, [], function (err) { + if (err) { + console.error('清空命令历史记录时出错:', err); + return reject(new Error('无法清空命令历史记录')); + } + resolve(this.changes); // 返回受影响的行数 + }); + }); +}; diff --git a/packages/backend/src/services/command-history.service.ts b/packages/backend/src/services/command-history.service.ts new file mode 100644 index 0000000..81c0aa9 --- /dev/null +++ b/packages/backend/src/services/command-history.service.ts @@ -0,0 +1,46 @@ +import * as CommandHistoryRepository from '../repositories/command-history.repository'; +import { CommandHistoryEntry } from '../repositories/command-history.repository'; + +/** + * 添加一条命令历史记录 + * @param command - 要添加的命令 + * @returns 返回添加记录的 ID + */ +export const addCommandHistory = async (command: string): Promise => { + // 可以在这里添加额外的业务逻辑,例如校验命令格式、长度限制等 + if (!command || command.trim().length === 0) { + throw new Error('命令不能为空'); + } + // 可以在此添加去重逻辑,如果不想记录重复的命令 + // const existing = await CommandHistoryRepository.findCommand(command); // 如果需要更复杂的去重逻辑 + // if (existing) { ... } + + // 调用 upsertCommand 来处理插入或更新时间戳 + return CommandHistoryRepository.upsertCommand(command.trim()); +}; + +/** + * 获取所有命令历史记录 + * @returns 返回所有历史记录条目数组,按时间戳升序 + */ +export const getAllCommandHistory = async (): Promise => { + return CommandHistoryRepository.getAllCommands(); +}; + +/** + * 根据 ID 删除一条命令历史记录 + * @param id - 要删除的记录 ID + * @returns 返回是否成功删除 (删除行数 > 0) + */ +export const deleteCommandHistoryById = async (id: number): Promise => { + const changes = await CommandHistoryRepository.deleteCommandById(id); + return changes > 0; +}; + +/** + * 清空所有命令历史记录 + * @returns 返回删除的记录条数 + */ +export const clearAllCommandHistory = async (): Promise => { + return CommandHistoryRepository.clearAllCommands(); +}; diff --git a/packages/frontend/src/components/CommandHistoryMenu.vue b/packages/frontend/src/components/CommandHistoryMenu.vue new file mode 100644 index 0000000..1d763a8 --- /dev/null +++ b/packages/frontend/src/components/CommandHistoryMenu.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/packages/frontend/src/components/TerminalTabBar.vue b/packages/frontend/src/components/TerminalTabBar.vue index b257559..05ef530 100644 --- a/packages/frontend/src/components/TerminalTabBar.vue +++ b/packages/frontend/src/components/TerminalTabBar.vue @@ -71,6 +71,7 @@ const paneLabels: Record = { fileManager: t('layout.pane.fileManager'), editor: t('layout.pane.editor'), statusMonitor: t('layout.pane.statusMonitor'), + commandHistory: t('layout.pane.commandHistory', '命令历史'), // 添加命令历史标签 }; // 获取所有可控制的面板名称 diff --git a/packages/frontend/src/composables/useSshTerminal.ts b/packages/frontend/src/composables/useSshTerminal.ts index d399502..a8c49fc 100644 --- a/packages/frontend/src/composables/useSshTerminal.ts +++ b/packages/frontend/src/composables/useSshTerminal.ts @@ -23,6 +23,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD const terminalInstance = ref(null); const terminalOutputBuffer = ref([]); // 缓冲 WebSocket 消息直到终端准备好 + const isSshConnected = ref(false); // 新增:跟踪 SSH 连接状态 // 辅助函数:获取终端消息文本 const getTerminalText = (key: string, params?: Record): string => { @@ -181,6 +182,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD } console.log(`[会话 ${sessionId}][SSH终端模块] SSH 会话已连接。`); + isSshConnected.value = true; // 更新状态 // 连接成功后聚焦终端 terminalInstance.value?.focus(); // 清空可能存在的旧缓冲(虽然理论上此时应该已经 ready 了) @@ -199,6 +201,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD const reason = payload || t('workspace.terminal.unknownReason'); // 使用 i18n 获取未知原因文本 console.log(`[会话 ${sessionId}][SSH终端模块] SSH 会话已断开:`, reason); + isSshConnected.value = false; // 更新状态 terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('disconnectMsg', { reason })}\x1b[0m`); // 可以在这里添加其他清理逻辑,例如禁用输入 }; @@ -211,6 +214,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD const errorMsg = payload || t('workspace.terminal.unknownSshError'); // 使用 i18n console.error(`[会话 ${sessionId}][SSH终端模块] SSH 错误:`, errorMsg); + isSshConnected.value = false; // 更新状态 terminalInstance.value?.writeln(`\r\n\x1b[31m${getTerminalText('genericErrorMsg', { message: errorMsg })}\x1b[0m`); }; @@ -299,7 +303,10 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD handleTerminalData, // 这个处理来自 xterm.js 的输入 handleTerminalResize, sendData, // 新增:允许外部直接发送数据 - cleanup + cleanup, + // --- 新增暴露 --- + isSshConnected: readonly(isSshConnected), // 暴露 SSH 连接状态 (只读) + terminalInstance // 暴露 terminal 实例,以便 WorkspaceView 可以写入提示信息 }; } diff --git a/packages/frontend/src/locales/en.json b/packages/frontend/src/locales/en.json index 94404d9..2ec0c4e 100644 --- a/packages/frontend/src/locales/en.json +++ b/packages/frontend/src/locales/en.json @@ -179,8 +179,9 @@ "disconnectMsg": "--- SSH Connection Closed ({reason}) ---", "wsCloseMsg": "--- WebSocket Connection Closed (Code: {code}) ---", "wsErrorMsg": "--- WebSocket Connection Error ---", - "decryptErrorMsg": "--- Error: Cannot decrypt credentials ---", - "genericErrorMsg": "--- Error: {message} ---" + "decryptErrorMsg": "--- Error: Could not decrypt connection credentials ---", + "genericErrorMsg": "--- Error: {message} ---", + "reconnectingMsg": "Attempting to reconnect..." } }, "fileManager": { diff --git a/packages/frontend/src/locales/zh.json b/packages/frontend/src/locales/zh.json index 6579aff..eecf9f6 100644 --- a/packages/frontend/src/locales/zh.json +++ b/packages/frontend/src/locales/zh.json @@ -181,7 +181,8 @@ "wsCloseMsg": "--- WebSocket 连接已关闭 (代码: {code}) ---", "wsErrorMsg": "--- WebSocket 连接错误 ---", "decryptErrorMsg": "--- 错误:无法解密连接凭证 ---", - "genericErrorMsg": "--- 错误: {message} ---" + "genericErrorMsg": "--- 错误: {message} ---", + "reconnectingMsg": "正在尝试重新连接..." } }, "fileManager": { @@ -557,7 +558,20 @@ "commandBar": "命令栏", "fileManager": "文件管理器", "editor": "编辑器", - "statusMonitor": "状态监视器" + "statusMonitor": "状态监视器", + "commandHistory": "命令历史" } +}, +"commandHistory": { + "title": "命令历史", + "searchPlaceholder": "搜索历史记录...", + "clear": "清空", + "copy": "复制", + "delete": "删除", + "loading": "加载中...", + "empty": "没有历史记录", + "confirmClear": "确定要清空所有历史记录吗?", + "copied": "已复制到剪贴板", + "copyFailed": "复制失败" } } diff --git a/packages/frontend/src/stores/commandHistory.store.ts b/packages/frontend/src/stores/commandHistory.store.ts new file mode 100644 index 0000000..ac2e149 --- /dev/null +++ b/packages/frontend/src/stores/commandHistory.store.ts @@ -0,0 +1,125 @@ +import { defineStore } from 'pinia'; +import axios from 'axios'; // 假设项目使用 axios +import { ref, computed } from 'vue'; +import { useUiNotificationsStore } from './uiNotifications.store'; // 用于显示通知 + +// 后端返回的原始历史记录条目接口 +interface CommandHistoryEntryBE { + id: number; + command: string; + timestamp: number; // Unix 时间戳 (秒) +} + +// 前端使用的历史记录条目接口 (可能需要添加其他字段) +export interface CommandHistoryEntryFE extends CommandHistoryEntryBE { + // 可以根据需要添加前端特定的字段 +} + +export const useCommandHistoryStore = defineStore('commandHistory', () => { + const historyList = ref([]); + const searchTerm = ref(''); + const isLoading = ref(false); + const error = ref(null); + const uiNotificationsStore = useUiNotificationsStore(); + + // --- Getters --- + + // 计算属性:根据搜索词过滤历史记录 + const filteredHistory = computed(() => { + const term = searchTerm.value.toLowerCase().trim(); + if (!term) { + return historyList.value; // 没有搜索词则返回全部 + } + return historyList.value.filter(entry => + entry.command.toLowerCase().includes(term) + ); + }); + + // --- Actions --- + + // 从后端获取历史记录 + const fetchHistory = async () => { + isLoading.value = true; + error.value = null; + try { + const response = await axios.get('/api/v1/command-history'); + // 后端返回的是按时间戳升序 (旧->新) + // 前端需要按时间戳降序 (新->旧),所以反转数组 + historyList.value = response.data.reverse(); + } catch (err: any) { + console.error('获取命令历史记录失败:', err); + error.value = err.response?.data?.message || '获取历史记录时发生错误'; + // 确保传递给 showError 的是字符串 + uiNotificationsStore.showError(error.value ?? '未知错误'); // 显示错误通知 + } finally { + isLoading.value = false; + } + }; + + // 添加命令到历史记录 (由 CommandInputBar 调用) + const addCommand = async (command: string) => { + if (!command || command.trim().length === 0) { + return; // 不添加空命令 + } + try { + const response = await axios.post<{ id: number }>('/api/v1/command-history', { command: command.trim() }); + // 添加成功后,重新获取列表以保证顺序和 ID 正确 + // 或者,可以在本地模拟添加,但为了简单和一致性,重新获取更好 + await fetchHistory(); + } catch (err: any) { + console.error('添加命令历史记录失败:', err); + const message = err.response?.data?.message || '添加历史记录时发生错误'; + uiNotificationsStore.showError(message); + } + }; + + + // 删除单条历史记录 + const deleteCommand = async (id: number) => { + try { + await axios.delete(`/api/v1/command-history/${id}`); + // 从本地列表中移除 + const index = historyList.value.findIndex(entry => entry.id === id); + if (index !== -1) { + historyList.value.splice(index, 1); + } + uiNotificationsStore.showSuccess('历史记录已删除'); + } catch (err: any) { + console.error('删除命令历史记录失败:', err); + const message = err.response?.data?.message || '删除历史记录时发生错误'; + uiNotificationsStore.showError(message); + } + }; + + // 清空所有历史记录 + const clearAllHistory = async () => { + // 可以在调用前添加确认逻辑 (例如在组件层) + try { + await axios.delete('/api/v1/command-history'); + historyList.value = []; // 清空本地列表 + uiNotificationsStore.showSuccess('所有历史记录已清空'); + } catch (err: any) { + console.error('清空命令历史记录失败:', err); + const message = err.response?.data?.message || '清空历史记录时发生错误'; + uiNotificationsStore.showError(message); + } + }; + + // 设置搜索词 + const setSearchTerm = (term: string) => { + searchTerm.value = term; + }; + + return { + historyList, + searchTerm, + isLoading, + error, + filteredHistory, + fetchHistory, + addCommand, // 导出 addCommand + deleteCommand, + clearAllHistory, + setSearchTerm, + }; +}); diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index df9f740..578247c 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -1,19 +1,20 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; -// 定义面板名称的类型,方便管理和引用 (恢复 commandBar) -export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor'; +// 定义面板名称的类型,方便管理和引用 (添加 commandHistory) +export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory'; // 定义 Store export const useLayoutStore = defineStore('layout', () => { - // 使用 ref 创建响应式状态,存储每个面板的可见性 (恢复 commandBar) + // 使用 ref 创建响应式状态,存储每个面板的可见性 (添加 commandHistory) const paneVisibility = ref>({ connections: true, terminal: true, - commandBar: true, // 恢复 + commandBar: true, fileManager: true, editor: true, statusMonitor: true, + commandHistory: true, // 默认可见 }); // Action: 切换指定面板的可见性 diff --git a/packages/frontend/src/views/CommandHistoryView.vue b/packages/frontend/src/views/CommandHistoryView.vue new file mode 100644 index 0000000..0973fa7 --- /dev/null +++ b/packages/frontend/src/views/CommandHistoryView.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/packages/frontend/src/views/WorkspaceView.vue b/packages/frontend/src/views/WorkspaceView.vue index 21e8eb0..af7aa43 100644 --- a/packages/frontend/src/views/WorkspaceView.vue +++ b/packages/frontend/src/views/WorkspaceView.vue @@ -10,11 +10,13 @@ import AddConnectionFormComponent from '../components/AddConnectionForm.vue'; import TerminalTabBar from '../components/TerminalTabBar.vue'; import CommandInputBar from '../components/CommandInputBar.vue'; import FileEditorContainer from '../components/FileEditorContainer.vue'; // 导入编辑器容器 +import CommandHistoryView from './CommandHistoryView.vue'; // 导入命令历史视图 import PaneTitleBar from '../components/PaneTitleBar.vue'; // 导入标题栏组件 import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance import { useSettingsStore } from '../stores/settings.store'; // 导入设置 Store import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入文件编辑器 Store import { useLayoutStore } from '../stores/layout.store'; // 导入布局 Store +import { useCommandHistoryStore } from '../stores/commandHistory.store'; // 导入命令历史 Store import type { ConnectionInfo } from '../stores/connections.store'; // 导入 splitpanes 组件 import { Splitpanes, Pane } from 'splitpanes'; @@ -27,6 +29,7 @@ const sessionStore = useSessionStore(); const settingsStore = useSettingsStore(); // 初始化设置 Store const fileEditorStore = useFileEditorStore(); // 初始化文件编辑器 Store (用于共享模式) const layoutStore = useLayoutStore(); // 初始化布局 Store +const commandHistoryStore = useCommandHistoryStore(); // 初始化命令历史 Store // --- 从 Store 获取响应式状态和 Getters --- const { sessionTabsWithStatus, activeSessionId, activeSession } = storeToRefs(sessionStore); @@ -99,18 +102,71 @@ onBeforeUnmount(() => { // 处理命令发送 const handleSendCommand = (command: string) => { - // 类型断言确保 terminalManager 存在 sendData 方法 - const terminalManager = activeSession.value?.terminalManager as (SshTerminalInstance | undefined); + const currentSession = activeSession.value; // 获取当前活动会话 + if (!currentSession) { + console.warn('[WorkspaceView] Cannot send command, no active session.'); + return; + } + + const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined); + + // 检查连接状态和命令内容 + if (terminalManager?.isSshConnected && !terminalManager.isSshConnected.value && command.trim() === '') { + // 如果连接断开且命令为空(仅按回车),则触发重连 + console.log(`[WorkspaceView] Command bar Enter detected in disconnected session ${currentSession.sessionId}, attempting reconnect...`); + // 可选:在终端显示提示 + if (terminalManager.terminalInstance?.value) { + terminalManager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`); + } + sessionStore.handleConnectRequest(currentSession.connectionId); + return; // 阻止发送空命令 + } + + // 否则,正常发送命令 if (terminalManager && typeof terminalManager.sendData === 'function') { - console.log(`[WorkspaceView] Sending command to active session ${activeSessionId.value}: ${command.trim()}`); + const commandToSend = command.trim(); // 获取去除首尾空格的命令 + console.log(`[WorkspaceView] Sending command to active session ${currentSession.sessionId}: ${commandToSend}`); // 注意:CommandInputBar 已经添加了 '\n' - terminalManager.sendData(command); + terminalManager.sendData(command); // 发送原始命令(包含换行符) + + // 记录非空命令到历史记录 + if (commandToSend.length > 0) { + commandHistoryStore.addCommand(commandToSend); + } } else { - console.warn('[WorkspaceView] Cannot send command, no active session or terminal manager with sendData method.'); + console.warn(`[WorkspaceView] Cannot send command for session ${currentSession.sessionId}, terminal manager or sendData method not available.`); // 可以考虑给用户一个提示 } }; - + + // --- 新增:处理终端输入,包含重连逻辑 --- + const handleTerminalInput = (sessionId: string, data: string) => { + const session = sessionStore.sessions.get(sessionId); // 获取整个 session 对象 + const manager = session?.terminalManager as (SshTerminalInstance | undefined); // 获取 terminalManager 并断言类型 + + if (!session || !manager) { + console.warn(`[WorkspaceView] handleTerminalInput: 未找到会话 ${sessionId} 或其 terminalManager`); + return; + } + + // 检查是否按下回车且 SSH 未连接 + // 确保 manager.isSshConnected 存在再访问 .value + if (data === '\r' && manager.isSshConnected && !manager.isSshConnected.value) { + console.log(`[WorkspaceView] 检测到在断开的会话 ${sessionId} 中按下回车,尝试重连...`); + // 可选:立即在终端显示提示 (需要 manager 暴露 terminalInstance) + if (manager.terminalInstance?.value) { + manager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`); + } else { + console.warn(`[WorkspaceView] 无法写入重连提示,terminalInstance 不可用。`); + } + // 调用 sessionStore 中现有的重连逻辑 + sessionStore.handleConnectRequest(session.connectionId); + } else { + // 否则,正常处理输入 + manager.handleTerminalData(data); + } + }; + // --- 编辑器操作处理 --- const handleCloseEditorTab = (tabId: string) => { const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式 @@ -199,8 +255,13 @@ onBeforeUnmount(() => { /> + + + + + - + @@ -217,7 +278,7 @@ onBeforeUnmount(() => { :session-id="tabInfo.sessionId" :is-active="tabInfo.sessionId === activeSessionId" @ready="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalReady" - @data="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalData" + @data="(data) => handleTerminalInput(tabInfo.sessionId, data)" @resize="(dims) => { console.log(`[工作区视图 ${tabInfo.sessionId}] 收到 resize 事件:`, dims); sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalResize(dims); }" /> @@ -352,7 +413,8 @@ onBeforeUnmount(() => { .file-editor-pane, /* 编辑器窗格样式 */ .file-manager-area-pane, /* 文件管理器区域 Pane */ .file-manager-pane, /* 内部文件管理器 Pane */ -.status-monitor-pane { /* 状态监视器样式 */ +.status-monitor-pane, /* 状态监视器样式 */ +.command-history-pane { /* 命令历史窗格样式 */ display: flex; /* 确保 flex 布局 */ flex-direction: column; /* 确保列方向 */ overflow: hidden; /* 默认隐藏溢出 */ @@ -365,7 +427,7 @@ onBeforeUnmount(() => { /* 命令栏 Pane 特定样式 - 恢复原样 */ .command-bar-pane { background-color: #e9ecef; /* 背景色 */ - justify-content: center; /* 垂直居中输入框 */ + /* justify-content: center; /* 垂直居中输入框 - 移除此行 */ overflow: hidden; /* 内容不应超出 */ display: flex; /* 确保 flex 布局 */ align-items: center; /* 垂直居中 */ @@ -375,8 +437,9 @@ onBeforeUnmount(() => { border: none; background-color: transparent; min-height: auto; - padding: 2px 10px; /* 恢复内边距 */ + padding: 2px 0; /* 移除水平内边距 */ flex-grow: 1; /* 让输入框填充 */ + width: 80%; /* 显式设置宽度为100% */ } .terminal-pane { @@ -418,6 +481,9 @@ onBeforeUnmount(() => { /* text-align: center; 由内部 wrapper 处理 */ /* padding: 1rem; 由内部 wrapper 处理 */ } +.command-history-pane { + background-color: #f8f9fa; /* 与其他侧边栏一致 */ +} .status-monitor-content-wrapper { text-align: center; padding: 1rem;