feat: 添加快捷指令功能

This commit is contained in:
Baobhan Sith
2025-04-17 13:20:27 +08:00
parent b62982faa0
commit 747c9491c4
14 changed files with 1114 additions and 11 deletions
+2
View File
@@ -17,6 +17,7 @@ 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 quickCommandsRoutes from './quick-commands/quick-commands.routes'; // 导入快捷指令路由
import { initializeWebSocket } from './websocket';
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件
@@ -104,6 +105,7 @@ 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.use('/api/v1/quick-commands', quickCommandsRoutes); // 挂载快捷指令相关的路由
// 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => {
+20
View File
@@ -139,6 +139,17 @@ CREATE TABLE IF NOT EXISTS command_history (
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
const createQuickCommandsTableSQL = `
CREATE TABLE IF NOT EXISTS quick_commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL, -- 名称可选
command TEXT NOT NULL, -- 指令必选
usage_count INTEGER NOT NULL DEFAULT 0, -- 使用频率
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
// --- 结束新增表结构定义 ---
@@ -271,6 +282,15 @@ export const runMigrations = async (db: Database): Promise<void> => {
});
});
// 创建 quick_commands 表
await new Promise<void>((resolve, reject) => {
db.run(createQuickCommandsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 quick_commands 表时出错: ${err.message}`));
console.log('Quick_Commands 表已检查/创建。');
resolve();
});
});
// --- 结束新增表创建逻辑 ---
@@ -0,0 +1,130 @@
import { Request, Response } from 'express';
import * as QuickCommandsService from '../services/quick-commands.service';
import { QuickCommandSortBy } from '../services/quick-commands.service';
/**
* 处理添加新快捷指令的请求
*/
export const addQuickCommand = async (req: Request, res: Response): Promise<void> => {
const { name, command } = req.body;
if (!command || typeof command !== 'string' || command.trim().length === 0) {
res.status(400).json({ message: '指令内容不能为空' });
return;
}
// 名称可以是 null 或 string
if (name !== null && typeof name !== 'string') {
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
try {
const newId = await QuickCommandsService.addQuickCommand(name, command);
res.status(201).json({ id: newId, message: '快捷指令已添加' });
} catch (error: any) {
console.error('添加快捷指令控制器出错:', error);
res.status(500).json({ message: error.message || '无法添加快捷指令' });
}
};
/**
* 处理获取所有快捷指令的请求 (支持排序)
*/
export const getAllQuickCommands = async (req: Request, res: Response): Promise<void> => {
const sortBy = req.query.sortBy as QuickCommandSortBy | undefined;
// 验证 sortBy 参数
const validSortBy: QuickCommandSortBy = (sortBy === 'name' || sortBy === 'usage_count') ? sortBy : 'name';
try {
const commands = await QuickCommandsService.getAllQuickCommands(validSortBy);
res.status(200).json(commands);
} catch (error: any) {
console.error('获取快捷指令控制器出错:', error);
res.status(500).json({ message: error.message || '无法获取快捷指令' });
}
};
/**
* 处理更新快捷指令的请求
*/
export const updateQuickCommand = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
const { name, command } = req.body;
if (isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
return;
}
if (!command || typeof command !== 'string' || command.trim().length === 0) {
res.status(400).json({ message: '指令内容不能为空' });
return;
}
if (name !== null && typeof name !== 'string') {
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
try {
const success = await QuickCommandsService.updateQuickCommand(id, name, command);
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 deleteQuickCommand = 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 QuickCommandsService.deleteQuickCommand(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 incrementUsage = 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 QuickCommandsService.incrementUsageCount(id);
if (success) {
res.status(200).json({ message: '使用次数已增加' });
} else {
// 即使没找到也可能返回成功,避免不必要的错误提示
console.warn(`尝试增加不存在的快捷指令 (ID: ${id}) 的使用次数`);
res.status(200).json({ message: '使用次数已记录 (或指令不存在)' });
// 或者严格一点返回 404:
// res.status(404).json({ message: '未找到要增加使用次数的快捷指令' });
}
} catch (error: any) {
console.error('增加快捷指令使用次数控制器出错:', error);
res.status(500).json({ message: error.message || '无法增加使用次数' });
}
};
@@ -0,0 +1,17 @@
import { Router } from 'express';
import * as QuickCommandsController from './quick-commands.controller';
import { isAuthenticated } from '../auth/auth.middleware'; // 引入认证中间件
const router = Router();
// 应用认证中间件到所有快捷指令相关的路由
router.use(isAuthenticated);
// 定义路由
router.post('/', QuickCommandsController.addQuickCommand); // POST /api/v1/quick-commands
router.get('/', QuickCommandsController.getAllQuickCommands); // GET /api/v1/quick-commands?sortBy=name|usage_count
router.put('/:id', QuickCommandsController.updateQuickCommand); // PUT /api/v1/quick-commands/:id
router.delete('/:id', QuickCommandsController.deleteQuickCommand); // DELETE /api/v1/quick-commands/:id
router.post('/:id/increment-usage', QuickCommandsController.incrementUsage); // POST /api/v1/quick-commands/:id/increment-usage
export default router;
@@ -0,0 +1,133 @@
import { getDb } from '../database';
// 定义快捷指令的接口
export interface QuickCommand {
id: number;
name: string | null; // 名称可选
command: string;
usage_count: number;
created_at: number; // Unix 时间戳 (秒)
updated_at: number; // Unix 时间戳 (秒)
}
/**
* 添加一条新的快捷指令
* @param name - 指令名称 (可选)
* @param command - 指令内容
* @returns 返回插入记录的 ID
*/
export const addQuickCommand = (name: string | null, command: string): Promise<number> => {
const db = getDb();
const sql = `INSERT INTO quick_commands (name, command, created_at, updated_at) VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
return new Promise((resolve, reject) => {
db.run(sql, [name, command], function (err) {
if (err) {
console.error('添加快捷指令时出错:', err);
return reject(new Error('无法添加快捷指令'));
}
resolve(this.lastID);
});
});
};
/**
* 更新指定的快捷指令
* @param id - 要更新的记录 ID
* @param name - 新的指令名称 (可选)
* @param command - 新的指令内容
* @returns 返回更新的行数 (通常是 1 或 0)
*/
export const updateQuickCommand = (id: number, name: string | null, command: string): Promise<number> => {
const db = getDb();
const sql = `UPDATE quick_commands SET name = ?, command = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [name, command, id], function (err) {
if (err) {
console.error('更新快捷指令时出错:', err);
return reject(new Error('无法更新快捷指令'));
}
resolve(this.changes);
});
});
};
/**
* 根据 ID 删除指定的快捷指令
* @param id - 要删除的记录 ID
* @returns 返回删除的行数 (通常是 1 或 0)
*/
export const deleteQuickCommand = (id: number): Promise<number> => {
const db = getDb();
const sql = `DELETE FROM quick_commands 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);
});
});
};
/**
* 获取所有快捷指令
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回包含所有快捷指令条目的数组
*/
export const getAllQuickCommands = (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommand[]> => {
const db = getDb();
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
if (sortBy === 'usage_count') {
orderByClause = 'ORDER BY usage_count DESC, name ASC'; // 按使用频率降序,同频率按名称升序
}
// SQLite 中 NULLS LAST/FIRST 的支持可能不一致,这里简单处理 NULL 名称排在前面
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands ${orderByClause}`;
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows: QuickCommand[]) => {
if (err) {
console.error('获取快捷指令时出错:', err);
return reject(new Error('无法获取快捷指令'));
}
resolve(rows);
});
});
};
/**
* 增加指定快捷指令的使用次数
* @param id - 要增加次数的记录 ID
* @returns 返回更新的行数 (通常是 1 或 0)
*/
export const incrementUsageCount = (id: number): Promise<number> => {
const db = getDb();
const sql = `UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime('%s', 'now') 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);
});
});
};
/**
* 根据 ID 查找快捷指令 (用于编辑前获取数据)
* @param id - 要查找的记录 ID
* @returns 返回找到的快捷指令条目,如果未找到则返回 undefined
*/
export const findQuickCommandById = (id: number): Promise<QuickCommand | undefined> => {
const db = getDb();
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands WHERE id = ?`;
return new Promise((resolve, reject) => {
db.get(sql, [id], (err, row: QuickCommand | undefined) => {
if (err) {
console.error('查找快捷指令时出错:', err);
return reject(new Error('无法查找快捷指令'));
}
resolve(row);
});
});
};
@@ -0,0 +1,74 @@
import * as QuickCommandsRepository from '../repositories/quick-commands.repository';
import { QuickCommand } from '../repositories/quick-commands.repository';
// 定义排序类型
export type QuickCommandSortBy = 'name' | 'usage_count';
/**
* 添加快捷指令
* @param name - 指令名称 (可选)
* @param command - 指令内容
* @returns 返回添加记录的 ID
*/
export const addQuickCommand = async (name: string | null, command: string): Promise<number> => {
if (!command || command.trim().length === 0) {
throw new Error('指令内容不能为空');
}
// 如果 name 是空字符串,则视为 null
const finalName = name && name.trim().length > 0 ? name.trim() : null;
return QuickCommandsRepository.addQuickCommand(finalName, command.trim());
};
/**
* 更新快捷指令
* @param id - 要更新的记录 ID
* @param name - 新的指令名称 (可选)
* @param command - 新的指令内容
* @returns 返回是否成功更新 (更新行数 > 0)
*/
export const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
if (!command || command.trim().length === 0) {
throw new Error('指令内容不能为空');
}
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const changes = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim());
return changes > 0;
};
/**
* 删除快捷指令
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (删除行数 > 0)
*/
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.deleteQuickCommand(id);
return changes > 0;
};
/**
* 获取所有快捷指令,并按指定方式排序
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回排序后的快捷指令数组
*/
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'): Promise<QuickCommand[]> => {
return QuickCommandsRepository.getAllQuickCommands(sortBy);
};
/**
* 增加快捷指令的使用次数
* @param id - 记录 ID
* @returns 返回是否成功更新 (更新行数 > 0)
*/
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.incrementUsageCount(id);
return changes > 0;
};
/**
* 根据 ID 获取单个快捷指令 (可能用于编辑)
* @param id - 记录 ID
* @returns 返回找到的快捷指令,或 undefined
*/
export const getQuickCommandById = async (id: number): Promise<QuickCommand | undefined> => {
return QuickCommandsRepository.findQuickCommandById(id);
};
@@ -0,0 +1,212 @@
<template>
<div class="modal-overlay" @click.self="closeForm">
<div class="modal-content">
<h2>{{ isEditing ? t('quickCommands.form.titleEdit', '编辑快捷指令') : t('quickCommands.form.titleAdd', '添加快捷指令') }}</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="qc-name">{{ t('quickCommands.form.name', '名称:') }}</label>
<input
id="qc-name"
type="text"
v-model="formData.name"
:placeholder="t('quickCommands.form.namePlaceholder', '可选,用于快速识别')"
/>
</div>
<div class="form-group">
<label for="qc-command">{{ t('quickCommands.form.command', '指令:') }} <span class="required">*</span></label>
<textarea
id="qc-command"
v-model="formData.command"
required
rows="3"
:placeholder="t('quickCommands.form.commandPlaceholder', '例如:ls -alh /home/user')"
></textarea>
<small v-if="commandError" class="error-message">{{ commandError }}</small>
</div>
<div class="form-actions">
<button type="button" @click="closeForm" class="cancel-btn">{{ t('common.cancel', '取消') }}</button>
<button type="submit" :disabled="isSubmitting || !!commandError" class="confirm-btn">
{{ isSubmitting ? t('common.saving', '保存中...') : (isEditing ? t('common.save', '保存') : t('quickCommands.form.add', '添加')) }}
</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
const props = defineProps<{
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象
}>();
const emit = defineEmits(['close']);
const { t } = useI18n();
const quickCommandsStore = useQuickCommandsStore();
const isSubmitting = ref(false);
const isEditing = computed(() => !!props.commandToEdit);
const formData = reactive({
name: '',
command: '',
});
const commandError = ref<string | null>(null);
// 监听指令内容变化,进行校验
watch(() => formData.command, (newCommand) => {
if (!newCommand || newCommand.trim().length === 0) {
commandError.value = t('quickCommands.form.errorCommandRequired', '指令内容不能为空');
} else {
commandError.value = null;
}
});
// 初始化表单数据 (如果是编辑模式)
onMounted(() => {
if (isEditing.value && props.commandToEdit) {
formData.name = props.commandToEdit.name ?? '';
formData.command = props.commandToEdit.command;
}
});
const handleSubmit = async () => {
if (commandError.value) return; // 如果校验失败则不提交
isSubmitting.value = true;
let success = false;
// 处理名称,空字符串视为 null
const finalName = formData.name.trim().length > 0 ? formData.name.trim() : null;
if (isEditing.value && props.commandToEdit) {
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim());
} else {
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim());
}
isSubmitting.value = false;
if (success) {
closeForm();
}
};
const closeForm = () => {
emit('close');
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1050; /* 比其他 UI 高 */
}
.modal-content {
background-color: #ffffff; /* 强制设置不透明白色背景 */
padding: 2rem;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 500px;
}
h2 {
margin: 0 0 1.5rem 0; /* 确保顶部 margin 为 0 */
color: #333; /* 使用具体的颜色值 */
text-align: center;
font-size: 1.4rem; /* 调整字体大小 */
font-weight: 500; /* 调整字重 */
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: var(--color-text-secondary);
}
input[type="text"],
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background-color: var(--color-input-bg);
color: var(--color-text);
box-sizing: border-box; /* 确保 padding 不会撑大元素 */
font-family: inherit;
font-size: 1rem;
}
textarea {
resize: vertical; /* 允许垂直调整大小 */
min-height: 80px;
}
.required {
color: var(--color-danger);
margin-left: 0.2rem;
}
.error-message {
color: var(--color-danger);
font-size: 0.85em;
margin-top: 0.3rem;
display: block;
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 1.5rem;
}
.cancel-btn,
.confirm-btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s ease;
}
.cancel-btn {
background-color: var(--color-bg-secondary);
color: var(--color-text);
margin-right: 0.5rem;
}
.cancel-btn:hover {
background-color: var(--color-bg-tertiary);
}
.confirm-btn {
background-color: var(--color-primary);
color: white;
}
.confirm-btn:hover:not(:disabled) {
background-color: var(--color-primary-dark);
}
.confirm-btn:disabled {
background-color: var(--color-disabled);
cursor: not-allowed;
}
</style>
@@ -529,7 +529,7 @@ onMounted(() => {
watchEffect((onCleanup) => {
let unregisterSuccess: (() => void) | undefined;
let unregisterError: (() => void) | undefined;
let timeoutId: number | undefined;
let timeoutId: NodeJS.Timeout | undefined; // 修正类型为 NodeJS.Timeout
const cleanupListeners = () => {
unregisterSuccess?.();
@@ -71,7 +71,8 @@ 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', '命令历史'), // 添加命令历史标签
commandHistory: t('layout.pane.commandHistory', '命令历史'),
quickCommands: t('layout.pane.quickCommands', '快捷指令'), // 添加快捷指令标签
};
// 获取所有可控制的面板名称
+21
View File
@@ -573,5 +573,26 @@
"confirmClear": "确定要清空所有历史记录吗?",
"copied": "已复制到剪贴板",
"copyFailed": "复制失败"
},
"quickCommands": {
"title": "快捷指令",
"searchPlaceholder": "搜索名称或指令...",
"add": "添加",
"sortBy": "排序:",
"sortByName": "名称",
"sortByUsage": "使用频率",
"usageCount": "使用次数",
"empty": "没有快捷指令。点击“添加”按钮创建一个吧!",
"confirmDelete": "确定要删除快捷指令 \"{name}\" 吗?",
"form": {
"titleAdd": "添加快捷指令",
"titleEdit": "编辑快捷指令",
"name": "名称:",
"namePlaceholder": "可选,用于快速识别",
"command": "指令:",
"commandPlaceholder": "例如:ls -alh /home/user",
"errorCommandRequired": "指令内容不能为空",
"add": "添加"
}
}
}
+4 -3
View File
@@ -1,8 +1,8 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
// 定义面板名称的类型,方便管理和引用 (添加 commandHistory)
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory';
// 定义面板名称的类型,方便管理和引用 (添加 quickCommands)
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands';
// 定义 Store
export const useLayoutStore = defineStore('layout', () => {
@@ -14,7 +14,8 @@ export const useLayoutStore = defineStore('layout', () => {
fileManager: true,
editor: true,
statusMonitor: true,
commandHistory: true, // 默认可见
commandHistory: true,
quickCommands: true, // 默认可见
});
// Action: 切换指定面板的可见性
@@ -0,0 +1,169 @@
import { defineStore } from 'pinia';
import axios from 'axios';
import { ref, computed } from 'vue';
import { useUiNotificationsStore } from './uiNotifications.store';
import type { QuickCommand } from '../../../backend/src/repositories/quick-commands.repository'; // 复用后端类型定义
// 定义前端使用的快捷指令接口 (可以与后端一致)
export type QuickCommandFE = QuickCommand;
// 定义排序类型
export type QuickCommandSortByType = 'name' | 'usage_count';
export const useQuickCommandsStore = defineStore('quickCommands', () => {
const quickCommandsList = ref<QuickCommandFE[]>([]);
const searchTerm = ref('');
const sortBy = ref<QuickCommandSortByType>('name'); // 默认按名称排序
const isLoading = ref(false);
const error = ref<string | null>(null);
const uiNotificationsStore = useUiNotificationsStore();
// --- Getters ---
// 计算属性:根据搜索词过滤和排序指令
const filteredAndSortedCommands = computed(() => {
const term = searchTerm.value.toLowerCase().trim();
let filtered = quickCommandsList.value;
if (term) {
filtered = filtered.filter(cmd =>
(cmd.name && cmd.name.toLowerCase().includes(term)) ||
cmd.command.toLowerCase().includes(term)
);
}
// Pinia store getter 中直接排序可能不是最佳实践,但这里为了简单起见先这样实现
// 更好的方式可能是在 fetch 时就按需排序,或者在组件层排序
// 注意:这里直接修改 ref 数组的顺序,如果需要在多处使用不同排序,需要创建副本
// return [...filtered].sort((a, b) => {
// if (sortBy.value === 'usage_count') {
// // 按使用次数降序,次数相同按名称升序
// if (b.usage_count !== a.usage_count) {
// return b.usage_count - a.usage_count;
// }
// }
// // 默认或次数相同时按名称升序 (null 名称排在前面)
// const nameA = a.name ?? '';
// const nameB = b.name ?? '';
// return nameA.localeCompare(nameB);
// });
// **修正:Getter 不应修改原始数组,返回过滤后的即可,排序由 fetch 控制**
return filtered;
});
// --- Actions ---
// 从后端获取快捷指令 (带排序)
const fetchQuickCommands = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await axios.get<QuickCommandFE[]>('/api/v1/quick-commands', {
params: { sortBy: sortBy.value } // 将排序参数传递给后端
});
quickCommandsList.value = response.data;
} catch (err: any) {
console.error('获取快捷指令失败:', err);
error.value = err.response?.data?.message || '获取快捷指令时发生错误';
uiNotificationsStore.showError(error.value ?? '未知错误');
} finally {
isLoading.value = false;
}
};
// 添加快捷指令
const addQuickCommand = async (name: string | null, command: string): Promise<boolean> => {
try {
await axios.post('/api/v1/quick-commands', { name, command });
await fetchQuickCommands(); // 添加成功后刷新列表
uiNotificationsStore.showSuccess('快捷指令已添加');
return true;
} catch (err: any) {
console.error('添加快捷指令失败:', err);
const message = err.response?.data?.message || '添加快捷指令时发生错误';
uiNotificationsStore.showError(message);
return false;
}
};
// 更新快捷指令
const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
try {
await axios.put(`/api/v1/quick-commands/${id}`, { name, command });
await fetchQuickCommands(); // 更新成功后刷新列表
uiNotificationsStore.showSuccess('快捷指令已更新');
return true;
} catch (err: any) {
console.error('更新快捷指令失败:', err);
const message = err.response?.data?.message || '更新快捷指令时发生错误';
uiNotificationsStore.showError(message);
return false;
}
};
// 删除快捷指令
const deleteQuickCommand = async (id: number) => {
try {
await axios.delete(`/api/v1/quick-commands/${id}`);
// 从本地列表中移除,避免重新请求
const index = quickCommandsList.value.findIndex(cmd => cmd.id === id);
if (index !== -1) {
quickCommandsList.value.splice(index, 1);
}
uiNotificationsStore.showSuccess('快捷指令已删除');
} catch (err: any) {
console.error('删除快捷指令失败:', err);
const message = err.response?.data?.message || '删除快捷指令时发生错误';
uiNotificationsStore.showError(message);
}
};
// 增加使用次数 (调用 API,然后更新本地数据)
const incrementUsage = async (id: number) => {
try {
await axios.post(`/api/v1/quick-commands/${id}/increment-usage`);
// 更新本地计数,避免重新请求整个列表
const command = quickCommandsList.value.find(cmd => cmd.id === id);
if (command) {
command.usage_count += 1;
// 如果当前是按使用次数排序,可能需要重新排序或刷新列表
if (sortBy.value === 'usage_count') {
// 简单起见,重新获取并排序
await fetchQuickCommands();
}
}
} catch (err: any) {
console.error('增加使用次数失败:', err);
// 这里可以选择不提示用户错误,因为这是一个后台操作
}
};
// 设置搜索词
const setSearchTerm = (term: string) => {
searchTerm.value = term;
};
// 设置排序方式并重新获取数据
const setSortBy = async (newSortBy: QuickCommandSortByType) => {
if (sortBy.value !== newSortBy) {
sortBy.value = newSortBy;
await fetchQuickCommands(); // 排序方式改变,重新获取数据
}
};
return {
quickCommandsList,
searchTerm,
sortBy,
isLoading,
error,
filteredAndSortedCommands, // 使用计算属性
fetchQuickCommands,
addQuickCommand,
updateQuickCommand,
deleteQuickCommand,
incrementUsage,
setSearchTerm,
setSortBy,
};
});
@@ -0,0 +1,313 @@
<template>
<div class="quick-commands-view">
<div class="view-header">
<input
type="text"
:placeholder="$t('quickCommands.searchPlaceholder', '搜索名称或指令...')"
:value="searchTerm"
@input="updateSearchTerm($event)"
class="search-input"
/>
<div class="sort-controls">
<label for="sort-select">{{ t('quickCommands.sortBy', '排序:') }}</label>
<select id="sort-select" :value="sortBy" @change="updateSortBy($event)">
<option value="name">{{ t('quickCommands.sortByName', '名称') }}</option>
<option value="usage_count">{{ t('quickCommands.sortByUsage', '使用频率') }}</option>
</select>
</div>
<button @click="openAddForm" class="add-button" :title="$t('quickCommands.add', '添加快捷指令')">
<i class="fas fa-plus"></i> {{ t('quickCommands.add', '添加') }}
</button>
</div>
<div class="commands-list-container">
<ul v-if="filteredAndSortedCommands.length > 0" class="commands-list">
<li
v-for="cmd in filteredAndSortedCommands"
:key="cmd.id"
class="command-item"
@mouseover="hoveredItemId = cmd.id"
@mouseleave="hoveredItemId = null"
@click="executeCommand(cmd)"
>
<div class="command-info">
<span v-if="cmd.name" class="command-name">{{ cmd.name }}</span>
<span class="command-text" :class="{ 'full-width': !cmd.name }">{{ cmd.command }}</span>
</div>
<div class="item-actions" v-show="hoveredItemId === cmd.id">
<span class="usage-count" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
<button @click.stop="openEditForm(cmd)" class="action-button edit" :title="$t('common.edit', '编辑')">
<i class="fas fa-edit"></i>
</button>
<button @click.stop="confirmDelete(cmd)" class="action-button delete" :title="$t('common.delete', '删除')">
<i class="fas fa-times"></i>
</button>
</div>
</li>
</ul>
<div v-else-if="isLoading" class="loading-message">
{{ t('common.loading', '加载中...') }}
</div>
<div v-else class="empty-message">
{{ $t('quickCommands.empty', '没有快捷指令。点击“添加”按钮创建一个吧!') }}
</div>
</div>
<!-- 添加/编辑表单模态框 -->
<AddEditQuickCommandForm
v-if="isFormVisible"
:command-to-edit="commandToEdit"
@close="closeForm"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useQuickCommandsStore, type QuickCommandFE, type QuickCommandSortByType } from '../stores/quickCommands.store';
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
import { useI18n } from 'vue-i18n';
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; //
const quickCommandsStore = useQuickCommandsStore();
const uiNotificationsStore = useUiNotificationsStore(); //
const { t } = useI18n();
const hoveredItemId = ref<number | null>(null);
const isFormVisible = ref(false);
const commandToEdit = ref<QuickCommandFE | null>(null);
// --- Store Getter ---
const searchTerm = computed(() => quickCommandsStore.searchTerm);
const sortBy = computed(() => quickCommandsStore.sortBy);
const filteredAndSortedCommands = computed(() => quickCommandsStore.filteredAndSortedCommands);
const isLoading = computed(() => quickCommandsStore.isLoading);
// --- ---
const emit = defineEmits<{
(e: 'execute-command', command: string): void; // WorkspaceView
}>();
// --- ---
onMounted(() => {
quickCommandsStore.fetchQuickCommands(); //
});
// --- ---
const updateSearchTerm = (event: Event) => {
const target = event.target as HTMLInputElement;
quickCommandsStore.setSearchTerm(target.value);
};
const updateSortBy = (event: Event) => {
const target = event.target as HTMLSelectElement;
const newSortBy = target.value as QuickCommandSortByType;
quickCommandsStore.setSortBy(newSortBy);
};
const openAddForm = () => {
commandToEdit.value = null;
isFormVisible.value = true;
};
const openEditForm = (command: QuickCommandFE) => {
commandToEdit.value = command;
isFormVisible.value = true;
};
const closeForm = () => {
isFormVisible.value = false;
commandToEdit.value = null;
};
const confirmDelete = (command: QuickCommandFE) => {
if (window.confirm(t('quickCommands.confirmDelete', { name: command.name || command.command }))) {
quickCommandsStore.deleteQuickCommand(command.id);
}
};
//
const executeCommand = (command: QuickCommandFE) => {
// 1. 使 ()
quickCommandsStore.incrementUsage(command.id);
// 2.
emit('execute-command', command.command);
};
</script>
<style scoped>
.quick-commands-view {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: var(--color-bg-secondary);
}
.view-header {
display: flex;
align-items: center;
padding: 8px 12px;
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: 12px;
font-size: 0.9em;
}
.sort-controls {
display: flex;
align-items: center;
margin-right: 12px;
}
.sort-controls label {
margin-right: 6px;
font-size: 0.9em;
color: var(--color-text-secondary);
}
.sort-controls select {
padding: 5px 8px;
border: 1px solid var(--color-border);
border-radius: 3px;
background-color: var(--color-input-bg);
color: var(--color-text);
font-size: 0.9em;
}
.add-button {
padding: 6px 12px;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.9em;
display: flex;
align-items: center;
}
.add-button i {
margin-right: 4px;
}
.add-button:hover {
background-color: var(--color-primary-dark);
}
.commands-list-container {
flex-grow: 1;
overflow-y: auto;
}
.commands-list {
list-style: none;
padding: 0;
margin: 0;
}
.command-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.2s ease;
}
.command-item:last-child {
border-bottom: none;
}
.command-item:hover {
background-color: var(--color-bg-hover);
}
.command-info {
display: flex;
flex-direction: column; /* 名称和指令垂直排列 */
overflow: hidden; /* 防止内容溢出 */
margin-right: 10px;
flex-grow: 1;
}
.command-name {
font-weight: bold;
color: var(--color-text);
font-size: 0.95em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px; /* 名称和指令间距 */
}
.command-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: var(--font-family-mono);
font-size: 0.85em;
color: var(--color-text-secondary);
}
.command-text.full-width { /* 当没有名称时,指令占据全部空间 */
font-size: 0.9em; /* 可以稍微大一点 */
color: var(--color-text); /* 颜色也可以更深 */
}
.item-actions {
display: flex;
align-items: center;
flex-shrink: 0;
}
.usage-count {
font-size: 0.8em;
color: var(--color-text-muted);
margin-right: 8px;
background-color: var(--color-bg-tertiary);
padding: 2px 5px;
border-radius: 3px;
min-width: 18px; /* 保证宽度 */
text-align: center;
}
.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.edit:hover {
color: var(--color-warning); /* 编辑按钮用警告色 */
}
.action-button.delete:hover {
color: var(--color-danger);
}
.loading-message,
.empty-message {
padding: 20px;
text-align: center;
color: var(--color-text-secondary);
}
</style>
+16 -6
View File
@@ -11,6 +11,7 @@ import TerminalTabBar from '../components/TerminalTabBar.vue';
import CommandInputBar from '../components/CommandInputBar.vue';
import FileEditorContainer from '../components/FileEditorContainer.vue'; //
import CommandHistoryView from './CommandHistoryView.vue'; //
import QuickCommandsView from './QuickCommandsView.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
@@ -124,10 +125,10 @@ onBeforeUnmount(() => {
//
if (terminalManager && typeof terminalManager.sendData === 'function') {
const commandToSend = 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 + '\r');
//
if (commandToSend.length > 0) {
@@ -257,11 +258,16 @@ onBeforeUnmount(() => {
<!-- 新增命令历史 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 -->
<CommandHistoryView class="pane-content" @execute-command="handleSendCommand" />
</pane>
<!-- 新增快捷指令 Pane -->
<pane v-if="paneVisibility.quickCommands" size="15" min-size="10" class="sidebar-pane quick-commands-pane">
<QuickCommandsView class="pane-content" @execute-command="handleSendCommand" /> <!-- 监听事件 -->
</pane>
<!-- 2. 中间区域 Pane (终端/命令栏/文件管理器) - 这个 Pane 本身通常保持可见内部 Pane 才切换 -->
<pane size="40" min-size="30" class="middle-pane"> <!-- 调整中间区域大小 -->
<pane size="30" min-size="20" class="middle-pane"> <!-- 再次调整中间区域大小 -->
<!-- 上下分割 (终端 | 命令栏 | 文件管理器) -->
<splitpanes :horizontal="true" style="height: 100%" :dbl-click-splitter="false">
<!-- 上方 Pane (终端) -->
@@ -414,7 +420,8 @@ onBeforeUnmount(() => {
.file-manager-area-pane, /* 文件管理器区域 Pane */
.file-manager-pane, /* 内部文件管理器 Pane */
.status-monitor-pane, /* 状态监视器样式 */
.command-history-pane { /* 命令历史窗格样式 */
.command-history-pane, /* 命令历史窗格样式 */
.quick-commands-pane { /* 快捷指令窗格样式 */
display: flex; /* 确保 flex 布局 */
flex-direction: column; /* 确保列方向 */
overflow: hidden; /* 默认隐藏溢出 */
@@ -484,6 +491,9 @@ onBeforeUnmount(() => {
.command-history-pane {
background-color: #f8f9fa; /* 与其他侧边栏一致 */
}
.quick-commands-pane {
background-color: #f8f9fa; /* 与其他侧边栏一致 */
}
.status-monitor-content-wrapper {
text-align: center;
padding: 1rem;