重构(前端): 持久化快速命令排序和密码切换
添加持久化排序字段并重新排序快速命令和标签的端点,更新前端以支持手动拖放排序,并为连接和凭据表单添加密码可见性切换。此外,将 SSH 连接测试作为连接列表中的默认操作,并刷新相关模块文档和更改日志。
This commit is contained in:
@@ -334,6 +334,42 @@ const definedMigrations: Migration[] = [
|
||||
|
||||
ALTER TABLE connections ADD COLUMN login_credential_id INTEGER NULL REFERENCES login_credentials(id) ON DELETE SET NULL;
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Add sort_order column to quick_commands table',
|
||||
check: async (db: Database): Promise<boolean> => {
|
||||
const columnAlreadyExists = await columnExists(db, 'quick_commands', 'sort_order');
|
||||
return !columnAlreadyExists;
|
||||
},
|
||||
sql: `
|
||||
ALTER TABLE quick_commands ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE quick_commands SET sort_order = id WHERE sort_order = 0;
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Add sort_order column to quick_command_tags table',
|
||||
check: async (db: Database): Promise<boolean> => {
|
||||
const columnAlreadyExists = await columnExists(db, 'quick_command_tags', 'sort_order');
|
||||
return !columnAlreadyExists;
|
||||
},
|
||||
sql: `
|
||||
ALTER TABLE quick_command_tags ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE quick_command_tags SET sort_order = id WHERE sort_order = 0;
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Add sort_order column to quick_command_tag_associations table',
|
||||
check: async (db: Database): Promise<boolean> => {
|
||||
const columnAlreadyExists = await columnExists(db, 'quick_command_tag_associations', 'sort_order');
|
||||
return !columnAlreadyExists;
|
||||
},
|
||||
sql: `
|
||||
ALTER TABLE quick_command_tag_associations ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE quick_command_tag_associations SET sort_order = rowid WHERE sort_order = 0;
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ CREATE TABLE IF NOT EXISTS quick_commands (
|
||||
command TEXT NOT NULL, -- 指令必选
|
||||
usage_count INTEGER NOT NULL DEFAULT 0, -- 使用频率
|
||||
variables TEXT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
@@ -198,6 +199,7 @@ export const createQuickCommandTagsTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS quick_command_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
@@ -207,6 +209,7 @@ export const createQuickCommandTagAssociationsTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS quick_command_tag_associations (
|
||||
quick_command_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (quick_command_id, tag_id),
|
||||
FOREIGN KEY (quick_command_id) REFERENCES quick_commands(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES quick_command_tags(id) ON DELETE CASCADE
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as QuickCommandTagService from './quick-command-tag.service';
|
||||
|
||||
/**
|
||||
* 处理获取所有快捷指令标签的请求
|
||||
*/
|
||||
const isNumberArray = (value: unknown): value is number[] =>
|
||||
Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item));
|
||||
|
||||
export const getAllQuickCommandTags = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const tags = await QuickCommandTagService.getAllQuickCommandTags();
|
||||
res.status(200).json(tags);
|
||||
} catch (error: any) {
|
||||
console.error('[Controller] 获取快捷指令标签列表失败:', error.message);
|
||||
console.error('[QuickCommandTagController] 获取快捷指令标签列表失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法获取快捷指令标签列表' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理添加新快捷指令标签的请求
|
||||
*/
|
||||
export const addQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
res.status(400).json({ message: '标签名称不能为空且必须是字符串' });
|
||||
return;
|
||||
@@ -27,37 +23,33 @@ export const addQuickCommandTag = async (req: Request, res: Response): Promise<v
|
||||
|
||||
try {
|
||||
const newId = await QuickCommandTagService.addQuickCommandTag(name);
|
||||
// 成功添加后,获取新创建的标签信息返回给前端
|
||||
const newTag = await QuickCommandTagService.getQuickCommandTagById(newId);
|
||||
|
||||
if (newTag) {
|
||||
res.status(201).json({ message: '快捷指令标签已添加', tag: newTag });
|
||||
} else {
|
||||
// 理论上不应该发生,但作为健壮性检查
|
||||
console.error(`[Controller] 添加快捷指令标签后未能找到 ID: ${newId}`);
|
||||
res.status(201).json({ message: '快捷指令标签已添加,但无法检索新记录', id: newId });
|
||||
res.status(201).json({ message: '快捷指令标签已添加', tag: newTag });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json({ message: '快捷指令标签已添加,但无法检索新记录', id: newId });
|
||||
} catch (error: any) {
|
||||
console.error('[Controller] 添加快捷指令标签失败:', error.message);
|
||||
// 检查是否是名称重复错误
|
||||
if (error.message && error.message.includes('已存在')) {
|
||||
res.status(409).json({ message: error.message }); // 409 Conflict
|
||||
} else {
|
||||
res.status(500).json({ message: error.message || '无法添加快捷指令标签' });
|
||||
console.error('[QuickCommandTagController] 添加快捷指令标签失败:', error.message);
|
||||
if (error.message?.includes('已存在')) {
|
||||
res.status(409).json({ message: error.message });
|
||||
return;
|
||||
}
|
||||
res.status(500).json({ message: error.message || '无法添加快捷指令标签' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理更新快捷指令标签的请求
|
||||
*/
|
||||
export const updateQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const id = Number.parseInt(req.params.id, 10);
|
||||
const { name } = req.body;
|
||||
|
||||
if (isNaN(id)) {
|
||||
if (Number.isNaN(id)) {
|
||||
res.status(400).json({ message: '无效的标签 ID' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
res.status(400).json({ message: '标签名称不能为空且必须是字符串' });
|
||||
return;
|
||||
@@ -65,66 +57,67 @@ export const updateQuickCommandTag = async (req: Request, res: Response): Promis
|
||||
|
||||
try {
|
||||
const success = await QuickCommandTagService.updateQuickCommandTag(id, name);
|
||||
if (success) {
|
||||
// 成功更新后,获取更新后的标签信息返回给前端
|
||||
const updatedTag = await QuickCommandTagService.getQuickCommandTagById(id);
|
||||
if (updatedTag) {
|
||||
res.status(200).json({ message: '快捷指令标签已更新', tag: updatedTag });
|
||||
} else {
|
||||
console.error(`[Controller] 更新快捷指令标签后未能找到 ID: ${id}`);
|
||||
res.status(200).json({ message: '快捷指令标签已更新,但无法检索更新后的记录' });
|
||||
}
|
||||
} else {
|
||||
// 检查标签是否真的不存在
|
||||
if (!success) {
|
||||
const tagExists = await QuickCommandTagService.getQuickCommandTagById(id);
|
||||
if (!tagExists) {
|
||||
res.status(404).json({ message: '未找到要更新的快捷指令标签' });
|
||||
} else {
|
||||
// 如果标签存在但更新失败(理论上不太可能,除非并发问题),返回服务器错误
|
||||
console.error(`[Controller] 更新快捷指令标签 ${id} 失败,但标签存在。`);
|
||||
res.status(500).json({ message: '更新快捷指令标签时发生未知错误' });
|
||||
}
|
||||
res.status(tagExists ? 500 : 404).json({
|
||||
message: tagExists ? '更新快捷指令标签时发生未知错误' : '未找到要更新的快捷指令标签',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTag = await QuickCommandTagService.getQuickCommandTagById(id);
|
||||
if (updatedTag) {
|
||||
res.status(200).json({ message: '快捷指令标签已更新', tag: updatedTag });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ message: '快捷指令标签已更新,但无法检索更新后的记录' });
|
||||
} catch (error: any) {
|
||||
console.error('[Controller] 更新快捷指令标签失败:', error.message);
|
||||
// 检查是否是名称重复错误
|
||||
if (error.message && error.message.includes('已存在')) {
|
||||
res.status(409).json({ message: error.message }); // 409 Conflict
|
||||
} else {
|
||||
res.status(500).json({ message: error.message || '无法更新快捷指令标签' });
|
||||
console.error('[QuickCommandTagController] 更新快捷指令标签失败:', error.message);
|
||||
if (error.message?.includes('已存在')) {
|
||||
res.status(409).json({ message: error.message });
|
||||
return;
|
||||
}
|
||||
res.status(500).json({ message: error.message || '无法更新快捷指令标签' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理删除快捷指令标签的请求
|
||||
*/
|
||||
export const deleteQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
const id = Number.parseInt(req.params.id, 10);
|
||||
if (Number.isNaN(id)) {
|
||||
res.status(400).json({ message: '无效的标签 ID' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先检查标签是否存在,以便返回 404
|
||||
const tagExists = await QuickCommandTagService.getQuickCommandTagById(id);
|
||||
if (!tagExists) {
|
||||
res.status(404).json({ message: '未找到要删除的快捷指令标签' });
|
||||
return;
|
||||
res.status(404).json({ message: '未找到要删除的快捷指令标签' });
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await QuickCommandTagService.deleteQuickCommandTag(id);
|
||||
if (success) {
|
||||
res.status(200).json({ message: '快捷指令标签已删除' });
|
||||
} else {
|
||||
// 如果上面检查存在但删除失败,说明有内部错误
|
||||
console.error(`[Controller] 删除快捷指令标签 ${id} 失败,但标签存在。`);
|
||||
res.status(500).json({ message: '删除快捷指令标签时发生未知错误' });
|
||||
}
|
||||
res.status(success ? 200 : 500).json({
|
||||
message: success ? '快捷指令标签已删除' : '删除快捷指令标签时发生未知错误',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Controller] 删除快捷指令标签失败:', error.message);
|
||||
console.error('[QuickCommandTagController] 删除快捷指令标签失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法删除快捷指令标签' });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const reorderQuickCommandTags = async (req: Request, res: Response): Promise<void> => {
|
||||
const { tagIds } = req.body;
|
||||
if (!isNumberArray(tagIds) || tagIds.length === 0) {
|
||||
res.status(400).json({ message: 'tagIds 必须是非空数字数组' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await QuickCommandTagService.reorderQuickCommandTags(tagIds);
|
||||
res.status(200).json({ message: '快捷指令标签顺序已更新' });
|
||||
} catch (error: any) {
|
||||
console.error('[QuickCommandTagController] 更新快捷指令标签顺序失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法更新快捷指令标签顺序' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,197 +1,268 @@
|
||||
import { Database } from 'sqlite3';
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
|
||||
// 定义 Quick Command Tag 类型
|
||||
export interface QuickCommandTag {
|
||||
id: number;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有快捷指令标签
|
||||
*/
|
||||
interface CommandTagAssociationRow {
|
||||
tag_id: number;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
const getNextTagSortOrder = async (db: Awaited<ReturnType<typeof getDbInstance>>): Promise<number> => {
|
||||
const row = await getDbRow<{ nextSortOrder?: number }>(
|
||||
db,
|
||||
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS nextSortOrder FROM quick_command_tags'
|
||||
);
|
||||
return row?.nextSortOrder ?? 1;
|
||||
};
|
||||
|
||||
const getNextAssociationSortOrder = async (
|
||||
db: Awaited<ReturnType<typeof getDbInstance>>,
|
||||
tagId: number,
|
||||
): Promise<number> => {
|
||||
const row = await getDbRow<{ nextSortOrder?: number }>(
|
||||
db,
|
||||
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS nextSortOrder FROM quick_command_tag_associations WHERE tag_id = ?',
|
||||
[tagId],
|
||||
);
|
||||
return row?.nextSortOrder ?? 1;
|
||||
};
|
||||
|
||||
export const findAllQuickCommandTags = async (): Promise<QuickCommandTag[]> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const rows = await allDb<QuickCommandTag>(db, `SELECT * FROM quick_command_tags ORDER BY name ASC`);
|
||||
return rows;
|
||||
return await allDb<QuickCommandTag>(
|
||||
db,
|
||||
'SELECT * FROM quick_command_tags ORDER BY sort_order ASC, name ASC',
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error('[仓库] 查询快捷指令标签列表时出错:', err.message);
|
||||
console.error('[QuickCommandTagRepository] 查询快捷指令标签列表失败:', err.message);
|
||||
throw new Error('获取快捷指令标签列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个快捷指令标签
|
||||
*/
|
||||
export const findQuickCommandTagById = async (id: number): Promise<QuickCommandTag | null> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const row = await getDbRow<QuickCommandTag>(db, `SELECT * FROM quick_command_tags WHERE id = ?`, [id]);
|
||||
return row || null;
|
||||
} catch (err: any) {
|
||||
console.error(`[仓库] 查询快捷指令标签 ${id} 时出错:`, err.message);
|
||||
throw new Error('获取快捷指令标签信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建新快捷指令标签
|
||||
*/
|
||||
export const createQuickCommandTag = async (name: string): Promise<number> => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const sql = `INSERT INTO quick_command_tags (name, created_at, updated_at) VALUES (?, ?, ?)`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [name, now, now]);
|
||||
const row = await getDbRow<QuickCommandTag>(db, 'SELECT * FROM quick_command_tags WHERE id = ?', [id]);
|
||||
return row ?? null;
|
||||
} catch (err: any) {
|
||||
console.error(`[QuickCommandTagRepository] 查询快捷指令标签 ${id} 失败:`, err.message);
|
||||
throw new Error('获取快捷指令标签信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
export const createQuickCommandTag = async (name: string): Promise<number> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const sortOrder = await getNextTagSortOrder(db);
|
||||
const result = await runDb(
|
||||
db,
|
||||
'INSERT INTO quick_command_tags (name, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?)',
|
||||
[name, sortOrder, now, now],
|
||||
);
|
||||
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('创建快捷指令标签后未能获取有效的 lastID');
|
||||
throw new Error('创建快捷指令标签后未能获取有效的 lastID');
|
||||
}
|
||||
|
||||
return result.lastID;
|
||||
} catch (err: any) {
|
||||
console.error('[仓库] 创建快捷指令标签时出错:', err.message);
|
||||
console.error('[QuickCommandTagRepository] 创建快捷指令标签失败:', err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
|
||||
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
|
||||
}
|
||||
throw new Error(`创建快捷指令标签失败: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新快捷指令标签名称
|
||||
*/
|
||||
export const updateQuickCommandTag = async (id: number, name: string): Promise<boolean> => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const sql = `UPDATE quick_command_tags SET name = ?, updated_at = ? WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [name, now, id]);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const result = await runDb(
|
||||
db,
|
||||
'UPDATE quick_command_tags SET name = ?, updated_at = ? WHERE id = ?',
|
||||
[name, now, id],
|
||||
);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`[仓库] 更新快捷指令标签 ${id} 时出错:`, err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
|
||||
}
|
||||
throw new Error(`更新快捷指令标签失败: ${err.message}`);
|
||||
console.error(`[QuickCommandTagRepository] 更新快捷指令标签 ${id} 失败:`, err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
|
||||
}
|
||||
throw new Error(`更新快捷指令标签失败: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除快捷指令标签 (同时会通过外键 CASCADE 删除关联)
|
||||
*/
|
||||
export const deleteQuickCommandTag = async (id: number): Promise<boolean> => {
|
||||
const sql = `DELETE FROM quick_command_tags WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// 由于 quick_command_tag_associations 设置了 ON DELETE CASCADE,
|
||||
// 删除 quick_command_tags 中的记录会自动删除关联表中的相关记录。
|
||||
const result = await runDb(db, sql, [id]);
|
||||
const result = await runDb(db, 'DELETE FROM quick_command_tags WHERE id = ?', [id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`[仓库] 删除快捷指令标签 ${id} 时出错:`, err.message);
|
||||
console.error(`[QuickCommandTagRepository] 删除快捷指令标签 ${id} 失败:`, err.message);
|
||||
throw new Error('删除快捷指令标签失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置单个快捷指令的标签关联 (先删除旧关联,再插入新关联)
|
||||
* @param commandId - 快捷指令 ID
|
||||
* @param tagIds - 新的标签 ID 数组 (空数组表示清除所有关联)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const setCommandTagAssociations = async (commandId: number, tagIds: number[]): Promise<void> => {
|
||||
const db = await getDbInstance();
|
||||
const deleteSql = `DELETE FROM quick_command_tag_associations WHERE quick_command_id = ?`;
|
||||
const insertSql = `INSERT INTO quick_command_tag_associations (quick_command_id, tag_id) VALUES (?, ?)`;
|
||||
export const reorderQuickCommandTags = async (tagIds: number[]): Promise<void> => {
|
||||
const normalizedTagIds = Array.from(new Set(tagIds.filter((tagId) => Number.isInteger(tagId) && tagId > 0)));
|
||||
if (normalizedTagIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await getDbInstance();
|
||||
try {
|
||||
await runDb(db, 'BEGIN TRANSACTION');
|
||||
// 1. 删除该指令的所有旧关联
|
||||
await runDb(db, deleteSql, [commandId]);
|
||||
|
||||
// 2. 插入新关联 (如果 tagIds 不为空)
|
||||
if (tagIds && tagIds.length > 0) {
|
||||
const stmt = await db.prepare(insertSql);
|
||||
for (const tagId of tagIds) {
|
||||
// 验证 tagId 是否为有效数字
|
||||
if (typeof tagId !== 'number' || isNaN(tagId)) {
|
||||
console.warn(`[Repo] setCommandTagAssociations: 无效的 tagId (${tagId}),跳过关联到指令 ${commandId}。`);
|
||||
continue;
|
||||
}
|
||||
await stmt.run(commandId, tagId);
|
||||
}
|
||||
await stmt.finalize();
|
||||
for (let index = 0; index < normalizedTagIds.length; index += 1) {
|
||||
await runDb(
|
||||
db,
|
||||
'UPDATE quick_command_tags SET sort_order = ?, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
|
||||
[index + 1, normalizedTagIds[index]],
|
||||
);
|
||||
}
|
||||
await runDb(db, 'COMMIT');
|
||||
} catch (err: any) {
|
||||
console.error('设置快捷指令标签关联时出错:', err.message);
|
||||
await runDb(db, 'ROLLBACK'); // 出错时回滚
|
||||
await runDb(db, 'ROLLBACK');
|
||||
console.error('[QuickCommandTagRepository] 重排快捷指令标签失败:', err.message);
|
||||
throw new Error('无法更新快捷指令标签顺序');
|
||||
}
|
||||
};
|
||||
|
||||
export const setCommandTagAssociations = async (commandId: number, tagIds: number[]): Promise<void> => {
|
||||
const normalizedTagIds = Array.from(new Set(tagIds.filter((tagId) => Number.isInteger(tagId) && tagId > 0)));
|
||||
const db = await getDbInstance();
|
||||
|
||||
try {
|
||||
const existingAssociations = await allDb<CommandTagAssociationRow>(
|
||||
db,
|
||||
'SELECT tag_id, sort_order FROM quick_command_tag_associations WHERE quick_command_id = ?',
|
||||
[commandId],
|
||||
);
|
||||
const existingTagIds = new Set(existingAssociations.map((association) => association.tag_id));
|
||||
|
||||
await runDb(db, 'BEGIN TRANSACTION');
|
||||
|
||||
if (normalizedTagIds.length === 0) {
|
||||
await runDb(db, 'DELETE FROM quick_command_tag_associations WHERE quick_command_id = ?', [commandId]);
|
||||
} else {
|
||||
const placeholders = normalizedTagIds.map(() => '?').join(', ');
|
||||
await runDb(
|
||||
db,
|
||||
`DELETE FROM quick_command_tag_associations WHERE quick_command_id = ? AND tag_id NOT IN (${placeholders})`,
|
||||
[commandId, ...normalizedTagIds],
|
||||
);
|
||||
|
||||
for (const tagId of normalizedTagIds) {
|
||||
if (existingTagIds.has(tagId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextSortOrder = await getNextAssociationSortOrder(db, tagId);
|
||||
await runDb(
|
||||
db,
|
||||
'INSERT INTO quick_command_tag_associations (quick_command_id, tag_id, sort_order) VALUES (?, ?, ?)',
|
||||
[commandId, tagId, nextSortOrder],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await runDb(db, 'COMMIT');
|
||||
} catch (err: any) {
|
||||
await runDb(db, 'ROLLBACK');
|
||||
console.error('[QuickCommandTagRepository] 设置快捷指令标签关联失败:', err.message);
|
||||
throw new Error('无法设置快捷指令标签关联');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将单个标签批量添加到多个快捷指令
|
||||
* @param commandIds - 需要添加标签的快捷指令 ID 数组
|
||||
* @param tagId - 要添加的标签 ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const addTagToCommands = async (commandIds: number[], tagId: number): Promise<void> => {
|
||||
if (!commandIds || commandIds.length === 0) {
|
||||
return; // 没有指令需要关联
|
||||
const normalizedCommandIds = Array.from(
|
||||
new Set(commandIds.filter((commandId) => Number.isInteger(commandId) && commandId > 0)),
|
||||
);
|
||||
|
||||
if (normalizedCommandIds.length === 0 || !Number.isInteger(tagId) || tagId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await getDbInstance();
|
||||
const insertSql = `INSERT OR IGNORE INTO quick_command_tag_associations (quick_command_id, tag_id) VALUES (?, ?)`;
|
||||
|
||||
try {
|
||||
await runDb(db, 'BEGIN TRANSACTION');
|
||||
// 准备批量插入语句
|
||||
const stmt = await db.prepare(insertSql);
|
||||
for (const commandId of commandIds) {
|
||||
// 验证 commandId 和 tagId 是否为有效数字(可选,但推荐)
|
||||
if (typeof commandId !== 'number' || isNaN(commandId) || typeof tagId !== 'number' || isNaN(tagId)) {
|
||||
console.warn(`[Repo] addTagToCommands: 无效的 commandId (${commandId}) 或 tagId (${tagId}),跳过关联。`);
|
||||
continue;
|
||||
for (const commandId of normalizedCommandIds) {
|
||||
const existingAssociation = await getDbRow<{ quick_command_id: number }>(
|
||||
db,
|
||||
'SELECT quick_command_id FROM quick_command_tag_associations WHERE quick_command_id = ? AND tag_id = ?',
|
||||
[commandId, tagId],
|
||||
);
|
||||
|
||||
if (existingAssociation) {
|
||||
continue;
|
||||
}
|
||||
await stmt.run(commandId, tagId);
|
||||
|
||||
const nextSortOrder = await getNextAssociationSortOrder(db, tagId);
|
||||
await runDb(
|
||||
db,
|
||||
'INSERT INTO quick_command_tag_associations (quick_command_id, tag_id, sort_order) VALUES (?, ?, ?)',
|
||||
[commandId, tagId, nextSortOrder],
|
||||
);
|
||||
}
|
||||
await stmt.finalize(); // 完成批量插入
|
||||
|
||||
await runDb(db, 'COMMIT');
|
||||
console.log(`[Repo] addTagToCommands: 成功将标签 ${tagId} 关联到 ${commandIds.length} 个指令。`);
|
||||
} catch (err: any) {
|
||||
console.error(`[Repo] addTagToCommands: 批量关联标签 ${tagId} 到指令时出错:`, err.message);
|
||||
await runDb(db, 'ROLLBACK');
|
||||
console.error(`[QuickCommandTagRepository] 批量关联标签 ${tagId} 失败:`, err.message);
|
||||
throw new Error('无法批量关联标签到快捷指令');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定快捷指令的标签关联 (使用事务)
|
||||
* @param commandId 快捷指令 ID
|
||||
* @param tagIds 新的快捷指令标签 ID 数组 (空数组表示清除所有标签)
|
||||
*/
|
||||
// Removed the duplicate function declaration that returned Promise<boolean>
|
||||
export const reorderCommandsInTag = async (tagId: number, commandIds: number[]): Promise<void> => {
|
||||
const normalizedCommandIds = Array.from(
|
||||
new Set(commandIds.filter((commandId) => Number.isInteger(commandId) && commandId > 0)),
|
||||
);
|
||||
|
||||
if (!Number.isInteger(tagId) || tagId <= 0 || normalizedCommandIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await getDbInstance();
|
||||
|
||||
try {
|
||||
await runDb(db, 'BEGIN TRANSACTION');
|
||||
for (let index = 0; index < normalizedCommandIds.length; index += 1) {
|
||||
await runDb(
|
||||
db,
|
||||
'UPDATE quick_command_tag_associations SET sort_order = ? WHERE tag_id = ? AND quick_command_id = ?',
|
||||
[index + 1, tagId, normalizedCommandIds[index]],
|
||||
);
|
||||
}
|
||||
await runDb(db, 'COMMIT');
|
||||
} catch (err: any) {
|
||||
await runDb(db, 'ROLLBACK');
|
||||
console.error(`[QuickCommandTagRepository] 重排标签 ${tagId} 内命令失败:`, err.message);
|
||||
throw new Error('无法更新标签内快捷指令顺序');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 查找指定快捷指令的所有标签
|
||||
* @param commandId 快捷指令 ID
|
||||
* @returns 标签对象数组 { id: number, name: string }[]
|
||||
*/
|
||||
export const findTagsByCommandId = async (commandId: number): Promise<QuickCommandTag[]> => {
|
||||
const sql = `
|
||||
SELECT t.id, t.name, t.created_at, t.updated_at
|
||||
SELECT t.id, t.name, t.sort_order, t.created_at, t.updated_at
|
||||
FROM quick_command_tags t
|
||||
JOIN quick_command_tag_associations ta ON t.id = ta.tag_id
|
||||
WHERE ta.quick_command_id = ?
|
||||
ORDER BY t.name ASC`;
|
||||
ORDER BY ta.sort_order ASC, t.name ASC`;
|
||||
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const rows = await allDb<QuickCommandTag>(db, sql, [commandId]);
|
||||
return rows;
|
||||
return await allDb<QuickCommandTag>(db, sql, [commandId]);
|
||||
} catch (err: any) {
|
||||
console.error(`Repository: 查询快捷指令 ${commandId} 的标签时出错:`, err.message);
|
||||
console.error(`[QuickCommandTagRepository] 查询快捷指令 ${commandId} 的标签失败:`, err.message);
|
||||
throw new Error('获取快捷指令标签失败');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import express from 'express';
|
||||
import * as QuickCommandTagController from './quick-command-tag.controller';
|
||||
import { isAuthenticated } from '../auth/auth.middleware'; // 假设需要认证
|
||||
import { isAuthenticated } from '../auth/auth.middleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 获取所有快捷指令标签
|
||||
router.get('/', isAuthenticated, QuickCommandTagController.getAllQuickCommandTags);
|
||||
|
||||
// 添加新的快捷指令标签
|
||||
router.post('/', isAuthenticated, QuickCommandTagController.addQuickCommandTag);
|
||||
|
||||
// 更新快捷指令标签
|
||||
router.put('/reorder', isAuthenticated, QuickCommandTagController.reorderQuickCommandTags);
|
||||
router.put('/:id', isAuthenticated, QuickCommandTagController.updateQuickCommandTag);
|
||||
|
||||
// 删除快捷指令标签
|
||||
router.delete('/:id', isAuthenticated, QuickCommandTagController.deleteQuickCommandTag);
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@@ -1,118 +1,50 @@
|
||||
import * as QuickCommandTagRepository from '../quick-command-tags/quick-command-tag.repository';
|
||||
import { QuickCommandTag } from '../quick-command-tags/quick-command-tag.repository';
|
||||
|
||||
/**
|
||||
* 获取所有快捷指令标签
|
||||
*/
|
||||
export const getAllQuickCommandTags = async (): Promise<QuickCommandTag[]> => {
|
||||
return QuickCommandTagRepository.findAllQuickCommandTags();
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个快捷指令标签
|
||||
*/
|
||||
export const getQuickCommandTagById = async (id: number): Promise<QuickCommandTag | null> => {
|
||||
return QuickCommandTagRepository.findQuickCommandTagById(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加新的快捷指令标签
|
||||
* @param name 标签名称
|
||||
* @returns 返回新标签的 ID
|
||||
*/
|
||||
export const addQuickCommandTag = async (name: string): Promise<number> => {
|
||||
if (!name || name.trim().length === 0) {
|
||||
throw new Error('标签名称不能为空');
|
||||
}
|
||||
const trimmedName = name.trim();
|
||||
// 可以在这里添加更多验证逻辑,例如检查名称格式等
|
||||
try {
|
||||
const newId = await QuickCommandTagRepository.createQuickCommandTag(trimmedName);
|
||||
return newId;
|
||||
} catch (error: any) {
|
||||
// Service 层可以重新抛出或处理 Repository 抛出的错误
|
||||
console.error(`[Service] 添加快捷指令标签 "${trimmedName}" 失败:`, error.message);
|
||||
throw error; // 重新抛出,让 Controller 处理 HTTP 响应
|
||||
}
|
||||
|
||||
return QuickCommandTagRepository.createQuickCommandTag(name.trim());
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新快捷指令标签
|
||||
* @param id 标签 ID
|
||||
* @param name 新的标签名称
|
||||
* @returns 返回是否成功更新
|
||||
*/
|
||||
export const updateQuickCommandTag = async (id: number, name: string): Promise<boolean> => {
|
||||
if (!name || name.trim().length === 0) {
|
||||
throw new Error('标签名称不能为空');
|
||||
}
|
||||
const trimmedName = name.trim();
|
||||
// 可以在这里添加更多验证逻辑
|
||||
try {
|
||||
const success = await QuickCommandTagRepository.updateQuickCommandTag(id, trimmedName);
|
||||
if (!success) {
|
||||
// 可能需要检查标签是否存在,或者让 Repository 处理
|
||||
console.warn(`[Service] 尝试更新不存在的快捷指令标签 ID: ${id}`);
|
||||
}
|
||||
return success;
|
||||
} catch (error: any) {
|
||||
console.error(`[Service] 更新快捷指令标签 ${id} 失败:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return QuickCommandTagRepository.updateQuickCommandTag(id, name.trim());
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除快捷指令标签
|
||||
* @param id 标签 ID
|
||||
* @returns 返回是否成功删除
|
||||
*/
|
||||
export const deleteQuickCommandTag = async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
const success = await QuickCommandTagRepository.deleteQuickCommandTag(id);
|
||||
if (!success) {
|
||||
console.warn(`[Service] 尝试删除不存在的快捷指令标签 ID: ${id}`);
|
||||
}
|
||||
return success;
|
||||
} catch (error: any) {
|
||||
console.error(`[Service] 删除快捷指令标签 ${id} 失败:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
return QuickCommandTagRepository.deleteQuickCommandTag(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置指定快捷指令的标签关联
|
||||
* @param commandId 快捷指令 ID
|
||||
* @param tagIds 新的快捷指令标签 ID 数组
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const setCommandTags = async (commandId: number, tagIds: number[]): Promise<void> => {
|
||||
// 验证 tagIds 是否为数字数组 (基本验证)
|
||||
if (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number')) {
|
||||
throw new Error('标签 ID 列表必须是一个数字数组');
|
||||
if (!Array.isArray(tagIds) || !tagIds.every((id) => typeof id === 'number')) {
|
||||
throw new Error('标签 ID 列表必须是数字数组');
|
||||
}
|
||||
// 可以在这里添加更复杂的验证,例如检查 tagIds 是否都存在于 quick_command_tags 表中
|
||||
// 但 Repository 中的 setCommandTagAssociations 已包含基本的检查和错误处理
|
||||
|
||||
try {
|
||||
// 直接调用 Repository 处理关联更新 (Repository 函数现在返回 void)
|
||||
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
|
||||
// Service 函数也返回 void,所以不需要 return
|
||||
} catch (error: any) {
|
||||
console.error(`[Service] 设置快捷指令 ${commandId} 的标签失败:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定快捷指令的所有标签
|
||||
* @param commandId 快捷指令 ID
|
||||
* @returns 标签对象数组
|
||||
*/
|
||||
export const getTagsForCommand = async (commandId: number): Promise<QuickCommandTag[]> => {
|
||||
try {
|
||||
return await QuickCommandTagRepository.findTagsByCommandId(commandId);
|
||||
} catch (error: any) {
|
||||
console.error(`[Service] 获取快捷指令 ${commandId} 的标签失败:`, error.message);
|
||||
throw error;
|
||||
return QuickCommandTagRepository.findTagsByCommandId(commandId);
|
||||
};
|
||||
|
||||
export const reorderQuickCommandTags = async (tagIds: number[]): Promise<void> => {
|
||||
if (!Array.isArray(tagIds) || !tagIds.every((id) => typeof id === 'number')) {
|
||||
throw new Error('tagIds 必须是数字数组');
|
||||
}
|
||||
};
|
||||
|
||||
await QuickCommandTagRepository.reorderQuickCommandTags(tagIds);
|
||||
};
|
||||
|
||||
@@ -2,202 +2,202 @@ import { Request, Response } from 'express';
|
||||
import * as QuickCommandsService from './quick-commands.service';
|
||||
import { QuickCommandSortBy } from './quick-commands.service';
|
||||
|
||||
/**
|
||||
* 处理添加新快捷指令的请求
|
||||
*/
|
||||
const isNumberArray = (value: unknown): value is number[] =>
|
||||
Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item));
|
||||
|
||||
export const addQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
||||
// 从请求体中解构出 name, command, 以及可选的 tagIds 和 variables
|
||||
const { name, command, tagIds, variables } = 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;
|
||||
}
|
||||
// 验证 tagIds (如果提供的话)
|
||||
if (tagIds !== undefined && (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number'))) {
|
||||
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
|
||||
res.status(400).json({ message: '名称必须是字符串或 null' });
|
||||
return;
|
||||
}
|
||||
// 验证 variables (如果提供的话)
|
||||
|
||||
if (tagIds !== undefined && !isNumberArray(tagIds)) {
|
||||
res.status(400).json({ message: 'tagIds 必须是数字数组' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (variables !== undefined && (typeof variables !== 'object' || variables === null || Array.isArray(variables))) {
|
||||
res.status(400).json({ message: 'variables 必须是一个对象' });
|
||||
res.status(400).json({ message: 'variables 必须是对象' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 将 tagIds 和 variables 传递给 Service 层
|
||||
const newId = await QuickCommandsService.addQuickCommand(name, command, tagIds, variables);
|
||||
// 尝试获取新创建的带标签的指令信息返回
|
||||
const newCommand = await QuickCommandsService.getQuickCommandById(newId);
|
||||
|
||||
if (newCommand) {
|
||||
res.status(201).json({ message: '快捷指令已添加', command: newCommand });
|
||||
} else {
|
||||
console.error(`[Controller] 添加快捷指令后未能找到 ID: ${newId}`);
|
||||
res.status(201).json({ message: '快捷指令已添加,但无法检索新记录', id: newId });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json({ message: '快捷指令已添加,但无法检索新记录', id: newId });
|
||||
} catch (error: any) {
|
||||
console.error('[Controller] 添加快捷指令失败:', error.message);
|
||||
console.error('[QuickCommandsController] 添加快捷指令失败:', error.message);
|
||||
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';
|
||||
const validSortBy: QuickCommandSortBy =
|
||||
sortBy === 'name' || sortBy === 'usage_count' || sortBy === 'manual' ? sortBy : 'manual';
|
||||
|
||||
try {
|
||||
const commands = await QuickCommandsService.getAllQuickCommands(validSortBy);
|
||||
res.status(200).json(commands);
|
||||
} catch (error: any) {
|
||||
console.error('获取快捷指令控制器出错:', error);
|
||||
console.error('[QuickCommandsController] 获取快捷指令失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法获取快捷指令' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理更新快捷指令的请求
|
||||
*/
|
||||
export const updateQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
// 从请求体中解构出 name, command, 以及可选的 tagIds 和 variables
|
||||
const id = Number.parseInt(req.params.id, 10);
|
||||
const { name, command, tagIds, variables } = req.body;
|
||||
|
||||
// --- 基本验证 ---
|
||||
if (isNaN(id)) {
|
||||
if (Number.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;
|
||||
}
|
||||
// 验证 tagIds (如果提供的话)
|
||||
// 注意: tagIds 为 undefined 表示不更新标签,空数组 [] 表示清除所有标签
|
||||
if (tagIds !== undefined && (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number'))) {
|
||||
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
|
||||
res.status(400).json({ message: '名称必须是字符串或 null' });
|
||||
return;
|
||||
}
|
||||
// 验证 variables (如果提供的话)
|
||||
// undefined 表示不更新 variables, null 或对象表示要更新
|
||||
if (variables !== undefined && variables !== null && (typeof variables !== 'object' || Array.isArray(variables))) {
|
||||
|
||||
if (tagIds !== undefined && !isNumberArray(tagIds)) {
|
||||
res.status(400).json({ message: 'tagIds 必须是数字数组' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
variables !== undefined &&
|
||||
variables !== null &&
|
||||
(typeof variables !== 'object' || Array.isArray(variables))
|
||||
) {
|
||||
res.status(400).json({ message: 'variables 必须是对象或 null' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将 tagIds 和 variables 传递给 Service 层
|
||||
const success = await QuickCommandsService.updateQuickCommand(id, name, command, tagIds, variables);
|
||||
if (success) {
|
||||
// 尝试获取更新后的带标签的指令信息返回
|
||||
const updatedCommand = await QuickCommandsService.getQuickCommandById(id);
|
||||
if (updatedCommand) {
|
||||
res.status(200).json({ message: '快捷指令已更新', command: updatedCommand });
|
||||
} else {
|
||||
console.error(`[Controller] 更新快捷指令后未能找到 ID: ${id}`);
|
||||
res.status(200).json({ message: '快捷指令已更新,但无法检索更新后的记录' });
|
||||
}
|
||||
} else {
|
||||
// 检查指令是否真的不存在
|
||||
if (!success) {
|
||||
const commandExists = await QuickCommandsService.getQuickCommandById(id);
|
||||
if (!commandExists) {
|
||||
res.status(404).json({ message: '未找到要更新的快捷指令' });
|
||||
} else {
|
||||
console.error(`[Controller] 更新快捷指令 ${id} 失败,但指令存在。`);
|
||||
res.status(500).json({ message: '更新快捷指令时发生未知错误' });
|
||||
}
|
||||
res.status(commandExists ? 500 : 404).json({
|
||||
message: commandExists ? '更新快捷指令时发生未知错误' : '未找到要更新的快捷指令',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedCommand = await QuickCommandsService.getQuickCommandById(id);
|
||||
if (updatedCommand) {
|
||||
res.status(200).json({ message: '快捷指令已更新', command: updatedCommand });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ message: '快捷指令已更新,但无法检索更新后的记录' });
|
||||
} catch (error: any) {
|
||||
console.error('更新快捷指令控制器出错:', error);
|
||||
console.error('[QuickCommandsController] 更新快捷指令失败:', error.message);
|
||||
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)) {
|
||||
const id = Number.parseInt(req.params.id, 10);
|
||||
if (Number.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: '未找到要删除的快捷指令' });
|
||||
}
|
||||
res.status(success ? 200 : 404).json({
|
||||
message: success ? '快捷指令已删除' : '未找到要删除的快捷指令',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('删除快捷指令控制器出错:', error);
|
||||
console.error('[QuickCommandsController] 删除快捷指令失败:', error.message);
|
||||
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)) {
|
||||
const id = Number.parseInt(req.params.id, 10);
|
||||
if (Number.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: '使用次数已记录 (或指令不存在)' });
|
||||
}
|
||||
res.status(200).json({
|
||||
message: success ? '使用次数已增加' : '使用次数已记录(或指令不存在)',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('增加快捷指令使用次数控制器出错:', error);
|
||||
console.error('[QuickCommandsController] 增加快捷指令使用次数失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法增加使用次数' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量将标签分配给多个快捷指令
|
||||
*/
|
||||
export const assignTagToCommands = async (req: Request, res: Response): Promise<void> => { // Add : Promise<void>
|
||||
export const assignTagToCommands = async (req: Request, res: Response): Promise<void> => {
|
||||
const { commandIds, tagId } = req.body;
|
||||
|
||||
// 基本验证
|
||||
if (!Array.isArray(commandIds) || commandIds.length === 0 || typeof tagId !== 'number') {
|
||||
res.status(400).json({ success: false, message: '请求体必须包含 commandIds (非空数组) 和 tagId (数字)。' });
|
||||
return; // Use return without value to exit early
|
||||
if (!isNumberArray(commandIds) || commandIds.length === 0 || typeof tagId !== 'number') {
|
||||
res.status(400).json({ success: false, message: '请求体必须包含 commandIds 和 tagId' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 Service 函数处理批量分配
|
||||
console.log(`[Controller] assignTagToCommands: Received commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`);
|
||||
await QuickCommandsService.assignTagToCommands(commandIds, tagId);
|
||||
res.status(200).json({ success: true, message: `标签 ${tagId} 已成功尝试关联到 ${commandIds.length} 个指令。` });
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `标签 ${tagId} 已成功关联到 ${commandIds.length} 个指令`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Controller] 批量分配标签时出错:', error.message);
|
||||
// 根据错误类型返回不同的状态码可能更好,但这里简化处理
|
||||
res.status(500).json({ success: false, message: error.message || '批量分配标签时发生内部服务器错误。' });
|
||||
// No return needed here, error handling completes the response
|
||||
console.error('[QuickCommandsController] 批量分配标签失败:', error.message);
|
||||
res.status(500).json({ success: false, message: error.message || '批量分配标签失败' });
|
||||
}
|
||||
};
|
||||
|
||||
export const reorderQuickCommands = async (req: Request, res: Response): Promise<void> => {
|
||||
const { commandIds } = req.body;
|
||||
if (!isNumberArray(commandIds) || commandIds.length === 0) {
|
||||
res.status(400).json({ message: 'commandIds 必须是非空数字数组' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await QuickCommandsService.reorderQuickCommands(commandIds);
|
||||
res.status(200).json({ message: '快捷指令顺序已更新' });
|
||||
} catch (error: any) {
|
||||
console.error('[QuickCommandsController] 更新快捷指令顺序失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法更新快捷指令顺序' });
|
||||
}
|
||||
};
|
||||
|
||||
export const reorderCommandsByTag = async (req: Request, res: Response): Promise<void> => {
|
||||
const { tagId, commandIds } = req.body;
|
||||
if (typeof tagId !== 'number' || !isNumberArray(commandIds) || commandIds.length === 0) {
|
||||
res.status(400).json({ message: 'tagId 和 commandIds 必须有效' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await QuickCommandsService.reorderCommandsByTag(tagId, commandIds);
|
||||
res.status(200).json({ message: '标签内快捷指令顺序已更新' });
|
||||
} catch (error: any) {
|
||||
console.error('[QuickCommandsController] 更新标签内快捷指令顺序失败:', error.message);
|
||||
res.status(500).json({ message: error.message || '无法更新标签内快捷指令顺序' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,193 +1,247 @@
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
|
||||
// 定义基础快捷指令接口
|
||||
export interface QuickCommand {
|
||||
id: number;
|
||||
name: string | null; // 名称可选
|
||||
name: string | null;
|
||||
command: string;
|
||||
usage_count: number;
|
||||
variables?: string; // 存储 JSON 格式的变量键值对
|
||||
created_at: number; // Unix 时间戳 (秒)
|
||||
updated_at: number; // Unix 时间戳 (秒)
|
||||
sort_order: number;
|
||||
variables?: string | null;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
// 定义包含标签 ID 和解析后变量的接口
|
||||
export type QuickCommandWithTags = Omit<QuickCommand, 'variables'> & {
|
||||
tagIds: number[];
|
||||
variables: Record<string, string> | null; // API 层面使用对象
|
||||
tagOrders: Record<number, number>;
|
||||
variables: Record<string, string> | null;
|
||||
};
|
||||
|
||||
// 用于从数据库获取带 tag_ids_str 的行
|
||||
interface DbQuickCommandWithTagsRow extends QuickCommand {
|
||||
tag_ids_str: string | null;
|
||||
// variables 字段已包含在 QuickCommand 中,这里不需要重复定义,因为 QuickCommand 将包含 variables?: string
|
||||
interface QuickCommandTagOrderRow {
|
||||
quick_command_id: number;
|
||||
tag_id: number;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
type QuickCommandSortBy = 'manual' | 'name' | 'usage_count';
|
||||
|
||||
/**
|
||||
* 添加一条新的快捷指令
|
||||
* @param name - 指令名称 (可选)
|
||||
* @param command - 指令内容
|
||||
* @param variables - 变量对象 (可选)
|
||||
* @returns 返回插入记录的 ID
|
||||
*/
|
||||
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'))`;
|
||||
const parseVariables = (variables: string | null | undefined, commandId: number): Record<string, string> | null => {
|
||||
if (!variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(variables);
|
||||
} catch (error) {
|
||||
console.error(`[QuickCommandsRepository] 解析快捷指令 ${commandId} 的 variables 失败:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getNextQuickCommandSortOrder = async (db: Awaited<ReturnType<typeof getDbInstance>>): Promise<number> => {
|
||||
const row = await getDbRow<{ nextSortOrder?: number }>(
|
||||
db,
|
||||
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS nextSortOrder FROM quick_commands',
|
||||
);
|
||||
return row?.nextSortOrder ?? 1;
|
||||
};
|
||||
|
||||
const getTagOrderMap = async (
|
||||
db: Awaited<ReturnType<typeof getDbInstance>>,
|
||||
commandIds: number[],
|
||||
): Promise<Map<number, { tagIds: number[]; tagOrders: Record<number, number> }>> => {
|
||||
const tagState = new Map<number, { tagIds: number[]; tagOrders: Record<number, number> }>();
|
||||
|
||||
if (commandIds.length === 0) {
|
||||
return tagState;
|
||||
}
|
||||
|
||||
const placeholders = commandIds.map(() => '?').join(', ');
|
||||
const rows = await allDb<QuickCommandTagOrderRow>(
|
||||
db,
|
||||
`SELECT quick_command_id, tag_id, sort_order
|
||||
FROM quick_command_tag_associations
|
||||
WHERE quick_command_id IN (${placeholders})
|
||||
ORDER BY quick_command_id ASC, sort_order ASC, tag_id ASC`,
|
||||
commandIds,
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
if (!tagState.has(row.quick_command_id)) {
|
||||
tagState.set(row.quick_command_id, { tagIds: [], tagOrders: {} });
|
||||
}
|
||||
|
||||
const currentState = tagState.get(row.quick_command_id)!;
|
||||
currentState.tagIds.push(row.tag_id);
|
||||
currentState.tagOrders[row.tag_id] = row.sort_order;
|
||||
}
|
||||
|
||||
return tagState;
|
||||
};
|
||||
|
||||
const buildQuickCommandsWithTags = async (
|
||||
db: Awaited<ReturnType<typeof getDbInstance>>,
|
||||
rows: QuickCommand[],
|
||||
): Promise<QuickCommandWithTags[]> => {
|
||||
const commandIds = rows.map((row) => row.id);
|
||||
const tagState = await getTagOrderMap(db, commandIds);
|
||||
|
||||
return rows.map((row) => {
|
||||
const { variables, ...rest } = row;
|
||||
const currentTagState = tagState.get(row.id);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
variables: parseVariables(variables, row.id),
|
||||
tagIds: currentTagState?.tagIds ?? [],
|
||||
tagOrders: currentTagState?.tagOrders ?? {},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const addQuickCommand = async (
|
||||
name: string | null,
|
||||
command: string,
|
||||
variables?: Record<string, string>,
|
||||
): Promise<number> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const variablesJson = variables ? JSON.stringify(variables) : null;
|
||||
const result = await runDb(db, sql, [name, command, variablesJson]);
|
||||
const sortOrder = await getNextQuickCommandSortOrder(db);
|
||||
const result = await runDb(
|
||||
db,
|
||||
`INSERT INTO quick_commands (name, command, usage_count, variables, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, 0, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`,
|
||||
[name, command, variablesJson, sortOrder],
|
||||
);
|
||||
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('添加快捷指令后未能获取有效的 lastID');
|
||||
throw new Error('添加快捷指令后未能获取有效的 lastID');
|
||||
}
|
||||
|
||||
return result.lastID;
|
||||
} catch (err: any) {
|
||||
console.error('添加快捷指令时出错:', err.message);
|
||||
console.error('[QuickCommandsRepository] 添加快捷指令失败:', err.message);
|
||||
throw new Error('无法添加快捷指令');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定的快捷指令
|
||||
* @param id - 要更新的记录 ID
|
||||
* @param name - 新的指令名称 (可选)
|
||||
* @param command - 新的指令内容
|
||||
* @param variables - 新的变量对象 (可选)
|
||||
* @returns 返回是否成功更新 (true/false)
|
||||
*/
|
||||
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 = ?`;
|
||||
export const updateQuickCommand = async (
|
||||
id: number,
|
||||
name: string | null,
|
||||
command: string,
|
||||
variables?: Record<string, string>,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const variablesJson = variables ? JSON.stringify(variables) : null;
|
||||
const result = await runDb(db, sql, [name, command, variablesJson, id]);
|
||||
const result = await runDb(
|
||||
db,
|
||||
'UPDATE quick_commands SET name = ?, command = ?, variables = ?, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
|
||||
[name, command, variablesJson, id],
|
||||
);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error('更新快捷指令时出错:', err.message);
|
||||
console.error('[QuickCommandsRepository] 更新快捷指令失败:', err.message);
|
||||
throw new Error('无法更新快捷指令');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 删除指定的快捷指令
|
||||
* @param id - 要删除的记录 ID
|
||||
* @returns 返回是否成功删除 (true/false)
|
||||
*/
|
||||
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
|
||||
const sql = `DELETE FROM quick_commands WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [id]);
|
||||
const result = await runDb(db, 'DELETE FROM quick_commands WHERE id = ?', [id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error('删除快捷指令时出错:', err.message);
|
||||
console.error('[QuickCommandsRepository] 删除快捷指令失败:', err.message);
|
||||
throw new Error('无法删除快捷指令');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有快捷指令及其关联的标签 ID
|
||||
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
||||
* @returns 返回包含所有快捷指令条目及标签 ID 的数组
|
||||
*/
|
||||
export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommandWithTags[]> => {
|
||||
let orderByClause = 'ORDER BY qc.name ASC'; // 默认按名称升序
|
||||
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'manual'): Promise<QuickCommandWithTags[]> => {
|
||||
let orderByClause = 'ORDER BY sort_order ASC, id ASC';
|
||||
if (sortBy === 'usage_count') {
|
||||
orderByClause = 'ORDER BY qc.usage_count DESC, qc.name ASC'; // 按使用频率降序,同频率按名称升序
|
||||
orderByClause = 'ORDER BY usage_count DESC, name ASC, id ASC';
|
||||
} else if (sortBy === 'name') {
|
||||
orderByClause = 'ORDER BY name ASC, id ASC';
|
||||
}
|
||||
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
|
||||
const sql = `
|
||||
SELECT
|
||||
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
|
||||
GROUP BY qc.id
|
||||
${orderByClause}`;
|
||||
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const rows = await allDb<DbQuickCommandWithTagsRow>(db, sql);
|
||||
// 将 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)) : []
|
||||
};
|
||||
});
|
||||
const rows = await allDb<QuickCommand>(
|
||||
db,
|
||||
`SELECT id, name, command, usage_count, sort_order, variables, created_at, updated_at
|
||||
FROM quick_commands
|
||||
${orderByClause}`,
|
||||
);
|
||||
return await buildQuickCommandsWithTags(db, rows);
|
||||
} catch (err: any) {
|
||||
console.error('获取快捷指令(带标签)时出错:', err.message);
|
||||
console.error('[QuickCommandsRepository] 获取快捷指令失败:', err.message);
|
||||
throw new Error('无法获取快捷指令');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 增加指定快捷指令的使用次数
|
||||
* @param id - 要增加次数的记录 ID
|
||||
* @returns 返回是否成功更新 (true/false)
|
||||
*/
|
||||
export const incrementUsageCount = async (id: number): Promise<boolean> => {
|
||||
const sql = `UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime('%s', 'now') WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [id]);
|
||||
const result = await runDb(
|
||||
db,
|
||||
'UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
|
||||
[id],
|
||||
);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error('增加快捷指令使用次数时出错:', err.message);
|
||||
console.error('[QuickCommandsRepository] 增加快捷指令使用次数失败:', err.message);
|
||||
throw new Error('无法增加快捷指令使用次数');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 查找快捷指令及其关联的标签 ID
|
||||
* @param id - 要查找的记录 ID
|
||||
* @returns 返回找到的快捷指令条目及标签 ID,如果未找到则返回 undefined
|
||||
*/
|
||||
export const findQuickCommandById = async (id: number): Promise<QuickCommandWithTags | undefined> => {
|
||||
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
|
||||
const sql = `
|
||||
SELECT
|
||||
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
|
||||
WHERE qc.id = ?
|
||||
GROUP BY qc.id`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const row = await getDbRow<DbQuickCommandWithTagsRow>(db, sql, [id]);
|
||||
if (row && typeof row.id !== 'undefined') {
|
||||
// 将 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 {
|
||||
...restOfRow,
|
||||
variables: parsedVariables,
|
||||
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||
};
|
||||
} else {
|
||||
const row = await getDbRow<QuickCommand>(
|
||||
db,
|
||||
`SELECT id, name, command, usage_count, sort_order, variables, created_at, updated_at
|
||||
FROM quick_commands
|
||||
WHERE id = ?`,
|
||||
[id],
|
||||
);
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [hydratedRow] = await buildQuickCommandsWithTags(db, [row]);
|
||||
return hydratedRow;
|
||||
} catch (err: any) {
|
||||
console.error('查找快捷指令(带标签)时出错:', err.message);
|
||||
throw new Error('无法查找快捷指令');
|
||||
console.error('[QuickCommandsRepository] 查询快捷指令失败:', err.message);
|
||||
throw new Error('无法查询快捷指令');
|
||||
}
|
||||
};
|
||||
|
||||
export const reorderQuickCommands = async (commandIds: number[]): Promise<void> => {
|
||||
const normalizedCommandIds = Array.from(
|
||||
new Set(commandIds.filter((commandId) => Number.isInteger(commandId) && commandId > 0)),
|
||||
);
|
||||
|
||||
if (normalizedCommandIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await getDbInstance();
|
||||
try {
|
||||
await runDb(db, 'BEGIN TRANSACTION');
|
||||
for (let index = 0; index < normalizedCommandIds.length; index += 1) {
|
||||
await runDb(
|
||||
db,
|
||||
'UPDATE quick_commands SET sort_order = ?, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
|
||||
[index + 1, normalizedCommandIds[index]],
|
||||
);
|
||||
}
|
||||
await runDb(db, 'COMMIT');
|
||||
} catch (err: any) {
|
||||
await runDb(db, 'ROLLBACK');
|
||||
console.error('[QuickCommandsRepository] 重排快捷指令失败:', err.message);
|
||||
throw new Error('无法更新快捷指令顺序');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,7 +9,9 @@ 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.get('/', QuickCommandsController.getAllQuickCommands); // GET /api/v1/quick-commands?sortBy=manual|name|usage_count
|
||||
router.put('/reorder', QuickCommandsController.reorderQuickCommands); // PUT /api/v1/quick-commands/reorder
|
||||
router.put('/reorder-by-tag', QuickCommandsController.reorderCommandsByTag); // PUT /api/v1/quick-commands/reorder-by-tag
|
||||
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
|
||||
|
||||
@@ -1,133 +1,102 @@
|
||||
import * as QuickCommandsRepository from '../quick-commands/quick-commands.repository';
|
||||
import { QuickCommandWithTags } from '../quick-commands/quick-commands.repository';
|
||||
import * as QuickCommandTagRepository from '../quick-command-tags/quick-command-tag.repository';
|
||||
import { QuickCommandWithTags } from '../quick-commands/quick-commands.repository';
|
||||
import * as QuickCommandTagRepository from '../quick-command-tags/quick-command-tag.repository';
|
||||
|
||||
// 定义排序类型
|
||||
export type QuickCommandSortBy = 'name' | 'usage_count';
|
||||
export type QuickCommandSortBy = 'manual' | '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[], variables?: Record<string, string>): 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(), variables);
|
||||
|
||||
// 添加成功后,设置标签关联
|
||||
if (commandId > 0 && tagIds && Array.isArray(tagIds)) {
|
||||
try {
|
||||
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
|
||||
} catch (tagError: any) {
|
||||
// 如果标签关联失败,可以选择记录警告或回滚(但通常不回滚主记录)
|
||||
console.warn(`[Service] 添加快捷指令 ${commandId} 成功,但设置标签关联失败:`, tagError.message);
|
||||
// 可以考虑是否需要通知用户部分操作失败
|
||||
console.warn(`[QuickCommandsService] 快捷指令 ${commandId} 已创建,但设置标签关联失败:`, tagError.message);
|
||||
}
|
||||
}
|
||||
|
||||
return commandId;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新快捷指令
|
||||
* @param id - 要更新的记录 ID
|
||||
* @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[], variables?: Record<string, string>): 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(), variables);
|
||||
|
||||
// 如果指令更新成功,并且提供了 tagIds (即使是空数组也表示要更新),则更新标签关联
|
||||
if (commandUpdated && typeof tagIds !== 'undefined') {
|
||||
try {
|
||||
try {
|
||||
await QuickCommandTagRepository.setCommandTagAssociations(id, tagIds);
|
||||
} catch (tagError: any) {
|
||||
console.warn(`[Service] 更新快捷指令 ${id} 成功,但更新标签关联失败:`, tagError.message);
|
||||
// 即使标签更新失败,主记录已更新,通常返回 true
|
||||
}
|
||||
} catch (tagError: any) {
|
||||
console.warn(`[QuickCommandsService] 快捷指令 ${id} 已更新,但更新标签关联失败:`, tagError.message);
|
||||
}
|
||||
}
|
||||
// 返回主记录是否更新成功
|
||||
|
||||
return commandUpdated;
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除快捷指令
|
||||
* @param id - 要删除的记录 ID
|
||||
* @returns 返回是否成功删除 (删除行数 > 0)
|
||||
*/
|
||||
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
|
||||
const changes = await QuickCommandsRepository.deleteQuickCommand(id);
|
||||
return changes;
|
||||
return QuickCommandsRepository.deleteQuickCommand(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有快捷指令,并按指定方式排序
|
||||
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
||||
* @returns 返回排序后的快捷指令数组 (包含 tagIds)
|
||||
*/
|
||||
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'): Promise<QuickCommandWithTags[]> => {
|
||||
// Repository 已返回带 tagIds 的数据
|
||||
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'manual'): Promise<QuickCommandWithTags[]> => {
|
||||
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;
|
||||
return QuickCommandsRepository.incrementUsageCount(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个快捷指令 (可能用于编辑)
|
||||
* @param id - 记录 ID
|
||||
* @returns 返回找到的快捷指令 (包含 tagIds),或 undefined
|
||||
*/
|
||||
export const getQuickCommandById = async (id: number): Promise<QuickCommandWithTags | undefined> => {
|
||||
// Repository 已返回带 tagIds 的数据
|
||||
return QuickCommandsRepository.findQuickCommandById(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将单个标签批量关联到多个快捷指令
|
||||
* @param commandIds - 需要添加标签的快捷指令 ID 数组
|
||||
* @param tagId - 要添加的标签 ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const assignTagToCommands = async (commandIds: number[], tagId: number): Promise<void> => {
|
||||
try {
|
||||
// 基本验证
|
||||
if (!Array.isArray(commandIds) || commandIds.some(id => typeof id !== 'number' || isNaN(id))) {
|
||||
throw new Error('无效的指令 ID 列表');
|
||||
}
|
||||
if (typeof tagId !== 'number' || isNaN(tagId)) {
|
||||
throw new Error('无效的标签 ID');
|
||||
}
|
||||
|
||||
// 调用 Repository 函数执行批量关联
|
||||
// 注意:这里需要导入 QuickCommandTagRepository
|
||||
console.log(`[Service] assignTagToCommands: Calling repo with commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`);
|
||||
await QuickCommandTagRepository.addTagToCommands(commandIds, tagId);
|
||||
console.log(`[Service] assignTagToCommands: Repo call finished for tag ${tagId}.`); // +++ 修改日志 +++
|
||||
// 可以在这里添加额外的业务逻辑,例如发送事件通知等
|
||||
} catch (error: any) {
|
||||
console.error(`[Service] assignTagToCommands: 批量关联标签 ${tagId} 到指令时出错:`, error.message);
|
||||
// 向上抛出错误,让 Controller 处理 HTTP 响应
|
||||
throw error;
|
||||
if (!Array.isArray(commandIds) || commandIds.some((id) => typeof id !== 'number' || Number.isNaN(id))) {
|
||||
throw new Error('无效的指令 ID 列表');
|
||||
}
|
||||
|
||||
if (typeof tagId !== 'number' || Number.isNaN(tagId)) {
|
||||
throw new Error('无效的标签 ID');
|
||||
}
|
||||
|
||||
await QuickCommandTagRepository.addTagToCommands(commandIds, tagId);
|
||||
};
|
||||
|
||||
export const reorderQuickCommands = async (commandIds: number[]): Promise<void> => {
|
||||
if (!Array.isArray(commandIds) || commandIds.some((id) => typeof id !== 'number' || Number.isNaN(id))) {
|
||||
throw new Error('commandIds 必须是数字数组');
|
||||
}
|
||||
|
||||
await QuickCommandsRepository.reorderQuickCommands(commandIds);
|
||||
};
|
||||
|
||||
export const reorderCommandsByTag = async (tagId: number, commandIds: number[]): Promise<void> => {
|
||||
if (typeof tagId !== 'number' || Number.isNaN(tagId)) {
|
||||
throw new Error('tagId 必须是数字');
|
||||
}
|
||||
|
||||
if (!Array.isArray(commandIds) || commandIds.some((id) => typeof id !== 'number' || Number.isNaN(id))) {
|
||||
throw new Error('commandIds 必须是数字数组');
|
||||
}
|
||||
|
||||
await QuickCommandTagRepository.reorderCommandsInTag(tagId, commandIds);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SshKeySelector from './SshKeySelector.vue'; // Assuming SshKeySelector is used here
|
||||
import LoginCredentialSelector from './LoginCredentialSelector.vue';
|
||||
@@ -19,6 +20,26 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visiblePasswordFields = reactive({
|
||||
sshPassword: false,
|
||||
rdpPassword: false,
|
||||
vncPassword: false,
|
||||
});
|
||||
|
||||
const resetPasswordVisibility = (): void => {
|
||||
visiblePasswordFields.sshPassword = false;
|
||||
visiblePasswordFields.rdpPassword = false;
|
||||
visiblePasswordFields.vncPassword = false;
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (field: keyof typeof visiblePasswordFields): void => {
|
||||
visiblePasswordFields[field] = !visiblePasswordFields[field];
|
||||
};
|
||||
|
||||
watch(() => props.formData.type, resetPasswordVisibility);
|
||||
watch(() => props.formData.auth_method, resetPasswordVisibility);
|
||||
watch(() => props.formData.credential_source, resetPasswordVisibility);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,8 +107,20 @@ const { t } = useI18n();
|
||||
|
||||
<div v-if="props.formData.auth_method === 'password'">
|
||||
<label for="conn-password" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password') }}</label>
|
||||
<input type="password" id="conn-password" v-model="props.formData.password" :required="props.formData.auth_method === 'password' && !isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<div class="relative">
|
||||
<input :type="visiblePasswordFields.sshPassword ? 'text' : 'password'" id="conn-password" v-model="props.formData.password" :required="props.formData.auth_method === 'password' && !isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.sshPassword"
|
||||
@click="togglePasswordVisibility('sshPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.sshPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.formData.auth_method === 'key'" class="space-y-4">
|
||||
@@ -102,8 +135,20 @@ const { t } = useI18n();
|
||||
<template v-if="props.formData.type === 'RDP'">
|
||||
<div>
|
||||
<label for="conn-password-rdp" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password') }}</label>
|
||||
<input type="password" id="conn-password-rdp" v-model="props.formData.password" :required="!isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<div class="relative">
|
||||
<input :type="visiblePasswordFields.rdpPassword ? 'text' : 'password'" id="conn-password-rdp" v-model="props.formData.password" :required="!isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.rdpPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.rdpPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.rdpPassword"
|
||||
@click="togglePasswordVisibility('rdpPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.rdpPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,8 +156,20 @@ const { t } = useI18n();
|
||||
<template v-if="props.formData.type === 'VNC'">
|
||||
<div>
|
||||
<label for="conn-password-vnc" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.vncPassword', 'VNC 密码') }}</label>
|
||||
<input type="password" id="conn-password-vnc" v-model="props.formData.vncPassword" :required="!isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<div class="relative">
|
||||
<input :type="visiblePasswordFields.vncPassword ? 'text' : 'password'" id="conn-password-vnc" v-model="props.formData.vncPassword" :required="!isEditMode" autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.vncPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.vncPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.vncPassword"
|
||||
@click="togglePasswordVisibility('vncPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.vncPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
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,
|
||||
width: resizableWidth ? `${resizableWidth}px` : `min(calc(100vw - ${MODAL_VIEWPORT_GUTTER_PX}px), ${MODAL_DEFAULT_WIDTH_RATIO * 100}vw)`,
|
||||
height: resizableHeight ? `${resizableHeight}px` : undefined,
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
maxWidth: `calc(100vw - ${MODAL_VIEWPORT_GUTTER_PX}px)`,
|
||||
maxHeight: 'calc(100vh - 2rem)',
|
||||
}"
|
||||
>
|
||||
@@ -183,6 +183,8 @@ const modalContentRef = ref<HTMLElement | null>(null);
|
||||
const commandTextareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const R_MIN_WIDTH = 580; // 可调整大小的最小宽度 (像素)
|
||||
const R_MIN_HEIGHT = 440; // 可调整大小的最小高度 (像素)
|
||||
const MODAL_DEFAULT_WIDTH_RATIO = 0.6;
|
||||
const MODAL_VIEWPORT_GUTTER_PX = 32;
|
||||
const placeholder = t('quickCommands.form.commandPlaceholder') + 'echo "Hello,\${USERNAME}"'
|
||||
|
||||
const { width: resizableWidth, height: resizableHeight } = useResizable(modalContentRef, {
|
||||
@@ -239,7 +241,7 @@ watch(() => formData.command, (newCommand) => {
|
||||
// 初始化表单数据 (如果是编辑模式)
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
let initialW = Math.min(window.innerWidth * 0.74, 860); // 目标 74vw,最大 860px
|
||||
let initialW = Math.min(window.innerWidth * MODAL_DEFAULT_WIDTH_RATIO, window.innerWidth - MODAL_VIEWPORT_GUTTER_PX);
|
||||
let initialH = Math.min(window.innerHeight * 0.68, 600); // 目标 68vh,最大 600px
|
||||
|
||||
initialW = Math.max(R_MIN_WIDTH, initialW);
|
||||
|
||||
@@ -39,6 +39,19 @@ const initialFormData: LoginCredentialInput = {
|
||||
|
||||
const formData = reactive({ ...initialFormData });
|
||||
const formError = ref<string | null>(null);
|
||||
const visiblePasswordFields = reactive({
|
||||
sshPassword: false,
|
||||
genericPassword: false,
|
||||
});
|
||||
|
||||
const resetPasswordVisibility = (): void => {
|
||||
visiblePasswordFields.sshPassword = false;
|
||||
visiblePasswordFields.genericPassword = false;
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (field: keyof typeof visiblePasswordFields): void => {
|
||||
visiblePasswordFields[field] = !visiblePasswordFields[field];
|
||||
};
|
||||
|
||||
watch(() => props.initialType, (newValue) => {
|
||||
if (!credentialToEdit.value && newValue) {
|
||||
@@ -47,12 +60,17 @@ watch(() => props.initialType, (newValue) => {
|
||||
});
|
||||
|
||||
watch(() => formData.type, (newType) => {
|
||||
resetPasswordVisibility();
|
||||
if (newType !== 'SSH') {
|
||||
formData.auth_method = 'password';
|
||||
formData.ssh_key_id = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => formData.auth_method, () => {
|
||||
resetPasswordVisibility();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loginCredentialsStore.fetchLoginCredentials();
|
||||
});
|
||||
@@ -60,6 +78,7 @@ onMounted(() => {
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, initialFormData, { type: props.initialType || 'SSH' });
|
||||
formError.value = null;
|
||||
resetPasswordVisibility();
|
||||
};
|
||||
|
||||
const showAddForm = () => {
|
||||
@@ -71,6 +90,7 @@ const showAddForm = () => {
|
||||
const showEditForm = async (credential: LoginCredentialBasicInfo) => {
|
||||
formError.value = null;
|
||||
credentialToEdit.value = credential;
|
||||
resetPasswordVisibility();
|
||||
|
||||
const details = await loginCredentialsStore.fetchLoginCredentialDetails(credential.id);
|
||||
if (!details) {
|
||||
@@ -300,7 +320,19 @@ const cancelForm = () => {
|
||||
</div>
|
||||
<div v-if="formData.auth_method === 'password'">
|
||||
<label for="credential-password" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password', '密码') }}</label>
|
||||
<input id="credential-password" v-model="formData.password" type="password" autocomplete="new-password" class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<div class="relative">
|
||||
<input id="credential-password" v-model="formData.password" :type="visiblePasswordFields.sshPassword ? 'text' : 'password'" autocomplete="new-password" class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.sshPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.sshPassword"
|
||||
@click="togglePasswordVisibility('sshPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.sshPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.sshKey', 'SSH 密钥') }}</label>
|
||||
@@ -313,7 +345,19 @@ const cancelForm = () => {
|
||||
|
||||
<div v-else>
|
||||
<label for="credential-password-generic" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password', '密码') }}</label>
|
||||
<input id="credential-password-generic" v-model="formData.password" type="password" autocomplete="new-password" class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<div class="relative">
|
||||
<input id="credential-password-generic" v-model="formData.password" :type="visiblePasswordFields.genericPassword ? 'text' : 'password'" autocomplete="new-password" class="w-full px-3 py-2 pr-11 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-3 text-text-secondary hover:text-foreground focus:outline-none focus:text-foreground"
|
||||
:title="visiblePasswordFields.genericPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-label="visiblePasswordFields.genericPassword ? t('connections.form.hidePassword', '隐藏密码') : t('connections.form.showPassword', '显示密码')"
|
||||
:aria-pressed="visiblePasswordFields.genericPassword"
|
||||
@click="togglePasswordVisibility('genericPassword')"
|
||||
>
|
||||
<i :class="visiblePasswordFields.genericPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -265,6 +265,8 @@
|
||||
"authMethodPassword": "Password",
|
||||
"authMethodKey": "SSH Key",
|
||||
"password": "Password:",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"privateKey": "Private Key:",
|
||||
"passphrase": "Passphrase:",
|
||||
"vncPassword": "VNC Password",
|
||||
|
||||
@@ -196,6 +196,8 @@
|
||||
"optional": "オプション",
|
||||
"passphrase": "パスフレーズ:",
|
||||
"password": "パスワード:",
|
||||
"showPassword": "パスワードを表示",
|
||||
"hidePassword": "パスワードを隠す",
|
||||
"port": "ポート:",
|
||||
"privateKey": "秘密鍵:",
|
||||
"noSshKey":"SSHキーなし",
|
||||
|
||||
@@ -266,6 +266,8 @@
|
||||
"authMethodPassword": "密码",
|
||||
"authMethodKey": "SSH 密钥",
|
||||
"password": "密码:",
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"privateKey": "私钥:",
|
||||
"passphrase": "私钥密码:",
|
||||
"vncPassword": "VNC 密码:",
|
||||
|
||||
@@ -3,62 +3,58 @@ import { ref } from 'vue';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||
|
||||
// 定义快捷指令标签接口 (与后端 QuickCommandTag 对应)
|
||||
export interface QuickCommandTag {
|
||||
id: number;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
const TAG_CACHE_KEY = 'quickCommandTagsCache';
|
||||
|
||||
const normalizeTag = (tag: any): QuickCommandTag => ({
|
||||
id: Number(tag.id),
|
||||
name: typeof tag.name === 'string' ? tag.name : '',
|
||||
sort_order: Number.isFinite(tag.sort_order) ? Number(tag.sort_order) : 0,
|
||||
created_at: Number(tag.created_at ?? 0),
|
||||
updated_at: Number(tag.updated_at ?? 0),
|
||||
});
|
||||
|
||||
export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
||||
const tags = ref<QuickCommandTag[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
|
||||
// 获取快捷指令标签列表 (带缓存)
|
||||
async function fetchTags() {
|
||||
const cacheKey = 'quickCommandTagsCache';
|
||||
error.value = null;
|
||||
|
||||
// 1. 尝试从 localStorage 加载缓存
|
||||
try {
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
const cachedData = localStorage.getItem(TAG_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
tags.value = JSON.parse(cachedData);
|
||||
isLoading.value = false;
|
||||
} else {
|
||||
isLoading.value = true;
|
||||
const parsedData = JSON.parse(cachedData);
|
||||
if (Array.isArray(parsedData)) {
|
||||
tags.value = parsedData.map(normalizeTag);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdTagStore] Failed to load or parse cache:', e);
|
||||
localStorage.removeItem(cacheKey);
|
||||
isLoading.value = true;
|
||||
} catch (cacheError) {
|
||||
console.error('[QuickCommandTagsStore] 读取标签缓存失败:', cacheError);
|
||||
localStorage.removeItem(TAG_CACHE_KEY);
|
||||
}
|
||||
|
||||
// 2. 后台获取最新数据
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
const response = await apiClient.get<QuickCommandTag[]>('/quick-command-tags');
|
||||
const freshData = response.data;
|
||||
const freshDataString = JSON.stringify(freshData);
|
||||
|
||||
// 3. 对比并更新
|
||||
const currentDataString = JSON.stringify(tags.value);
|
||||
if (currentDataString !== freshDataString) {
|
||||
tags.value = freshData;
|
||||
localStorage.setItem(cacheKey, freshDataString);
|
||||
} else {
|
||||
}
|
||||
error.value = null;
|
||||
const freshTags = Array.isArray(response.data) ? response.data.map(normalizeTag) : [];
|
||||
tags.value = freshTags;
|
||||
localStorage.setItem(TAG_CACHE_KEY, JSON.stringify(freshTags));
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to fetch tags:', err);
|
||||
console.error('[QuickCommandTagsStore] 获取标签失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '获取快捷指令标签列表失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
uiNotificationsStore.showError(error.value); // 显示错误通知
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
@@ -66,22 +62,19 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新快捷指令标签 (添加后清除缓存)
|
||||
async function addTag(name: string): Promise<QuickCommandTag | null> {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
const response = await apiClient.post<{ message: string, tag: QuickCommandTag }>('/quick-command-tags', { name });
|
||||
const newTag = response.data.tag;
|
||||
localStorage.removeItem('quickCommandTagsCache'); // 清除缓存
|
||||
await fetchTags(); // 重新获取以更新列表
|
||||
const response = await apiClient.post<{ message: string; tag: QuickCommandTag }>('/quick-command-tags', { name });
|
||||
localStorage.removeItem(TAG_CACHE_KEY);
|
||||
await fetchTags();
|
||||
uiNotificationsStore.showSuccess('快捷指令标签已添加');
|
||||
return newTag;
|
||||
return response.data.tag ? normalizeTag(response.data.tag) : null;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to add tag:', err);
|
||||
console.error('[QuickCommandTagsStore] 添加标签失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '添加快捷指令标签失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return null;
|
||||
@@ -90,21 +83,19 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新快捷指令标签
|
||||
async function updateTag(id: number, name: string): Promise<boolean> {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
await apiClient.put(`/quick-command-tags/${id}`, { name });
|
||||
localStorage.removeItem('quickCommandTagsCache');
|
||||
localStorage.removeItem(TAG_CACHE_KEY);
|
||||
await fetchTags();
|
||||
uiNotificationsStore.showSuccess('快捷指令标签已更新');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to update tag:', err);
|
||||
console.error('[QuickCommandTagsStore] 更新标签失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '更新快捷指令标签失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
@@ -113,21 +104,43 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除快捷指令标签
|
||||
async function deleteTag(id: number): Promise<boolean> {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
await apiClient.delete(`/quick-command-tags/${id}`);
|
||||
localStorage.removeItem('quickCommandTagsCache');
|
||||
localStorage.removeItem(TAG_CACHE_KEY);
|
||||
await fetchTags();
|
||||
uiNotificationsStore.showSuccess('快捷指令标签已删除');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to delete tag:', err);
|
||||
console.error('[QuickCommandTagsStore] 删除标签失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '删除快捷指令标签失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reorderTags(tagIds: number[]): Promise<boolean> {
|
||||
if (!Array.isArray(tagIds) || tagIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
await apiClient.put('/quick-command-tags/reorder', { tagIds });
|
||||
localStorage.removeItem(TAG_CACHE_KEY);
|
||||
await fetchTags();
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCommandTagsStore] 更新标签顺序失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '更新快捷指令标签顺序失败';
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
@@ -144,5 +157,6 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
||||
addTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
reorderTags,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,170 +1,221 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuickCommandTagsStore } from './quickCommandTags.store';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
|
||||
|
||||
// 定义前端使用的快捷指令接口 (包含 tagIds)
|
||||
export interface QuickCommandFE { // Renamed from QuickCommand if necessary
|
||||
export interface QuickCommandFE {
|
||||
id: number;
|
||||
name: string | null;
|
||||
command: string;
|
||||
usage_count: number;
|
||||
sort_order: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
tagIds: number[]; // +++ Add tagIds +++
|
||||
variables?: Record<string, string>; // New: Add variables
|
||||
tagIds: number[];
|
||||
tagOrders: Record<number, number>;
|
||||
variables?: Record<string, string> | null;
|
||||
}
|
||||
|
||||
// 定义排序类型
|
||||
export type QuickCommandSortByType = 'name' | 'usage_count' | 'last_used';
|
||||
export type QuickCommandSortByType = 'manual' | 'name' | 'usage_count' | 'last_used';
|
||||
|
||||
// 定义分组后的数据结构
|
||||
export interface GroupedQuickCommands {
|
||||
groupName: string;
|
||||
tagId: number | null; // null for "Untagged" group
|
||||
tagId: number | null;
|
||||
commands: QuickCommandFE[];
|
||||
}
|
||||
|
||||
// +++ localStorage key for expanded groups +++
|
||||
const EXPANDED_GROUPS_STORAGE_KEY = 'quickCommandsExpandedGroups';
|
||||
const QUICK_COMMANDS_CACHE_KEY = 'quickCommandsListCache';
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const normalizeTagOrders = (tagOrders: unknown): Record<number, number> => {
|
||||
if (!isRecord(tagOrders)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(tagOrders).reduce<Record<number, number>>((result, [tagId, sortOrder]) => {
|
||||
const numericTagId = Number(tagId);
|
||||
const numericSortOrder = Number(sortOrder);
|
||||
if (Number.isInteger(numericTagId) && Number.isFinite(numericSortOrder)) {
|
||||
result[numericTagId] = numericSortOrder;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const normalizeQuickCommand = (command: any): QuickCommandFE => ({
|
||||
id: Number(command.id),
|
||||
name: typeof command.name === 'string' ? command.name : null,
|
||||
command: typeof command.command === 'string' ? command.command : '',
|
||||
usage_count: Number(command.usage_count ?? 0),
|
||||
sort_order: Number(command.sort_order ?? 0),
|
||||
created_at: Number(command.created_at ?? 0),
|
||||
updated_at: Number(command.updated_at ?? 0),
|
||||
tagIds: Array.isArray(command.tagIds)
|
||||
? Array.from(new Set(command.tagIds.filter((tagId: unknown) => Number.isInteger(tagId) && Number(tagId) > 0)))
|
||||
: [],
|
||||
tagOrders: normalizeTagOrders(command.tagOrders),
|
||||
variables: isRecord(command.variables) ? (command.variables as Record<string, string>) : undefined,
|
||||
});
|
||||
|
||||
const compareByLabel = (a: QuickCommandFE, b: QuickCommandFE): number => {
|
||||
const labelA = a.name ?? a.command;
|
||||
const labelB = b.name ?? b.command;
|
||||
return labelA.localeCompare(labelB);
|
||||
};
|
||||
|
||||
const compareCommands = (
|
||||
a: QuickCommandFE,
|
||||
b: QuickCommandFE,
|
||||
sortBy: QuickCommandSortByType,
|
||||
tagId?: number | null,
|
||||
): number => {
|
||||
if (sortBy === 'manual') {
|
||||
if (typeof tagId === 'number') {
|
||||
const tagOrderA = a.tagOrders[tagId];
|
||||
const tagOrderB = b.tagOrders[tagId];
|
||||
|
||||
if (typeof tagOrderA === 'number' && typeof tagOrderB === 'number' && tagOrderA !== tagOrderB) {
|
||||
return tagOrderA - tagOrderB;
|
||||
}
|
||||
|
||||
if (typeof tagOrderA === 'number' && typeof tagOrderB !== 'number') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (typeof tagOrderA !== 'number' && typeof tagOrderB === 'number') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (a.sort_order !== b.sort_order) {
|
||||
return a.sort_order - b.sort_order;
|
||||
}
|
||||
}
|
||||
|
||||
if (sortBy === 'usage_count' && a.usage_count !== b.usage_count) {
|
||||
return b.usage_count - a.usage_count;
|
||||
}
|
||||
|
||||
if (sortBy === 'last_used' && a.updated_at !== b.updated_at) {
|
||||
return b.updated_at - a.updated_at;
|
||||
}
|
||||
|
||||
return compareByLabel(a, b);
|
||||
};
|
||||
|
||||
export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
const quickCommandsList = ref<QuickCommandFE[]>([]); // Should now contain QuickCommandFE with tagIds
|
||||
const quickCommandsList = ref<QuickCommandFE[]>([]);
|
||||
const searchTerm = ref('');
|
||||
const sortBy = ref<QuickCommandSortByType>('name'); // 默认按名称排序
|
||||
const sortBy = ref<QuickCommandSortByType>('manual');
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Inject new tag store +++
|
||||
const { t } = useI18n(); // +++ For "Untagged" translation +++
|
||||
const selectedIndex = ref<number>(-1); // Index in the flatVisibleCommands list
|
||||
|
||||
// +++ State for expanded groups +++
|
||||
const selectedIndex = ref<number>(-1);
|
||||
const expandedGroups = ref<Record<string, boolean>>({});
|
||||
|
||||
// --- Getters ---
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// +++ 重写 Getter: 过滤、分组、排序指令 +++
|
||||
const filteredAndGroupedCommands = computed((): GroupedQuickCommands[] => {
|
||||
const filteredCommands = computed(() => {
|
||||
const term = searchTerm.value.toLowerCase().trim();
|
||||
const allTags = quickCommandTagsStore.tags; // 获取快捷指令专属标签
|
||||
const tagMap = new Map(allTags.map(tag => [tag.id, tag.name]));
|
||||
const untaggedGroupName = t('quickCommands.untagged', '未标记'); // 获取 "未标记" 的翻译
|
||||
if (!term) {
|
||||
return quickCommandsList.value;
|
||||
}
|
||||
|
||||
// 1. 过滤 (New logic: filter by command name, command content, OR tag name)
|
||||
let filtered = quickCommandsList.value;
|
||||
if (term) {
|
||||
filtered = filtered.filter(cmd => {
|
||||
// Check command name
|
||||
if (cmd.name && cmd.name.toLowerCase().includes(term)) {
|
||||
return true;
|
||||
}
|
||||
// Check command content
|
||||
if (cmd.command.toLowerCase().includes(term)) {
|
||||
return true;
|
||||
}
|
||||
// Check associated tag names
|
||||
if (cmd.tagIds && cmd.tagIds.length > 0) {
|
||||
for (const tagId of cmd.tagIds) {
|
||||
const tagName = tagMap.get(tagId);
|
||||
if (tagName && tagName.toLowerCase().includes(term)) {
|
||||
return true; // Match found in tag name
|
||||
}
|
||||
}
|
||||
}
|
||||
// No match found
|
||||
return false;
|
||||
const tagMap = new Map(quickCommandTagsStore.tags.map((tag) => [tag.id, tag.name]));
|
||||
return quickCommandsList.value.filter((command) => {
|
||||
if (command.name && command.name.toLowerCase().includes(term)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command.command.toLowerCase().includes(term)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return command.tagIds.some((tagId) => {
|
||||
const tagName = tagMap.get(tagId);
|
||||
return typeof tagName === 'string' && tagName.toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const sortedFlatCommands = computed(() => {
|
||||
return [...filteredCommands.value].sort((a, b) => compareCommands(a, b, sortBy.value));
|
||||
});
|
||||
|
||||
const filteredAndGroupedCommands = computed((): GroupedQuickCommands[] => {
|
||||
const untaggedGroupName = t('quickCommands.untagged', '未标记');
|
||||
const sortedTags = [...quickCommandTagsStore.tags].sort((a, b) => {
|
||||
if (a.sort_order !== b.sort_order) {
|
||||
return a.sort_order - b.sort_order;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
const groups = new Map<number, GroupedQuickCommands>();
|
||||
const untaggedCommands: QuickCommandFE[] = [];
|
||||
|
||||
for (const tag of sortedTags) {
|
||||
if (expandedGroups.value[tag.name] === undefined) {
|
||||
expandedGroups.value[tag.name] = true;
|
||||
}
|
||||
groups.set(tag.id, {
|
||||
groupName: tag.name,
|
||||
tagId: tag.id,
|
||||
commands: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 分组
|
||||
const groups: Record<string, { commands: QuickCommandFE[], tagId: number | null }> = {};
|
||||
const untaggedCommands: QuickCommandFE[] = [];
|
||||
|
||||
filtered.forEach(cmd => {
|
||||
let isTagged = false;
|
||||
if (cmd.tagIds && cmd.tagIds.length > 0) {
|
||||
cmd.tagIds.forEach(tagId => {
|
||||
const tagName = tagMap.get(tagId);
|
||||
if (tagName) {
|
||||
if (!groups[tagName]) {
|
||||
groups[tagName] = { commands: [], tagId: tagId };
|
||||
// 初始化展开状态 (如果未定义,默认为 true)
|
||||
if (expandedGroups.value[tagName] === undefined) {
|
||||
expandedGroups.value[tagName] = true;
|
||||
}
|
||||
}
|
||||
// 避免重复添加(如果一个指令有多个相同标签ID? 不太可能但做个防御)
|
||||
if (!groups[tagName].commands.some(c => c.id === cmd.id)) {
|
||||
groups[tagName].commands.push(cmd);
|
||||
}
|
||||
isTagged = true;
|
||||
}
|
||||
});
|
||||
for (const command of filteredCommands.value) {
|
||||
const validTagIds = command.tagIds.filter((tagId) => groups.has(tagId));
|
||||
if (validTagIds.length === 0) {
|
||||
untaggedCommands.push(command);
|
||||
continue;
|
||||
}
|
||||
if (!isTagged) {
|
||||
untaggedCommands.push(cmd);
|
||||
|
||||
for (const tagId of validTagIds) {
|
||||
groups.get(tagId)!.commands.push(command);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 排序分组内指令 & 格式化输出
|
||||
const sortedGroupNames = Object.keys(groups).sort((a, b) => a.localeCompare(b));
|
||||
const result: GroupedQuickCommands[] = sortedGroupNames.map(groupName => {
|
||||
const groupData = groups[groupName];
|
||||
// 组内排序
|
||||
groupData.commands.sort((a, b) => {
|
||||
if (sortBy.value === 'usage_count') {
|
||||
if (b.usage_count !== a.usage_count) return b.usage_count - a.usage_count;
|
||||
} else if (sortBy.value === 'last_used') {
|
||||
if (b.updated_at !== a.updated_at) return b.updated_at - a.updated_at;
|
||||
}
|
||||
const nameA = a.name ?? a.command; // Fallback to command if name is null
|
||||
const nameB = b.name ?? b.command;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
return {
|
||||
groupName: groupName,
|
||||
tagId: groupData.tagId,
|
||||
commands: groupData.commands
|
||||
};
|
||||
});
|
||||
const result: GroupedQuickCommands[] = [];
|
||||
for (const tag of sortedTags) {
|
||||
const group = groups.get(tag.id);
|
||||
if (!group || group.commands.length === 0) {
|
||||
continue;
|
||||
}
|
||||
group.commands.sort((a, b) => compareCommands(a, b, sortBy.value, tag.id));
|
||||
result.push(group);
|
||||
}
|
||||
|
||||
// 4. 处理未标记的分组
|
||||
if (untaggedCommands.length > 0) {
|
||||
// 初始化展开状态 (如果未定义,默认为 true)
|
||||
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
||||
expandedGroups.value[untaggedGroupName] = true;
|
||||
}
|
||||
// 组内排序
|
||||
untaggedCommands.sort((a, b) => {
|
||||
if (sortBy.value === 'usage_count') {
|
||||
if (b.usage_count !== a.usage_count) return b.usage_count - a.usage_count;
|
||||
} else if (sortBy.value === 'last_used') {
|
||||
if (b.updated_at !== a.updated_at) return b.updated_at - a.updated_at;
|
||||
}
|
||||
const nameA = a.name ?? a.command;
|
||||
const nameB = b.name ?? b.command;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
result.push({
|
||||
groupName: untaggedGroupName,
|
||||
tagId: null,
|
||||
commands: untaggedCommands
|
||||
});
|
||||
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
||||
expandedGroups.value[untaggedGroupName] = true;
|
||||
}
|
||||
result.push({
|
||||
groupName: untaggedGroupName,
|
||||
tagId: null,
|
||||
commands: [...untaggedCommands].sort((a, b) => compareCommands(a, b, sortBy.value, null)),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// +++ Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++
|
||||
const flatVisibleCommands = computed((): QuickCommandFE[] => {
|
||||
if (!settingsStore.showQuickCommandTagsBoolean) {
|
||||
return sortedFlatCommands.value;
|
||||
}
|
||||
|
||||
const flatList: QuickCommandFE[] = [];
|
||||
filteredAndGroupedCommands.value.forEach(group => {
|
||||
// 只添加已展开分组中的指令
|
||||
filteredAndGroupedCommands.value.forEach((group) => {
|
||||
if (expandedGroups.value[group.groupName]) {
|
||||
flatList.push(...group.commands);
|
||||
}
|
||||
@@ -172,130 +223,91 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
return flatList;
|
||||
});
|
||||
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
// +++ Load initial expanded groups state from localStorage +++
|
||||
const loadExpandedGroups = () => {
|
||||
try {
|
||||
const storedState = localStorage.getItem(EXPANDED_GROUPS_STORAGE_KEY);
|
||||
if (storedState) {
|
||||
const parsedState = JSON.parse(storedState);
|
||||
if (typeof parsedState === 'object' && parsedState !== null) {
|
||||
expandedGroups.value = parsedState;
|
||||
console.log('[QuickCmdStore] Loaded expanded groups state from localStorage.');
|
||||
if (isRecord(parsedState)) {
|
||||
expandedGroups.value = Object.entries(parsedState).reduce<Record<string, boolean>>((result, [key, value]) => {
|
||||
result[key] = Boolean(value);
|
||||
return result;
|
||||
}, {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdStore] Failed to load or parse expanded groups state:', e);
|
||||
} catch (cacheError) {
|
||||
console.error('[QuickCommandsStore] 读取分组展开状态失败:', cacheError);
|
||||
localStorage.removeItem(EXPANDED_GROUPS_STORAGE_KEY);
|
||||
}
|
||||
// Default to empty object if no valid state found
|
||||
|
||||
expandedGroups.value = {};
|
||||
};
|
||||
|
||||
// +++ Save expanded groups state to localStorage +++
|
||||
const saveExpandedGroups = () => {
|
||||
try {
|
||||
localStorage.setItem(EXPANDED_GROUPS_STORAGE_KEY, JSON.stringify(expandedGroups.value));
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdStore] Failed to save expanded groups state:', e);
|
||||
} catch (cacheError) {
|
||||
console.error('[QuickCommandsStore] 保存分组展开状态失败:', cacheError);
|
||||
}
|
||||
};
|
||||
|
||||
// +++ Watch for changes and save +++
|
||||
watch(expandedGroups, saveExpandedGroups, { deep: true });
|
||||
|
||||
// +++ Action to toggle group expansion +++
|
||||
const toggleGroup = (groupName: string) => {
|
||||
// Ensure the group exists in the state before toggling
|
||||
if (expandedGroups.value[groupName] === undefined) {
|
||||
// Default to true if toggling a group that wasn't explicitly set (e.g., newly appeared group)
|
||||
expandedGroups.value[groupName] = false; // Start collapsed if toggled first time? Or true? Let's start true.
|
||||
} else {
|
||||
expandedGroups.value[groupName] = !expandedGroups.value[groupName];
|
||||
}
|
||||
// The watcher will automatically save the state
|
||||
// Reset selection when a group is toggled? Maybe not necessary.
|
||||
// selectedIndex.value = -1;
|
||||
expandedGroups.value[groupName] = expandedGroups.value[groupName] === undefined
|
||||
? false
|
||||
: !expandedGroups.value[groupName];
|
||||
};
|
||||
|
||||
// Action to select the next command in the *visible* flat list
|
||||
const selectNextCommand = () => {
|
||||
const commands = flatVisibleCommands.value; // Use the flat visible list
|
||||
const commands = flatVisibleCommands.value;
|
||||
if (commands.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
|
||||
};
|
||||
|
||||
// Action to select the previous command in the *visible* flat list
|
||||
const selectPreviousCommand = () => {
|
||||
const commands = flatVisibleCommands.value; // Use the flat visible list
|
||||
const commands = flatVisibleCommands.value;
|
||||
if (commands.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
|
||||
};
|
||||
|
||||
// 从后端获取快捷指令 (包含 tagIds,不再发送 sortBy)
|
||||
const clearQuickCommandsCache = () => {
|
||||
localStorage.removeItem(QUICK_COMMANDS_CACHE_KEY);
|
||||
};
|
||||
|
||||
const fetchQuickCommands = async () => {
|
||||
// 简化缓存:只缓存原始列表,不再区分排序
|
||||
const cacheKey = 'quickCommandsListCache';
|
||||
error.value = null;
|
||||
|
||||
// 1. 尝试从 localStorage 加载缓存
|
||||
try {
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
const cachedData = localStorage.getItem(QUICK_COMMANDS_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds 和 variables)
|
||||
const parsedData = JSON.parse(cachedData) as QuickCommandFE[];
|
||||
// 基本验证,确保 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 {
|
||||
console.warn('[QuickCmdStore] Cached data format invalid, ignoring cache.');
|
||||
localStorage.removeItem(cacheKey);
|
||||
isLoading.value = true;
|
||||
const parsedData = JSON.parse(cachedData);
|
||||
if (Array.isArray(parsedData)) {
|
||||
quickCommandsList.value = parsedData.map(normalizeQuickCommand);
|
||||
}
|
||||
} else {
|
||||
isLoading.value = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdStore] Failed to load or parse commands cache:', e);
|
||||
localStorage.removeItem(cacheKey);
|
||||
isLoading.value = true;
|
||||
} catch (cacheError) {
|
||||
console.error('[QuickCommandsStore] 读取快捷指令缓存失败:', cacheError);
|
||||
clearQuickCommandsCache();
|
||||
}
|
||||
|
||||
// 2. 后台获取最新数据
|
||||
isLoading.value = true;
|
||||
try {
|
||||
console.log(`[QuickCmdStore] Fetching latest commands from server...`);
|
||||
// 不再发送 sortBy 参数
|
||||
const response = await apiClient.get<QuickCommandFE[]>('/quick-commands');
|
||||
// 确保返回的数据包含 tagIds 数组和 variables 对象
|
||||
const freshData = response.data.map(cmd => ({
|
||||
...cmd,
|
||||
tagIds: Array.isArray(cmd.tagIds) ? cmd.tagIds : [], // 确保 tagIds 是数组
|
||||
variables: typeof cmd.variables === 'object' ? cmd.variables : undefined // 确保 variables 是对象或 undefined
|
||||
}));
|
||||
const freshDataString = JSON.stringify(freshData);
|
||||
|
||||
// 3. 对比并更新
|
||||
const currentDataString = JSON.stringify(quickCommandsList.value);
|
||||
if (currentDataString !== freshDataString) {
|
||||
console.log('[QuickCmdStore] Commands data changed, updating state and cache.');
|
||||
quickCommandsList.value = freshData;
|
||||
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
||||
} else {
|
||||
}
|
||||
error.value = null;
|
||||
const freshData = Array.isArray(response.data) ? response.data.map(normalizeQuickCommand) : [];
|
||||
quickCommandsList.value = freshData;
|
||||
localStorage.setItem(QUICK_COMMANDS_CACHE_KEY, JSON.stringify(freshData));
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdStore] 获取快捷指令失败:', err);
|
||||
console.error('[QuickCommandsStore] 获取快捷指令失败:', err);
|
||||
error.value = err.response?.data?.message || '获取快捷指令时发生错误';
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
@@ -305,109 +317,154 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 清除快捷指令列表缓存
|
||||
const clearQuickCommandsCache = () => {
|
||||
localStorage.removeItem('quickCommandsListCache');
|
||||
console.log('[QuickCmdStore] Cleared quick commands list cache.');
|
||||
};
|
||||
|
||||
|
||||
// 添加快捷指令 (发送 tagIds 和 variables)
|
||||
const addQuickCommand = async (name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<boolean> => {
|
||||
const addQuickCommand = async (
|
||||
name: string | null,
|
||||
command: string,
|
||||
tagIds?: number[],
|
||||
variables?: Record<string, string>,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
// 在请求体中包含 tagIds 和 variables
|
||||
const response = await apiClient.post<{ message: string, command: QuickCommandFE }>('/quick-commands', { name, command, tagIds, variables });
|
||||
// 后端现在返回完整的 command 对象,可以直接使用或触发刷新
|
||||
clearQuickCommandsCache(); // 清除缓存
|
||||
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||
await apiClient.post('/quick-commands', { name, command, tagIds, variables });
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
uiNotificationsStore.showSuccess('快捷指令已添加');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('添加快捷指令失败:', err);
|
||||
console.error('[QuickCommandsStore] 添加快捷指令失败:', err);
|
||||
const message = err.response?.data?.message || '添加快捷指令时发生错误';
|
||||
uiNotificationsStore.showError(message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新快捷指令 (发送 tagIds 和 variables)
|
||||
const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<boolean> => {
|
||||
try {
|
||||
// 在请求体中包含 tagIds 和 variables (即使是 undefined 也要发送,让后端知道是否要更新)
|
||||
const response = await apiClient.put<{ message: string, command: QuickCommandFE }>(`/quick-commands/${id}`, { name, command, tagIds, variables });
|
||||
// 后端现在返回完整的 command 对象
|
||||
clearQuickCommandsCache(); // 清除缓存
|
||||
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||
const updateQuickCommand = async (
|
||||
id: number,
|
||||
name: string | null,
|
||||
command: string,
|
||||
tagIds?: number[],
|
||||
variables?: Record<string, string>,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await apiClient.put(`/quick-commands/${id}`, { name, command, tagIds, variables });
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
uiNotificationsStore.showSuccess('快捷指令已更新');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('更新快捷指令失败:', err);
|
||||
console.error('[QuickCommandsStore] 更新快捷指令失败:', err);
|
||||
const message = err.response?.data?.message || '更新快捷指令时发生错误';
|
||||
uiNotificationsStore.showError(message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除快捷指令
|
||||
const deleteQuickCommand = async (id: number) => {
|
||||
try {
|
||||
await apiClient.delete(`/quick-commands/${id}`);
|
||||
clearQuickCommandsCache(); // 清除所有排序缓存
|
||||
// 从本地列表中移除
|
||||
const index = quickCommandsList.value.findIndex(cmd => cmd.id === id);
|
||||
if (index !== -1) {
|
||||
quickCommandsList.value.splice(index, 1);
|
||||
}
|
||||
quickCommandsList.value = quickCommandsList.value.filter((command) => command.id !== id);
|
||||
clearQuickCommandsCache();
|
||||
uiNotificationsStore.showSuccess('快捷指令已删除');
|
||||
} catch (err: any) {
|
||||
console.error('删除快捷指令失败:', err);
|
||||
console.error('[QuickCommandsStore] 删除快捷指令失败:', err);
|
||||
const message = err.response?.data?.message || '删除快捷指令时发生错误';
|
||||
uiNotificationsStore.showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
// 增加使用次数 (调用 API,然后更新本地数据)
|
||||
const incrementUsage = async (id: number) => {
|
||||
try {
|
||||
await apiClient.post(`/quick-commands/${id}/increment-usage`); // 使用 apiClient
|
||||
// 更新本地计数,避免重新请求整个列表
|
||||
const command = quickCommandsList.value.find(cmd => cmd.id === id);
|
||||
try {
|
||||
await apiClient.post(`/quick-commands/${id}/increment-usage`);
|
||||
const command = quickCommandsList.value.find((item) => item.id === id);
|
||||
if (command) {
|
||||
command.usage_count += 1;
|
||||
// 如果当前是按使用次数排序,可能需要重新排序或刷新列表
|
||||
if (sortBy.value === 'usage_count') {
|
||||
// 清除所有排序缓存并重新获取当前排序
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
}
|
||||
command.updated_at = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('增加使用次数失败:', err);
|
||||
// 这里可以选择不提示用户错误,因为这是一个后台操作
|
||||
} catch (err) {
|
||||
console.error('[QuickCommandsStore] 增加快捷指令使用次数失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置搜索词
|
||||
const setSearchTerm = (term: string) => {
|
||||
searchTerm.value = term;
|
||||
selectedIndex.value = -1; // Reset selection when search term changes
|
||||
selectedIndex.value = -1;
|
||||
};
|
||||
|
||||
// 设置排序方式 (只更新本地状态,不再重新获取数据)
|
||||
const setSortBy = (newSortBy: QuickCommandSortByType) => {
|
||||
if (sortBy.value !== newSortBy) {
|
||||
sortBy.value = newSortBy;
|
||||
// 排序现在由 filteredAndGroupedCommands getter 处理,无需重新 fetch
|
||||
selectedIndex.value = -1; // Reset selection when sort changes
|
||||
selectedIndex.value = -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Action to reset the selection
|
||||
const resetSelection = () => {
|
||||
selectedIndex.value = -1;
|
||||
};
|
||||
|
||||
// Removed duplicate resetSelection definition
|
||||
const reorderQuickCommands = async (commandIds: number[]): Promise<boolean> => {
|
||||
if (!Array.isArray(commandIds) || commandIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
setSortBy('manual');
|
||||
await apiClient.put('/quick-commands/reorder', { commandIds });
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCommandsStore] 更新快捷指令顺序失败:', err);
|
||||
const message = err.response?.data?.message || '更新快捷指令顺序失败';
|
||||
uiNotificationsStore.showError(message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const reorderCommandsInTag = async (tagId: number, commandIds: number[]): Promise<boolean> => {
|
||||
if (!Number.isInteger(tagId) || !Array.isArray(commandIds) || commandIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
setSortBy('manual');
|
||||
await apiClient.put('/quick-commands/reorder-by-tag', { tagId, commandIds });
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCommandsStore] 更新标签内快捷指令顺序失败:', err);
|
||||
const message = err.response?.data?.message || '更新标签内快捷指令顺序失败';
|
||||
uiNotificationsStore.showError(message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const assignCommandsToTagAction = async (commandIds: number[], tagId: number): Promise<boolean> => {
|
||||
if (!Array.isArray(commandIds) || commandIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await apiClient.post('/quick-commands/bulk-assign-tag', { commandIds, tagId });
|
||||
if (!response.data?.success) {
|
||||
throw new Error(response.data?.message || '批量分配标签失败');
|
||||
}
|
||||
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCommandsStore] 批量分配标签失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '批量分配标签失败';
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
quickCommandsList,
|
||||
@@ -415,10 +472,10 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
sortBy,
|
||||
isLoading,
|
||||
error,
|
||||
filteredAndGroupedCommands, // Expose the grouped data
|
||||
flatVisibleCommands, // Expose the flat visible list for navigation logic if needed outside
|
||||
selectedIndex, // Index within flatVisibleCommands
|
||||
expandedGroups, // Expose expanded groups state
|
||||
filteredAndGroupedCommands,
|
||||
flatVisibleCommands,
|
||||
selectedIndex,
|
||||
expandedGroups,
|
||||
fetchQuickCommands,
|
||||
addQuickCommand,
|
||||
updateQuickCommand,
|
||||
@@ -429,60 +486,11 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
selectNextCommand,
|
||||
selectPreviousCommand,
|
||||
resetSelection,
|
||||
toggleGroup, // +++ Expose toggleGroup action +++
|
||||
loadExpandedGroups, // +++ Expose load action +++
|
||||
|
||||
// +++ Action to assign a tag to multiple commands +++
|
||||
async assignCommandsToTagAction(commandIds: number[], tagId: number): Promise<boolean> {
|
||||
if (!commandIds || commandIds.length === 0) {
|
||||
console.warn('[Store] assignCommandsToTagAction: No command IDs provided.');
|
||||
return false;
|
||||
}
|
||||
isLoading.value = true; // Use the store's isLoading state
|
||||
error.value = null; // Use the store's error state
|
||||
try {
|
||||
const response = await apiClient.post('/quick-commands/bulk-assign-tag', { commandIds, tagId });
|
||||
if (response.data.success) {
|
||||
console.log(`[Store] Successfully assigned tag ${tagId} to ${commandIds.length} commands via API.`);
|
||||
|
||||
// --- Manual state update for immediate UI feedback ---
|
||||
let updatedCount = 0;
|
||||
commandIds.forEach(cmdId => {
|
||||
const commandIndex = quickCommandsList.value.findIndex(cmd => cmd.id === cmdId);
|
||||
if (commandIndex !== -1) {
|
||||
const command = quickCommandsList.value[commandIndex];
|
||||
// Ensure tagIds exists and add the new tagId if not already present
|
||||
if (!Array.isArray(command.tagIds)) {
|
||||
command.tagIds = [];
|
||||
}
|
||||
if (!command.tagIds.includes(tagId)) {
|
||||
command.tagIds.push(tagId);
|
||||
updatedCount++;
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Store] assignCommandsToTagAction: Command ID ${cmdId} not found in local list for manual update.`);
|
||||
}
|
||||
});
|
||||
console.log(`[Store] Manually updated tagIds for ${updatedCount} commands in local state.`);
|
||||
|
||||
// Optionally, still fetch for full consistency, but UI should update based on manual change first.
|
||||
// clearQuickCommandsCache();
|
||||
// await fetchQuickCommands();
|
||||
return true;
|
||||
} else {
|
||||
// This case might not happen if backend throws errors instead
|
||||
error.value = response.data.message || '批量分配标签失败 (未知)';
|
||||
if (error.value) uiNotificationsStore.showError(error.value); // Check if error.value is not null
|
||||
return false;
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[Store] Error assigning tag to commands:', err);
|
||||
error.value = err.response?.data?.message || err.message || '批量分配标签时发生网络或服务器错误';
|
||||
if (error.value) uiNotificationsStore.showError(error.value); // Check if error.value is not null
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
toggleGroup,
|
||||
loadExpandedGroups,
|
||||
clearQuickCommandsCache,
|
||||
reorderQuickCommands,
|
||||
reorderCommandsInTag,
|
||||
assignCommandsToTagAction,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1597,6 +1597,17 @@ onBeforeUnmount(() => {
|
||||
<span>{{ t('connections.actions.connect', '连接') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="conn.type === 'SSH'"
|
||||
:disabled="getSingleTestButtonInfo(conn.id, conn.type).disabled"
|
||||
:title="getSingleTestButtonInfo(conn.id, conn.type).title"
|
||||
class="px-4 py-2 rounded-lg border border-border bg-background text-foreground hover:bg-border transition-colors inline-flex items-center gap-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click.stop="handleTestSingleConnection(conn)"
|
||||
>
|
||||
<i :class="getSingleTestButtonInfo(conn.id, conn.type).iconClass"></i>
|
||||
<span>{{ getSingleTestButtonInfo(conn.id, conn.type).text }}</span>
|
||||
</button>
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
@click.stop="toggleMoreMenu(conn.id)"
|
||||
@@ -1618,16 +1629,6 @@ onBeforeUnmount(() => {
|
||||
<i class="fas fa-pen w-4 text-center"></i>
|
||||
<span>{{ t('connections.actions.edit', '编辑') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="conn.type === 'SSH'"
|
||||
:disabled="getSingleTestButtonInfo(conn.id, conn.type).disabled"
|
||||
:title="getSingleTestButtonInfo(conn.id, conn.type).title"
|
||||
class="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-header transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click.stop="handleTestSingleConnection(conn); closeMoreMenu()"
|
||||
>
|
||||
<i :class="[getSingleTestButtonInfo(conn.id, conn.type).iconClass, 'w-4 text-center']"></i>
|
||||
<span>{{ getSingleTestButtonInfo(conn.id, conn.type).text }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-header transition-colors flex items-center gap-2"
|
||||
@click.stop="handleCloneConnection(conn); closeMoreMenu()"
|
||||
|
||||
@@ -68,8 +68,17 @@
|
||||
<div
|
||||
class="group font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
||||
:style="{ padding: isCompactMode ? `calc(0.25rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.5rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
|
||||
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
||||
:draggable="groupData.tagId !== null && !dragDisabledBySearch"
|
||||
:class="{
|
||||
'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId),
|
||||
'cursor-grab': groupData.tagId !== null && !dragDisabledBySearch,
|
||||
'qc-drop-target': isGroupDropTarget(groupData.tagId),
|
||||
}"
|
||||
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
||||
@dragstart="handleGroupDragStart($event, groupData.tagId)"
|
||||
@dragover.prevent="handleGroupDragOver(groupData.tagId)"
|
||||
@drop.prevent="handleGroupDrop(groupData.tagId)"
|
||||
@dragend="resetDragState"
|
||||
>
|
||||
<i
|
||||
:class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out', {'transform rotate-0': !expandedGroups[groupData.groupName]}]"
|
||||
@@ -114,10 +123,20 @@
|
||||
:title="cmd.command"
|
||||
class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||
:style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
|
||||
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||
:draggable="!dragDisabledBySearch"
|
||||
:class="{
|
||||
'bg-primary/20 font-medium': isCommandSelected(cmd.id),
|
||||
'cursor-grab': !dragDisabledBySearch,
|
||||
'qc-drop-target': isCommandDropTarget(cmd.id, groupData.tagId),
|
||||
'opacity-70': isDraggingCommand(cmd.id, groupData.tagId),
|
||||
}"
|
||||
@click="selectCommand(cmd.id)"
|
||||
@dblclick="executeCommand(cmd)"
|
||||
@contextmenu.prevent="showQuickCommandContextMenu($event, cmd)"
|
||||
@dragstart="handleCommandDragStart($event, cmd.id, groupData.tagId)"
|
||||
@dragover.prevent="handleCommandDragOver(cmd.id, groupData.tagId)"
|
||||
@drop.prevent="handleCommandDrop(cmd.id, groupData.tagId)"
|
||||
@dragend="resetDragState"
|
||||
>
|
||||
<!-- Command Info -->
|
||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||
@@ -162,10 +181,20 @@
|
||||
:title="cmd.command"
|
||||
class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||
:style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
|
||||
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||
:draggable="!dragDisabledBySearch"
|
||||
:class="{
|
||||
'bg-primary/20 font-medium': isCommandSelected(cmd.id),
|
||||
'cursor-grab': !dragDisabledBySearch,
|
||||
'qc-drop-target': isCommandDropTarget(cmd.id, null),
|
||||
'opacity-70': isDraggingCommand(cmd.id, null),
|
||||
}"
|
||||
@click="selectCommand(cmd.id)"
|
||||
@dblclick="executeCommand(cmd)"
|
||||
@contextmenu.prevent="showQuickCommandContextMenu($event, cmd)"
|
||||
@dragstart="handleCommandDragStart($event, cmd.id, null)"
|
||||
@dragover.prevent="handleCommandDragOver(cmd.id, null)"
|
||||
@drop.prevent="handleCommandDrop(cmd.id, null)"
|
||||
@dragend="resetDragState"
|
||||
>
|
||||
<!-- Command Info -->
|
||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||
@@ -379,6 +408,17 @@ const flatFilteredCommands = computed(() => {
|
||||
return quickCommandsStore.flatVisibleCommands;
|
||||
});
|
||||
|
||||
const dragDisabledBySearch = computed(() => searchTerm.value.trim().length > 0);
|
||||
const draggingGroupTagId = ref<number | null>(null);
|
||||
const groupDropTargetTagId = ref<number | null>(null);
|
||||
const draggingCommand = ref<{ commandId: number; groupTagId: number | null } | null>(null);
|
||||
const commandDropTarget = ref<{ commandId: number; groupTagId: number | null } | null>(null);
|
||||
const dragDisabledTitle = computed(() =>
|
||||
dragDisabledBySearch.value
|
||||
? t('quickCommands.dragDisabledBySearch', '搜索结果中不可拖动排序')
|
||||
: t('quickCommands.dragCommand', '拖动排序快捷指令')
|
||||
);
|
||||
|
||||
// --- Compact Mode ---
|
||||
const isCompactMode = computed(() => quickCommandsCompactModeBoolean.value);
|
||||
|
||||
@@ -403,6 +443,163 @@ const selectCommand = (commandId: number) => {
|
||||
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
const resetDragState = () => {
|
||||
draggingGroupTagId.value = null;
|
||||
groupDropTargetTagId.value = null;
|
||||
draggingCommand.value = null;
|
||||
commandDropTarget.value = null;
|
||||
};
|
||||
|
||||
const moveById = <T extends { id: number }>(items: T[], sourceId: number, targetId: number): T[] => {
|
||||
const clonedItems = [...items];
|
||||
const sourceIndex = clonedItems.findIndex((item) => item.id === sourceId);
|
||||
const targetIndex = clonedItems.findIndex((item) => item.id === targetId);
|
||||
|
||||
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) {
|
||||
return clonedItems;
|
||||
}
|
||||
|
||||
const [sourceItem] = clonedItems.splice(sourceIndex, 1);
|
||||
clonedItems.splice(targetIndex, 0, sourceItem);
|
||||
return clonedItems;
|
||||
};
|
||||
|
||||
const isGroupDropTarget = (tagId: number | null): boolean =>
|
||||
tagId !== null && groupDropTargetTagId.value === tagId;
|
||||
|
||||
const isDraggingCommand = (commandId: number, groupTagId: number | null): boolean =>
|
||||
draggingCommand.value?.commandId === commandId && draggingCommand.value?.groupTagId === groupTagId;
|
||||
|
||||
const isCommandDropTarget = (commandId: number, groupTagId: number | null): boolean =>
|
||||
commandDropTarget.value?.commandId === commandId && commandDropTarget.value?.groupTagId === groupTagId;
|
||||
|
||||
const handleGroupDragStart = (event: DragEvent, tagId: number | null) => {
|
||||
if (dragDisabledBySearch.value || tagId === null) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
draggingGroupTagId.value = tagId;
|
||||
groupDropTargetTagId.value = null;
|
||||
event.dataTransfer?.setData('text/plain', String(tagId));
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
};
|
||||
|
||||
const handleGroupDragOver = (tagId: number | null) => {
|
||||
if (draggingGroupTagId.value === null || dragDisabledBySearch.value || tagId === null || tagId === draggingGroupTagId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
groupDropTargetTagId.value = tagId;
|
||||
};
|
||||
|
||||
const handleGroupDrop = async (tagId: number | null) => {
|
||||
if (draggingGroupTagId.value === null || dragDisabledBySearch.value || tagId === null || tagId === draggingGroupTagId.value) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const taggedGroups = filteredAndGroupedCommands.value
|
||||
.filter((group) => group.tagId !== null)
|
||||
.map((group) => ({ ...group, id: group.tagId as number }));
|
||||
const reorderedGroups = moveById(taggedGroups, draggingGroupTagId.value, tagId);
|
||||
const reorderedVisibleTagIds = reorderedGroups.map((group) => group.id);
|
||||
const globalTagIds = [...quickCommandTagsStore.tags]
|
||||
.sort((a, b) => (a.sort_order - b.sort_order) || (a.id - b.id))
|
||||
.map((tag) => tag.id);
|
||||
const visibleTagIdSet = new Set(reorderedVisibleTagIds);
|
||||
let visibleIndex = 0;
|
||||
const mergedTagIds = globalTagIds.map((existingTagId) => {
|
||||
if (!visibleTagIdSet.has(existingTagId)) {
|
||||
return existingTagId;
|
||||
}
|
||||
|
||||
const nextVisibleTagId = reorderedVisibleTagIds[visibleIndex];
|
||||
visibleIndex += 1;
|
||||
return nextVisibleTagId ?? existingTagId;
|
||||
});
|
||||
|
||||
await quickCommandTagsStore.reorderTags(mergedTagIds);
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
const handleCommandDragStart = (event: DragEvent, commandId: number, groupTagId: number | null) => {
|
||||
if (dragDisabledBySearch.value) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
draggingCommand.value = { commandId, groupTagId };
|
||||
commandDropTarget.value = null;
|
||||
event.dataTransfer?.setData('text/plain', String(commandId));
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
};
|
||||
|
||||
const handleCommandDragOver = (commandId: number, groupTagId: number | null) => {
|
||||
if (!draggingCommand.value || dragDisabledBySearch.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (draggingCommand.value.groupTagId !== groupTagId || draggingCommand.value.commandId === commandId) {
|
||||
return;
|
||||
}
|
||||
|
||||
commandDropTarget.value = { commandId, groupTagId };
|
||||
};
|
||||
|
||||
const handleCommandDrop = async (commandId: number, groupTagId: number | null) => {
|
||||
if (!draggingCommand.value || dragDisabledBySearch.value) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (draggingCommand.value.groupTagId !== groupTagId || draggingCommand.value.commandId === commandId) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
let currentCommands: QuickCommandFE[] = [];
|
||||
if (showQuickCommandTagsBoolean.value) {
|
||||
currentCommands = filteredAndGroupedCommands.value.find((group) => group.tagId === groupTagId)?.commands ?? [];
|
||||
} else {
|
||||
currentCommands = flatFilteredCommands.value;
|
||||
}
|
||||
|
||||
const reorderedCommands = moveById(currentCommands, draggingCommand.value.commandId, commandId);
|
||||
if (showQuickCommandTagsBoolean.value) {
|
||||
if (groupTagId !== null) {
|
||||
await quickCommandsStore.reorderCommandsInTag(groupTagId, reorderedCommands.map((item) => item.id));
|
||||
} else {
|
||||
const reorderedUntaggedIds = reorderedCommands.map((item) => item.id);
|
||||
const globalCommandIds = [...quickCommandsStore.quickCommandsList]
|
||||
.sort((a, b) => (a.sort_order - b.sort_order) || (a.id - b.id))
|
||||
.map((command) => command.id);
|
||||
|
||||
let untaggedIndex = 0;
|
||||
const mergedCommandIds = globalCommandIds.map((existingCommandId) => {
|
||||
const command = quickCommandsStore.quickCommandsList.find((item) => item.id === existingCommandId);
|
||||
if (!command || command.tagIds.length > 0) {
|
||||
return existingCommandId;
|
||||
}
|
||||
|
||||
const nextUntaggedId = reorderedUntaggedIds[untaggedIndex];
|
||||
untaggedIndex += 1;
|
||||
return nextUntaggedId ?? existingCommandId;
|
||||
});
|
||||
|
||||
await quickCommandsStore.reorderQuickCommands(mergedCommandIds);
|
||||
}
|
||||
} else {
|
||||
await quickCommandsStore.reorderQuickCommands(reorderedCommands.map((item) => item.id));
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
onMounted(async () => { // Make onMounted async
|
||||
// Load expanded groups state first
|
||||
quickCommandsStore.loadExpandedGroups();
|
||||
@@ -518,8 +715,10 @@ const handleSearchInputBlur = () => {
|
||||
|
||||
// 切换排序方式 (Action remains the same, store handles the logic change)
|
||||
const toggleSortBy = () => {
|
||||
const newSortBy = sortBy.value === 'name' ? 'last_used' : 'name';
|
||||
quickCommandsStore.setSortBy(newSortBy);
|
||||
const sortModes: QuickCommandSortByType[] = ['manual', 'name', 'last_used'];
|
||||
const currentIndex = sortModes.indexOf(sortBy.value);
|
||||
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % sortModes.length;
|
||||
quickCommandsStore.setSortBy(sortModes[nextIndex]);
|
||||
};
|
||||
|
||||
// +++ Action to toggle group expansion +++
|
||||
@@ -896,3 +1095,10 @@ const handleQuickCommandMenuAction = async (action: QuickCommandContextAction, c
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qc-drop-target {
|
||||
outline: 1px dashed color-mix(in srgb, var(--color-primary, #3b82f6) 72%, transparent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user