@@ -123,7 +123,6 @@ const definedMigrations: Migration[] = [
|
||||
name: 'Add notes column to connections table',
|
||||
check: async (db: Database): Promise<boolean> => {
|
||||
const notesColumnExists = await columnExists(db, 'connections', 'notes');
|
||||
// Only run if the column does NOT exist
|
||||
return !notesColumnExists;
|
||||
},
|
||||
sql: `
|
||||
@@ -297,6 +296,17 @@ const definedMigrations: Migration[] = [
|
||||
return !jumpChainColumnExists || !proxyTypeColumnExists;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Add variables column to quick_commands table',
|
||||
check: async (db: Database): Promise<boolean> => {
|
||||
const columnAlreadyExists = await columnExists(db, 'quick_commands', 'variables');
|
||||
return !columnAlreadyExists;
|
||||
},
|
||||
sql: `
|
||||
ALTER TABLE quick_commands ADD COLUMN variables TEXT NULL;
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -166,6 +166,7 @@ CREATE TABLE IF NOT EXISTS quick_commands (
|
||||
name TEXT NULL, -- 名称可选
|
||||
command TEXT NOT NULL, -- 指令必选
|
||||
usage_count INTEGER NOT NULL DEFAULT 0, -- 使用频率
|
||||
variables TEXT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
@@ -6,8 +6,8 @@ import { QuickCommandSortBy } from '../services/quick-commands.service';
|
||||
* 处理添加新快捷指令的请求
|
||||
*/
|
||||
export const addQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
||||
// 从请求体中解构出 name, command, 以及可选的 tagIds
|
||||
const { name, command, tagIds } = req.body;
|
||||
// 从请求体中解构出 name, command, 以及可选的 tagIds 和 variables
|
||||
const { name, command, tagIds, variables } = req.body;
|
||||
|
||||
// --- 基本验证 ---
|
||||
if (!command || typeof command !== 'string' || command.trim().length === 0) {
|
||||
@@ -24,11 +24,16 @@ export const addQuickCommand = async (req: Request, res: Response): Promise<void
|
||||
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
|
||||
return;
|
||||
}
|
||||
// --- 结束验证 ---
|
||||
// 验证 variables (如果提供的话)
|
||||
if (variables !== undefined && (typeof variables !== 'object' || variables === null || Array.isArray(variables))) {
|
||||
res.status(400).json({ message: 'variables 必须是一个对象' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 将 tagIds 传递给 Service 层
|
||||
const newId = await QuickCommandsService.addQuickCommand(name, command, tagIds);
|
||||
// 将 tagIds 和 variables 传递给 Service 层
|
||||
const newId = await QuickCommandsService.addQuickCommand(name, command, tagIds, variables);
|
||||
// 尝试获取新创建的带标签的指令信息返回
|
||||
const newCommand = await QuickCommandsService.getQuickCommandById(newId);
|
||||
if (newCommand) {
|
||||
@@ -65,8 +70,8 @@ export const getAllQuickCommands = async (req: Request, res: Response): Promise<
|
||||
*/
|
||||
export const updateQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
// 从请求体中解构出 name, command, 以及可选的 tagIds
|
||||
const { name, command, tagIds } = req.body;
|
||||
// 从请求体中解构出 name, command, 以及可选的 tagIds 和 variables
|
||||
const { name, command, tagIds, variables } = req.body;
|
||||
|
||||
// --- 基本验证 ---
|
||||
if (isNaN(id)) {
|
||||
@@ -87,11 +92,16 @@ export const updateQuickCommand = async (req: Request, res: Response): Promise<v
|
||||
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
|
||||
return;
|
||||
}
|
||||
// --- 结束验证 ---
|
||||
// 验证 variables (如果提供的话)
|
||||
// undefined 表示不更新 variables, null 或对象表示要更新
|
||||
if (variables !== undefined && variables !== null && (typeof variables !== 'object' || Array.isArray(variables))) {
|
||||
res.status(400).json({ message: 'variables 必须是对象或 null' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将 tagIds 传递给 Service 层
|
||||
const success = await QuickCommandsService.updateQuickCommand(id, name, command, tagIds);
|
||||
// 将 tagIds 和 variables 传递给 Service 层
|
||||
const success = await QuickCommandsService.updateQuickCommand(id, name, command, tagIds, variables);
|
||||
if (success) {
|
||||
// 尝试获取更新后的带标签的指令信息返回
|
||||
const updatedCommand = await QuickCommandsService.getQuickCommandById(id);
|
||||
|
||||
@@ -6,18 +6,21 @@ export interface QuickCommand {
|
||||
name: string | null; // 名称可选
|
||||
command: string;
|
||||
usage_count: number;
|
||||
variables?: string; // 存储 JSON 格式的变量键值对
|
||||
created_at: number; // Unix 时间戳 (秒)
|
||||
updated_at: number; // Unix 时间戳 (秒)
|
||||
}
|
||||
|
||||
// 定义包含标签 ID 的接口
|
||||
export interface QuickCommandWithTags extends QuickCommand {
|
||||
// 定义包含标签 ID 和解析后变量的接口
|
||||
export type QuickCommandWithTags = Omit<QuickCommand, 'variables'> & {
|
||||
tagIds: number[];
|
||||
}
|
||||
variables: Record<string, string> | null; // API 层面使用对象
|
||||
};
|
||||
|
||||
// 用于从数据库获取带 tag_ids_str 的行
|
||||
interface DbQuickCommandWithTagsRow extends QuickCommand {
|
||||
tag_ids_str: string | null;
|
||||
// variables 字段已包含在 QuickCommand 中,这里不需要重复定义,因为 QuickCommand 将包含 variables?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +28,15 @@ interface DbQuickCommandWithTagsRow extends QuickCommand {
|
||||
* 添加一条新的快捷指令
|
||||
* @param name - 指令名称 (可选)
|
||||
* @param command - 指令内容
|
||||
* @param variables - 变量对象 (可选)
|
||||
* @returns 返回插入记录的 ID
|
||||
*/
|
||||
export const addQuickCommand = async (name: string | null, command: string): Promise<number> => {
|
||||
const sql = `INSERT INTO quick_commands (name, command, created_at, updated_at) VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
|
||||
export const addQuickCommand = async (name: string | null, command: string, variables?: Record<string, string>): Promise<number> => {
|
||||
const sql = `INSERT INTO quick_commands (name, command, variables, created_at, updated_at) VALUES (?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [name, command]);
|
||||
const variablesJson = variables ? JSON.stringify(variables) : null;
|
||||
const result = await runDb(db, sql, [name, command, variablesJson]);
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('添加快捷指令后未能获取有效的 lastID');
|
||||
}
|
||||
@@ -47,13 +52,15 @@ export const addQuickCommand = async (name: string | null, command: string): Pro
|
||||
* @param id - 要更新的记录 ID
|
||||
* @param name - 新的指令名称 (可选)
|
||||
* @param command - 新的指令内容
|
||||
* @param variables - 新的变量对象 (可选)
|
||||
* @returns 返回是否成功更新 (true/false)
|
||||
*/
|
||||
export const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
|
||||
const sql = `UPDATE quick_commands SET name = ?, command = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
|
||||
export const updateQuickCommand = async (id: number, name: string | null, command: string, variables?: Record<string, string>): Promise<boolean> => {
|
||||
const sql = `UPDATE quick_commands SET name = ?, command = ?, variables = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [name, command, id]);
|
||||
const variablesJson = variables ? JSON.stringify(variables) : null;
|
||||
const result = await runDb(db, sql, [name, command, variablesJson, id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error('更新快捷指令时出错:', err.message);
|
||||
@@ -91,7 +98,7 @@ export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name
|
||||
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
|
||||
const sql = `
|
||||
SELECT
|
||||
qc.id, qc.name, qc.command, qc.usage_count, qc.created_at, qc.updated_at,
|
||||
qc.id, qc.name, qc.command, qc.usage_count, qc.variables, qc.created_at, qc.updated_at,
|
||||
GROUP_CONCAT(qta.tag_id) as tag_ids_str
|
||||
FROM quick_commands qc
|
||||
LEFT JOIN quick_command_tag_associations qta ON qc.id = qta.quick_command_id
|
||||
@@ -100,11 +107,24 @@ export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const rows = await allDb<DbQuickCommandWithTagsRow>(db, sql);
|
||||
// 将 tag_ids_str 解析为数字数组
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||
}));
|
||||
// 将 tag_ids_str 解析为数字数组,并解析 variables
|
||||
return rows.map(row => {
|
||||
let parsedVariables: Record<string, string> | null = null;
|
||||
if (row.variables) {
|
||||
try {
|
||||
parsedVariables = JSON.parse(row.variables);
|
||||
} catch (e) {
|
||||
console.error(`Error parsing variables for quick command ${row.id}:`, e);
|
||||
//保持 parsedVariables 为 null
|
||||
}
|
||||
}
|
||||
const { variables, ...restOfRow } = row; // 从 row 中移除原始的 string 类型的 variables
|
||||
return {
|
||||
...restOfRow,
|
||||
variables: parsedVariables,
|
||||
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||
};
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('获取快捷指令(带标签)时出错:', err.message);
|
||||
throw new Error('无法获取快捷指令');
|
||||
@@ -137,7 +157,7 @@ export const findQuickCommandById = async (id: number): Promise<QuickCommandWith
|
||||
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
|
||||
const sql = `
|
||||
SELECT
|
||||
qc.id, qc.name, qc.command, qc.usage_count, qc.created_at, qc.updated_at,
|
||||
qc.id, qc.name, qc.command, qc.usage_count, qc.variables, qc.created_at, qc.updated_at,
|
||||
GROUP_CONCAT(qta.tag_id) as tag_ids_str
|
||||
FROM quick_commands qc
|
||||
LEFT JOIN quick_command_tag_associations qta ON qc.id = qta.quick_command_id
|
||||
@@ -147,9 +167,20 @@ export const findQuickCommandById = async (id: number): Promise<QuickCommandWith
|
||||
const db = await getDbInstance();
|
||||
const row = await getDbRow<DbQuickCommandWithTagsRow>(db, sql, [id]);
|
||||
if (row && typeof row.id !== 'undefined') {
|
||||
// 将 tag_ids_str 解析为数字数组
|
||||
// 将 tag_ids_str 解析为数字数组,并解析 variables
|
||||
let parsedVariables: Record<string, string> | null = null;
|
||||
if (row.variables) {
|
||||
try {
|
||||
parsedVariables = JSON.parse(row.variables);
|
||||
} catch (e) {
|
||||
console.error(`Error parsing variables for quick command ${row.id}:`, e);
|
||||
//保持 parsedVariables 为 null
|
||||
}
|
||||
}
|
||||
const { variables, ...restOfRow } = row; // 从 row 中移除原始的 string 类型的 variables
|
||||
return {
|
||||
...row,
|
||||
...restOfRow,
|
||||
variables: parsedVariables,
|
||||
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -10,15 +10,16 @@ export type QuickCommandSortBy = 'name' | 'usage_count';
|
||||
* @param name - 指令名称 (可选)
|
||||
* @param command - 指令内容
|
||||
* @param tagIds - 关联的快捷指令标签 ID 数组 (可选)
|
||||
* @param variables - 变量对象 (可选)
|
||||
* @returns 返回添加记录的 ID
|
||||
*/
|
||||
export const addQuickCommand = async (name: string | null, command: string, tagIds?: number[]): Promise<number> => {
|
||||
export const addQuickCommand = async (name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<number> => {
|
||||
if (!command || command.trim().length === 0) {
|
||||
throw new Error('指令内容不能为空');
|
||||
}
|
||||
// 如果 name 是空字符串,则视为 null
|
||||
const finalName = name && name.trim().length > 0 ? name.trim() : null;
|
||||
const commandId = await QuickCommandsRepository.addQuickCommand(finalName, command.trim());
|
||||
const commandId = await QuickCommandsRepository.addQuickCommand(finalName, command.trim(), variables);
|
||||
|
||||
// 添加成功后,设置标签关联
|
||||
if (commandId > 0 && tagIds && Array.isArray(tagIds)) {
|
||||
@@ -39,14 +40,15 @@ export const addQuickCommand = async (name: string | null, command: string, tagI
|
||||
* @param name - 新的指令名称 (可选)
|
||||
* @param command - 新的指令内容
|
||||
* @param tagIds - 新的关联标签 ID 数组 (可选, undefined 表示不更新标签)
|
||||
* @param variables - 新的变量对象 (可选)
|
||||
* @returns 返回是否成功更新 (更新行数 > 0)
|
||||
*/
|
||||
export const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||
export const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<boolean> => {
|
||||
if (!command || command.trim().length === 0) {
|
||||
throw new Error('指令内容不能为空');
|
||||
}
|
||||
const finalName = name && name.trim().length > 0 ? name.trim() : null;
|
||||
const commandUpdated = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim());
|
||||
const commandUpdated = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim(), variables);
|
||||
|
||||
// 如果指令更新成功,并且提供了 tagIds (即使是空数组也表示要更新),则更新标签关联
|
||||
if (commandUpdated && typeof tagIds !== 'undefined') {
|
||||
|
||||
@@ -1,31 +1,74 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-overlay flex justify-center items-center z-50" @click.self="closeForm">
|
||||
<div class="bg-background text-foreground p-6 rounded-xl border border-border/50 shadow-2xl w-[90%] max-w-lg">
|
||||
<div class="fixed inset-0 bg-overlay flex justify-center items-center z-50">
|
||||
<div
|
||||
ref="modalContentRef"
|
||||
class="bg-background text-foreground p-6 rounded-xl border border-border/50 shadow-2xl flex flex-col"
|
||||
:style="{
|
||||
width: resizableWidth ? `${resizableWidth}px` : undefined,
|
||||
height: resizableHeight ? `${resizableHeight}px` : undefined,
|
||||
}"
|
||||
>
|
||||
<h2 class="m-0 mb-6 text-center text-xl font-semibold">{{ isEditing ? t('quickCommands.form.titleEdit', '编辑快捷指令') : t('quickCommands.form.titleAdd', '添加快捷指令') }}</h2>
|
||||
<form @submit.prevent="handleSubmit" class="space-y-5">
|
||||
<div>
|
||||
<label for="qc-name" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.name', '名称:') }}</label>
|
||||
<input
|
||||
id="qc-name"
|
||||
<div class="flex-grow flex space-x-6 min-h-0">
|
||||
<!-- 左侧:变量管理 -->
|
||||
<div class="w-1/3 border-r border-border/30 pr-6 flex flex-col overflow-y-auto">
|
||||
<h3 class="text-md font-medium mb-3 text-text-secondary">{{ t('quickCommands.form.variablesTitle', '变量管理') }}</h3>
|
||||
<div class="space-y-3 overflow-y-auto flex-grow pr-1 pb-2">
|
||||
<div v-if="localVariables.length === 0" class="text-sm text-text-tertiary p-2 border border-dashed border-border/30 rounded-md">
|
||||
{{ t('quickCommands.form.noVariables', '暂无变量。点击下方按钮添加。') }}
|
||||
</div>
|
||||
<div v-for="(variable, index) in localVariables" :key="variable.id" class="p-2.5 border border-border/40 rounded-lg bg-input/30 space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
v-model="variable.name"
|
||||
:placeholder="t('quickCommands.form.variableNamePlaceholder', '变量名')"
|
||||
class="w-full px-3 py-1.5 border border-border/50 rounded-md bg-input text-foreground text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
<textarea
|
||||
v-model="variable.value"
|
||||
:placeholder="t('quickCommands.form.variableValuePlaceholder', '变量值')"
|
||||
rows="2"
|
||||
class="w-full px-3 py-1.5 border border-border/50 rounded-md bg-input text-foreground text-xs resize-y min-h-[40px] shadow-sm focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
@click="deleteVariable(variable.id)"
|
||||
class="w-full py-1 px-3 text-xs text-error hover:bg-error/10 border border-error/50 rounded-md transition-colors duration-150"
|
||||
>
|
||||
{{ t('common.delete', '删除') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="addVariable" class="mt-3 w-full py-2 px-4 border border-primary/50 text-primary text-sm rounded-md hover:bg-primary/10 transition-colors duration-150">
|
||||
{{ t('quickCommands.form.addVariable', '+ 添加变量') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:现有表单 -->
|
||||
<form @submit.prevent="handleSubmit" class="w-2/3 space-y-5 flex flex-col">
|
||||
<div class="flex-grow space-y-5 pr-1 flex flex-col">
|
||||
<div>
|
||||
<label for="qc-name" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.name', '名称:') }}</label>
|
||||
<input
|
||||
id="qc-name"
|
||||
type="text"
|
||||
v-model="formData.name"
|
||||
:placeholder="t('quickCommands.form.namePlaceholder', '可选,用于快速识别')"
|
||||
class="w-full px-4 py-2 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-col flex-grow">
|
||||
<label for="qc-command" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.command', '指令:') }} <span class="text-error">*</span></label>
|
||||
<textarea
|
||||
id="qc-command"
|
||||
v-model="formData.command"
|
||||
required
|
||||
rows="4"
|
||||
:placeholder="t('quickCommands.form.commandPlaceholder', '例如:ls -alh /home/user')"
|
||||
class="w-full px-4 py-2 border border-border/50 rounded-lg bg-input text-foreground text-sm resize-y min-h-[100px] shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
|
||||
:placeholder="placeholder"
|
||||
class="w-full px-4 py-2 border border-border/50 rounded-lg bg-input text-foreground text-sm min-h-[80px] shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out whitespace-nowrap overflow-x-auto flex-grow"
|
||||
></textarea>
|
||||
<small v-if="commandError" class="text-error text-xs mt-1 block">{{ commandError }}</small>
|
||||
</div>
|
||||
<!-- +++ Tag Input Section +++ -->
|
||||
<!-- +++ 标签输入区 +++ -->
|
||||
<div>
|
||||
<label for="qc-tags" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.tags', '标签:') }}</label>
|
||||
<TagInput
|
||||
@@ -39,51 +82,77 @@
|
||||
@delete-tag="handleDeleteTag"
|
||||
class="w-full"
|
||||
/>
|
||||
<!-- Add styling/classes as needed for TagInput -->
|
||||
</div>
|
||||
<!-- +++ End Tag Input Section +++ -->
|
||||
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
|
||||
<!-- Secondary/Cancel Button -->
|
||||
<button type="button" @click="closeForm" class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">{{ t('common.cancel', '取消') }}</button>
|
||||
<!-- Primary/Submit Button -->
|
||||
<button type="submit" :disabled="isSubmitting || !!commandError" class="py-2 px-5 rounded-lg text-sm font-semibold transition-colors duration-150 bg-primary text-white border-none shadow-md hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
{{ isSubmitting ? t('common.saving', '保存中...') : (isEditing ? t('common.save', '保存') : t('quickCommands.form.add', '添加')) }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- 根据需要为 TagInput 添加样式/类 -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- +++ 标签输入区结束 +++ -->
|
||||
<div class="flex justify-end mt-auto pt-4 border-t border-border/50">
|
||||
<!-- 次要/取消按钮 -->
|
||||
<button type="button" @click="closeForm" class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">{{ t('common.cancel', '取消') }}</button>
|
||||
<!-- 执行按钮 -->
|
||||
<button type="button" @click="handleExecute" class="py-2 px-5 rounded-lg text-sm font-semibold transition-colors duration-150 bg-[var(--color-success)] text-white border-none shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-success)] mr-3">
|
||||
{{ t('quickCommands.form.execute', '执行') }}
|
||||
</button>
|
||||
<!-- 主要/提交按钮 -->
|
||||
<button type="submit" :disabled="isSubmitting || !!commandError" class="py-2 px-5 rounded-lg text-sm font-semibold transition-colors duration-150 bg-primary text-white border-none shadow-md hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
{{ isSubmitting ? t('common.saving', '保存中...') : (isEditing ? t('common.save', '保存') : t('quickCommands.form.add', '添加')) }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||
import { useResizable } from '../composables/useResizable';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
|
||||
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store';
|
||||
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
||||
import TagInput from './TagInput.vue';
|
||||
import { useConfirmDialog } from '../composables/useConfirmDialog';
|
||||
import { useAlertDialog } from '../composables/useAlertDialog'; // +++ 导入 useAlertDialog +++
|
||||
import { useAlertDialog } from '../composables/useAlertDialog';
|
||||
|
||||
const props = defineProps<{
|
||||
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
|
||||
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (应包含标签ID和变量)
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { showConfirmDialog } = useConfirmDialog();
|
||||
const { showAlertDialog } = useAlertDialog(); // +++ 获取 showAlertDialog 函数 +++
|
||||
const { showAlertDialog } = useAlertDialog();
|
||||
const quickCommandsStore = useQuickCommandsStore();
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate tag store +++
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const emitWorkspaceEvent = useWorkspaceEventEmitter();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const modalContentRef = ref<HTMLElement | null>(null);
|
||||
const R_MIN_WIDTH = 800; // 可调整大小的最小宽度 (像素)
|
||||
const R_MIN_HEIGHT = 700; // 可调整大小的最小高度 (像素)
|
||||
const placeholder = t('quickCommands.form.commandPlaceholder') + 'echo "Hello,\${USERNAME}"'
|
||||
|
||||
const { width: resizableWidth, height: resizableHeight } = useResizable(modalContentRef, {
|
||||
minWidth: R_MIN_WIDTH,
|
||||
minHeight: R_MIN_HEIGHT,
|
||||
// 如果需要,可以在此处添加最大宽度和最大高度,例如:window.innerWidth * 0.95
|
||||
});
|
||||
|
||||
const isEditing = computed(() => !!props.commandToEdit);
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
command: '',
|
||||
tagIds: [] as number[], // +++ Add tagIds +++
|
||||
tagIds: [] as number[], // +++ 添加标签ID +++
|
||||
});
|
||||
const localVariables = ref<{ name: string; value: string; id: string }[]>([]);
|
||||
|
||||
|
||||
const commandError = ref<string | null>(null);
|
||||
|
||||
@@ -98,20 +167,33 @@ watch(() => formData.command, (newCommand) => {
|
||||
|
||||
// 初始化表单数据 (如果是编辑模式)
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
let initialW = Math.min(window.innerWidth * 0.9, 1152); // 目标 90vw,最大 1152px
|
||||
let initialH = window.innerHeight * 0.85; // 目标 85vh
|
||||
|
||||
initialW = Math.max(R_MIN_WIDTH, initialW);
|
||||
initialH = Math.max(R_MIN_HEIGHT, initialH);
|
||||
|
||||
resizableWidth.value = initialW;
|
||||
resizableHeight.value = initialH;
|
||||
}
|
||||
|
||||
if (isEditing.value && props.commandToEdit) {
|
||||
formData.name = props.commandToEdit.name ?? '';
|
||||
formData.command = props.commandToEdit.command;
|
||||
// Initialize tagIds if editing
|
||||
formData.tagIds = props.commandToEdit.tagIds ? [...props.commandToEdit.tagIds] : [];
|
||||
if (props.commandToEdit.variables) {
|
||||
localVariables.value = Object.entries(props.commandToEdit.variables).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
id: `var-${Date.now()}-${Math.random().toString(36).substring(7)}` // 生成唯一ID
|
||||
}));
|
||||
} else {
|
||||
localVariables.value = [];
|
||||
}
|
||||
}
|
||||
// Fetch tags if not already loaded (optional, might be better in parent)
|
||||
// if (quickCommandTagsStore.tags.length === 0) {
|
||||
// quickCommandTagsStore.fetchTags();
|
||||
// }
|
||||
});
|
||||
|
||||
// --- Tag Creation Handling ---
|
||||
// Assuming TagInput emits 'create-tag' with the tag name
|
||||
const handleCreateTag = async (tagName: string) => {
|
||||
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`);
|
||||
if (!tagName || tagName.trim().length === 0) return;
|
||||
@@ -119,12 +201,10 @@ const handleCreateTag = async (tagName: string) => {
|
||||
const newTag = await quickCommandTagsStore.addTag(tagName.trim());
|
||||
if (newTag && !formData.tagIds.includes(newTag.id)) {
|
||||
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`);
|
||||
// Add the new tag's ID to the selected list
|
||||
formData.tagIds.push(newTag.id);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tag Deletion Handling ---
|
||||
const handleDeleteTag = async (tagId: number) => {
|
||||
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`);
|
||||
const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId);
|
||||
@@ -137,9 +217,9 @@ const handleDeleteTag = async (tagId: number) => {
|
||||
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`);
|
||||
const success = await quickCommandTagsStore.deleteTag(tagId);
|
||||
if (success) {
|
||||
// If deletion is successful, TagInput's availableTags will update,
|
||||
// and the tag should disappear from the input.
|
||||
// We also need to remove it from the local formData.tagIds if it was selected.
|
||||
// 如果删除成功,TagInput的availableTags将会更新,
|
||||
// 并且标签应该从输入框中消失。
|
||||
// 如果该标签已被选中,我们还需要从本地的formData.tagIds中移除它。
|
||||
const index = formData.tagIds.indexOf(tagId);
|
||||
if (index > -1) {
|
||||
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`);
|
||||
@@ -160,12 +240,17 @@ const handleSubmit = async () => {
|
||||
// 处理名称,空字符串视为 null
|
||||
const finalName = formData.name.trim().length > 0 ? formData.name.trim() : null;
|
||||
|
||||
const variablesToSave: Record<string, string> = localVariables.value.reduce((acc, curr) => {
|
||||
if (curr.name.trim()) { // 只保存带有名称的变量
|
||||
acc[curr.name.trim()] = curr.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
if (isEditing.value && props.commandToEdit) {
|
||||
// Pass tagIds to update action
|
||||
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim(), formData.tagIds);
|
||||
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim(), formData.tagIds, variablesToSave);
|
||||
} else {
|
||||
// Pass tagIds to add action
|
||||
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim(), formData.tagIds);
|
||||
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim(), formData.tagIds, variablesToSave);
|
||||
}
|
||||
|
||||
isSubmitting.value = false;
|
||||
@@ -177,5 +262,67 @@ const handleSubmit = async () => {
|
||||
const closeForm = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
//向 localVariables 数组添加一个新变量
|
||||
const addVariable = () => {
|
||||
localVariables.value.push({
|
||||
name: '',
|
||||
value: '',
|
||||
id: `var-${Date.now()}-${Math.random().toString(36).substring(7)}` // 生成唯一ID
|
||||
});
|
||||
};
|
||||
|
||||
// 通过 ID 从 localVariables 数组中删除变量
|
||||
const deleteVariable = (variableId: string) => {
|
||||
localVariables.value = localVariables.value.filter(v => v.id !== variableId);
|
||||
};
|
||||
|
||||
// 使用当前变量执行命令
|
||||
const handleExecute = () => {
|
||||
let processedCommand = formData.command;
|
||||
const currentVariables = localVariables.value.reduce((acc, curr) => {
|
||||
if (curr.name.trim()) {
|
||||
acc[curr.name.trim()] = curr.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
// 执行变量替换
|
||||
for (const varName in currentVariables) {
|
||||
const placeholder = new RegExp(`\\$\\{${varName}\\}`, 'g');
|
||||
processedCommand = processedCommand.replace(placeholder, currentVariables[varName]);
|
||||
}
|
||||
|
||||
// 检查模板中是否存在未定义的变量
|
||||
const variablePlaceholders = formData.command.match(/\$\{[^\}]+\}/g) || [];
|
||||
const undefinedVariables: string[] = [];
|
||||
variablePlaceholders.forEach(placeholder => {
|
||||
const varName = placeholder.substring(2, placeholder.length - 1);
|
||||
if (!currentVariables.hasOwnProperty(varName)) {
|
||||
undefinedVariables.push(varName);
|
||||
}
|
||||
});
|
||||
|
||||
if (undefinedVariables.length > 0) {
|
||||
uiNotificationsStore.showWarning(
|
||||
t('quickCommands.form.warningUndefinedVariables', { variables: undefinedVariables.join(', ') })
|
||||
);
|
||||
}
|
||||
|
||||
const activeSessionId = sessionStore.activeSessionId;
|
||||
if (!activeSessionId) {
|
||||
uiNotificationsStore.showError(t('quickCommands.form.errorNoActiveSession', '没有活动的SSH会话可执行指令。'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[QuickCmdForm] Executing processed command: "${processedCommand}" on session ${activeSessionId}`);
|
||||
emitWorkspaceEvent('quickCommand:executeProcessed', {
|
||||
command: processedCommand,
|
||||
sessionId: activeSessionId
|
||||
});
|
||||
|
||||
closeForm();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ref, onMounted, onBeforeUnmount, type Ref, watch } from 'vue';
|
||||
|
||||
interface UseResizableOptions {
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
edgeThreshold?: number; // How close to an edge to consider it a drag handle
|
||||
initialWidth?: number | string; // Allow string for % or vh/vw, or number for px
|
||||
initialHeight?: number | string; // Allow string for % or vh/vw, or number for px
|
||||
}
|
||||
|
||||
type Edge = 'right' | 'bottom' | 'left' | 'top' | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | null;
|
||||
|
||||
export function useResizable(
|
||||
elementRef: Ref<HTMLElement | null>,
|
||||
options: UseResizableOptions = {}
|
||||
) {
|
||||
const {
|
||||
minWidth = 100, // Default min width
|
||||
minHeight = 100, // Default min height
|
||||
maxWidth = Infinity,
|
||||
maxHeight = Infinity,
|
||||
edgeThreshold = 8, // pixels, sensitivity for edge detection
|
||||
} = options;
|
||||
|
||||
const width = ref<number | null>(null);
|
||||
const height = ref<number | null>(null);
|
||||
const isResizing = ref(false);
|
||||
const currentEdge = ref<Edge>(null);
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startWidth = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
const getEdge = (event: MouseEvent, el: HTMLElement): Edge => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const { clientX, clientY } = event;
|
||||
|
||||
// Check corners first
|
||||
const onRight = Math.abs(clientX - rect.right) < edgeThreshold;
|
||||
const onLeft = Math.abs(clientX - rect.left) < edgeThreshold;
|
||||
const onBottom = Math.abs(clientY - rect.bottom) < edgeThreshold;
|
||||
const onTop = Math.abs(clientY - rect.top) < edgeThreshold;
|
||||
|
||||
if (onRight && onBottom) return 'bottom-right';
|
||||
if (onLeft && onBottom) return 'bottom-left';
|
||||
if (onRight && onTop) return 'top-right';
|
||||
if (onLeft && onTop) return 'top-left';
|
||||
if (onRight) return 'right';
|
||||
if (onLeft) return 'left';
|
||||
if (onBottom) return 'bottom';
|
||||
if (onTop) return 'top';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateCursorStyle = (el: HTMLElement, edge: Edge) => {
|
||||
if (edge === 'left' || edge === 'right') el.style.cursor = 'ew-resize';
|
||||
else if (edge === 'top' || edge === 'bottom') el.style.cursor = 'ns-resize';
|
||||
else if (edge === 'top-left' || edge === 'bottom-right') el.style.cursor = 'nwse-resize';
|
||||
else if (edge === 'top-right' || edge === 'bottom-left') el.style.cursor = 'nesw-resize';
|
||||
else el.style.cursor = 'default';
|
||||
};
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
if (!elementRef.value) return;
|
||||
const edge = getEdge(event, elementRef.value);
|
||||
|
||||
if (!edge) return;
|
||||
event.preventDefault(); // Prevent text selection, etc.
|
||||
|
||||
isResizing.value = true;
|
||||
currentEdge.value = edge;
|
||||
startX = event.clientX;
|
||||
startY = event.clientY;
|
||||
|
||||
// Ensure width and height refs have current dimensions
|
||||
const rect = elementRef.value.getBoundingClientRect();
|
||||
startWidth = rect.width;
|
||||
startHeight = rect.height;
|
||||
width.value = startWidth;
|
||||
height.value = startHeight;
|
||||
|
||||
elementRef.value.style.userSelect = 'none'; // Prevent text selection
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isResizing.value || !elementRef.value || !currentEdge.value) return;
|
||||
event.preventDefault();
|
||||
|
||||
const deltaX = event.clientX - startX;
|
||||
const deltaY = event.clientY - startY;
|
||||
|
||||
let newWidth = width.value ?? startWidth;
|
||||
let newHeight = height.value ?? startHeight;
|
||||
|
||||
if (currentEdge.value.includes('right')) {
|
||||
newWidth = startWidth + deltaX;
|
||||
}
|
||||
if (currentEdge.value.includes('left')) {
|
||||
newWidth = startWidth - deltaX;
|
||||
}
|
||||
if (currentEdge.value.includes('bottom')) {
|
||||
newHeight = startHeight + deltaY;
|
||||
}
|
||||
if (currentEdge.value.includes('top')) {
|
||||
newHeight = startHeight - deltaY;
|
||||
}
|
||||
|
||||
// Apply constraints
|
||||
width.value = Math.max(minWidth, Math.min(maxWidth, newWidth));
|
||||
height.value = Math.max(minHeight, Math.min(maxHeight, newHeight));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isResizing.value) return;
|
||||
isResizing.value = false;
|
||||
if (elementRef.value) {
|
||||
elementRef.value.style.userSelect = '';
|
||||
updateCursorStyle(elementRef.value, null); // Reset to default or hover state
|
||||
}
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
const handleElementHover = (event: MouseEvent) => {
|
||||
if (!elementRef.value || isResizing.value) return;
|
||||
const edge = getEdge(event, elementRef.value);
|
||||
updateCursorStyle(elementRef.value, edge);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (elementRef.value) {
|
||||
const el = elementRef.value;
|
||||
// Initialize width and height from element's current computed size
|
||||
// This ensures that initial CSS (like %, vw, vh, or fixed values) is respected
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
let parsedWidth = parseFloat(computedStyle.width);
|
||||
let parsedHeight = parseFloat(computedStyle.height);
|
||||
|
||||
// Fallback to minWidth/minHeight if parsing results in NaN, or ensure value is at least minWidth/minHeight
|
||||
width.value = isNaN(parsedWidth) ? minWidth : Math.max(minWidth, parsedWidth);
|
||||
height.value = isNaN(parsedHeight) ? minHeight : Math.max(minHeight, parsedHeight);
|
||||
|
||||
el.addEventListener('mousedown', handleMouseDown);
|
||||
el.addEventListener('mousemove', handleElementHover); // For cursor changes
|
||||
// Reset cursor when mouse leaves the element
|
||||
el.addEventListener('mouseleave', () => {
|
||||
if (!isResizing.value && el) {
|
||||
el.style.cursor = 'default';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (elementRef.value) {
|
||||
elementRef.value.removeEventListener('mousedown', handleMouseDown);
|
||||
elementRef.value.removeEventListener('mousemove', handleElementHover);
|
||||
elementRef.value.removeEventListener('mouseleave', () => {
|
||||
if (elementRef.value) elementRef.value.style.cursor = 'default';
|
||||
});
|
||||
}
|
||||
window.removeEventListener('mousemove', handleMouseMove); // Cleanup just in case
|
||||
window.removeEventListener('mouseup', handleMouseUp); // Cleanup just in case
|
||||
});
|
||||
|
||||
// Watch for external changes to elementRef if it can become null
|
||||
watch(elementRef, (newEl, oldEl) => {
|
||||
if (oldEl) {
|
||||
oldEl.removeEventListener('mousedown', handleMouseDown);
|
||||
oldEl.removeEventListener('mousemove', handleElementHover);
|
||||
oldEl.removeEventListener('mouseleave', () => {
|
||||
if (oldEl) oldEl.style.cursor = 'default';
|
||||
});
|
||||
}
|
||||
if (newEl) {
|
||||
const computedStyle = window.getComputedStyle(newEl);
|
||||
let parsedWidth = parseFloat(computedStyle.width);
|
||||
let parsedHeight = parseFloat(computedStyle.height);
|
||||
|
||||
// Fallback to minWidth/minHeight if parsing results in NaN, or ensure value is at least minWidth/minHeight
|
||||
width.value = isNaN(parsedWidth) ? minWidth : Math.max(minWidth, parsedWidth);
|
||||
height.value = isNaN(parsedHeight) ? minHeight : Math.max(minHeight, parsedHeight);
|
||||
|
||||
newEl.addEventListener('mousedown', handleMouseDown);
|
||||
newEl.addEventListener('mousemove', handleElementHover);
|
||||
newEl.addEventListener('mouseleave', () => {
|
||||
if (newEl && !isResizing.value) newEl.style.cursor = 'default';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
isResizing,
|
||||
};
|
||||
}
|
||||
@@ -53,6 +53,9 @@ export type WorkspaceEventPayloads = {
|
||||
|
||||
// Suspended SSH Session Events
|
||||
'suspendedSession:actionCompleted': void; // Emitted when a resume/remove action is completed
|
||||
|
||||
// Quick Command Events
|
||||
'quickCommand:executeProcessed': { command: string; sessionId?: string };
|
||||
};
|
||||
|
||||
// 创建 mitt 事件发射器实例
|
||||
|
||||
@@ -606,7 +606,8 @@
|
||||
"createSuccess": "Tag created successfully.",
|
||||
"updateSuccess": "Tag updated successfully.",
|
||||
"deleteSuccess": "Tag \"{name}\" deleted successfully.",
|
||||
"deleteFailed": "Failed to delete tag \"{name}\": {error}"
|
||||
"deleteFailed": "Failed to delete tag \"{name}\": {error}",
|
||||
"errorDelete": "Error deleting tag: {error}"
|
||||
},
|
||||
"settings": {
|
||||
"popupFileManager": {
|
||||
@@ -1041,6 +1042,7 @@
|
||||
"testMessageUnsaved": "Test triggered for unsaved {channelType} configuration"
|
||||
},
|
||||
"common": {
|
||||
"confirm": "Confirm",
|
||||
"ok": "OK",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
@@ -1293,11 +1295,19 @@
|
||||
"name": "Name:",
|
||||
"namePlaceholder": "Optional, for quick identification",
|
||||
"command": "Command:",
|
||||
"commandPlaceholder": "e.g., ls -alh /home/user",
|
||||
"commandPlaceholder": "e.g.,",
|
||||
"errorCommandRequired": "Command cannot be empty",
|
||||
"add": "Add",
|
||||
"tags": "Tags:",
|
||||
"tagsPlaceholder": "Select or create tags..."
|
||||
"tagsPlaceholder": "Select or create tags...",
|
||||
"variablesTitle": "Variable Management",
|
||||
"noVariables": "No variables yet. Click the button below to add one.",
|
||||
"variableNamePlaceholder": "Variable Name",
|
||||
"variableValuePlaceholder": "Variable Value",
|
||||
"addVariable": "+ Add Variable",
|
||||
"execute": "Execute",
|
||||
"warningUndefinedVariables": "Warning: Undefined variables in command template: {variables}",
|
||||
"errorNoActiveSession": "No active SSH session to execute the command."
|
||||
},
|
||||
"untagged": "Untagged",
|
||||
"tags": {
|
||||
@@ -1626,3 +1636,4 @@
|
||||
"copiedError": "Failed to copy path"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"clearTerminal": "ターミナルをクリア"
|
||||
},
|
||||
"common": {
|
||||
"confirm": "確認",
|
||||
"ok": "確認",
|
||||
"success": "成功",
|
||||
"error": "失敗",
|
||||
@@ -683,12 +684,20 @@
|
||||
"tags": "タグ:",
|
||||
"tagsPlaceholder": "タグを選択または作成...",
|
||||
"command": "コマンド:",
|
||||
"commandPlaceholder": "例:ls -alh /home/user",
|
||||
"commandPlaceholder": "例:",
|
||||
"errorCommandRequired": "コマンドは空にできません",
|
||||
"name": "名前:",
|
||||
"namePlaceholder": "オプション。素早く認識するために使用",
|
||||
"titleAdd": "クイックコマンドの追加",
|
||||
"titleEdit": "クイックコマンドの編集"
|
||||
"titleEdit": "クイックコマンドの編集",
|
||||
"variablesTitle": "変数管理",
|
||||
"noVariables": "変数はまだありません。下のボタンをクリックして追加してください。",
|
||||
"variableNamePlaceholder": "変数名",
|
||||
"variableValuePlaceholder": "変数値",
|
||||
"addVariable": "+ 変数を追加",
|
||||
"execute": "実行",
|
||||
"warningUndefinedVariables": "警告:コマンドテンプレートに未定義の変数があります: {variables}",
|
||||
"errorNoActiveSession": "コマンドを実行するためのアクティブなSSHセッションがありません。"
|
||||
},
|
||||
"untagged": "タグなし",
|
||||
"tags": {
|
||||
@@ -1373,7 +1382,8 @@
|
||||
"createSuccess": "タグが正常に作成されました。",
|
||||
"updateSuccess": "タグが正常に更新されました。",
|
||||
"deleteSuccess": "タグ「{name}」が正常に削除されました。",
|
||||
"deleteFailed": "タグ「{name}」の削除に失敗しました: {error}"
|
||||
"deleteFailed": "タグ「{name}」の削除に失敗しました: {error}",
|
||||
"errorDelete": "タグの削除中にエラーが発生しました: {error}"
|
||||
},
|
||||
"terminalTabBar": {
|
||||
"selectServerTitle": "接続するサーバーを選択",
|
||||
|
||||
@@ -606,7 +606,8 @@
|
||||
"createSuccess": "标签创建成功。",
|
||||
"updateSuccess": "标签更新成功。",
|
||||
"deleteSuccess": "标签 \"{name}\" 删除成功。",
|
||||
"deleteFailed": "标签 \"{name}\" 删除失败: {error}"
|
||||
"deleteFailed": "标签 \"{name}\" 删除失败: {error}",
|
||||
"errorDelete": "删除标签时出错: {error}"
|
||||
},
|
||||
"settings": {
|
||||
"popupFileManager": {
|
||||
@@ -1077,6 +1078,7 @@
|
||||
"success":"成功",
|
||||
"error":"失败",
|
||||
"alert":"提示",
|
||||
"confirm":"确认",
|
||||
"updateSuccess":"更新成功"
|
||||
},
|
||||
"layoutConfigurator": {
|
||||
@@ -1297,11 +1299,19 @@
|
||||
"name": "名称:",
|
||||
"namePlaceholder": "可选,用于快速识别",
|
||||
"command": "指令:",
|
||||
"commandPlaceholder": "例如:ls -alh /home/user",
|
||||
"commandPlaceholder": "例如:",
|
||||
"errorCommandRequired": "指令内容不能为空",
|
||||
"add": "添加",
|
||||
"tags": "标签:",
|
||||
"tagsPlaceholder": "选择或创建标签..."
|
||||
"tagsPlaceholder": "选择或创建标签...",
|
||||
"variablesTitle": "变量管理",
|
||||
"noVariables": "暂无变量。点击下方按钮添加。",
|
||||
"variableNamePlaceholder": "变量名",
|
||||
"variableValuePlaceholder": "变量值",
|
||||
"addVariable": "+ 添加变量",
|
||||
"execute": "执行",
|
||||
"warningUndefinedVariables": "警告:指令模板中存在未定义的变量: {variables}",
|
||||
"errorNoActiveSession": "没有活动的SSH会话可执行指令。"
|
||||
},
|
||||
"untagged": "未标记",
|
||||
"tags": {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface QuickCommandFE { // Renamed from QuickCommand if necessary
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
tagIds: number[]; // +++ Add tagIds +++
|
||||
variables?: Record<string, string>; // New: Add variables
|
||||
}
|
||||
|
||||
// 定义排序类型
|
||||
@@ -250,10 +251,10 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
try {
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
if (cachedData) {
|
||||
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds)
|
||||
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds 和 variables)
|
||||
const parsedData = JSON.parse(cachedData) as QuickCommandFE[];
|
||||
// 基本验证,确保 tagIds 是数组
|
||||
if (Array.isArray(parsedData) && parsedData.every(item => Array.isArray(item.tagIds))) {
|
||||
// 基本验证,确保 tagIds 是数组,variables 是对象或undefined
|
||||
if (Array.isArray(parsedData) && parsedData.every(item => Array.isArray(item.tagIds) && (item.variables === undefined || typeof item.variables === 'object'))) {
|
||||
quickCommandsList.value = parsedData;
|
||||
isLoading.value = false;
|
||||
} else {
|
||||
@@ -276,10 +277,11 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
console.log(`[QuickCmdStore] Fetching latest commands from server...`);
|
||||
// 不再发送 sortBy 参数
|
||||
const response = await apiClient.get<QuickCommandFE[]>('/quick-commands');
|
||||
// 确保返回的数据包含 tagIds 数组
|
||||
// 确保返回的数据包含 tagIds 数组和 variables 对象
|
||||
const freshData = response.data.map(cmd => ({
|
||||
...cmd,
|
||||
tagIds: Array.isArray(cmd.tagIds) ? cmd.tagIds : [] // 确保 tagIds 是数组
|
||||
tagIds: Array.isArray(cmd.tagIds) ? cmd.tagIds : [], // 确保 tagIds 是数组
|
||||
variables: typeof cmd.variables === 'object' ? cmd.variables : undefined // 确保 variables 是对象或 undefined
|
||||
}));
|
||||
const freshDataString = JSON.stringify(freshData);
|
||||
|
||||
@@ -310,11 +312,11 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
};
|
||||
|
||||
|
||||
// 添加快捷指令 (发送 tagIds)
|
||||
const addQuickCommand = async (name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||
// 添加快捷指令 (发送 tagIds 和 variables)
|
||||
const addQuickCommand = async (name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<boolean> => {
|
||||
try {
|
||||
// 在请求体中包含 tagIds
|
||||
const response = await apiClient.post<{ message: string, command: QuickCommandFE }>('/quick-commands', { name, command, tagIds });
|
||||
// 在请求体中包含 tagIds 和 variables
|
||||
const response = await apiClient.post<{ message: string, command: QuickCommandFE }>('/quick-commands', { name, command, tagIds, variables });
|
||||
// 后端现在返回完整的 command 对象,可以直接使用或触发刷新
|
||||
clearQuickCommandsCache(); // 清除缓存
|
||||
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||
@@ -328,11 +330,11 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更新快捷指令 (发送 tagIds)
|
||||
const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||
// 更新快捷指令 (发送 tagIds 和 variables)
|
||||
const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<boolean> => {
|
||||
try {
|
||||
// 在请求体中包含 tagIds (即使是 undefined 也要发送,让后端知道是否要更新)
|
||||
const response = await apiClient.put<{ message: string, command: QuickCommandFE }>(`/quick-commands/${id}`, { name, command, tagIds });
|
||||
// 在请求体中包含 tagIds 和 variables (即使是 undefined 也要发送,让后端知道是否要更新)
|
||||
const response = await apiClient.put<{ message: string, command: QuickCommandFE }>(`/quick-commands/${id}`, { name, command, tagIds, variables });
|
||||
// 后端现在返回完整的 command 对象
|
||||
clearQuickCommandsCache(); // 清除缓存
|
||||
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||
|
||||
@@ -552,13 +552,47 @@ const copyCommand = async (command: string) => {
|
||||
};
|
||||
|
||||
// 执行命令
|
||||
const executeCommand = (command: QuickCommandFE) => {
|
||||
// 1. 增加使用次数 (后台操作,不阻塞执行)
|
||||
quickCommandsStore.incrementUsage(command.id);
|
||||
// 2. 发出执行事件给父组件
|
||||
emitWorkspaceEvent('terminal:sendCommand', { command: command.command });
|
||||
// Optionally reset selection after execution
|
||||
// selectedIndex.value = -1; // REMOVED: Store handles index
|
||||
const executeCommand = (cmd: QuickCommandFE) => {
|
||||
// 1. 增加使用次数
|
||||
quickCommandsStore.incrementUsage(cmd.id);
|
||||
|
||||
let processedCommand = cmd.command;
|
||||
const savedVariables = cmd.variables || {}; // 使用已保存的变量
|
||||
|
||||
// 2. 执行变量替换
|
||||
for (const varName in savedVariables) {
|
||||
const placeholder = new RegExp(`\\$\\{${varName}\\}`, 'g');
|
||||
processedCommand = processedCommand.replace(placeholder, savedVariables[varName]);
|
||||
}
|
||||
|
||||
// 3. 检查未定义变量
|
||||
const variablePlaceholders = cmd.command.match(/\$\{[^\}]+\}/g) || [];
|
||||
const undefinedVariables: string[] = [];
|
||||
variablePlaceholders.forEach(placeholder => {
|
||||
const varName = placeholder.substring(2, placeholder.length - 1);
|
||||
if (!savedVariables.hasOwnProperty(varName)) {
|
||||
undefinedVariables.push(varName);
|
||||
}
|
||||
});
|
||||
|
||||
if (undefinedVariables.length > 0) {
|
||||
uiNotificationsStore.showWarning(
|
||||
t('quickCommands.form.warningUndefinedVariables', { variables: undefinedVariables.join(', ') })
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 获取当前激活的 SSH 会话 ID
|
||||
const activeSessionId = sessionStore.activeSessionId;
|
||||
if (!activeSessionId) {
|
||||
uiNotificationsStore.showError(t('quickCommands.form.errorNoActiveSession', '没有活动的SSH会话可执行指令。'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 触发 quickCommand:executeProcessed 事件
|
||||
emitWorkspaceEvent('quickCommand:executeProcessed', {
|
||||
command: processedCommand,
|
||||
sessionId: activeSessionId
|
||||
});
|
||||
};
|
||||
|
||||
// +++ 聚焦搜索框的方法 +++
|
||||
|
||||
@@ -174,6 +174,7 @@ onMounted(() => {
|
||||
subscribeToWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
|
||||
subscribeToWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
|
||||
subscribeToWorkspaceEvents('fileManager:openModalRequest', handleFileManagerOpenRequest); // +++ 订阅文件管理器打开请求 +++
|
||||
subscribeToWorkspaceEvents('quickCommand:executeProcessed', handleQuickCommandExecuteProcessed);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -218,6 +219,7 @@ onBeforeUnmount(() => {
|
||||
unsubscribeFromWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
|
||||
unsubscribeFromWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
|
||||
unsubscribeFromWorkspaceEvents('fileManager:openModalRequest', handleFileManagerOpenRequest); // +++ 取消订阅文件管理器打开请求 +++
|
||||
unsubscribeFromWorkspaceEvents('quickCommand:executeProcessed', handleQuickCommandExecuteProcessed);
|
||||
});
|
||||
|
||||
const subscribeToWorkspaceEvents = useWorkspaceEventSubscriber(); // +++ 定义订阅和取消订阅函数 +++
|
||||
@@ -683,6 +685,16 @@ const handleFileManagerOpenRequest = (payload: { sessionId: string }) => {
|
||||
console.log(`[WorkspaceView] Opening FileManager modal with props for session ${sessionId}:`, newProps);
|
||||
};
|
||||
|
||||
// --- 处理 quickCommand:executeProcessed 事件 ---
|
||||
const handleQuickCommandExecuteProcessed = (payload: WorkspaceEventPayloads['quickCommand:executeProcessed']) => {
|
||||
const { command, sessionId: targetSessionId } = payload;
|
||||
console.log(`[WorkspaceView] Received quickCommand:executeProcessed event. Command: "${command}", TargetSessionID: ${targetSessionId}`);
|
||||
|
||||
// 使用现有的 handleSendCommand 逻辑来发送指令
|
||||
// handleSendCommand 会处理 sessionId 未定义时使用 activeSessionId 的情况
|
||||
handleSendCommand(command, targetSessionId);
|
||||
};
|
||||
|
||||
const closeFileManagerModal = () => {
|
||||
showFileManagerModal.value = false;
|
||||
console.log('[WorkspaceView] FileManager modal hidden (kept alive).');
|
||||
@@ -902,7 +914,6 @@ const closeFileManagerModal = () => {
|
||||
/* 可以添加更多样式,例如背景色、边框等 */
|
||||
}
|
||||
|
||||
/* Ensure modals are still displayed correctly (they are outside the main flow) */
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user