@@ -73,8 +73,41 @@ const definedMigrations: Migration[] = [
|
|||||||
-- UPDATE connections SET encrypted_passphrase = NULL WHERE encrypted_passphrase = ''; -- 示例
|
-- UPDATE connections SET encrypted_passphrase = NULL WHERE encrypted_passphrase = ''; -- 示例
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
// --- Quick Command Tags Migrations ---
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Create quick_command_tags table',
|
||||||
|
check: async (db: Database): Promise<boolean> => {
|
||||||
|
const tableAlreadyExists = await tableExists(db, 'quick_command_tags');
|
||||||
|
return !tableAlreadyExists; // Only run if the table does NOT exist
|
||||||
|
},
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE IF NOT EXISTS quick_command_tags (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Create quick_command_tag_associations table',
|
||||||
|
check: async (db: Database): Promise<boolean> => {
|
||||||
|
const tableAlreadyExists = await tableExists(db, 'quick_command_tag_associations');
|
||||||
|
return !tableAlreadyExists; // Only run if the table does NOT exist
|
||||||
|
},
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE IF NOT EXISTS quick_command_tag_associations (
|
||||||
|
quick_command_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
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
|
||||||
|
);
|
||||||
|
`
|
||||||
|
}
|
||||||
// --- 未来可以添加更多迁移 ---
|
// --- 未来可以添加更多迁移 ---
|
||||||
// { id: 2, name: '...', sql: '...' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -152,6 +152,30 @@ CREATE TABLE IF NOT EXISTS quick_commands (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// --- Quick Command Tags ---
|
||||||
|
|
||||||
|
export const createQuickCommandTagsTableSQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS quick_command_tags (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const createQuickCommandTagAssociationsTableSQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS quick_command_tag_associations (
|
||||||
|
quick_command_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
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
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
// --- End Quick Command Tags ---
|
||||||
|
|
||||||
|
|
||||||
// 从 database.ts 移动过来的,保持一致性
|
// 从 database.ts 移动过来的,保持一致性
|
||||||
export const createTerminalThemesTableSQL = `
|
export const createTerminalThemesTableSQL = `
|
||||||
CREATE TABLE IF NOT EXISTS terminal_themes (
|
CREATE TABLE IF NOT EXISTS terminal_themes (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import quickCommandsRoutes from './quick-commands/quick-commands.routes';
|
|||||||
import terminalThemeRoutes from './terminal-themes/terminal-theme.routes';
|
import terminalThemeRoutes from './terminal-themes/terminal-theme.routes';
|
||||||
import appearanceRoutes from './appearance/appearance.routes';
|
import appearanceRoutes from './appearance/appearance.routes';
|
||||||
import sshKeysRouter from './ssh_keys/ssh_keys.routes'; // +++ Import SSH Key routes +++
|
import sshKeysRouter from './ssh_keys/ssh_keys.routes'; // +++ Import SSH Key routes +++
|
||||||
|
import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes'; // +++ Import Quick Command Tag routes +++
|
||||||
import { initializeWebSocket } from './websocket';
|
import { initializeWebSocket } from './websocket';
|
||||||
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
||||||
|
|
||||||
@@ -261,7 +262,8 @@ const startServer = () => {
|
|||||||
app.use('/api/v1/terminal-themes', terminalThemeRoutes);
|
app.use('/api/v1/terminal-themes', terminalThemeRoutes);
|
||||||
app.use('/api/v1/appearance', appearanceRoutes);
|
app.use('/api/v1/appearance', appearanceRoutes);
|
||||||
app.use('/api/v1/ssh-keys', sshKeysRouter); // +++ Register SSH Key routes +++
|
app.use('/api/v1/ssh-keys', sshKeysRouter); // +++ Register SSH Key routes +++
|
||||||
|
app.use('/api/v1/quick-command-tags', quickCommandTagRoutes); // +++ Register Quick Command Tag routes +++
|
||||||
|
|
||||||
// 状态检查接口
|
// 状态检查接口
|
||||||
app.get('/api/v1/status', (req: Request, res: Response) => {
|
app.get('/api/v1/status', (req: Request, res: Response) => {
|
||||||
res.json({ status: '后端服务运行中!' });
|
res.json({ status: '后端服务运行中!' });
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as QuickCommandTagService from '../services/quick-command-tag.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理获取所有快捷指令标签的请求
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
} 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 || '无法添加快捷指令标签' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新快捷指令标签的请求
|
||||||
|
*/
|
||||||
|
export const updateQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的标签 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||||
|
res.status(400).json({ message: '标签名称不能为空且必须是字符串' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// 检查标签是否真的不存在
|
||||||
|
const tagExists = await QuickCommandTagService.getQuickCommandTagById(id);
|
||||||
|
if (!tagExists) {
|
||||||
|
res.status(404).json({ message: '未找到要更新的快捷指令标签' });
|
||||||
|
} else {
|
||||||
|
// 如果标签存在但更新失败(理论上不太可能,除非并发问题),返回服务器错误
|
||||||
|
console.error(`[Controller] 更新快捷指令标签 ${id} 失败,但标签存在。`);
|
||||||
|
res.status(500).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 || '无法更新快捷指令标签' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除快捷指令标签的请求
|
||||||
|
*/
|
||||||
|
export const deleteQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的标签 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先检查标签是否存在,以便返回 404
|
||||||
|
const tagExists = await QuickCommandTagService.getQuickCommandTagById(id);
|
||||||
|
if (!tagExists) {
|
||||||
|
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: '删除快捷指令标签时发生未知错误' });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Controller] 删除快捷指令标签失败:', error.message);
|
||||||
|
res.status(500).json({ message: error.message || '无法删除快捷指令标签' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import * as QuickCommandTagController from './quick-command-tag.controller';
|
||||||
|
import { isAuthenticated } from '../auth/auth.middleware'; // 假设需要认证
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 获取所有快捷指令标签
|
||||||
|
router.get('/', isAuthenticated, QuickCommandTagController.getAllQuickCommandTags);
|
||||||
|
|
||||||
|
// 添加新的快捷指令标签
|
||||||
|
router.post('/', isAuthenticated, QuickCommandTagController.addQuickCommandTag);
|
||||||
|
|
||||||
|
// 更新快捷指令标签
|
||||||
|
router.put('/:id', isAuthenticated, QuickCommandTagController.updateQuickCommandTag);
|
||||||
|
|
||||||
|
// 删除快捷指令标签
|
||||||
|
router.delete('/:id', isAuthenticated, QuickCommandTagController.deleteQuickCommandTag);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -6,8 +6,10 @@ import { QuickCommandSortBy } from '../services/quick-commands.service';
|
|||||||
* 处理添加新快捷指令的请求
|
* 处理添加新快捷指令的请求
|
||||||
*/
|
*/
|
||||||
export const addQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
export const addQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
||||||
const { name, command } = req.body;
|
// 从请求体中解构出 name, command, 以及可选的 tagIds
|
||||||
|
const { name, command, tagIds } = req.body;
|
||||||
|
|
||||||
|
// --- 基本验证 ---
|
||||||
if (!command || typeof command !== 'string' || command.trim().length === 0) {
|
if (!command || typeof command !== 'string' || command.trim().length === 0) {
|
||||||
res.status(400).json({ message: '指令内容不能为空' });
|
res.status(400).json({ message: '指令内容不能为空' });
|
||||||
return;
|
return;
|
||||||
@@ -17,12 +19,26 @@ export const addQuickCommand = async (req: Request, res: Response): Promise<void
|
|||||||
res.status(400).json({ message: '名称必须是字符串或 null' });
|
res.status(400).json({ message: '名称必须是字符串或 null' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 验证 tagIds (如果提供的话)
|
||||||
|
if (tagIds !== undefined && (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number'))) {
|
||||||
|
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// --- 结束验证 ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newId = await QuickCommandsService.addQuickCommand(name, command);
|
// 将 tagIds 传递给 Service 层
|
||||||
res.status(201).json({ id: newId, message: '快捷指令已添加' });
|
const newId = await QuickCommandsService.addQuickCommand(name, command, tagIds);
|
||||||
|
// 尝试获取新创建的带标签的指令信息返回
|
||||||
|
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 });
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('添加快捷指令控制器出错:', error);
|
console.error('[Controller] 添加快捷指令失败:', error.message);
|
||||||
res.status(500).json({ message: error.message || '无法添加快捷指令' });
|
res.status(500).json({ message: error.message || '无法添加快捷指令' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -49,8 +65,10 @@ export const getAllQuickCommands = async (req: Request, res: Response): Promise<
|
|||||||
*/
|
*/
|
||||||
export const updateQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
export const updateQuickCommand = async (req: Request, res: Response): Promise<void> => {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
const { name, command } = req.body;
|
// 从请求体中解构出 name, command, 以及可选的 tagIds
|
||||||
|
const { name, command, tagIds } = req.body;
|
||||||
|
|
||||||
|
// --- 基本验证 ---
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
res.status(400).json({ message: '无效的 ID' });
|
res.status(400).json({ message: '无效的 ID' });
|
||||||
return;
|
return;
|
||||||
@@ -63,13 +81,35 @@ export const updateQuickCommand = async (req: Request, res: Response): Promise<v
|
|||||||
res.status(400).json({ message: '名称必须是字符串或 null' });
|
res.status(400).json({ message: '名称必须是字符串或 null' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 验证 tagIds (如果提供的话)
|
||||||
|
// 注意: tagIds 为 undefined 表示不更新标签,空数组 [] 表示清除所有标签
|
||||||
|
if (tagIds !== undefined && (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number'))) {
|
||||||
|
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// --- 结束验证 ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await QuickCommandsService.updateQuickCommand(id, name, command);
|
// 将 tagIds 传递给 Service 层
|
||||||
|
const success = await QuickCommandsService.updateQuickCommand(id, name, command, tagIds);
|
||||||
if (success) {
|
if (success) {
|
||||||
res.status(200).json({ message: '快捷指令已更新' });
|
// 尝试获取更新后的带标签的指令信息返回
|
||||||
|
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 {
|
} else {
|
||||||
res.status(404).json({ message: '未找到要更新的快捷指令' });
|
// 检查指令是否真的不存在
|
||||||
|
const commandExists = await QuickCommandsService.getQuickCommandById(id);
|
||||||
|
if (!commandExists) {
|
||||||
|
res.status(404).json({ message: '未找到要更新的快捷指令' });
|
||||||
|
} else {
|
||||||
|
console.error(`[Controller] 更新快捷指令 ${id} 失败,但指令存在。`);
|
||||||
|
res.status(500).json({ message: '更新快捷指令时发生未知错误' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('更新快捷指令控制器出错:', error);
|
console.error('更新快捷指令控制器出错:', error);
|
||||||
@@ -126,3 +166,28 @@ export const incrementUsage = async (req: Request, res: Response): Promise<void>
|
|||||||
res.status(500).json({ message: error.message || '无法增加使用次数' });
|
res.status(500).json({ message: error.message || '无法增加使用次数' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量将标签分配给多个快捷指令
|
||||||
|
*/
|
||||||
|
export const assignTagToCommands = async (req: Request, res: Response): Promise<void> => { // Add : 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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} 个指令。` });
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ router.get('/', QuickCommandsController.getAllQuickCommands); // GET /api/v1/qui
|
|||||||
router.put('/:id', QuickCommandsController.updateQuickCommand); // PUT /api/v1/quick-commands/:id
|
router.put('/:id', QuickCommandsController.updateQuickCommand); // PUT /api/v1/quick-commands/:id
|
||||||
router.delete('/:id', QuickCommandsController.deleteQuickCommand); // DELETE /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
|
router.post('/:id/increment-usage', QuickCommandsController.incrementUsage); // POST /api/v1/quick-commands/:id/increment-usage
|
||||||
|
router.post('/bulk-assign-tag', QuickCommandsController.assignTagToCommands); // POST /api/v1/quick-commands/bulk-assign-tag
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { Database } from 'sqlite3';
|
||||||
|
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||||
|
|
||||||
|
// 定义 Quick Command Tag 类型
|
||||||
|
export interface QuickCommandTag {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有快捷指令标签
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[仓库] 查询快捷指令标签列表时出错:', 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]);
|
||||||
|
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||||
|
throw new Error('创建快捷指令标签后未能获取有效的 lastID');
|
||||||
|
}
|
||||||
|
return result.lastID;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[仓库] 创建快捷指令标签时出错:', err.message);
|
||||||
|
if (err.message.includes('UNIQUE constraint failed')) {
|
||||||
|
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]);
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除快捷指令标签 (同时会通过外键 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]);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[仓库] 删除快捷指令标签 ${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 (?, ?)`;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
await runDb(db, 'COMMIT');
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('设置快捷指令标签关联时出错:', err.message);
|
||||||
|
await runDb(db, 'ROLLBACK'); // 出错时回滚
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
await stmt.run(commandId, tagId);
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
throw new Error('无法批量关联标签到快捷指令');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定快捷指令的标签关联 (使用事务)
|
||||||
|
* @param commandId 快捷指令 ID
|
||||||
|
* @param tagIds 新的快捷指令标签 ID 数组 (空数组表示清除所有标签)
|
||||||
|
*/
|
||||||
|
// Removed the duplicate function declaration that returned Promise<boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找指定快捷指令的所有标签
|
||||||
|
* @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
|
||||||
|
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`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const rows = await allDb<QuickCommandTag>(db, sql, [commandId]);
|
||||||
|
return rows;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`Repository: 查询快捷指令 ${commandId} 的标签时出错:`, err.message);
|
||||||
|
throw new Error('获取快捷指令标签失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||||
|
|
||||||
// 定义快捷指令的接口
|
// 定义基础快捷指令接口
|
||||||
export interface QuickCommand {
|
export interface QuickCommand {
|
||||||
id: number;
|
id: number;
|
||||||
name: string | null; // 名称可选
|
name: string | null; // 名称可选
|
||||||
@@ -10,7 +10,16 @@ export interface QuickCommand {
|
|||||||
updated_at: number; // Unix 时间戳 (秒)
|
updated_at: number; // Unix 时间戳 (秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DbQuickCommandRow = QuickCommand;
|
// 定义包含标签 ID 的接口
|
||||||
|
export interface QuickCommandWithTags extends QuickCommand {
|
||||||
|
tagIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于从数据库获取带 tag_ids_str 的行
|
||||||
|
interface DbQuickCommandWithTagsRow extends QuickCommand {
|
||||||
|
tag_ids_str: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加一条新的快捷指令
|
* 添加一条新的快捷指令
|
||||||
@@ -70,22 +79,34 @@ export const deleteQuickCommand = async (id: number): Promise<boolean> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有快捷指令
|
* 获取所有快捷指令及其关联的标签 ID
|
||||||
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
||||||
* @returns 返回包含所有快捷指令条目的数组
|
* @returns 返回包含所有快捷指令条目及标签 ID 的数组
|
||||||
*/
|
*/
|
||||||
export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommand[]> => {
|
export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommandWithTags[]> => {
|
||||||
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
|
let orderByClause = 'ORDER BY qc.name ASC'; // 默认按名称升序
|
||||||
if (sortBy === 'usage_count') {
|
if (sortBy === 'usage_count') {
|
||||||
orderByClause = 'ORDER BY usage_count DESC, name ASC'; // 按使用频率降序,同频率按名称升序
|
orderByClause = 'ORDER BY qc.usage_count DESC, qc.name ASC'; // 按使用频率降序,同频率按名称升序
|
||||||
}
|
}
|
||||||
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands ${orderByClause}`;
|
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
qc.id, qc.name, qc.command, qc.usage_count, 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 {
|
try {
|
||||||
const db = await getDbInstance();
|
const db = await getDbInstance();
|
||||||
const rows = await allDb<DbQuickCommandRow>(db, sql);
|
const rows = await allDb<DbQuickCommandWithTagsRow>(db, sql);
|
||||||
return rows;
|
// 将 tag_ids_str 解析为数字数组
|
||||||
|
return rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||||
|
}));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('获取快捷指令时出错:', err.message);
|
console.error('获取快捷指令(带标签)时出错:', err.message);
|
||||||
throw new Error('无法获取快捷指令');
|
throw new Error('无法获取快捷指令');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -108,18 +129,34 @@ export const incrementUsageCount = async (id: number): Promise<boolean> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 ID 查找快捷指令 (用于编辑前获取数据)
|
* 根据 ID 查找快捷指令及其关联的标签 ID
|
||||||
* @param id - 要查找的记录 ID
|
* @param id - 要查找的记录 ID
|
||||||
* @returns 返回找到的快捷指令条目,如果未找到则返回 undefined
|
* @returns 返回找到的快捷指令条目及标签 ID,如果未找到则返回 undefined
|
||||||
*/
|
*/
|
||||||
export const findQuickCommandById = async (id: number): Promise<QuickCommand | undefined> => {
|
export const findQuickCommandById = async (id: number): Promise<QuickCommandWithTags | undefined> => {
|
||||||
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands WHERE id = ?`;
|
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
qc.id, qc.name, qc.command, qc.usage_count, 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 {
|
try {
|
||||||
const db = await getDbInstance();
|
const db = await getDbInstance();
|
||||||
const row = await getDbRow<DbQuickCommandRow>(db, sql, [id]);
|
const row = await getDbRow<DbQuickCommandWithTagsRow>(db, sql, [id]);
|
||||||
return row;
|
if (row && typeof row.id !== 'undefined') {
|
||||||
|
// 将 tag_ids_str 解析为数字数组
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('查找快捷指令时出错:', err.message);
|
console.error('查找快捷指令(带标签)时出错:', err.message);
|
||||||
throw new Error('无法查找快捷指令');
|
throw new Error('无法查找快捷指令');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import * as QuickCommandTagRepository from '../repositories/quick-command-tag.repository';
|
||||||
|
import { QuickCommandTag } from '../repositories/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 响应
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新快捷指令标签
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除快捷指令标签
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定快捷指令的标签关联
|
||||||
|
* @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 列表必须是一个数字数组');
|
||||||
|
}
|
||||||
|
// 可以在这里添加更复杂的验证,例如检查 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定快捷指令的所有标签
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as QuickCommandsRepository from '../repositories/quick-commands.repository';
|
import * as QuickCommandsRepository from '../repositories/quick-commands.repository';
|
||||||
import { QuickCommand } from '../repositories/quick-commands.repository';
|
import { QuickCommandWithTags } from '../repositories/quick-commands.repository'; // Import the type with tags
|
||||||
|
import * as QuickCommandTagRepository from '../repositories/quick-command-tag.repository'; // Import the new tag repository
|
||||||
|
|
||||||
// 定义排序类型
|
// 定义排序类型
|
||||||
export type QuickCommandSortBy = 'name' | 'usage_count';
|
export type QuickCommandSortBy = 'name' | 'usage_count';
|
||||||
@@ -8,15 +9,28 @@ export type QuickCommandSortBy = 'name' | 'usage_count';
|
|||||||
* 添加快捷指令
|
* 添加快捷指令
|
||||||
* @param name - 指令名称 (可选)
|
* @param name - 指令名称 (可选)
|
||||||
* @param command - 指令内容
|
* @param command - 指令内容
|
||||||
|
* @param tagIds - 关联的快捷指令标签 ID 数组 (可选)
|
||||||
* @returns 返回添加记录的 ID
|
* @returns 返回添加记录的 ID
|
||||||
*/
|
*/
|
||||||
export const addQuickCommand = async (name: string | null, command: string): Promise<number> => {
|
export const addQuickCommand = async (name: string | null, command: string, tagIds?: number[]): Promise<number> => {
|
||||||
if (!command || command.trim().length === 0) {
|
if (!command || command.trim().length === 0) {
|
||||||
throw new Error('指令内容不能为空');
|
throw new Error('指令内容不能为空');
|
||||||
}
|
}
|
||||||
// 如果 name 是空字符串,则视为 null
|
// 如果 name 是空字符串,则视为 null
|
||||||
const finalName = name && name.trim().length > 0 ? name.trim() : null;
|
const finalName = name && name.trim().length > 0 ? name.trim() : null;
|
||||||
return QuickCommandsRepository.addQuickCommand(finalName, command.trim());
|
const commandId = await QuickCommandsRepository.addQuickCommand(finalName, command.trim());
|
||||||
|
|
||||||
|
// 添加成功后,设置标签关联
|
||||||
|
if (commandId > 0 && tagIds && Array.isArray(tagIds)) {
|
||||||
|
try {
|
||||||
|
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
|
||||||
|
} catch (tagError: any) {
|
||||||
|
// 如果标签关联失败,可以选择记录警告或回滚(但通常不回滚主记录)
|
||||||
|
console.warn(`[Service] 添加快捷指令 ${commandId} 成功,但设置标签关联失败:`, tagError.message);
|
||||||
|
// 可以考虑是否需要通知用户部分操作失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commandId;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,15 +38,27 @@ export const addQuickCommand = async (name: string | null, command: string): Pro
|
|||||||
* @param id - 要更新的记录 ID
|
* @param id - 要更新的记录 ID
|
||||||
* @param name - 新的指令名称 (可选)
|
* @param name - 新的指令名称 (可选)
|
||||||
* @param command - 新的指令内容
|
* @param command - 新的指令内容
|
||||||
|
* @param tagIds - 新的关联标签 ID 数组 (可选, undefined 表示不更新标签)
|
||||||
* @returns 返回是否成功更新 (更新行数 > 0)
|
* @returns 返回是否成功更新 (更新行数 > 0)
|
||||||
*/
|
*/
|
||||||
export const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
|
export const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||||
if (!command || command.trim().length === 0) {
|
if (!command || command.trim().length === 0) {
|
||||||
throw new Error('指令内容不能为空');
|
throw new Error('指令内容不能为空');
|
||||||
}
|
}
|
||||||
const finalName = name && name.trim().length > 0 ? name.trim() : null;
|
const finalName = name && name.trim().length > 0 ? name.trim() : null;
|
||||||
const changes = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim());
|
const commandUpdated = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim());
|
||||||
return changes;
|
|
||||||
|
// 如果指令更新成功,并且提供了 tagIds (即使是空数组也表示要更新),则更新标签关联
|
||||||
|
if (commandUpdated && typeof tagIds !== 'undefined') {
|
||||||
|
try {
|
||||||
|
await QuickCommandTagRepository.setCommandTagAssociations(id, tagIds);
|
||||||
|
} catch (tagError: any) {
|
||||||
|
console.warn(`[Service] 更新快捷指令 ${id} 成功,但更新标签关联失败:`, tagError.message);
|
||||||
|
// 即使标签更新失败,主记录已更新,通常返回 true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 返回主记录是否更新成功
|
||||||
|
return commandUpdated;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,9 +74,10 @@ export const deleteQuickCommand = async (id: number): Promise<boolean> => {
|
|||||||
/**
|
/**
|
||||||
* 获取所有快捷指令,并按指定方式排序
|
* 获取所有快捷指令,并按指定方式排序
|
||||||
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
||||||
* @returns 返回排序后的快捷指令数组
|
* @returns 返回排序后的快捷指令数组 (包含 tagIds)
|
||||||
*/
|
*/
|
||||||
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'): Promise<QuickCommand[]> => {
|
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'): Promise<QuickCommandWithTags[]> => {
|
||||||
|
// Repository 已返回带 tagIds 的数据
|
||||||
return QuickCommandsRepository.getAllQuickCommands(sortBy);
|
return QuickCommandsRepository.getAllQuickCommands(sortBy);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,8 +94,38 @@ export const incrementUsageCount = async (id: number): Promise<boolean> => {
|
|||||||
/**
|
/**
|
||||||
* 根据 ID 获取单个快捷指令 (可能用于编辑)
|
* 根据 ID 获取单个快捷指令 (可能用于编辑)
|
||||||
* @param id - 记录 ID
|
* @param id - 记录 ID
|
||||||
* @returns 返回找到的快捷指令,或 undefined
|
* @returns 返回找到的快捷指令 (包含 tagIds),或 undefined
|
||||||
*/
|
*/
|
||||||
export const getQuickCommandById = async (id: number): Promise<QuickCommand | undefined> => {
|
export const getQuickCommandById = async (id: number): Promise<QuickCommandWithTags | undefined> => {
|
||||||
|
// Repository 已返回带 tagIds 的数据
|
||||||
return QuickCommandsRepository.findQuickCommandById(id);
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -275,6 +275,35 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Tag Creation/Deletion Handling ---
|
||||||
|
const handleCreateTag = async (tagName: string) => {
|
||||||
|
console.log(`[ConnForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++
|
||||||
|
if (!tagName || tagName.trim().length === 0) return;
|
||||||
|
console.log(`[ConnForm] Calling tagsStore.addTag...`); // +++ 添加日志 +++
|
||||||
|
const newTag = await tagsStore.addTag(tagName.trim()); // Use the correct store
|
||||||
|
if (newTag && !formData.tag_ids.includes(newTag.id)) {
|
||||||
|
console.log(`[ConnForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++
|
||||||
|
// Add the new tag's ID to the selected list
|
||||||
|
formData.tag_ids.push(newTag.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTag = async (tagId: number) => {
|
||||||
|
const tagToDelete = tags.value.find(t => t.id === tagId);
|
||||||
|
if (!tagToDelete) return;
|
||||||
|
|
||||||
|
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
||||||
|
const success = await tagsStore.deleteTag(tagId); // Use the correct store
|
||||||
|
if (success) {
|
||||||
|
// TagInput's modelValue will update automatically via watch
|
||||||
|
// No need to manually remove from formData.tag_ids here
|
||||||
|
} else {
|
||||||
|
// Optional: Show error notification if deletion fails
|
||||||
|
alert(t('tags.errorDelete', { error: tagsStore.error || '未知错误' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理测试连接
|
// 处理测试连接
|
||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
testStatus.value = 'testing';
|
testStatus.value = 'testing';
|
||||||
@@ -487,9 +516,19 @@ const testButtonText = computed(() => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.tags') }} ({{ t('connections.form.optional') }})</label>
|
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.tags') }} ({{ t('connections.form.optional') }})</label>
|
||||||
<TagInput v-model="formData.tag_ids" />
|
<TagInput
|
||||||
|
v-model="formData.tag_ids"
|
||||||
|
:available-tags="tags"
|
||||||
|
:allow-create="true"
|
||||||
|
:allow-delete="true"
|
||||||
|
@create-tag="handleCreateTag"
|
||||||
|
@delete-tag="handleDeleteTag"
|
||||||
|
:placeholder="t('tags.inputPlaceholder', '添加或选择标签...')"
|
||||||
|
/>
|
||||||
|
<div v-if="isTagLoading" class="mt-1 text-xs text-text-secondary">{{ t('tags.loading') }}</div>
|
||||||
|
<div v-if="tagStoreError" class="mt-1 text-xs text-error">{{ t('tags.error', { error: tagStoreError }) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error message -->
|
<!-- Error message -->
|
||||||
<div v-if="formError || storeError" class="text-error bg-error/10 border border-error/30 rounded-md p-3 text-sm text-center font-medium">
|
<div v-if="formError || storeError" class="text-error bg-error/10 border border-error/30 rounded-md p-3 text-sm text-center font-medium">
|
||||||
|
|||||||
@@ -25,6 +25,23 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
<small v-if="commandError" class="text-error text-xs mt-1 block">{{ commandError }}</small>
|
<small v-if="commandError" class="text-error text-xs mt-1 block">{{ commandError }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- +++ Tag Input Section +++ -->
|
||||||
|
<div>
|
||||||
|
<label for="qc-tags" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.tags', '标签:') }}</label>
|
||||||
|
<TagInput
|
||||||
|
id="qc-tags"
|
||||||
|
v-model="formData.tagIds"
|
||||||
|
:available-tags="quickCommandTagsStore.tags"
|
||||||
|
:placeholder="t('quickCommands.form.tagsPlaceholder', '添加或选择标签...')"
|
||||||
|
@create-tag="handleCreateTag"
|
||||||
|
:allow-create="true"
|
||||||
|
:allow-delete="true"
|
||||||
|
@delete-tag="handleDeleteTag"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<!-- Add styling/classes as needed for TagInput -->
|
||||||
|
</div>
|
||||||
|
<!-- +++ End Tag Input Section +++ -->
|
||||||
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
|
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
|
||||||
<!-- Secondary/Cancel Button -->
|
<!-- Secondary/Cancel Button -->
|
||||||
<button type="button" @click="closeForm" class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">{{ t('common.cancel', '取消') }}</button>
|
<button type="button" @click="closeForm" class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">{{ t('common.cancel', '取消') }}</button>
|
||||||
@@ -42,22 +59,26 @@
|
|||||||
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
|
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
|
||||||
|
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import new tag store +++
|
||||||
|
import TagInput from './TagInput.vue'; // +++ Import TagInput component (assuming it exists) +++
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象
|
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const quickCommandsStore = useQuickCommandsStore();
|
const quickCommandsStore = useQuickCommandsStore();
|
||||||
|
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate tag store +++
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
const isEditing = computed(() => !!props.commandToEdit);
|
const isEditing = computed(() => !!props.commandToEdit);
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
command: '',
|
command: '',
|
||||||
|
tagIds: [] as number[], // +++ Add tagIds +++
|
||||||
});
|
});
|
||||||
|
|
||||||
const commandError = ref<string | null>(null);
|
const commandError = ref<string | null>(null);
|
||||||
@@ -76,9 +97,54 @@ onMounted(() => {
|
|||||||
if (isEditing.value && props.commandToEdit) {
|
if (isEditing.value && props.commandToEdit) {
|
||||||
formData.name = props.commandToEdit.name ?? '';
|
formData.name = props.commandToEdit.name ?? '';
|
||||||
formData.command = props.commandToEdit.command;
|
formData.command = props.commandToEdit.command;
|
||||||
|
// Initialize tagIds if editing
|
||||||
|
formData.tagIds = props.commandToEdit.tagIds ? [...props.commandToEdit.tagIds] : [];
|
||||||
}
|
}
|
||||||
|
// Fetch tags if not already loaded (optional, might be better in parent)
|
||||||
|
// if (quickCommandTagsStore.tags.length === 0) {
|
||||||
|
// quickCommandTagsStore.fetchTags();
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Tag Creation Handling ---
|
||||||
|
// Assuming TagInput emits 'create-tag' with the tag name
|
||||||
|
const handleCreateTag = async (tagName: string) => {
|
||||||
|
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++
|
||||||
|
if (!tagName || tagName.trim().length === 0) return;
|
||||||
|
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`); // +++ 添加日志 +++
|
||||||
|
const newTag = await quickCommandTagsStore.addTag(tagName.trim());
|
||||||
|
if (newTag && !formData.tagIds.includes(newTag.id)) {
|
||||||
|
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++
|
||||||
|
// Add the new tag's ID to the selected list
|
||||||
|
formData.tagIds.push(newTag.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Tag Deletion Handling ---
|
||||||
|
const handleDeleteTag = async (tagId: number) => {
|
||||||
|
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`); // +++ 添加日志 +++
|
||||||
|
const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId);
|
||||||
|
if (!tagToDelete) return;
|
||||||
|
|
||||||
|
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
||||||
|
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`); // +++ 添加日志 +++
|
||||||
|
const success = await quickCommandTagsStore.deleteTag(tagId);
|
||||||
|
if (success) {
|
||||||
|
// If deletion is successful, TagInput's availableTags will update,
|
||||||
|
// and the tag should disappear from the input.
|
||||||
|
// We also need to remove it from the local formData.tagIds if it was selected.
|
||||||
|
const index = formData.tagIds.indexOf(tagId);
|
||||||
|
if (index > -1) {
|
||||||
|
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`); // +++ 添加日志 +++
|
||||||
|
formData.tagIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Optional: Show error notification if deletion fails
|
||||||
|
alert(t('tags.errorDelete', { error: quickCommandTagsStore.error || '未知错误' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (commandError.value) return; // 如果校验失败则不提交
|
if (commandError.value) return; // 如果校验失败则不提交
|
||||||
|
|
||||||
@@ -89,9 +155,11 @@ const handleSubmit = async () => {
|
|||||||
const finalName = formData.name.trim().length > 0 ? formData.name.trim() : null;
|
const finalName = formData.name.trim().length > 0 ? formData.name.trim() : null;
|
||||||
|
|
||||||
if (isEditing.value && props.commandToEdit) {
|
if (isEditing.value && props.commandToEdit) {
|
||||||
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim());
|
// Pass tagIds to update action
|
||||||
|
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim(), formData.tagIds);
|
||||||
} else {
|
} else {
|
||||||
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim());
|
// Pass tagIds to add action
|
||||||
|
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim(), formData.tagIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick } from 'vue';
|
import { ref, computed, watch, nextTick } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
// import { storeToRefs } from 'pinia'; // No longer needed directly
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useTagsStore, TagInfo } from '../stores/tags.store';
|
// import { useTagsStore, TagInfo } from '../stores/tags.store'; // REMOVE dependency on specific store
|
||||||
|
|
||||||
|
// Define a generic tag structure for the prop
|
||||||
|
interface GenericTag {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: number[]; // 接收选中的 tag_ids
|
modelValue: number[]; // 接收选中的 tag_ids
|
||||||
|
availableTags?: GenericTag[]; // Optional: The list of tags to choose from/display
|
||||||
|
placeholder?: string; // Optional: Placeholder for the input
|
||||||
|
allowCreate?: boolean; // Optional: Allow creating new tags via Enter (default true)
|
||||||
|
allowDelete?: boolean; // Optional: Allow showing the global delete button (default true)
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue', 'create-tag', 'delete-tag']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const tagsStore = useTagsStore();
|
// const tagsStore = useTagsStore(); // REMOVE
|
||||||
const { tags, isLoading, error } = storeToRefs(tagsStore);
|
// const { tags, isLoading, error } = storeToRefs(tagsStore); // REMOVE
|
||||||
|
|
||||||
const inputValue = ref(''); // 输入框的值
|
const inputValue = ref(''); // 输入框的值
|
||||||
const inputRef = ref<HTMLInputElement | null>(null); // 输入框引用
|
const inputRef = ref<HTMLInputElement | null>(null); // 输入框引用
|
||||||
const showSuggestions = ref(false); // 是否显示建议列表
|
const showSuggestions = ref(false); // 是否显示建议列表
|
||||||
const selectedTagIds = ref<number[]>([]); // 本地维护选中的 tag_ids
|
const selectedTagIds = ref<number[]>([]); // 本地维护选中的 tag_ids
|
||||||
|
|
||||||
|
// Default values for props
|
||||||
|
const availableTags = computed(() => props.availableTags ?? []);
|
||||||
|
const placeholder = computed(() => props.placeholder ?? t('tags.inputPlaceholder', '添加或选择标签...'));
|
||||||
|
const allowCreate = computed(() => props.allowCreate !== false); // Default true
|
||||||
|
const allowDelete = computed(() => props.allowDelete !== false); // Default true
|
||||||
|
|
||||||
// 监听 props.modelValue 的变化,同步到本地 selectedTagIds
|
// 监听 props.modelValue 的变化,同步到本地 selectedTagIds
|
||||||
watch(() => props.modelValue, (newVal) => {
|
watch(() => props.modelValue, (newVal) => {
|
||||||
// 只有在值确实不同的情况下才更新,避免无限循环
|
// 只有在值确实不同的情况下才更新,避免无限循环
|
||||||
@@ -37,66 +53,59 @@ watch(selectedTagIds, (newVal) => {
|
|||||||
|
|
||||||
|
|
||||||
// 计算属性:所有标签的 Map,方便通过 ID 查找
|
// 计算属性:所有标签的 Map,方便通过 ID 查找
|
||||||
|
// Use availableTags prop for the map
|
||||||
const tagsMap = computed(() => {
|
const tagsMap = computed(() => {
|
||||||
const map = new Map<number, TagInfo>();
|
const map = new Map<number, GenericTag>();
|
||||||
tags.value.forEach(tag => map.set(tag.id, tag));
|
availableTags.value.forEach(tag => map.set(tag.id, tag));
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性:已选中的标签对象
|
// 计算属性:已选中的标签对象
|
||||||
const selectedTags = computed(() => {
|
const selectedTags = computed(() => {
|
||||||
// 确保先从 map 中查找,再过滤掉未找到的 (可能标签已被删除)
|
// 确保先从 map 中查找,再过滤掉未找到的 (可能标签已被删除)
|
||||||
return selectedTagIds.value
|
return selectedTagIds.value
|
||||||
.map(id => tagsMap.value.get(id))
|
.map(id => tagsMap.value.get(id)) // Get from the map based on prop
|
||||||
.filter((tag): tag is TagInfo => tag !== undefined);
|
.filter((tag): tag is GenericTag => tag !== undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性:过滤后的建议列表
|
// 计算属性:过滤后的建议列表 (based on availableTags)
|
||||||
const suggestions = computed(() => {
|
const suggestions = computed(() => {
|
||||||
if (!showSuggestions.value) { // 仅在需要显示时计算
|
if (!showSuggestions.value) { // 仅在需要显示时计算
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let result: TagInfo[];
|
let result: GenericTag[]; // Use GenericTag type
|
||||||
// 如果输入框为空,显示所有未选中的标签
|
// Use availableTags from prop
|
||||||
|
const currentAvailableTags = availableTags.value;
|
||||||
|
// 如果输入框为空,显示所有未选中的可用标签
|
||||||
if (!inputValue.value) {
|
if (!inputValue.value) {
|
||||||
result = tags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
|
result = currentAvailableTags.filter(tag => !selectedTagIds.value.includes(tag.id));
|
||||||
} else {
|
} else {
|
||||||
const lowerCaseInput = inputValue.value.toLowerCase();
|
const lowerCaseInput = inputValue.value.toLowerCase();
|
||||||
result = tags.value.filter(tag =>
|
result = currentAvailableTags.filter(tag =>
|
||||||
tag.name.toLowerCase().includes(lowerCaseInput) &&
|
tag.name.toLowerCase().includes(lowerCaseInput) &&
|
||||||
!selectedTagIds.value.includes(tag.id) // 排除已选中的
|
!selectedTagIds.value.includes(tag.id) // 排除已选中的
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理输入框聚焦
|
// 处理输入框聚焦 (不再 fetch, 仅根据现有 availableTags 判断是否显示)
|
||||||
const handleFocus = async () => {
|
const handleFocus = () => {
|
||||||
showSuggestions.value = false; // 在异步操作前显式设置为 false
|
// 计算建议 (不依赖 showSuggestions ref)
|
||||||
// 1. 首先获取最新的标签
|
let potentialSuggestions: GenericTag[];
|
||||||
await tagsStore.fetchTags();
|
const currentInput = inputValue.value;
|
||||||
|
const currentAvailableTags = availableTags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
|
||||||
|
|
||||||
// 2. 基于更新后的标签列表和当前输入值,计算出实际可以显示的建议标签
|
if (!currentInput) {
|
||||||
// (这部分逻辑与 computed 'suggestions' 类似,但不依赖 showSuggestions.value)
|
potentialSuggestions = currentAvailableTags;
|
||||||
let potentialSuggestions: TagInfo[];
|
} else {
|
||||||
const currentInput = inputValue.value; // 获取当前输入框的值
|
const lowerCaseInput = currentInput.toLowerCase();
|
||||||
// 过滤掉已选中的标签
|
potentialSuggestions = currentAvailableTags.filter(tag =>
|
||||||
const availableTags = tags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
|
tag.name.toLowerCase().includes(lowerCaseInput)
|
||||||
|
);
|
||||||
if (!currentInput) {
|
}
|
||||||
// 如果输入框为空,所有未选中的标签都是潜在建议
|
// 只有当确实存在潜在建议时,才显示建议列表
|
||||||
potentialSuggestions = availableTags;
|
showSuggestions.value = potentialSuggestions.length > 0;
|
||||||
} else {
|
|
||||||
// 如果输入框有值,则根据输入值过滤可用标签
|
|
||||||
const lowerCaseInput = currentInput.toLowerCase();
|
|
||||||
potentialSuggestions = availableTags.filter(tag =>
|
|
||||||
tag.name.toLowerCase().includes(lowerCaseInput)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 只有当确实存在潜在建议时,才显示建议列表
|
|
||||||
const shouldShow = potentialSuggestions.length > 0;
|
|
||||||
showSuggestions.value = shouldShow; // 最终状态由计算结果决定
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理输入框失焦
|
// 处理输入框失焦
|
||||||
@@ -111,23 +120,18 @@ const handleKeyDown = async (event: KeyboardEvent) => {
|
|||||||
event.preventDefault(); // 阻止表单提交等默认行为
|
event.preventDefault(); // 阻止表单提交等默认行为
|
||||||
const trimmedInput = inputValue.value.trim();
|
const trimmedInput = inputValue.value.trim();
|
||||||
const lowerCaseInput = trimmedInput.toLowerCase();
|
const lowerCaseInput = trimmedInput.toLowerCase();
|
||||||
const existingTag = tags.value.find(tag => tag.name.toLowerCase() === lowerCaseInput);
|
// Check against availableTags prop
|
||||||
|
const existingTag = availableTags.value.find(tag => tag.name.toLowerCase() === lowerCaseInput);
|
||||||
|
|
||||||
if (existingTag && !selectedTagIds.value.includes(existingTag.id)) {
|
if (existingTag && !selectedTagIds.value.includes(existingTag.id)) {
|
||||||
// 如果是现有标签且未选中,则选中它
|
// 如果是现有标签且未选中,则选中它
|
||||||
selectTag(existingTag);
|
selectTag(existingTag);
|
||||||
} else if (!existingTag) {
|
} else if (!existingTag && allowCreate.value) { // Only create if allowed and not existing
|
||||||
// 如果是新标签,则创建并选中
|
// 如果是新标签,则 emit 事件让父组件处理创建
|
||||||
const success = await tagsStore.addTag(trimmedInput);
|
console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`); // +++ 添加日志 +++
|
||||||
if (success) {
|
emit('create-tag', trimmedInput);
|
||||||
// addTag 内部会 fetchTags, store 会更新
|
// 父组件负责创建、更新 availableTags prop,然后 TagInput 会响应式更新
|
||||||
// 需要等待 DOM 更新和 store 更新完成
|
// 父组件也负责将新创建的 tag ID 添加到 modelValue
|
||||||
await nextTick(); // 等待 store 更新
|
|
||||||
const newTag = tags.value.find(tag => tag.name === trimmedInput); // 再次查找确保获取到 ID
|
|
||||||
if (newTag) {
|
|
||||||
selectTag(newTag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inputValue.value = ''; // 清空输入框
|
inputValue.value = ''; // 清空输入框
|
||||||
showSuggestions.value = false; // 创建或选择后隐藏建议
|
showSuggestions.value = false; // 创建或选择后隐藏建议
|
||||||
@@ -137,12 +141,11 @@ const handleKeyDown = async (event: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选中一个标签 (来自建议列表或 Enter 创建)
|
// 选中一个标签 (来自建议列表或 Enter 匹配)
|
||||||
const selectTag = (tag: TagInfo) => {
|
const selectTag = (tag: GenericTag) => {
|
||||||
if (!selectedTagIds.value.includes(tag.id)) {
|
if (!selectedTagIds.value.includes(tag.id)) {
|
||||||
// 使用 .push() 来触发 watch
|
// 使用 .push() 来触发 watch -> emit update:modelValue
|
||||||
const updatedIds = [...selectedTagIds.value, tag.id];
|
selectedTagIds.value = [...selectedTagIds.value, tag.id];
|
||||||
selectedTagIds.value = updatedIds;
|
|
||||||
}
|
}
|
||||||
inputValue.value = ''; // 清空输入框
|
inputValue.value = ''; // 清空输入框
|
||||||
showSuggestions.value = false; // 选择后隐藏建议
|
showSuggestions.value = false; // 选择后隐藏建议
|
||||||
@@ -150,27 +153,22 @@ const selectTag = (tag: TagInfo) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 仅从本地选择中移除一个标签 (点击选中标签的 'x' 或 Backspace)
|
// 仅从本地选择中移除一个标签 (点击选中标签的 'x' 或 Backspace)
|
||||||
const removeTagLocally = (tagToRemove: TagInfo) => {
|
const removeTagLocally = (tagToRemove: GenericTag) => {
|
||||||
selectedTagIds.value = selectedTagIds.value.filter(id => id !== tagToRemove.id);
|
// This will trigger the watch and emit update:modelValue
|
||||||
|
selectedTagIds.value = selectedTagIds.value.filter(id => id !== tagToRemove.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理全局删除标签 (点击标签上的 'x' 图标) - 这是全局删除
|
// 处理全局删除标签 (点击标签上的 'x' 图标) - Emit event
|
||||||
const handleDeleteTagGlobally = async (tagToDelete: TagInfo) => {
|
const handleDeleteTagGlobally = (tagToDelete: GenericTag) => {
|
||||||
// 弹出确认框,防止误删
|
console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`); // +++ 添加日志 +++
|
||||||
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
// Emit event for parent to handle deletion confirmation and API call
|
||||||
const success = await tagsStore.deleteTag(tagToDelete.id);
|
console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`); // +++ 添加日志 +++
|
||||||
if (success) {
|
emit('delete-tag', tagToDelete.id);
|
||||||
// deleteTag 内部会 fetchTags, store 会更新
|
// Parent should handle confirmation, call store action, and update modelValue/availableTags
|
||||||
// selectedTagIds 会因为 watch props.modelValue 而自动更新 (如果父组件也更新了)
|
// We might still want to remove it locally immediately for better UX,
|
||||||
// 或者手动从 selectedTagIds 中移除 (更保险)
|
// but relying on parent updating modelValue is cleaner.
|
||||||
removeTagLocally(tagToDelete);
|
// removeTagLocally(tagToDelete); // Optional: remove locally immediately
|
||||||
// 可选:显示成功提示
|
}; // Remove the extra closing brace here if it exists, ensure function closes correctly
|
||||||
} else {
|
|
||||||
// 可选:显示错误提示
|
|
||||||
alert(t('tags.errorDelete', { error: tagsStore.error || '未知错误' }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -186,13 +184,15 @@ const handleDeleteTagGlobally = async (tagToDelete: TagInfo) => {
|
|||||||
@click.stop="removeTagLocally(tag)"
|
@click.stop="removeTagLocally(tag)"
|
||||||
:title="t('tags.removeSelection')"
|
:title="t('tags.removeSelection')"
|
||||||
>×</button>
|
>×</button>
|
||||||
|
<!-- Only show delete button if allowDelete is true -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
v-if="allowDelete"
|
||||||
class="ml-1 p-0 bg-transparent border-none cursor-pointer text-text-alt hover:text-error text-xs leading-none"
|
type="button"
|
||||||
@click.stop="handleDeleteTagGlobally(tag)"
|
class="ml-1 p-0 bg-transparent border-none cursor-pointer text-text-alt hover:text-error text-xs leading-none"
|
||||||
:title="t('tags.deleteTagGlobally')"
|
@click.stop="handleDeleteTagGlobally(tag)"
|
||||||
|
:title="t('tags.deleteTagGlobally')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,9 +217,10 @@ const handleDeleteTagGlobally = async (tagToDelete: TagInfo) => {
|
|||||||
>
|
>
|
||||||
{{ suggestion.name }}
|
{{ suggestion.name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-if="isLoading" class="absolute bottom-[-1.5em] left-0 text-xs text-text-secondary mt-1">{{ t('tags.loading') }}</div>
|
<!-- Remove isLoading and error display as they are no longer managed here -->
|
||||||
<div v-if="error" class="absolute bottom-[-1.5em] left-0 text-xs text-error mt-1">{{ t('tags.error', { error: error }) }}</div>
|
<!-- <div v-if="isLoading" ...></div> -->
|
||||||
|
<!-- <div v-if="error" ...></div> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -118,11 +118,27 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
const tagMap = new Map(tags.value.map(tag => [tag.id, tag]));
|
const tagMap = new Map(tags.value.map(tag => [tag.id, tag]));
|
||||||
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||||
|
|
||||||
// 1. 过滤连接
|
// 1. 过滤连接 (New logic: filter by connection name, host, OR tag name)
|
||||||
const filteredConnections = connections.value.filter(conn => {
|
const filteredConnections = connections.value.filter(conn => {
|
||||||
const nameMatch = conn.name && conn.name.toLowerCase().includes(lowerSearchTerm);
|
// Check connection name
|
||||||
const hostMatch = conn.host.toLowerCase().includes(lowerSearchTerm);
|
if (conn.name && conn.name.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
return nameMatch || hostMatch;
|
return true;
|
||||||
|
}
|
||||||
|
// Check connection host
|
||||||
|
if (conn.host.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check associated tag names
|
||||||
|
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
||||||
|
for (const tagId of conn.tag_ids) {
|
||||||
|
const tag = tagMap.get(tagId); // Use the existing tagMap
|
||||||
|
if (tag && tag.name.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
|
return true; // Match found in tag name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No match found
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 分组过滤后的连接
|
// 2. 分组过滤后的连接
|
||||||
|
|||||||
@@ -906,7 +906,13 @@
|
|||||||
"command": "Command:",
|
"command": "Command:",
|
||||||
"commandPlaceholder": "e.g., ls -alh /home/user",
|
"commandPlaceholder": "e.g., ls -alh /home/user",
|
||||||
"errorCommandRequired": "Command cannot be empty",
|
"errorCommandRequired": "Command cannot be empty",
|
||||||
"add": "Add"
|
"add": "Add",
|
||||||
|
"tags": "Tags:",
|
||||||
|
"tagsPlaceholder": "Select or create tags..."
|
||||||
|
},
|
||||||
|
"untagged": "Untagged",
|
||||||
|
"tags": {
|
||||||
|
"clickToEditTag": "Click to edit tag name"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -492,6 +492,8 @@
|
|||||||
"empty": "クイックコマンドはありません。'+'ボタンをクリックして作成してください!",
|
"empty": "クイックコマンドはありません。'+'ボタンをクリックして作成してください!",
|
||||||
"form": {
|
"form": {
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
|
"tags": "タグ:",
|
||||||
|
"tagsPlaceholder": "タグを選択または作成...",
|
||||||
"command": "コマンド:",
|
"command": "コマンド:",
|
||||||
"commandPlaceholder": "例:ls -alh /home/user",
|
"commandPlaceholder": "例:ls -alh /home/user",
|
||||||
"errorCommandRequired": "コマンドは空にできません",
|
"errorCommandRequired": "コマンドは空にできません",
|
||||||
@@ -500,6 +502,10 @@
|
|||||||
"titleAdd": "クイックコマンドの追加",
|
"titleAdd": "クイックコマンドの追加",
|
||||||
"titleEdit": "クイックコマンドの編集"
|
"titleEdit": "クイックコマンドの編集"
|
||||||
},
|
},
|
||||||
|
"untagged": "タグなし",
|
||||||
|
"tags": {
|
||||||
|
"clickToEditTag": "クリックしてタグ名を編集"
|
||||||
|
},
|
||||||
"searchPlaceholder": "名前またはコマンドを検索...",
|
"searchPlaceholder": "名前またはコマンドを検索...",
|
||||||
"sortByName": "名前",
|
"sortByName": "名前",
|
||||||
"sortByUsage": "使用頻度",
|
"sortByUsage": "使用頻度",
|
||||||
|
|||||||
@@ -909,7 +909,13 @@
|
|||||||
"command": "指令:",
|
"command": "指令:",
|
||||||
"commandPlaceholder": "例如:ls -alh /home/user",
|
"commandPlaceholder": "例如:ls -alh /home/user",
|
||||||
"errorCommandRequired": "指令内容不能为空",
|
"errorCommandRequired": "指令内容不能为空",
|
||||||
"add": "添加"
|
"add": "添加",
|
||||||
|
"tags": "标签:",
|
||||||
|
"tagsPlaceholder": "选择或创建标签..."
|
||||||
|
},
|
||||||
|
"untagged": "未标记",
|
||||||
|
"tags": {
|
||||||
|
"clickToEditTag": "点击编辑标签名称"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import apiClient from '../utils/apiClient';
|
||||||
|
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||||
|
|
||||||
|
// 定义快捷指令标签接口 (与后端 QuickCommandTag 对应)
|
||||||
|
export interface QuickCommandTag {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (cachedData) {
|
||||||
|
console.log('[QuickCmdTagStore] Loading quick command tags from cache.');
|
||||||
|
tags.value = JSON.parse(cachedData);
|
||||||
|
isLoading.value = false;
|
||||||
|
} else {
|
||||||
|
isLoading.value = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[QuickCmdTagStore] Failed to load or parse cache:', e);
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
isLoading.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 后台获取最新数据
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
console.log('[QuickCmdTagStore] Fetching latest quick command tags from server...');
|
||||||
|
// 使用新的 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) {
|
||||||
|
console.log('[QuickCmdTagStore] Tags data changed, updating state and cache.');
|
||||||
|
tags.value = freshData;
|
||||||
|
localStorage.setItem(cacheKey, freshDataString);
|
||||||
|
} else {
|
||||||
|
console.log('[QuickCmdTagStore] Tags data is up-to-date.');
|
||||||
|
}
|
||||||
|
error.value = null;
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[QuickCmdTagStore] Failed to fetch tags:', err);
|
||||||
|
error.value = err.response?.data?.message || err.message || '获取快捷指令标签列表失败';
|
||||||
|
if (error.value) { // Check if error.value is not null
|
||||||
|
uiNotificationsStore.showError(error.value); // 显示错误通知
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新快捷指令标签 (添加后清除缓存)
|
||||||
|
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(); // 重新获取以更新列表
|
||||||
|
uiNotificationsStore.showSuccess('快捷指令标签已添加');
|
||||||
|
return newTag;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[QuickCmdTagStore] Failed to add tag:', err);
|
||||||
|
error.value = err.response?.data?.message || err.message || '添加快捷指令标签失败';
|
||||||
|
if (error.value) { // Check if error.value is not null
|
||||||
|
uiNotificationsStore.showError(error.value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新快捷指令标签
|
||||||
|
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');
|
||||||
|
await fetchTags();
|
||||||
|
uiNotificationsStore.showSuccess('快捷指令标签已更新');
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[QuickCmdTagStore] Failed to update tag:', err);
|
||||||
|
error.value = err.response?.data?.message || err.message || '更新快捷指令标签失败';
|
||||||
|
if (error.value) { // Check if error.value is not null
|
||||||
|
uiNotificationsStore.showError(error.value);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除快捷指令标签
|
||||||
|
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');
|
||||||
|
await fetchTags();
|
||||||
|
uiNotificationsStore.showSuccess('快捷指令标签已删除');
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[QuickCmdTagStore] Failed to delete tag:', err);
|
||||||
|
error.value = err.response?.data?.message || err.message || '删除快捷指令标签失败';
|
||||||
|
if (error.value) { // Check if error.value is not null
|
||||||
|
uiNotificationsStore.showError(error.value);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
fetchTags,
|
||||||
|
addTag,
|
||||||
|
updateTag,
|
||||||
|
deleteTag,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,62 +1,224 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch } from 'vue'; // Import watch
|
||||||
import { useUiNotificationsStore } from './uiNotifications.store';
|
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||||
import type { QuickCommand } from '../types/quick-commands.types'; // 引入本地 QuickCommand 类型
|
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store'; // +++ Import new tag store +++
|
||||||
|
import { useI18n } from 'vue-i18n'; // +++ Import i18n for "Untagged" +++
|
||||||
|
// Assuming QuickCommand type in types includes tagIds now, or define it here
|
||||||
|
// import type { QuickCommand } from '../types/quick-commands.types';
|
||||||
|
|
||||||
// 定义前端使用的快捷指令接口 (可以与后端一致)
|
// 定义前端使用的快捷指令接口 (包含 tagIds)
|
||||||
export type QuickCommandFE = QuickCommand;
|
export interface QuickCommandFE { // Renamed from QuickCommand if necessary
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
command: string;
|
||||||
|
usage_count: number;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
tagIds: number[]; // +++ Add tagIds +++
|
||||||
|
}
|
||||||
|
|
||||||
// 定义排序类型
|
// 定义排序类型
|
||||||
export type QuickCommandSortByType = 'name' | 'usage_count';
|
export type QuickCommandSortByType = 'name' | 'usage_count';
|
||||||
|
|
||||||
|
// 定义分组后的数据结构
|
||||||
|
export interface GroupedQuickCommands {
|
||||||
|
groupName: string;
|
||||||
|
tagId: number | null; // null for "Untagged" group
|
||||||
|
commands: QuickCommandFE[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// +++ localStorage key for expanded groups +++
|
||||||
|
const EXPANDED_GROUPS_STORAGE_KEY = 'quickCommandsExpandedGroups';
|
||||||
|
|
||||||
export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||||
const quickCommandsList = ref<QuickCommandFE[]>([]);
|
const quickCommandsList = ref<QuickCommandFE[]>([]); // Should now contain QuickCommandFE with tagIds
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
const sortBy = ref<QuickCommandSortByType>('name'); // 默认按名称排序
|
const sortBy = ref<QuickCommandSortByType>('name'); // 默认按名称排序
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const uiNotificationsStore = useUiNotificationsStore();
|
const uiNotificationsStore = useUiNotificationsStore();
|
||||||
const selectedIndex = ref<number>(-1); // NEW: Index of the selected command in the filtered list
|
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 expandedGroups = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
// --- Getters ---
|
// --- Getters ---
|
||||||
|
|
||||||
// 计算属性:根据搜索词过滤和排序指令
|
// +++ 重写 Getter: 过滤、分组、排序指令 +++
|
||||||
const filteredAndSortedCommands = computed(() => {
|
const filteredAndGroupedCommands = computed((): GroupedQuickCommands[] => {
|
||||||
const term = searchTerm.value.toLowerCase().trim();
|
const term = searchTerm.value.toLowerCase().trim();
|
||||||
let filtered = quickCommandsList.value;
|
const allTags = quickCommandTagsStore.tags; // 获取快捷指令专属标签
|
||||||
|
const tagMap = new Map(allTags.map(tag => [tag.id, tag.name]));
|
||||||
|
const untaggedGroupName = t('quickCommands.untagged', '未标记'); // 获取 "未标记" 的翻译
|
||||||
|
|
||||||
|
// 1. 过滤 (New logic: filter by command name, command content, OR tag name)
|
||||||
|
let filtered = quickCommandsList.value;
|
||||||
if (term) {
|
if (term) {
|
||||||
filtered = filtered.filter(cmd =>
|
filtered = filtered.filter(cmd => {
|
||||||
(cmd.name && cmd.name.toLowerCase().includes(term)) ||
|
// Check command name
|
||||||
cmd.command.toLowerCase().includes(term)
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pinia store getter 中直接排序可能不是最佳实践,但这里为了简单起见先这样实现
|
// 2. 分组
|
||||||
// 更好的方式可能是在 fetch 时就按需排序,或者在组件层排序
|
const groups: Record<string, { commands: QuickCommandFE[], tagId: number | null }> = {};
|
||||||
// 注意:这里直接修改 ref 数组的顺序,如果需要在多处使用不同排序,需要创建副本
|
const untaggedCommands: QuickCommandFE[] = [];
|
||||||
// return [...filtered].sort((a, b) => {
|
|
||||||
// if (sortBy.value === 'usage_count') {
|
filtered.forEach(cmd => {
|
||||||
// // 按使用次数降序,次数相同按名称升序
|
let isTagged = false;
|
||||||
// if (b.usage_count !== a.usage_count) {
|
if (cmd.tagIds && cmd.tagIds.length > 0) {
|
||||||
// return b.usage_count - a.usage_count;
|
cmd.tagIds.forEach(tagId => {
|
||||||
// }
|
const tagName = tagMap.get(tagId);
|
||||||
// }
|
if (tagName) {
|
||||||
// // 默认或次数相同时按名称升序 (null 名称排在前面)
|
if (!groups[tagName]) {
|
||||||
// const nameA = a.name ?? '';
|
groups[tagName] = { commands: [], tagId: tagId };
|
||||||
// const nameB = b.name ?? '';
|
// 初始化展开状态 (如果未定义,默认为 true)
|
||||||
// return nameA.localeCompare(nameB);
|
if (expandedGroups.value[tagName] === undefined) {
|
||||||
// });
|
expandedGroups.value[tagName] = true;
|
||||||
// **修正:Getter 不应修改原始数组,返回过滤后的即可,排序由 fetch 控制**
|
}
|
||||||
return filtered;
|
}
|
||||||
|
// 避免重复添加(如果一个指令有多个相同标签ID? 不太可能但做个防御)
|
||||||
|
if (!groups[tagName].commands.some(c => c.id === cmd.id)) {
|
||||||
|
groups[tagName].commands.push(cmd);
|
||||||
|
}
|
||||||
|
isTagged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isTagged) {
|
||||||
|
untaggedCommands.push(cmd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
const nameA = a.name ?? a.command;
|
||||||
|
const nameB = b.name ?? b.command;
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
groupName: untaggedGroupName,
|
||||||
|
tagId: null,
|
||||||
|
commands: untaggedCommands
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// +++ 新增 Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++
|
||||||
|
const flatVisibleCommands = computed((): QuickCommandFE[] => {
|
||||||
|
const flatList: QuickCommandFE[] = [];
|
||||||
|
filteredAndGroupedCommands.value.forEach(group => {
|
||||||
|
// 只添加已展开分组中的指令
|
||||||
|
if (expandedGroups.value[group.groupName]) {
|
||||||
|
flatList.push(...group.commands);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flatList;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
||||||
// NEW: Action to select the next command in the filtered list
|
// +++ 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.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[QuickCmdStore] Failed to load or parse expanded groups state:', e);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// +++ 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action to select the next command in the *visible* flat list
|
||||||
const selectNextCommand = () => {
|
const selectNextCommand = () => {
|
||||||
const commands = filteredAndSortedCommands.value;
|
const commands = flatVisibleCommands.value; // Use the flat visible list
|
||||||
if (commands.length === 0) {
|
if (commands.length === 0) {
|
||||||
selectedIndex.value = -1;
|
selectedIndex.value = -1;
|
||||||
return;
|
return;
|
||||||
@@ -64,9 +226,9 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
|
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NEW: Action to select the previous command in the filtered list
|
// Action to select the previous command in the *visible* flat list
|
||||||
const selectPreviousCommand = () => {
|
const selectPreviousCommand = () => {
|
||||||
const commands = filteredAndSortedCommands.value;
|
const commands = flatVisibleCommands.value; // Use the flat visible list
|
||||||
if (commands.length === 0) {
|
if (commands.length === 0) {
|
||||||
selectedIndex.value = -1;
|
selectedIndex.value = -1;
|
||||||
return;
|
return;
|
||||||
@@ -74,37 +236,48 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
|
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从后端获取快捷指令 (带缓存和排序)
|
// 从后端获取快捷指令 (包含 tagIds,不再发送 sortBy)
|
||||||
const fetchQuickCommands = async () => {
|
const fetchQuickCommands = async () => {
|
||||||
const cacheKey = 'quickCommandsCache';
|
// 简化缓存:只缓存原始列表,不再区分排序
|
||||||
// 将排序方式加入缓存键,确保不同排序有不同缓存
|
const cacheKey = 'quickCommandsListCache';
|
||||||
const cacheKeyWithSort = `${cacheKey}_${sortBy.value}`;
|
error.value = null;
|
||||||
error.value = null; // 重置错误
|
|
||||||
|
|
||||||
// 1. 尝试从 localStorage 加载缓存
|
// 1. 尝试从 localStorage 加载缓存
|
||||||
try {
|
try {
|
||||||
const cachedData = localStorage.getItem(cacheKeyWithSort);
|
const cachedData = localStorage.getItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
console.log(`[QuickCmdStore] Loading commands from cache (sort: ${sortBy.value}).`);
|
console.log(`[QuickCmdStore] Loading commands from cache.`);
|
||||||
quickCommandsList.value = JSON.parse(cachedData);
|
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds)
|
||||||
isLoading.value = false; // 先显示缓存
|
const parsedData = JSON.parse(cachedData) as QuickCommandFE[];
|
||||||
|
// 基本验证,确保 tagIds 是数组
|
||||||
|
if (Array.isArray(parsedData) && parsedData.every(item => Array.isArray(item.tagIds))) {
|
||||||
|
quickCommandsList.value = parsedData;
|
||||||
|
isLoading.value = false;
|
||||||
|
} else {
|
||||||
|
console.warn('[QuickCmdStore] Cached data format invalid, ignoring cache.');
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
isLoading.value = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
isLoading.value = true; // 无缓存,初始加载
|
isLoading.value = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[QuickCmdStore] Failed to load or parse commands cache:', e);
|
console.error('[QuickCmdStore] Failed to load or parse commands cache:', e);
|
||||||
localStorage.removeItem(cacheKeyWithSort); // 解析失败则移除缓存
|
localStorage.removeItem(cacheKey);
|
||||||
isLoading.value = true; // 缓存无效,需要加载
|
isLoading.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 后台获取最新数据
|
// 2. 后台获取最新数据
|
||||||
isLoading.value = true; // 标记正在后台获取
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
console.log(`[QuickCmdStore] Fetching latest commands from server (sort: ${sortBy.value})...`);
|
console.log(`[QuickCmdStore] Fetching latest commands from server...`);
|
||||||
const response = await apiClient.get<QuickCommandFE[]>('/quick-commands', {
|
// 不再发送 sortBy 参数
|
||||||
params: { sortBy: sortBy.value }
|
const response = await apiClient.get<QuickCommandFE[]>('/quick-commands');
|
||||||
});
|
// 确保返回的数据包含 tagIds 数组
|
||||||
const freshData = response.data;
|
const freshData = response.data.map(cmd => ({
|
||||||
|
...cmd,
|
||||||
|
tagIds: Array.isArray(cmd.tagIds) ? cmd.tagIds : [] // 确保 tagIds 是数组
|
||||||
|
}));
|
||||||
const freshDataString = JSON.stringify(freshData);
|
const freshDataString = JSON.stringify(freshData);
|
||||||
|
|
||||||
// 3. 对比并更新
|
// 3. 对比并更新
|
||||||
@@ -112,37 +285,37 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
if (currentDataString !== freshDataString) {
|
if (currentDataString !== freshDataString) {
|
||||||
console.log('[QuickCmdStore] Commands data changed, updating state and cache.');
|
console.log('[QuickCmdStore] Commands data changed, updating state and cache.');
|
||||||
quickCommandsList.value = freshData;
|
quickCommandsList.value = freshData;
|
||||||
localStorage.setItem(cacheKeyWithSort, freshDataString); // 更新对应排序的缓存
|
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
||||||
} else {
|
} else {
|
||||||
console.log('[QuickCmdStore] Commands data is up-to-date.');
|
console.log('[QuickCmdStore] Commands data is up-to-date.');
|
||||||
}
|
}
|
||||||
error.value = null; // 清除错误
|
error.value = null;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[QuickCmdStore] 获取快捷指令失败:', err);
|
console.error('[QuickCmdStore] 获取快捷指令失败:', err);
|
||||||
error.value = err.response?.data?.message || '获取快捷指令时发生错误';
|
error.value = err.response?.data?.message || '获取快捷指令时发生错误';
|
||||||
// 保留缓存数据,仅设置错误状态
|
if (error.value) {
|
||||||
uiNotificationsStore.showError(error.value ?? '未知错误');
|
uiNotificationsStore.showError(error.value);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false; // 加载完成
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清除所有排序的快捷指令缓存
|
// 清除快捷指令列表缓存
|
||||||
const clearQuickCommandsCache = () => {
|
const clearQuickCommandsCache = () => {
|
||||||
const cacheKeyBase = 'quickCommandsCache';
|
localStorage.removeItem('quickCommandsListCache');
|
||||||
// 移除两种排序的缓存
|
console.log('[QuickCmdStore] Cleared quick commands list cache.');
|
||||||
localStorage.removeItem(`${cacheKeyBase}_name`);
|
|
||||||
localStorage.removeItem(`${cacheKeyBase}_usage_count`);
|
|
||||||
console.log('[QuickCmdStore] Cleared all quick commands caches.');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 添加快捷指令 (添加后清除缓存)
|
// 添加快捷指令 (发送 tagIds)
|
||||||
const addQuickCommand = async (name: string | null, command: string): Promise<boolean> => {
|
const addQuickCommand = async (name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/quick-commands', { name, command });
|
// 在请求体中包含 tagIds
|
||||||
clearQuickCommandsCache(); // 清除所有排序缓存
|
const response = await apiClient.post<{ message: string, command: QuickCommandFE }>('/quick-commands', { name, command, tagIds });
|
||||||
await fetchQuickCommands(); // 刷新当前排序的列表和缓存
|
// 后端现在返回完整的 command 对象,可以直接使用或触发刷新
|
||||||
|
clearQuickCommandsCache(); // 清除缓存
|
||||||
|
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||||
uiNotificationsStore.showSuccess('快捷指令已添加');
|
uiNotificationsStore.showSuccess('快捷指令已添加');
|
||||||
return true;
|
return true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -153,12 +326,14 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新快捷指令
|
// 更新快捷指令 (发送 tagIds)
|
||||||
const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
|
const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.put(`/quick-commands/${id}`, { name, command });
|
// 在请求体中包含 tagIds (即使是 undefined 也要发送,让后端知道是否要更新)
|
||||||
clearQuickCommandsCache(); // 清除所有排序缓存
|
const response = await apiClient.put<{ message: string, command: QuickCommandFE }>(`/quick-commands/${id}`, { name, command, tagIds });
|
||||||
await fetchQuickCommands(); // 刷新当前排序的列表和缓存
|
// 后端现在返回完整的 command 对象
|
||||||
|
clearQuickCommandsCache(); // 清除缓存
|
||||||
|
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||||
uiNotificationsStore.showSuccess('快捷指令已更新');
|
uiNotificationsStore.showSuccess('快捷指令已更新');
|
||||||
return true;
|
return true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -214,12 +389,12 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
selectedIndex.value = -1; // Reset selection when search term changes
|
selectedIndex.value = -1; // Reset selection when search term changes
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置排序方式并重新获取数据
|
// 设置排序方式 (只更新本地状态,不再重新获取数据)
|
||||||
const setSortBy = async (newSortBy: QuickCommandSortByType) => {
|
const setSortBy = (newSortBy: QuickCommandSortByType) => {
|
||||||
if (sortBy.value !== newSortBy) {
|
if (sortBy.value !== newSortBy) {
|
||||||
sortBy.value = newSortBy;
|
sortBy.value = newSortBy;
|
||||||
// 排序方式改变,不需要清除缓存,fetchQuickCommands 会读取对应排序的缓存或重新获取
|
// 排序现在由 filteredAndGroupedCommands getter 处理,无需重新 fetch
|
||||||
await fetchQuickCommands();
|
selectedIndex.value = -1; // Reset selection when sort changes
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,8 +411,10 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
sortBy,
|
sortBy,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
filteredAndSortedCommands, // 使用计算属性
|
filteredAndGroupedCommands, // Expose the grouped data
|
||||||
selectedIndex, // NEW: Expose selected index
|
flatVisibleCommands, // Expose the flat visible list for navigation logic if needed outside
|
||||||
|
selectedIndex, // Index within flatVisibleCommands
|
||||||
|
expandedGroups, // Expose expanded groups state
|
||||||
fetchQuickCommands,
|
fetchQuickCommands,
|
||||||
addQuickCommand,
|
addQuickCommand,
|
||||||
updateQuickCommand,
|
updateQuickCommand,
|
||||||
@@ -245,8 +422,64 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
incrementUsage,
|
incrementUsage,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
setSortBy,
|
setSortBy,
|
||||||
selectNextCommand, // NEW: Expose action
|
selectNextCommand,
|
||||||
selectPreviousCommand, // NEW: Expose action
|
selectPreviousCommand,
|
||||||
resetSelection, // Ensure resetSelection is exported
|
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.`);
|
||||||
|
// --- End manual state update ---
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,49 +26,89 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- List Area -->
|
<!-- List Area -->
|
||||||
<div class="flex-grow overflow-y-auto p-2">
|
<div class="flex-grow overflow-y-auto p-2">
|
||||||
<!-- Loading State -->
|
<!-- Loading State (Show if loading and no groups are ready yet) -->
|
||||||
<!-- Loading State (Only show if loading AND no commands are displayed yet) -->
|
<div v-if="isLoading && filteredAndGroupedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||||
<div v-if="isLoading && filteredAndSortedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
|
||||||
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
|
<p>{{ t('common.loading', '加载中...') }}</p>
|
||||||
<p>{{ t('common.loading', '加载中...') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Empty State -->
|
<!-- Empty State (Show if not loading and no groups exist) -->
|
||||||
<div v-else-if="filteredAndSortedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
<div v-else-if="!isLoading && filteredAndGroupedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||||
<i class="fas fa-bolt text-xl mb-2"></i>
|
<i class="fas fa-bolt text-xl mb-2"></i>
|
||||||
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
|
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
|
||||||
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||||
{{ $t('quickCommands.addFirst', '创建第一个快捷指令') }}
|
{{ $t('quickCommands.addFirst', '创建第一个快捷指令') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Command List -->
|
<!-- Grouped Command List -->
|
||||||
<ul v-else class="list-none p-0 m-0" ref="commandListRef">
|
<div v-else class="list-none p-0 m-0" ref="commandListContainerRef"> <!-- Changed ref name -->
|
||||||
<li
|
<div v-for="groupData in filteredAndGroupedCommands" :key="groupData.groupName" class="mb-1 last:mb-0">
|
||||||
v-for="(cmd, index) in filteredAndSortedCommands"
|
<!-- Group Header -->
|
||||||
:key="cmd.id"
|
<!-- Group Header - Modified for inline editing -->
|
||||||
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
<div
|
||||||
:class="{ 'bg-primary/20 font-medium': index === storeSelectedIndex }"
|
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
||||||
@click="executeCommand(cmd)"
|
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
||||||
>
|
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
||||||
<!-- Command Info -->
|
>
|
||||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
<i
|
||||||
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
: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]}]"
|
||||||
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
@click.stop="toggleGroup(groupData.groupName)"
|
||||||
</div>
|
class="cursor-pointer flex-shrink-0"
|
||||||
<!-- Actions (Show on Hover) -->
|
></i>
|
||||||
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
<!-- Editing State -->
|
||||||
<!-- Usage Count -->
|
<input
|
||||||
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
v-if="editingTagId === (groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||||
<!-- Edit Button -->
|
:key="groupData.tagId === null ? 'untagged-input' : `tag-input-${groupData.tagId}`"
|
||||||
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
:ref="(el) => setTagInputRef(el, groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||||
<i class="fas fa-edit text-sm"></i>
|
type="text"
|
||||||
</button>
|
v-model="editedTagName"
|
||||||
<!-- Delete Button -->
|
class="text-sm bg-input border border-primary rounded px-1 py-0 w-full"
|
||||||
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
@blur="finishEditingTag"
|
||||||
<i class="fas fa-times text-sm"></i>
|
@keydown.enter.prevent="finishEditingTag"
|
||||||
</button>
|
@keydown.esc.prevent="cancelEditingTag"
|
||||||
</div>
|
@click.stop
|
||||||
</li>
|
/>
|
||||||
</ul>
|
<!-- Display State -->
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-sm inline-block overflow-hidden text-ellipsis whitespace-nowrap flex-grow"
|
||||||
|
:class="{ 'cursor-pointer hover:underline': true }"
|
||||||
|
:title="t('quickCommands.tags.clickToEditTag', '点击编辑标签')"
|
||||||
|
@click.stop="startEditingTag(groupData.tagId, groupData.groupName)"
|
||||||
|
>
|
||||||
|
{{ groupData.groupName }}
|
||||||
|
</span>
|
||||||
|
<!-- Optional: Add count? -->
|
||||||
|
<!-- <span v-if="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId)" class="ml-auto text-xs text-text-secondary pl-2">({{ groupData.commands.length }})</span> -->
|
||||||
|
</div>
|
||||||
|
<!-- Command Items List (only show if expanded) -->
|
||||||
|
<ul v-show="quickCommandsStore.expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
|
||||||
|
<li
|
||||||
|
v-for="(cmd) in groupData.commands"
|
||||||
|
:key="cmd.id"
|
||||||
|
:data-command-id="cmd.id"
|
||||||
|
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||||
|
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||||
|
@click="executeCommand(cmd)"
|
||||||
|
>
|
||||||
|
<!-- Command Info (Structure remains the same) -->
|
||||||
|
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||||
|
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
||||||
|
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Actions (Structure remains the same) -->
|
||||||
|
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||||
|
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
||||||
|
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
||||||
|
<i class="fas fa-edit text-sm"></i>
|
||||||
|
</button>
|
||||||
|
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
||||||
|
<i class="fas fa-times text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -84,13 +124,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, computed, nextTick, defineExpose, watch } from 'vue'; // Import watch
|
import { ref, onMounted, onBeforeUnmount, computed, nextTick, defineExpose, watch } from 'vue'; // Import watch
|
||||||
import { storeToRefs } from 'pinia'; // Import storeToRefs
|
import { storeToRefs } from 'pinia'; // Import storeToRefs
|
||||||
import { useQuickCommandsStore, type QuickCommandFE, type QuickCommandSortByType } from '../stores/quickCommands.store';
|
import { useQuickCommandsStore, type QuickCommandFE, type QuickCommandSortByType, type GroupedQuickCommands } from '../stores/quickCommands.store'; // Import GroupedQuickCommands
|
||||||
|
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import the new tag store +++
|
||||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; // 导入表单组件
|
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; // 导入表单组件
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
||||||
|
|
||||||
const quickCommandsStore = useQuickCommandsStore();
|
const quickCommandsStore = useQuickCommandsStore();
|
||||||
|
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate the new tag store +++
|
||||||
const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通知
|
const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通知
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||||
@@ -99,16 +141,32 @@ const hoveredItemId = ref<number | null>(null);
|
|||||||
const isFormVisible = ref(false);
|
const isFormVisible = ref(false);
|
||||||
const commandToEdit = ref<QuickCommandFE | null>(null);
|
const commandToEdit = ref<QuickCommandFE | null>(null);
|
||||||
// const selectedIndex = ref<number>(-1); // REMOVED: Use store's selectedIndex
|
// const selectedIndex = ref<number>(-1); // REMOVED: Use store's selectedIndex
|
||||||
const commandListRef = ref<HTMLUListElement | null>(null); // Ref for the command list UL
|
const commandListContainerRef = ref<HTMLDivElement | null>(null); // Changed ref name to match template
|
||||||
const searchInputRef = ref<HTMLInputElement | null>(null); // +++ Ref for the search input +++
|
const searchInputRef = ref<HTMLInputElement | null>(null); // +++ Ref for the search input +++
|
||||||
let unregisterFocus: (() => void) | null = null; // +++ 保存注销函数 +++
|
let unregisterFocus: (() => void) | null = null; // +++ 保存注销函数 +++
|
||||||
|
|
||||||
|
// +++ State for inline tag editing +++
|
||||||
|
const editingTagId = ref<number | null | 'untagged'>(null);
|
||||||
|
const editedTagName = ref('');
|
||||||
|
const tagInputRefs = ref(new Map<string | number, HTMLInputElement | null>());
|
||||||
|
|
||||||
// --- 从 Store 获取状态和 Getter ---
|
// --- 从 Store 获取状态和 Getter ---
|
||||||
const searchTerm = computed(() => quickCommandsStore.searchTerm);
|
const searchTerm = computed(() => quickCommandsStore.searchTerm);
|
||||||
const sortBy = computed(() => quickCommandsStore.sortBy);
|
const sortBy = computed(() => quickCommandsStore.sortBy);
|
||||||
const filteredAndSortedCommands = computed(() => quickCommandsStore.filteredAndSortedCommands);
|
// Use the new grouped getter
|
||||||
|
const filteredAndGroupedCommands = computed(() => quickCommandsStore.filteredAndGroupedCommands);
|
||||||
const isLoading = computed(() => quickCommandsStore.isLoading);
|
const isLoading = computed(() => quickCommandsStore.isLoading);
|
||||||
const { selectedIndex: storeSelectedIndex } = storeToRefs(quickCommandsStore); // Get selectedIndex reactively
|
// selectedIndex now refers to the index within the flatVisibleCommands list
|
||||||
|
// Also get expandedGroups reactively for the template
|
||||||
|
const { selectedIndex: storeSelectedIndex, flatVisibleCommands, expandedGroups } = storeToRefs(quickCommandsStore);
|
||||||
|
|
||||||
|
// --- Helper function for selection check ---
|
||||||
|
const isCommandSelected = (commandId: number): boolean => {
|
||||||
|
if (storeSelectedIndex.value < 0 || !flatVisibleCommands.value[storeSelectedIndex.value]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return flatVisibleCommands.value[storeSelectedIndex.value].id === commandId;
|
||||||
|
};
|
||||||
|
|
||||||
// --- 事件定义 ---
|
// --- 事件定义 ---
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -116,10 +174,30 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
onMounted(() => {
|
onMounted(async () => { // Make onMounted async
|
||||||
quickCommandsStore.fetchQuickCommands(); // 组件挂载时获取数据
|
// Load expanded groups state first
|
||||||
|
quickCommandsStore.loadExpandedGroups();
|
||||||
|
// Then fetch commands (which might initialize expandedGroups for new groups)
|
||||||
|
await quickCommandsStore.fetchQuickCommands();
|
||||||
|
// Also fetch the quick command tags using the correct store instance
|
||||||
|
await quickCommandTagsStore.fetchTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// +++ Watcher to focus input when editing starts +++
|
||||||
|
watch(editingTagId, async (newId) => {
|
||||||
|
if (newId !== null) {
|
||||||
|
await nextTick();
|
||||||
|
const inputRef = tagInputRefs.value.get(newId);
|
||||||
|
if (inputRef) {
|
||||||
|
inputRef.focus();
|
||||||
|
inputRef.select();
|
||||||
|
} else {
|
||||||
|
console.error(`[QuickCmdView] Watcher: Input ref for ID ${newId} not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// +++ 注册/注销自定义聚焦动作 +++
|
// +++ 注册/注销自定义聚焦动作 +++
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// +++ 保存返回的注销函数 +++
|
// +++ 保存返回的注销函数 +++
|
||||||
@@ -140,21 +218,25 @@ const updateSearchTerm = (event: Event) => {
|
|||||||
// selectedIndex.value = -1; // REMOVED: Store handles resetting index
|
// selectedIndex.value = -1; // REMOVED: Store handles resetting index
|
||||||
};
|
};
|
||||||
|
|
||||||
// 滚动到选中的项目
|
// +++ 重构滚动逻辑 +++
|
||||||
const scrollToSelected = async (index: number) => { // Accept index as argument
|
const scrollToSelected = async (index: number) => {
|
||||||
await nextTick(); // 等待 DOM 更新
|
await nextTick(); // 等待 DOM 更新
|
||||||
if (index < 0 || !commandListRef.value) return;
|
if (index < 0 || !commandListContainerRef.value || !flatVisibleCommands.value[index]) return;
|
||||||
|
|
||||||
const listElement = commandListRef.value;
|
const selectedCommandId = flatVisibleCommands.value[index].id;
|
||||||
const selectedItem = listElement.children[index] as HTMLLIElement;
|
const listContainer = commandListContainerRef.value;
|
||||||
|
|
||||||
if (selectedItem) {
|
// Find the element using the data attribute
|
||||||
// 使用 scrollIntoView 使元素可见,滚动最小距离
|
const selectedElement = listContainer.querySelector(`li[data-command-id="${selectedCommandId}"]`) as HTMLLIElement;
|
||||||
selectedItem.scrollIntoView({
|
|
||||||
behavior: 'smooth', // 可以使用 'auto' 来实现即时滚动
|
if (selectedElement) {
|
||||||
block: 'nearest',
|
selectedElement.scrollIntoView({
|
||||||
});
|
behavior: 'smooth',
|
||||||
}
|
block: 'nearest',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`[QuickCmdView] scrollToSelected: Could not find element for command ID ${selectedCommandId}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes in the store's selectedIndex and scroll
|
// Watch for changes in the store's selectedIndex and scroll
|
||||||
@@ -162,12 +244,13 @@ watch(storeSelectedIndex, (newIndex) => {
|
|||||||
scrollToSelected(newIndex);
|
scrollToSelected(newIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Renamed function to avoid conflict if needed, and added logic
|
// Keyboard navigation now operates on the flat visible list
|
||||||
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
||||||
const commands = filteredAndSortedCommands.value;
|
// Use flatVisibleCommands for navigation logic
|
||||||
if (!commands.length) return;
|
const commands = flatVisibleCommands.value;
|
||||||
|
if (!commands.length) return;
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
quickCommandsStore.selectNextCommand(); // Use store action
|
quickCommandsStore.selectNextCommand(); // Use store action
|
||||||
@@ -193,16 +276,37 @@ const handleSearchInputBlur = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 检查焦点是否还在组件内部的其他可聚焦元素上(例如按钮)
|
// 检查焦点是否还在组件内部的其他可聚焦元素上(例如按钮)
|
||||||
// 如果焦点移出整个组件区域,则重置选择
|
// 如果焦点移出整个组件区域,则重置选择
|
||||||
if (document.activeElement !== searchInputRef.value && !commandListRef.value?.contains(document.activeElement)) {
|
if (document.activeElement !== searchInputRef.value && !commandListContainerRef.value?.contains(document.activeElement)) {
|
||||||
quickCommandsStore.resetSelection();
|
quickCommandsStore.resetSelection();
|
||||||
}
|
}
|
||||||
}, 100); // 短暂延迟
|
}, 100); // 短暂延迟
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换排序方式
|
// 切换排序方式 (Action remains the same, store handles the logic change)
|
||||||
const toggleSortBy = () => {
|
const toggleSortBy = () => {
|
||||||
const newSortBy = sortBy.value === 'name' ? 'usage_count' : 'name';
|
const newSortBy = sortBy.value === 'name' ? 'usage_count' : 'name';
|
||||||
quickCommandsStore.setSortBy(newSortBy);
|
quickCommandsStore.setSortBy(newSortBy);
|
||||||
|
};
|
||||||
|
|
||||||
|
// +++ Action to toggle group expansion +++
|
||||||
|
const toggleGroup = (groupName: string) => {
|
||||||
|
quickCommandsStore.toggleGroup(groupName);
|
||||||
|
// After toggling, selection might become invalid if the selected item is now hidden
|
||||||
|
// Reset selection or check if the selected item is still visible
|
||||||
|
nextTick(() => { // Wait for DOM update potentially caused by v-show
|
||||||
|
const selectedCmdId = storeSelectedIndex.value >= 0 && flatVisibleCommands.value[storeSelectedIndex.value]
|
||||||
|
? flatVisibleCommands.value[storeSelectedIndex.value].id
|
||||||
|
: null;
|
||||||
|
if (selectedCmdId !== null) {
|
||||||
|
const newIndex = flatVisibleCommands.value.findIndex(cmd => cmd.id === selectedCmdId);
|
||||||
|
if (newIndex === -1) { // Selected item is no longer visible
|
||||||
|
quickCommandsStore.resetSelection();
|
||||||
|
} else {
|
||||||
|
// Update index if it shifted, though usually reset is safer/simpler
|
||||||
|
// storeSelectedIndex.value = newIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算排序按钮的 title 和 icon
|
// 计算排序按钮的 title 和 icon
|
||||||
@@ -259,5 +363,117 @@ const focusSearchInput = (): boolean => {
|
|||||||
};
|
};
|
||||||
defineExpose({ focusSearchInput });
|
defineExpose({ focusSearchInput });
|
||||||
|
|
||||||
|
// +++ Methods for inline tag editing +++
|
||||||
|
const setTagInputRef = (el: any, id: string | number) => {
|
||||||
|
if (el) {
|
||||||
|
tagInputRefs.value.set(id, el as HTMLInputElement);
|
||||||
|
} else {
|
||||||
|
tagInputRefs.value.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEditingTag = (tagId: number | null, currentName: string) => {
|
||||||
|
editingTagId.value = tagId === null ? 'untagged' : tagId;
|
||||||
|
editedTagName.value = tagId === null ? '' : currentName; // Clear input for "Untagged"
|
||||||
|
// Focus logic is handled by the watcher
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishEditingTag = async () => {
|
||||||
|
const currentEditingId = editingTagId.value;
|
||||||
|
const newName = editedTagName.value.trim();
|
||||||
|
const originalGroup = filteredAndGroupedCommands.value.find(g => g.tagId === currentEditingId); // Find original group data
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (newName === '' && currentEditingId !== 'untagged') {
|
||||||
|
cancelEditingTag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newName === '' && currentEditingId === 'untagged') {
|
||||||
|
cancelEditingTag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let operationSuccess = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentEditingId === 'untagged') {
|
||||||
|
// --- Create new tag and assign commands ---
|
||||||
|
console.log(`[QuickCmdView] Creating new tag: ${newName}`);
|
||||||
|
const newTag = await quickCommandTagsStore.addTag(newName);
|
||||||
|
if (newTag) {
|
||||||
|
operationSuccess = true;
|
||||||
|
uiNotificationsStore.showSuccess(t('quickCommands.tags.createSuccess')); // Use specific translation key
|
||||||
|
const untaggedGroup = filteredAndGroupedCommands.value.find(g => g.tagId === null);
|
||||||
|
const commandIdsToAssign = untaggedGroup ? untaggedGroup.commands.map(c => c.id) : [];
|
||||||
|
|
||||||
|
if (commandIdsToAssign.length > 0) {
|
||||||
|
console.log(`[QuickCmdView] Assigning ${commandIdsToAssign.length} commands to new tag ID: ${newTag.id}`);
|
||||||
|
console.log(`[QuickCmdView] Command IDs to assign: ${JSON.stringify(commandIdsToAssign)}`); // +++ 添加日志 +++
|
||||||
|
// Call the store action to assign commands to the new tag
|
||||||
|
const assignSuccess = await quickCommandsStore.assignCommandsToTagAction(commandIdsToAssign, newTag.id);
|
||||||
|
if (assignSuccess) {
|
||||||
|
// Success/Error Notifications and list refresh are handled within the store action
|
||||||
|
console.log(`[QuickCmdView] assignCommandsToTagAction reported success.`);
|
||||||
|
} else {
|
||||||
|
console.error(`[QuickCmdView] assignCommandsToTagAction reported failure.`);
|
||||||
|
// Optionally show a specific error here if the store action doesn't cover all cases
|
||||||
|
}
|
||||||
|
// Remove TODO and temporary warning/refresh
|
||||||
|
// console.warn("TODO: Implement assignCommandsToTagAction in quickCommands.store and backend");
|
||||||
|
// uiNotificationsStore.showWarning("标签已创建,但指令分配功能尚未实现");
|
||||||
|
// await quickCommandsStore.fetchQuickCommands(); // Store action handles refresh
|
||||||
|
} else {
|
||||||
|
uiNotificationsStore.showInfo(t('quickCommands.tags.noCommandsToAssign'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update expanded group state
|
||||||
|
const untaggedGroupName = t('quickCommands.untagged', '未标记');
|
||||||
|
if (expandedGroups.value[untaggedGroupName] !== undefined) {
|
||||||
|
const currentState = expandedGroups.value[untaggedGroupName];
|
||||||
|
delete expandedGroups.value[untaggedGroupName]; // Remove old key
|
||||||
|
expandedGroups.value[newName] = currentState; // Add new key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// addTag failure handled in store
|
||||||
|
} else if (typeof currentEditingId === 'number') {
|
||||||
|
// --- Update existing tag ---
|
||||||
|
const originalTagName = originalGroup?.groupName;
|
||||||
|
if (!originalTagName) {
|
||||||
|
console.error(`[QuickCmdView] Cannot find original group name for tag ID ${currentEditingId}`);
|
||||||
|
cancelEditingTag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (originalTagName === newName) {
|
||||||
|
operationSuccess = true; // No change needed
|
||||||
|
} else {
|
||||||
|
console.log(`[QuickCmdView] Updating tag ID ${currentEditingId} from "${originalTagName}" to "${newName}"`);
|
||||||
|
const updateResult = await quickCommandTagsStore.updateTag(currentEditingId, newName);
|
||||||
|
if (updateResult) {
|
||||||
|
operationSuccess = true;
|
||||||
|
// uiNotificationsStore.showSuccess(t('quickCommands.tags.updateSuccess'));
|
||||||
|
// Update expanded group state
|
||||||
|
if (expandedGroups.value[originalTagName] !== undefined) {
|
||||||
|
const currentState = expandedGroups.value[originalTagName];
|
||||||
|
delete expandedGroups.value[originalTagName];
|
||||||
|
expandedGroups.value[newName] = currentState;
|
||||||
|
}
|
||||||
|
// Refresh commands to reflect potential grouping changes if names clashed etc.
|
||||||
|
await quickCommandsStore.fetchQuickCommands();
|
||||||
|
}
|
||||||
|
// updateTag failure handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("[QuickCmdView] Error during finishEditingTag:", error);
|
||||||
|
uiNotificationsStore.showError(t('common.unexpectedError'));
|
||||||
|
} finally {
|
||||||
|
editingTagId.value = null; // Exit edit mode regardless of success
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditingTag = () => {
|
||||||
|
editingTagId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Plan: Implement Independent Tags for Quick Commands
|
||||||
|
|
||||||
|
This plan outlines the steps to add a tagging and grouping feature for Quick Commands, ensuring the tag system is completely separate from the existing tag system used for SSH Connections.
|
||||||
|
|
||||||
|
## 1. Database Migration (`packages/backend/src/database/migrations.ts`)
|
||||||
|
|
||||||
|
* **Goal:** Add two new tables to the database schema to manage quick command specific tags and their associations.
|
||||||
|
* **Steps:**
|
||||||
|
* Analyze `migrations.ts` to understand the current migration process (Done - uses versioned migrations).
|
||||||
|
* Add a new migration (e.g., `id: 2`, `name: 'Create quick_command_tags table'`):
|
||||||
|
* SQL: `CREATE TABLE IF NOT EXISTS quick_command_tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')));`
|
||||||
|
* Include a `check` function using `tableExists(db, 'quick_command_tags')`.
|
||||||
|
* Add another new migration (e.g., `id: 3`, `name: 'Create quick_command_tag_associations table'`):
|
||||||
|
* SQL: `CREATE TABLE IF NOT EXISTS quick_command_tag_associations (quick_command_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, 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);`
|
||||||
|
* Include a `check` function using `tableExists(db, 'quick_command_tag_associations')`.
|
||||||
|
|
||||||
|
## 2. Database Schema (`packages/backend/src/database/schema.ts`)
|
||||||
|
|
||||||
|
* **Goal:** Keep schema definitions consistent.
|
||||||
|
* **Steps:**
|
||||||
|
* Add `createQuickCommandTagsTableSQL` constant with the SQL from Migration 2.
|
||||||
|
* Add `createQuickCommandTagAssociationsTableSQL` constant with the SQL from Migration 3.
|
||||||
|
|
||||||
|
## 3. Backend (`packages/backend`)
|
||||||
|
|
||||||
|
* **Goal:** Create dedicated modules for managing quick command tags and integrate them with the existing quick command logic.
|
||||||
|
|
||||||
|
* **New Module: Quick Command Tags**
|
||||||
|
* **`repositories/quick-command-tag.repository.ts`:**
|
||||||
|
* Define `QuickCommandTag` interface (`id`, `name`, `created_at`, `updated_at`).
|
||||||
|
* Implement CRUD functions for `quick_command_tags` table (`findAll`, `findById`, `create`, `update`, `delete`).
|
||||||
|
* Implement functions to manage `quick_command_tag_associations`:
|
||||||
|
* `setCommandTagAssociations(commandId: number, tagIds: number[]): Promise<boolean>` (Transactional: delete old, insert new).
|
||||||
|
* `findTagsByCommandId(commandId: number): Promise<QuickCommandTag[]>` (JOIN `quick_command_tag_associations` and `quick_command_tags`).
|
||||||
|
* (Optional) `addTagAssociation`, `removeTagAssociation`.
|
||||||
|
* **`services/quick-command-tag.service.ts`:**
|
||||||
|
* Inject `QuickCommandTagRepository`.
|
||||||
|
* Implement business logic for managing quick command tags (validation, orchestration).
|
||||||
|
* **`quick-command-tags/quick-command-tag.controller.ts` & `quick-command-tags/quick-command-tag.routes.ts`:**
|
||||||
|
* Create new API endpoints (e.g., `POST /quick-command-tags`, `GET /quick-command-tags`, `PUT /quick-command-tags/:id`, `DELETE /quick-command-tags/:id`).
|
||||||
|
* Inject `QuickCommandTagService`.
|
||||||
|
* Handle HTTP requests/responses.
|
||||||
|
|
||||||
|
* **Modify Module: Quick Commands**
|
||||||
|
* **`repositories/quick-commands.repository.ts`:**
|
||||||
|
* Update `QuickCommand` interface (or create `QuickCommandWithTags`) to include `tagIds: number[]` (referencing `quick_command_tags.id`).
|
||||||
|
* Modify `getAllQuickCommands` and `findQuickCommandById`: Use `LEFT JOIN quick_command_tag_associations` and `GROUP_CONCAT` to fetch `tag_ids_str`, parse into `tagIds` array.
|
||||||
|
* **`services/quick-commands.service.ts`:**
|
||||||
|
* Inject **new** `QuickCommandTagRepository` or `QuickCommandTagService`.
|
||||||
|
* Modify `addQuickCommand` signature: accept `tagIds?: number[]`. Implementation: call repo `addQuickCommand`, then call **new** repo/service `setCommandTagAssociations`.
|
||||||
|
* Modify `updateQuickCommand` signature: accept `tagIds?: number[]`. Implementation: call repo `updateQuickCommand`, then call **new** repo/service `setCommandTagAssociations`.
|
||||||
|
* Modify `getAllQuickCommands` return type to include `tagIds`.
|
||||||
|
* **`quick-commands/quick-commands.controller.ts`:**
|
||||||
|
* Modify `addQuickCommand` and `updateQuickCommand` request validation to accept optional `tagIds: number[]`.
|
||||||
|
* Pass `tagIds` to the service layer.
|
||||||
|
* Ensure `getAllQuickCommands` response includes `tagIds`.
|
||||||
|
|
||||||
|
## 4. Frontend (`packages/frontend`)
|
||||||
|
|
||||||
|
* **Goal:** Create a dedicated state management for quick command tags and update the UI to display grouped commands and allow tag selection.
|
||||||
|
|
||||||
|
* **Types (`types/quick-commands.types.ts` or similar):**
|
||||||
|
* Update `QuickCommand` interface to include `tagIds: number[]`.
|
||||||
|
|
||||||
|
* **New Store (`stores/quickCommandTags.store.ts`):**
|
||||||
|
* Define `QuickCommandTag` interface (matching backend).
|
||||||
|
* State: `tags: ref<QuickCommandTag[]>`, `isLoading`, `error`.
|
||||||
|
* Actions: `fetchTags` (from `/quick-command-tags`), `addTag`, `updateTag`, `deleteTag`. Include caching logic if desired.
|
||||||
|
|
||||||
|
* **Modify Store (`stores/quickCommands.store.ts`):**
|
||||||
|
* Inject **new** `useQuickCommandTagsStore`.
|
||||||
|
* State: Add `expandedGroups: ref<Record<string, boolean>>({})`.
|
||||||
|
* Actions:
|
||||||
|
* Add `toggleGroup(groupName: string)` action (manage `expandedGroups`, potentially save to localStorage).
|
||||||
|
* Modify `fetchQuickCommands`: Fetch data including `tagIds` from `/quick-commands`. Remove `sortBy` API parameter.
|
||||||
|
* Modify `addQuickCommand`, `updateQuickCommand`: Send `tagIds` in the request body.
|
||||||
|
* Modify `incrementUsage`: Adjust logic if sorting is now purely frontend within groups.
|
||||||
|
* Getters:
|
||||||
|
* **Rewrite** `filteredAndSortedCommands` (or create `filteredAndGroupedCommands`):
|
||||||
|
* Filter `quickCommandsList` by `searchTerm`.
|
||||||
|
* Use `quickCommandTagsStore.tags` to group filtered commands by `tagIds` (create "Untagged" group).
|
||||||
|
* Sort commands *within* each group based on `sortBy` state.
|
||||||
|
* Sort groups (e.g., alphabetically).
|
||||||
|
* Return nested structure: `Array<{ groupName: string; tagId: number | null; commands: QuickCommandFE[] }>`.
|
||||||
|
* **Add** `flatVisibleCommands`: Compute a flat list of commands only from currently expanded groups.
|
||||||
|
* Keyboard Navigation Actions (`selectNextCommand`, `selectPreviousCommand`): Modify to operate on `flatVisibleCommands` and `selectedIndex`.
|
||||||
|
* Caching: Re-evaluate or simplify caching strategy.
|
||||||
|
|
||||||
|
* **Modify View (`views/QuickCommandsView.vue`):**
|
||||||
|
* `<template>`:
|
||||||
|
* Use nested `v-for` to iterate through grouped data from the store.
|
||||||
|
* Render clickable group headers with expand/collapse icons, bound to `expandedGroups` state and `toggleGroup` action.
|
||||||
|
* Render command items (`<li>`) within each group.
|
||||||
|
* `<script>`:
|
||||||
|
* Inject **new** `useQuickCommandTagsStore`.
|
||||||
|
* Bind template to new/modified store getters (`filteredAndGroupedCommands`, `expandedGroups`).
|
||||||
|
* Adjust keyboard navigation highlighting based on `flatVisibleCommands` and `selectedIndex`.
|
||||||
|
* Modify `scrollToSelected` to find the correct DOM element based on the selected command in `flatVisibleCommands`.
|
||||||
|
|
||||||
|
* **Modify Component (`components/AddEditQuickCommandForm.vue`):**
|
||||||
|
* Inject **new** `useQuickCommandTagsStore`.
|
||||||
|
* Add a tag selection UI component (e.g., multi-select dropdown, chip input using `TagInput.vue` if adaptable, or a new component).
|
||||||
|
* Populate options using `quickCommandTagsStore.tags`.
|
||||||
|
* Allow creating new tags (calling `quickCommandTagsStore.addTag`).
|
||||||
|
* Bind selected `tagIds` to form state.
|
||||||
|
* Modify form submission logic to include `tagIds` when calling `quickCommandsStore.add/updateQuickCommand`.
|
||||||
Reference in New Issue
Block a user