feat: 添加快捷指令功能
This commit is contained in:
@@ -17,6 +17,7 @@ import settingsRoutes from './settings/settings.routes'; // 导入设置路由
|
|||||||
import notificationRoutes from './notifications/notification.routes'; // 导入通知路由
|
import notificationRoutes from './notifications/notification.routes'; // 导入通知路由
|
||||||
import auditRoutes from './audit/audit.routes'; // 导入审计路由
|
import auditRoutes from './audit/audit.routes'; // 导入审计路由
|
||||||
import commandHistoryRoutes from './command-history/command-history.routes'; // 导入命令历史记录路由
|
import commandHistoryRoutes from './command-history/command-history.routes'; // 导入命令历史记录路由
|
||||||
|
import quickCommandsRoutes from './quick-commands/quick-commands.routes'; // 导入快捷指令路由
|
||||||
import { initializeWebSocket } from './websocket';
|
import { initializeWebSocket } from './websocket';
|
||||||
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件
|
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/notifications', notificationRoutes); // 挂载通知相关的路由
|
||||||
app.use('/api/v1/audit-logs', auditRoutes); // 挂载审计日志相关的路由
|
app.use('/api/v1/audit-logs', auditRoutes); // 挂载审计日志相关的路由
|
||||||
app.use('/api/v1/command-history', commandHistoryRoutes); // 挂载命令历史记录相关的路由
|
app.use('/api/v1/command-history', commandHistoryRoutes); // 挂载命令历史记录相关的路由
|
||||||
|
app.use('/api/v1/quick-commands', quickCommandsRoutes); // 挂载快捷指令相关的路由
|
||||||
|
|
||||||
// 状态检查接口
|
// 状态检查接口
|
||||||
app.get('/api/v1/status', (req: Request, res: Response) => {
|
app.get('/api/v1/status', (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -139,6 +139,17 @@ CREATE TABLE IF NOT EXISTS command_history (
|
|||||||
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
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) => {
|
watchEffect((onCleanup) => {
|
||||||
let unregisterSuccess: (() => void) | undefined;
|
let unregisterSuccess: (() => void) | undefined;
|
||||||
let unregisterError: (() => void) | undefined;
|
let unregisterError: (() => void) | undefined;
|
||||||
let timeoutId: number | undefined;
|
let timeoutId: NodeJS.Timeout | undefined; // 修正类型为 NodeJS.Timeout
|
||||||
|
|
||||||
const cleanupListeners = () => {
|
const cleanupListeners = () => {
|
||||||
unregisterSuccess?.();
|
unregisterSuccess?.();
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ const paneLabels: Record<PaneName, string> = {
|
|||||||
fileManager: t('layout.pane.fileManager'),
|
fileManager: t('layout.pane.fileManager'),
|
||||||
editor: t('layout.pane.editor'),
|
editor: t('layout.pane.editor'),
|
||||||
statusMonitor: t('layout.pane.statusMonitor'),
|
statusMonitor: t('layout.pane.statusMonitor'),
|
||||||
commandHistory: t('layout.pane.commandHistory', '命令历史'), // 添加命令历史标签
|
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
||||||
|
quickCommands: t('layout.pane.quickCommands', '快捷指令'), // 添加快捷指令标签
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取所有可控制的面板名称
|
// 获取所有可控制的面板名称
|
||||||
|
|||||||
@@ -573,5 +573,26 @@
|
|||||||
"confirmClear": "确定要清空所有历史记录吗?",
|
"confirmClear": "确定要清空所有历史记录吗?",
|
||||||
"copied": "已复制到剪贴板",
|
"copied": "已复制到剪贴板",
|
||||||
"copyFailed": "复制失败"
|
"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": "添加"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
// 定义面板名称的类型,方便管理和引用 (添加 commandHistory)
|
// 定义面板名称的类型,方便管理和引用 (添加 quickCommands)
|
||||||
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory';
|
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands';
|
||||||
|
|
||||||
// 定义 Store
|
// 定义 Store
|
||||||
export const useLayoutStore = defineStore('layout', () => {
|
export const useLayoutStore = defineStore('layout', () => {
|
||||||
@@ -14,7 +14,8 @@ export const useLayoutStore = defineStore('layout', () => {
|
|||||||
fileManager: true,
|
fileManager: true,
|
||||||
editor: true,
|
editor: true,
|
||||||
statusMonitor: true,
|
statusMonitor: true,
|
||||||
commandHistory: true, // 默认可见
|
commandHistory: true,
|
||||||
|
quickCommands: true, // 默认可见
|
||||||
});
|
});
|
||||||
|
|
||||||
// Action: 切换指定面板的可见性
|
// 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>
|
||||||
@@ -11,6 +11,7 @@ import TerminalTabBar from '../components/TerminalTabBar.vue';
|
|||||||
import CommandInputBar from '../components/CommandInputBar.vue';
|
import CommandInputBar from '../components/CommandInputBar.vue';
|
||||||
import FileEditorContainer from '../components/FileEditorContainer.vue'; // 导入编辑器容器
|
import FileEditorContainer from '../components/FileEditorContainer.vue'; // 导入编辑器容器
|
||||||
import CommandHistoryView from './CommandHistoryView.vue'; // 导入命令历史视图
|
import CommandHistoryView from './CommandHistoryView.vue'; // 导入命令历史视图
|
||||||
|
import QuickCommandsView from './QuickCommandsView.vue'; // 导入快捷指令视图
|
||||||
import PaneTitleBar from '../components/PaneTitleBar.vue'; // 导入标题栏组件
|
import PaneTitleBar from '../components/PaneTitleBar.vue'; // 导入标题栏组件
|
||||||
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
||||||
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 Store
|
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 Store
|
||||||
@@ -124,10 +125,10 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
// 否则,正常发送命令
|
// 否则,正常发送命令
|
||||||
if (terminalManager && typeof terminalManager.sendData === 'function') {
|
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}`);
|
console.log(`[WorkspaceView] Sending command to active session ${currentSession.sessionId}: ${commandToSend}`);
|
||||||
// 注意:CommandInputBar 已经添加了 '\n'
|
// 发送给终端时,需要添加回车符以模拟执行
|
||||||
terminalManager.sendData(command); // 发送原始命令(包含换行符)
|
terminalManager.sendData(command + '\r');
|
||||||
|
|
||||||
// 记录非空命令到历史记录
|
// 记录非空命令到历史记录
|
||||||
if (commandToSend.length > 0) {
|
if (commandToSend.length > 0) {
|
||||||
@@ -257,11 +258,16 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<!-- 新增:命令历史 Pane -->
|
<!-- 新增:命令历史 Pane -->
|
||||||
<pane v-if="paneVisibility.commandHistory" size="15" min-size="10" class="sidebar-pane command-history-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>
|
</pane>
|
||||||
|
|
||||||
<!-- 2. 中间区域 Pane (终端/命令栏/文件管理器) - 这个 Pane 本身通常保持可见,内部 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">
|
<splitpanes :horizontal="true" style="height: 100%" :dbl-click-splitter="false">
|
||||||
<!-- 上方 Pane (终端) -->
|
<!-- 上方 Pane (终端) -->
|
||||||
@@ -414,7 +420,8 @@ onBeforeUnmount(() => {
|
|||||||
.file-manager-area-pane, /* 文件管理器区域 Pane */
|
.file-manager-area-pane, /* 文件管理器区域 Pane */
|
||||||
.file-manager-pane, /* 内部文件管理器 Pane */
|
.file-manager-pane, /* 内部文件管理器 Pane */
|
||||||
.status-monitor-pane, /* 状态监视器样式 */
|
.status-monitor-pane, /* 状态监视器样式 */
|
||||||
.command-history-pane { /* 命令历史窗格样式 */
|
.command-history-pane, /* 命令历史窗格样式 */
|
||||||
|
.quick-commands-pane { /* 快捷指令窗格样式 */
|
||||||
display: flex; /* 确保 flex 布局 */
|
display: flex; /* 确保 flex 布局 */
|
||||||
flex-direction: column; /* 确保列方向 */
|
flex-direction: column; /* 确保列方向 */
|
||||||
overflow: hidden; /* 默认隐藏溢出 */
|
overflow: hidden; /* 默认隐藏溢出 */
|
||||||
@@ -484,6 +491,9 @@ onBeforeUnmount(() => {
|
|||||||
.command-history-pane {
|
.command-history-pane {
|
||||||
background-color: #f8f9fa; /* 与其他侧边栏一致 */
|
background-color: #f8f9fa; /* 与其他侧边栏一致 */
|
||||||
}
|
}
|
||||||
|
.quick-commands-pane {
|
||||||
|
background-color: #f8f9fa; /* 与其他侧边栏一致 */
|
||||||
|
}
|
||||||
.status-monitor-content-wrapper {
|
.status-monitor-content-wrapper {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user