feat: 添加快捷指令功能
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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', '快捷指令'), // 添加快捷指令标签
|
||||
};
|
||||
|
||||
// 获取所有可控制的面板名称
|
||||
|
||||
@@ -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": "添加"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user