feat: 添加历史命令功能
This commit is contained in:
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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 || '无法清空命令历史记录' });
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<void> => {
|
||||
});
|
||||
});
|
||||
|
||||
// 创建 command_history 表
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(createCommandHistoryTableSQL, (err: Error | null) => {
|
||||
if (err) return reject(new Error(`创建 command_history 表时出错: ${err.message}`));
|
||||
console.log('Command_History 表已检查/创建。');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// --- 结束新增表创建逻辑 ---
|
||||
|
||||
|
||||
|
||||
@@ -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<number> => {
|
||||
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<CommandHistoryEntry[]> => {
|
||||
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<number> => {
|
||||
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<number> => {
|
||||
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); // 返回受影响的行数
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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<number> => {
|
||||
// 可以在这里添加额外的业务逻辑,例如校验命令格式、长度限制等
|
||||
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<CommandHistoryEntry[]> => {
|
||||
return CommandHistoryRepository.getAllCommands();
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 删除一条命令历史记录
|
||||
* @param id - 要删除的记录 ID
|
||||
* @returns 返回是否成功删除 (删除行数 > 0)
|
||||
*/
|
||||
export const deleteCommandHistoryById = async (id: number): Promise<boolean> => {
|
||||
const changes = await CommandHistoryRepository.deleteCommandById(id);
|
||||
return changes > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空所有命令历史记录
|
||||
* @returns 返回删除的记录条数
|
||||
*/
|
||||
export const clearAllCommandHistory = async (): Promise<number> => {
|
||||
return CommandHistoryRepository.clearAllCommands();
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="command-history-menu">
|
||||
<div class="history-header">
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('commandHistory.searchPlaceholder', '搜索历史记录...')"
|
||||
:value="searchTerm"
|
||||
@input="updateSearchTerm($event)"
|
||||
class="search-input"
|
||||
/>
|
||||
<button @click="confirmClearAll" class="clear-button" :title="$t('commandHistory.clear', '清空')">
|
||||
<i class="fas fa-trash-alt"></i> <!-- 假设使用 Font Awesome -->
|
||||
</button>
|
||||
</div>
|
||||
<div class="history-list-container" ref="listContainer">
|
||||
<ul v-if="filteredHistory.length > 0" class="history-list">
|
||||
<li
|
||||
v-for="entry in filteredHistory"
|
||||
:key="entry.id"
|
||||
class="history-item"
|
||||
@mouseover="hoveredItemId = entry.id"
|
||||
@mouseleave="hoveredItemId = null"
|
||||
@click="selectCommand(entry.command)"
|
||||
>
|
||||
<span class="command-text">{{ entry.command }}</span>
|
||||
<div class="item-actions" v-show="hoveredItemId === entry.id">
|
||||
<button @click.stop="copyCommand(entry.command)" class="action-button" :title="$t('commandHistory.copy', '复制')">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button @click.stop="deleteSingleCommand(entry.id)" class="action-button delete" :title="$t('commandHistory.delete', '删除')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="isLoading" class="loading-message">
|
||||
{{ $t('commandHistory.loading', '加载中...') }}
|
||||
</div>
|
||||
<div v-else class="empty-message">
|
||||
{{ $t('commandHistory.empty', '没有历史记录') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 引入 UI 通知 store
|
||||
import { useI18n } from 'vue-i18n'; // 引入 i18n
|
||||
|
||||
const commandHistoryStore = useCommandHistoryStore();
|
||||
const uiNotificationsStore = useUiNotificationsStore(); // 实例化 UI 通知 store
|
||||
const { t } = useI18n(); // 使用 i18n
|
||||
const hoveredItemId = ref<number | null>(null);
|
||||
const listContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
// --- 从 Store 获取状态和 Getter ---
|
||||
const searchTerm = computed(() => commandHistoryStore.searchTerm);
|
||||
const filteredHistory = computed(() => commandHistoryStore.filteredHistory);
|
||||
const isLoading = computed(() => commandHistoryStore.isLoading);
|
||||
|
||||
// --- 事件定义 ---
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-command', command: string): void;
|
||||
}>();
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
commandHistoryStore.fetchHistory(); // 组件挂载时获取历史记录
|
||||
});
|
||||
|
||||
// --- 事件处理 ---
|
||||
|
||||
// 更新搜索词 (防抖可以后续优化)
|
||||
const updateSearchTerm = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
commandHistoryStore.setSearchTerm(target.value);
|
||||
};
|
||||
|
||||
// 确认清空所有历史记录
|
||||
const confirmClearAll = () => {
|
||||
// 使用浏览器的 confirm 对话框进行确认
|
||||
if (window.confirm(t('commandHistory.confirmClear', '确定要清空所有历史记录吗?'))) {
|
||||
commandHistoryStore.clearAllHistory();
|
||||
}
|
||||
};
|
||||
|
||||
// 复制命令到剪贴板
|
||||
const copyCommand = async (command: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
// 可以选择性地显示一个复制成功的提示
|
||||
uiNotificationsStore.showSuccess(t('commandHistory.copied', '已复制到剪贴板')); // 使用独立的 uiNotificationsStore
|
||||
} catch (err) {
|
||||
console.error('复制命令失败:', err);
|
||||
uiNotificationsStore.showError(t('commandHistory.copyFailed', '复制失败')); // 使用独立的 uiNotificationsStore
|
||||
}
|
||||
};
|
||||
|
||||
// 删除单条历史记录
|
||||
const deleteSingleCommand = (id: number) => {
|
||||
// 可以选择性地添加确认对话框
|
||||
commandHistoryStore.deleteCommand(id);
|
||||
};
|
||||
|
||||
// 选中命令 (通知父组件)
|
||||
const selectCommand = (command: string) => {
|
||||
emit('select-command', command);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.command-history-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-bg-secondary); /* 使用 CSS 变量 */
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex-grow: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-input-bg);
|
||||
color: var(--color-text);
|
||||
margin-right: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
}
|
||||
.clear-button:hover {
|
||||
color: var(--color-danger); /* 悬浮时变红 */
|
||||
}
|
||||
|
||||
.history-list-container {
|
||||
max-height: 300px; /* 限制最大高度 */
|
||||
overflow-y: auto; /* 超出时显示滚动条 */
|
||||
}
|
||||
|
||||
.history-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.command-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* 超长时显示省略号 */
|
||||
margin-right: 10px; /* 给右侧按钮留出空间 */
|
||||
flex-grow: 1; /* 占据剩余空间 */
|
||||
font-family: var(--font-family-mono); /* 使用等宽字体 */
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* 防止按钮被压缩 */
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
margin-left: 6px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.action-button.delete:hover {
|
||||
color: var(--color-danger); /* 删除按钮悬浮时变红 */
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.empty-message {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -71,6 +71,7 @@ const paneLabels: Record<PaneName, string> = {
|
||||
fileManager: t('layout.pane.fileManager'),
|
||||
editor: t('layout.pane.editor'),
|
||||
statusMonitor: t('layout.pane.statusMonitor'),
|
||||
commandHistory: t('layout.pane.commandHistory', '命令历史'), // 添加命令历史标签
|
||||
};
|
||||
|
||||
// 获取所有可控制的面板名称
|
||||
|
||||
@@ -23,6 +23,7 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
|
||||
|
||||
const terminalInstance = ref<Terminal | null>(null);
|
||||
const terminalOutputBuffer = ref<string[]>([]); // 缓冲 WebSocket 消息直到终端准备好
|
||||
const isSshConnected = ref(false); // 新增:跟踪 SSH 连接状态
|
||||
|
||||
// 辅助函数:获取终端消息文本
|
||||
const getTerminalText = (key: string, params?: Record<string, any>): 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 可以写入提示信息
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": "复制失败"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CommandHistoryEntryFE[]>([]);
|
||||
const searchTerm = ref('');
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(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<CommandHistoryEntryBE[]>('/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,
|
||||
};
|
||||
});
|
||||
@@ -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<Record<PaneName, boolean>>({
|
||||
connections: true,
|
||||
terminal: true,
|
||||
commandBar: true, // 恢复
|
||||
commandBar: true,
|
||||
fileManager: true,
|
||||
editor: true,
|
||||
statusMonitor: true,
|
||||
commandHistory: true, // 默认可见
|
||||
});
|
||||
|
||||
// Action: 切换指定面板的可见性
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div class="command-history-view">
|
||||
<!-- 移除 PaneTitleBar -->
|
||||
<div class="history-controls">
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('commandHistory.searchPlaceholder', '搜索历史记录...')"
|
||||
:value="searchTerm"
|
||||
@input="updateSearchTerm($event)"
|
||||
class="search-input"
|
||||
/>
|
||||
<button @click="confirmClearAll" class="clear-button" :title="$t('commandHistory.clear', '清空')">
|
||||
<i class="fas fa-trash-alt"></i> <!-- 假设使用 Font Awesome -->
|
||||
</button>
|
||||
</div>
|
||||
<div class="history-list-container">
|
||||
<ul v-if="filteredHistory.length > 0" class="history-list">
|
||||
<li
|
||||
v-for="entry in filteredHistory"
|
||||
:key="entry.id"
|
||||
class="history-item"
|
||||
@mouseover="hoveredItemId = entry.id"
|
||||
@mouseleave="hoveredItemId = null"
|
||||
@click="executeCommand(entry.command)"
|
||||
>
|
||||
<span class="command-text">{{ entry.command }}</span>
|
||||
<div class="item-actions" v-show="hoveredItemId === entry.id">
|
||||
<button @click.stop="copyCommand(entry.command)" class="action-button" :title="$t('commandHistory.copy', '复制')">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button @click.stop="deleteSingleCommand(entry.id)" class="action-button delete" :title="$t('commandHistory.delete', '删除')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="isLoading" class="loading-message">
|
||||
{{ $t('commandHistory.loading', '加载中...') }}
|
||||
</div>
|
||||
<div v-else class="empty-message">
|
||||
{{ $t('commandHistory.empty', '没有历史记录') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import PaneTitleBar from '../components/PaneTitleBar.vue'; // 导入标题栏
|
||||
|
||||
const commandHistoryStore = useCommandHistoryStore();
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const { t } = useI18n();
|
||||
const hoveredItemId = ref<number | null>(null);
|
||||
|
||||
// --- 从 Store 获取状态和 Getter ---
|
||||
const searchTerm = computed(() => commandHistoryStore.searchTerm);
|
||||
// 使用 store 的 filteredHistory getter
|
||||
const filteredHistory = computed(() => commandHistoryStore.filteredHistory);
|
||||
const isLoading = computed(() => commandHistoryStore.isLoading);
|
||||
|
||||
// --- 事件定义 ---
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'execute-command', command: string): void; // 定义新事件
|
||||
}>();
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
// 视图挂载时获取历史记录 (如果 store 中还没有的话)
|
||||
if (commandHistoryStore.historyList.length === 0) {
|
||||
commandHistoryStore.fetchHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// --- 事件处理 ---
|
||||
|
||||
// 更新搜索词
|
||||
const updateSearchTerm = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
commandHistoryStore.setSearchTerm(target.value);
|
||||
};
|
||||
|
||||
// 确认清空所有历史记录
|
||||
const confirmClearAll = () => {
|
||||
if (window.confirm(t('commandHistory.confirmClear', '确定要清空所有历史记录吗?'))) {
|
||||
commandHistoryStore.clearAllHistory();
|
||||
}
|
||||
};
|
||||
|
||||
// 复制命令到剪贴板
|
||||
const copyCommand = async (command: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
uiNotificationsStore.showSuccess(t('commandHistory.copied', '已复制到剪贴板'));
|
||||
} catch (err) {
|
||||
console.error('复制命令失败:', err);
|
||||
uiNotificationsStore.showError(t('commandHistory.copyFailed', '复制失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除单条历史记录
|
||||
const deleteSingleCommand = (id: number) => {
|
||||
commandHistoryStore.deleteCommand(id);
|
||||
};
|
||||
|
||||
// 新增:执行命令 (发出事件)
|
||||
const executeCommand = (command: string) => {
|
||||
emit('execute-command', command);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.command-history-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; /* 填充父 Pane 高度 */
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.history-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-tertiary);
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex-grow: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-input-bg);
|
||||
color: var(--color-text);
|
||||
margin-right: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
}
|
||||
.clear-button:hover {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.history-list-container {
|
||||
flex-grow: 1; /* 占据剩余空间 */
|
||||
overflow-y: auto; /* 超出时显示滚动条 */
|
||||
}
|
||||
|
||||
.history-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: default; /* 列表项本身不可点击 */
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.command-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 10px;
|
||||
flex-grow: 1;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
margin-left: 6px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.action-button.delete:hover {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.empty-message {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -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(() => {
|
||||
/>
|
||||
</pane>
|
||||
|
||||
<!-- 新增:命令历史 Pane -->
|
||||
<pane v-if="paneVisibility.commandHistory" size="15" min-size="10" class="sidebar-pane command-history-pane">
|
||||
<CommandHistoryView class="pane-content" @execute-command="handleSendCommand" /> <!-- 监听事件并调用 handleSendCommand -->
|
||||
</pane>
|
||||
|
||||
<!-- 2. 中间区域 Pane (终端/命令栏/文件管理器) - 这个 Pane 本身通常保持可见,内部 Pane 才切换 -->
|
||||
<pane size="50" min-size="30" class="middle-pane">
|
||||
<pane size="40" min-size="30" class="middle-pane"> <!-- 调整中间区域大小 -->
|
||||
<!-- 上下分割 (终端 | 命令栏 | 文件管理器) -->
|
||||
<splitpanes :horizontal="true" style="height: 100%" :dbl-click-splitter="false">
|
||||
<!-- 上方 Pane (终端) -->
|
||||
@@ -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); }"
|
||||
/>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user