chore(docs): archive quickcommands double-click tooltip implementation

move quickcommands-double-click-tooltip records from plan to archive and
mark status as completed. update changelog, archive index, and frontend
module documentation to reflect the finalized interaction change and
traceability metadata
This commit is contained in:
yinjianm
2026-04-12 23:05:41 +08:00
parent 8c130adcc9
commit b660fc1f37
21 changed files with 917 additions and 28 deletions
+98 -6
View File
@@ -10,6 +10,19 @@ export interface TagData {
updated_at: number;
}
export interface BatchDeleteTagsSummary {
deleted_tag_ids: number[];
deleted_tags_count: number;
affected_connection_ids: number[];
affected_connections_count: number;
deleted_connections_count: number;
delete_connections: boolean;
}
const buildInClause = (count: number): string => {
return new Array(count).fill('?').join(', ');
};
/**
* 获取所有标签
*/
@@ -84,14 +97,93 @@ export const updateTag = async (id: number, name: string): Promise<boolean> => {
* 删除标签
*/
export const deleteTag = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM tags WHERE id = ?`;
const summary = await deleteTagsBatch([id], false);
return summary.deleted_tags_count > 0;
};
/**
* 批量删除标签,并根据策略可选地同时删除关联连接。
*/
export const deleteTagsBatch = async (tagIds: number[], deleteConnections: boolean): Promise<BatchDeleteTagsSummary> => {
const normalizedTagIds = Array.from(new Set(tagIds.filter((tagId) => Number.isInteger(tagId) && tagId > 0)));
if (normalizedTagIds.length === 0) {
return {
deleted_tag_ids: [],
deleted_tags_count: 0,
affected_connection_ids: [],
affected_connections_count: 0,
deleted_connections_count: 0,
delete_connections: Boolean(deleteConnections),
};
}
const db = await getDbInstance();
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
await runDb(db, 'BEGIN TRANSACTION');
const tagPlaceholders = buildInClause(normalizedTagIds.length);
const existingTags = await allDb<{ id: number }>(
db,
`SELECT id FROM tags WHERE id IN (${tagPlaceholders}) ORDER BY id ASC`,
normalizedTagIds,
);
const existingTagIds = existingTags.map((row) => row.id);
if (existingTagIds.length === 0) {
await runDb(db, 'COMMIT');
return {
deleted_tag_ids: [],
deleted_tags_count: 0,
affected_connection_ids: [],
affected_connections_count: 0,
deleted_connections_count: 0,
delete_connections: Boolean(deleteConnections),
};
}
const existingTagPlaceholders = buildInClause(existingTagIds.length);
const affectedConnections = await allDb<{ connection_id: number }>(
db,
`SELECT DISTINCT connection_id FROM connection_tags WHERE tag_id IN (${existingTagPlaceholders}) ORDER BY connection_id ASC`,
existingTagIds,
);
const affectedConnectionIds = affectedConnections.map((row) => row.connection_id);
let deletedConnectionsCount = 0;
if (deleteConnections && affectedConnectionIds.length > 0) {
const connectionPlaceholders = buildInClause(affectedConnectionIds.length);
const deleteConnectionsResult = await runDb(
db,
`DELETE FROM connections WHERE id IN (${connectionPlaceholders})`,
affectedConnectionIds,
);
deletedConnectionsCount = deleteConnectionsResult.changes ?? 0;
}
const deleteTagsResult = await runDb(
db,
`DELETE FROM tags WHERE id IN (${existingTagPlaceholders})`,
existingTagIds,
);
await runDb(db, 'COMMIT');
return {
deleted_tag_ids: existingTagIds,
deleted_tags_count: deleteTagsResult.changes ?? 0,
affected_connection_ids: affectedConnectionIds,
affected_connections_count: affectedConnectionIds.length,
deleted_connections_count: deletedConnectionsCount,
delete_connections: Boolean(deleteConnections),
};
} catch (err: any) {
console.error(`[仓库] 删除标签 ${id} 时出错:`, err.message);
throw new Error('删除标签失败');
try {
await runDb(db, 'ROLLBACK');
} catch (rollbackError: any) {
console.error('[仓库] 批量删除标签回滚失败:', rollbackError.message);
}
console.error(`[仓库] 批量删除标签时出错:`, err.message);
throw new Error(`批量删除标签失败: ${err.message}`);
}
};
+13
View File
@@ -2,6 +2,7 @@ import * as TagRepository from '../tags/tag.repository';
// Re-export or define types
export interface TagData extends TagRepository.TagData {}
export interface BatchDeleteTagsSummary extends TagRepository.BatchDeleteTagsSummary {}
/**
* 获取所有标签
@@ -76,6 +77,18 @@ export const deleteTag = async (id: number): Promise<boolean> => {
return TagRepository.deleteTag(id);
};
/**
* 批量删除标签,并根据策略决定是否删除关联连接。
*/
export const deleteTagsBatch = async (tagIds: number[], deleteConnections: boolean): Promise<BatchDeleteTagsSummary> => {
const normalizedTagIds = Array.from(new Set(tagIds.filter((tagId) => Number.isInteger(tagId) && tagId > 0)));
if (normalizedTagIds.length === 0) {
throw new Error('至少需要提供一个有效的标签 ID。');
}
return TagRepository.deleteTagsBatch(normalizedTagIds, Boolean(deleteConnections));
};
/**
* 更新标签与连接的关联关系
*/
@@ -131,6 +131,44 @@ export const deleteTag = async (req: Request, res: Response): Promise<void> => {
}
};
/**
* 批量删除标签 (POST /api/v1/tags/bulk-delete)
*/
export const bulkDeleteTags = async (req: Request, res: Response): Promise<void> => {
const { tag_ids, delete_connections } = req.body;
if (!Array.isArray(tag_ids) || !tag_ids.every((id) => typeof id === 'number')) {
res.status(400).json({ message: 'tag_ids 必须是一个数字数组。' });
return;
}
if (delete_connections !== undefined && typeof delete_connections !== 'boolean') {
res.status(400).json({ message: 'delete_connections 必须是布尔值。' });
return;
}
try {
const summary = await TagService.deleteTagsBatch(tag_ids, Boolean(delete_connections));
if (summary.deleted_tags_count === 0) {
res.status(404).json({ message: '未找到可删除的标签。' });
return;
}
auditLogService.logAction('TAG_DELETED', {
mode: 'batch',
...summary,
});
res.status(200).json({ message: '标签批量删除成功。', summary });
} catch (error: any) {
console.error('Controller: 批量删除标签时发生错误:', error);
if (error.message.includes('至少需要提供')) {
res.status(400).json({ message: error.message });
} else {
res.status(500).json({ message: error.message || '批量删除标签时发生内部服务器错误。' });
}
}
};
/**
* 更新标签与连接的关联关系 (PUT /api/v1/tags/:id/connections)
*/
+2
View File
@@ -6,6 +6,7 @@ import {
getTagById,
updateTag,
deleteTag,
bulkDeleteTags,
updateTagConnections // +++ 导入新的控制器方法 +++
} from './tags.controller';
@@ -16,6 +17,7 @@ router.use(isAuthenticated);
// 定义标签相关的路由
router.post('/', createTag); // POST /api/v1/tags - 创建新标签
router.post('/bulk-delete', bulkDeleteTags); // POST /api/v1/tags/bulk-delete - 批量删除标签
router.get('/', getTags); // GET /api/v1/tags - 获取标签列表
router.get('/:id', getTagById); // GET /api/v1/tags/:id - 获取单个标签
router.put('/:id', updateTag); // PUT /api/v1/tags/:id - 更新标签