feat: 实现连接列表中标签的内联编辑和创建

允许用户直接在连接列表的标签分组标题上进行编辑
This commit is contained in:
Baobhan Sith
2025-05-03 09:48:47 +08:00
parent b40be119d4
commit bc85d52aa5
10 changed files with 477 additions and 71 deletions
@@ -412,3 +412,76 @@ export const cloneConnection = async (req: Request, res: Response): Promise<void
}
}
};
/**
* 为多个连接添加一个标签 (POST /api/v1/connections/add-tag)
* 注意:我们改变了路由和方法 (POST),并使用请求体传递所有信息,以避免嵌套事务。
*/
export const addTagToConnections = async (req: Request, res: Response): Promise<void> => {
try {
const { connection_ids, tag_id } = req.body;
// 验证输入
if (!Array.isArray(connection_ids) || !connection_ids.every(id => typeof id === 'number')) {
res.status(400).json({ message: 'connection_ids 必须是一个数字数组。' });
return;
}
if (typeof tag_id !== 'number' || tag_id <= 0) {
res.status(400).json({ message: 'tag_id 必须是一个有效的正整数。' });
return;
}
if (connection_ids.length === 0) {
res.status(400).json({ message: 'connection_ids 不能为空数组。' });
return;
}
// 调用服务层批量添加标签
await ConnectionService.addTagToConnections(connection_ids, tag_id);
res.status(200).json({ message: '标签已成功添加到指定连接。' });
} catch (error: any) {
console.error(`Controller: 为多个连接添加标签 ${req.body?.tag_id} 时发生错误:`, error);
// 可以根据服务层抛出的错误类型返回更具体的错误码
if (error.message.includes('标签 ID') && error.message.includes('不存在')) {
res.status(400).json({ message: error.message }); // Bad request if tag doesn't exist
} else {
res.status(500).json({ message: error.message || '为连接添加标签时发生内部服务器错误。' });
}
}
};
/**
* 更新单个连接的标签 (PUT /api/v1/connections/:id/tags)
* (保留此接口,但主要逻辑由 addTagToConnections 处理)
*/
export const updateConnectionTags = async (req: Request, res: Response): Promise<void> => {
try {
const connectionId = parseInt(req.params.id, 10);
const { tag_ids } = req.body;
if (isNaN(connectionId)) {
res.status(400).json({ message: '无效的连接 ID。' });
return;
}
if (!Array.isArray(tag_ids) || !tag_ids.every(id => typeof id === 'number')) {
res.status(400).json({ message: 'tag_ids 必须是一个数字数组。' });
return;
}
const success = await ConnectionService.updateConnectionTags(connectionId, tag_ids);
if (!success) {
res.status(404).json({ message: '连接未找到或更新标签失败。' });
} else {
res.status(200).json({ message: '连接标签更新成功。' });
}
} catch (error: any) {
console.error(`Controller: 更新连接 ${req.params.id} 的标签时发生错误:`, error);
if (error.message.includes('未找到')) {
res.status(404).json({ message: error.message });
} else {
res.status(500).json({ message: error.message || '更新连接标签时发生内部服务器错误。' });
}
}
};
@@ -12,7 +12,9 @@ import {
exportConnections,
importConnections,
getRdpSessionToken, // Import the new controller function
cloneConnection // +++ Import the clone controller function +++
cloneConnection, // +++ Import the clone controller function +++
// updateConnectionTags, // No longer directly used by primary flow
addTagToConnections // +++ Import the new controller function for adding tag to multiple connections +++
} from './connections.controller';
const router = Router();
@@ -84,4 +86,10 @@ router.post('/:id/rdp-session', getRdpSessionToken);
// +++ POST /api/v1/connections/:id/clone - 克隆连接 +++
router.post('/:id/clone', cloneConnection);
// +++ POST /api/v1/connections/add-tag - 为多个连接添加一个标签 +++
router.post('/add-tag', addTagToConnections);
// Note: PUT /:id/tags route is removed as the primary flow uses the bulk add endpoint now.
// It could be kept if there's a separate use case for updating a single connection's tags.
export default router;
@@ -260,34 +260,57 @@ export const updateLastConnected = async (id: number, timestamp: number): Promis
* @param connectionId 连接 ID
* @param tagIds 新的标签 ID 数组 (空数组表示清除所有标签)
*/
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<void> => {
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<boolean> => { // 修改返回类型为 boolean
const db = await getDbInstance();
// 1. 检查连接是否存在
try {
const connectionExists = await getDbRow<{ id: number }>(db, `SELECT id FROM connections WHERE id = ?`, [connectionId]);
if (!connectionExists) {
console.warn(`Repository: updateConnectionTags - Connection with ID ${connectionId} not found.`);
return false; // 连接不存在,返回 false
}
} catch (checkErr: any) {
console.error(`Repository: 检查连接 ${connectionId} 是否存在时出错:`, checkErr.message);
throw new Error('检查连接是否存在时失败'); // 抛出检查错误
}
// 2. 执行标签更新事务
try {
await runDb(db, 'BEGIN TRANSACTION');
// 删除旧关联
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
// 插入新关联 (如果 tagIds 不为空)
if (tagIds.length > 0) {
const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
const insertPromises = tagIds
.filter(tagId => typeof tagId === 'number' && tagId > 0)
.map(tagId => runDb(db, insertSql, [connectionId, tagId]).catch(err => {
console.warn(`Repository: 更新连接 ${connectionId} 标签时,插入 tag_id ${tagId} 失败: ${err.message}`);
}));
// 过滤无效 ID
const validTagIds = tagIds.filter(tagId => typeof tagId === 'number' && tagId > 0);
// 使用 Promise.all 确保所有插入完成或失败
const insertPromises = validTagIds.map(tagId =>
runDb(db, insertSql, [connectionId, tagId])
);
// 如果任何插入失败,Promise.all 会 reject,错误会被下面的 catch 捕获
await Promise.all(insertPromises);
}
await runDb(db, 'COMMIT');
return true; // 事务成功提交,返回 true
} catch (err: any) {
console.error(`Repository: 更新连接 ${connectionId} 的标签关联出错:`, err.message);
console.error(`Repository: 更新连接 ${connectionId} 的标签关联事务出错:`, err.message);
try {
await runDb(db, 'ROLLBACK');
await runDb(db, 'ROLLBACK');
console.log(`Repository: Transaction rolled back for connection ${connectionId} tag update.`);
} catch (rollbackErr: any) {
console.error(`Repository: 回滚连接 ${connectionId} 的标签更新事务失败:`, rollbackErr.message);
// 即使回滚失败,原始错误也更重要
}
throw new Error('处理标签关联失败');
// 直接重新抛出原始事务错误,让上层处理
// SQLite 在事务中遇到错误时通常会自动回滚
throw err;
}
};
@@ -326,7 +349,6 @@ export const bulkInsertConnections = async (
const results: { connectionId: number, originalData: any }[] = [];
const now = Math.floor(Date.now() / 1000);
for (const connData of connections) {
const params = [
connData.name ?? null, connData.type, connData.host, connData.port, connData.username, connData.auth_method, // Add type parameter
@@ -349,3 +371,37 @@ export const bulkInsertConnections = async (
}
return results;
};
/**
* 为多个连接添加同一个标签 (使用事务)
* @param connectionIds 连接 ID 数组
* @param tagId 要添加的标签 ID
*/
export const addTagToMultipleConnections = async (connectionIds: number[], tagId: number): Promise<void> => {
if (connectionIds.length === 0 || typeof tagId !== 'number' || tagId <= 0) {
console.warn('[Repository] addTagToMultipleConnections called with empty connectionIds or invalid tagId.');
return; // 无需操作
}
const db = await getDbInstance();
try {
await runDb(db, 'BEGIN TRANSACTION');
const insertSql = `INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
// 使用 Promise.all 确保所有插入完成或失败
const insertPromises = connectionIds.map(connId =>
runDb(db, insertSql, [connId, tagId])
);
await Promise.all(insertPromises);
await runDb(db, 'COMMIT');
} catch (err: any) {
console.error(`Repository: 为多个连接添加标签 ${tagId} 时事务出错:`, err.message);
try {
await runDb(db, 'ROLLBACK');
} catch (rollbackErr: any) {
console.error(`Repository: 回滚为多个连接添加标签 ${tagId} 的事务失败:`, rollbackErr.message);
}
throw new Error(`为多个连接添加标签失败: ${err.message}`);
}
};
@@ -500,3 +500,52 @@ export const cloneConnection = async (originalId: number, newName: string): Prom
// 7. 返回新创建的带标签的连接
return clonedConnection;
};
// 注意:updateConnectionTags 现在主要由 updateConnection 内部调用,
// 或者可以保留用于单独更新单个连接标签的场景(如果需要的话)。
// 为了解决嵌套事务问题,我们添加一个新的批量添加函数。
/**
* 为指定的一组连接添加一个标签
* @param connectionIds 连接 ID 数组
* @param tagId 要添加的标签 ID
*/
export const addTagToConnections = async (connectionIds: number[], tagId: number): Promise<void> => {
// 1. 验证 tagId 是否有效(可选,但建议)
// const tagExists = await TagRepository.findTagById(tagId); // 需要导入 TagRepository
// if (!tagExists) {
// throw new Error(`标签 ID ${tagId} 不存在。`);
// }
// 2. 调用仓库层批量添加标签
try {
await ConnectionRepository.addTagToMultipleConnections(connectionIds, tagId);
// 记录审计日志 (可以考虑为批量操作定义新的审计类型)
// TODO: 定义 'CONNECTIONS_TAG_ADDED' 审计日志类型
// auditLogService.logAction('CONNECTIONS_TAG_ADDED', { connectionIds, tagId });
} catch (error: any) {
console.error(`Service: 为连接 ${connectionIds.join(', ')} 添加标签 ${tagId} 时发生错误:`, error);
throw error; // 重新抛出错误
}
};
/**
* 更新指定连接的标签关联 (保留此函数用于可能的其他用途,但主要逻辑转移到 addTagToConnections)
* @param connectionId 连接 ID
* @param tagIds 新的标签 ID 数组
* @returns boolean 指示操作是否成功(找到连接并尝试更新)
*/
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<boolean> => {
try {
const updated = await ConnectionRepository.updateConnectionTags(connectionId, tagIds);
// if (updated) {
// // TODO: 定义 'CONNECTION_TAGS_UPDATED' 审计日志类型
// // auditLogService.logAction('CONNECTION_TAGS_UPDATED', { connectionId, tagIds });
// }
return updated;
} catch (error: any) {
console.error(`Service: 更新连接 ${connectionId} 的标签时发生错误:`, error);
throw error;
}
};