feat: 实现连接列表中标签的内联编辑和创建
允许用户直接在连接列表的标签分组标题上进行编辑
This commit is contained in:
@@ -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,
|
exportConnections,
|
||||||
importConnections,
|
importConnections,
|
||||||
getRdpSessionToken, // Import the new controller function
|
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';
|
} from './connections.controller';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -84,4 +86,10 @@ router.post('/:id/rdp-session', getRdpSessionToken);
|
|||||||
// +++ POST /api/v1/connections/:id/clone - 克隆连接 +++
|
// +++ POST /api/v1/connections/:id/clone - 克隆连接 +++
|
||||||
router.post('/:id/clone', cloneConnection);
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -260,34 +260,57 @@ export const updateLastConnected = async (id: number, timestamp: number): Promis
|
|||||||
* @param connectionId 连接 ID
|
* @param connectionId 连接 ID
|
||||||
* @param tagIds 新的标签 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();
|
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 {
|
try {
|
||||||
await runDb(db, 'BEGIN TRANSACTION');
|
await runDb(db, 'BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// 删除旧关联
|
||||||
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
|
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
|
||||||
|
|
||||||
|
// 插入新关联 (如果 tagIds 不为空)
|
||||||
if (tagIds.length > 0) {
|
if (tagIds.length > 0) {
|
||||||
const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
|
const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
|
||||||
|
// 过滤无效 ID
|
||||||
const insertPromises = tagIds
|
const validTagIds = tagIds.filter(tagId => typeof tagId === 'number' && tagId > 0);
|
||||||
.filter(tagId => typeof tagId === 'number' && tagId > 0)
|
|
||||||
.map(tagId => runDb(db, insertSql, [connectionId, tagId]).catch(err => {
|
// 使用 Promise.all 确保所有插入完成或失败
|
||||||
console.warn(`Repository: 更新连接 ${connectionId} 标签时,插入 tag_id ${tagId} 失败: ${err.message}`);
|
const insertPromises = validTagIds.map(tagId =>
|
||||||
}));
|
runDb(db, insertSql, [connectionId, tagId])
|
||||||
|
);
|
||||||
|
// 如果任何插入失败,Promise.all 会 reject,错误会被下面的 catch 捕获
|
||||||
await Promise.all(insertPromises);
|
await Promise.all(insertPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
await runDb(db, 'COMMIT');
|
await runDb(db, 'COMMIT');
|
||||||
|
return true; // 事务成功提交,返回 true
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, err.message);
|
console.error(`Repository: 更新连接 ${connectionId} 的标签关联事务出错:`, err.message);
|
||||||
try {
|
try {
|
||||||
await runDb(db, 'ROLLBACK');
|
await runDb(db, 'ROLLBACK');
|
||||||
|
console.log(`Repository: Transaction rolled back for connection ${connectionId} tag update.`);
|
||||||
} catch (rollbackErr: any) {
|
} catch (rollbackErr: any) {
|
||||||
console.error(`Repository: 回滚连接 ${connectionId} 的标签更新事务失败:`, rollbackErr.message);
|
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 results: { connectionId: number, originalData: any }[] = [];
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
|
||||||
for (const connData of connections) {
|
for (const connData of connections) {
|
||||||
const params = [
|
const params = [
|
||||||
connData.name ?? null, connData.type, connData.host, connData.port, connData.username, connData.auth_method, // Add type parameter
|
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;
|
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. 返回新创建的带标签的连接
|
// 7. 返回新创建的带标签的连接
|
||||||
return clonedConnection;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store';
|
import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store';
|
||||||
import { useTagsStore, TagInfo } from '../stores/tags.store';
|
import { useTagsStore, TagInfo } from '../stores/tags.store'; // 确保 TagInfo 已导入
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
||||||
|
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // +++ 修正导入大小写 +++
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -24,6 +25,7 @@ const connectionsStore = useConnectionsStore();
|
|||||||
const tagsStore = useTagsStore();
|
const tagsStore = useTagsStore();
|
||||||
const sessionStore = useSessionStore(); // 获取 session store 实例
|
const sessionStore = useSessionStore(); // 获取 session store 实例
|
||||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||||
|
const uiNotificationsStore = useUiNotificationsStore(); // +++ 修正实例化大小写 +++
|
||||||
|
|
||||||
const { connections, isLoading: connectionsLoading, error: connectionsError } = storeToRefs(connectionsStore);
|
const { connections, isLoading: connectionsLoading, error: connectionsError } = storeToRefs(connectionsStore);
|
||||||
const { tags, isLoading: tagsLoading, error: tagsError } = storeToRefs(tagsStore);
|
const { tags, isLoading: tagsLoading, error: tagsError } = storeToRefs(tagsStore);
|
||||||
@@ -91,9 +93,27 @@ const highlightedConnectionId = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// +++ 编辑标签状态 +++
|
||||||
|
// editingTagId: number -> 编辑现有标签, null -> 编辑 "未标记" 分组 (准备创建新标签)
|
||||||
|
const editingTagId = ref<number | null | 'untagged'>(null); // 使用 'untagged' 字符串更清晰地区分
|
||||||
|
const editedTagName = ref(''); // 存储 input 中的临时名称
|
||||||
|
// const tagInputRef = ref<HTMLInputElement | null>(null); // Removed single ref
|
||||||
|
const tagInputRefs = ref(new Map<string | number, HTMLInputElement | null>()); // Map to store refs
|
||||||
|
|
||||||
|
// Function to set refs in the map
|
||||||
|
const setTagInputRef = (el: any, id: string | number) => {
|
||||||
|
if (el) {
|
||||||
|
tagInputRefs.value.set(id, el as HTMLInputElement);
|
||||||
|
} else {
|
||||||
|
// Clean up the ref when the element is unmounted
|
||||||
|
tagInputRefs.value.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 计算属性:过滤并按标签分组连接
|
// 计算属性:过滤并按标签分组连接
|
||||||
|
// 需要修改 filteredAndGroupedConnections,使其包含 tagId
|
||||||
const filteredAndGroupedConnections = computed(() => {
|
const filteredAndGroupedConnections = computed(() => {
|
||||||
const groups: Record<string, ConnectionInfo[]> = {};
|
const groups: Record<string, { connections: ConnectionInfo[], tagId: number | null }> = {}; // 修改:添加 tagId
|
||||||
const untagged: ConnectionInfo[] = [];
|
const untagged: ConnectionInfo[] = [];
|
||||||
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();
|
||||||
@@ -102,35 +122,29 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
const filteredConnections = connections.value.filter(conn => {
|
const filteredConnections = connections.value.filter(conn => {
|
||||||
const nameMatch = conn.name && conn.name.toLowerCase().includes(lowerSearchTerm);
|
const nameMatch = conn.name && conn.name.toLowerCase().includes(lowerSearchTerm);
|
||||||
const hostMatch = conn.host.toLowerCase().includes(lowerSearchTerm);
|
const hostMatch = conn.host.toLowerCase().includes(lowerSearchTerm);
|
||||||
// 如果有 IP 地址字段,也应包含在此处
|
return nameMatch || hostMatch;
|
||||||
// const ipMatch = conn.ipAddress && conn.ipAddress.toLowerCase().includes(lowerSearchTerm);
|
|
||||||
return nameMatch || hostMatch; // || ipMatch;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 分组过滤后的连接
|
// 2. 分组过滤后的连接
|
||||||
filteredConnections.forEach(conn => {
|
filteredConnections.forEach(conn => {
|
||||||
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
||||||
let tagged = false; // 标记是否至少加入了一个分组
|
let tagged = false;
|
||||||
conn.tag_ids.forEach(tagId => {
|
conn.tag_ids.forEach(tagId => {
|
||||||
const tag = tagMap.get(tagId);
|
const tag = tagMap.get(tagId);
|
||||||
// 确保标签存在才分组
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const groupName = tag.name;
|
const groupName = tag.name;
|
||||||
if (!groups[groupName]) {
|
if (!groups[groupName]) {
|
||||||
groups[groupName] = [];
|
groups[groupName] = { connections: [], tagId: tag.id }; // 修改:存储 tagId
|
||||||
// +++ 如果状态未定义,则明确设为 true (展开) +++
|
|
||||||
if (expandedGroups.value[groupName] === undefined) {
|
if (expandedGroups.value[groupName] === undefined) {
|
||||||
expandedGroups.value[groupName] = true;
|
expandedGroups.value[groupName] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 避免重复添加(如果一个连接有多个标签)
|
if (!groups[groupName].connections.some(c => c.id === conn.id)) {
|
||||||
if (!groups[groupName].some(c => c.id === conn.id)) {
|
groups[groupName].connections.push(conn);
|
||||||
groups[groupName].push(conn);
|
|
||||||
}
|
}
|
||||||
tagged = true;
|
tagged = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 如果所有标签都无效或未找到,则归入未标记
|
|
||||||
if (!tagged) {
|
if (!tagged) {
|
||||||
untagged.push(conn);
|
untagged.push(conn);
|
||||||
}
|
}
|
||||||
@@ -141,27 +155,29 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
|
|
||||||
// 3. 排序和格式化输出
|
// 3. 排序和格式化输出
|
||||||
for (const groupName in groups) {
|
for (const groupName in groups) {
|
||||||
groups[groupName].sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
groups[groupName].connections.sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
||||||
}
|
}
|
||||||
untagged.sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
untagged.sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
||||||
|
|
||||||
const sortedGroupNames = Object.keys(groups).sort();
|
const sortedGroupNames = Object.keys(groups).sort();
|
||||||
const result: { groupName: string; connections: ConnectionInfo[] }[] = sortedGroupNames.map(name => ({
|
// 修改:结果包含 tagId
|
||||||
|
const result: { groupName: string; connections: ConnectionInfo[]; tagId: number | null }[] = sortedGroupNames.map(name => ({
|
||||||
groupName: name,
|
groupName: name,
|
||||||
connections: groups[name]
|
connections: groups[name].connections,
|
||||||
|
tagId: groups[name].tagId // 添加 tagId
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (untagged.length > 0) {
|
if (untagged.length > 0) {
|
||||||
const untaggedGroupName = t('workspaceConnectionList.untagged');
|
const untaggedGroupName = t('workspaceConnectionList.untagged');
|
||||||
// +++ 如果状态未定义,则明确设为 true (展开) +++
|
|
||||||
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
||||||
expandedGroups.value[untaggedGroupName] = true;
|
expandedGroups.value[untaggedGroupName] = true;
|
||||||
}
|
}
|
||||||
result.push({ groupName: untaggedGroupName, connections: untagged });
|
// 未标记的分组没有 tagId
|
||||||
|
result.push({ groupName: untaggedGroupName, connections: untagged, tagId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
// +++ 监听分组状态变化并保存到 localStorage +++
|
// +++ 监听分组状态变化并保存到 localStorage +++
|
||||||
watch(expandedGroups, (newState) => {
|
watch(expandedGroups, (newState) => {
|
||||||
@@ -181,6 +197,19 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
watch(expandedGroups, () => {
|
watch(expandedGroups, () => {
|
||||||
highlightedIndex.value = -1;
|
highlightedIndex.value = -1;
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
// +++ 监听编辑状态,自动聚焦输入框 +++
|
||||||
|
watch(editingTagId, async (newId) => {
|
||||||
|
if (newId !== null) {
|
||||||
|
await nextTick();
|
||||||
|
const inputRef = tagInputRefs.value.get(newId); // Get ref from map using the ID
|
||||||
|
if (inputRef) {
|
||||||
|
inputRef.focus();
|
||||||
|
inputRef.select();
|
||||||
|
} else {
|
||||||
|
console.error(`[WkspConnList] Watcher: Input ref for ID ${newId} not found in map after nextTick.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 切换分组展开/折叠
|
// 切换分组展开/折叠
|
||||||
const toggleGroup = (groupName: string) => {
|
const toggleGroup = (groupName: string) => {
|
||||||
@@ -378,15 +407,112 @@ const scrollToHighlighted = async () => {
|
|||||||
highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// +++ 启动编辑标签 (或准备创建新标签) +++
|
||||||
|
const startEditingTag = (tagId: number | null, currentName: string) => { // Removed async
|
||||||
|
// 如果 tagId 是 null,表示是 "未标记" 分组
|
||||||
|
editingTagId.value = tagId === null ? 'untagged' : tagId;
|
||||||
|
editedTagName.value = tagId === null ? '' : currentName; // 未标记组开始编辑时清空输入框
|
||||||
|
// Focus logic moved to watcher
|
||||||
|
};
|
||||||
|
|
||||||
|
// +++ 完成编辑标签 (或创建新标签并分配) +++
|
||||||
|
const finishEditingTag = async () => {
|
||||||
|
const currentEditingId = editingTagId.value;
|
||||||
|
const newName = editedTagName.value.trim();
|
||||||
|
const originalTag = typeof currentEditingId === 'number' ? tags.value.find(t => t.id === currentEditingId) : null;
|
||||||
|
|
||||||
|
// 如果新名称为空 (除非是 'untagged' 状态,否则取消编辑)
|
||||||
|
if (newName === '' && currentEditingId !== 'untagged') {
|
||||||
|
editingTagId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果是 'untagged' 状态且新名称为空,也取消
|
||||||
|
if (newName === '' && currentEditingId === 'untagged') {
|
||||||
|
editingTagId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let operationSuccess = false; // Track if the core operation (add/update) succeeded
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentEditingId === 'untagged') {
|
||||||
|
// --- 创建新标签并分配 ---
|
||||||
|
const newTag = await tagsStore.addTag(newName); // Returns TagInfo | null
|
||||||
|
if (newTag) {
|
||||||
|
operationSuccess = true; // Core tag creation succeeded
|
||||||
|
uiNotificationsStore.addNotification({ message: t('tags.createSuccess'), type: 'success' });
|
||||||
|
const untaggedGroup = filteredAndGroupedConnections.value.find(g => g.tagId === null);
|
||||||
|
const untaggedConnectionIds = untaggedGroup ? untaggedGroup.connections.map(c => c.id) : [];
|
||||||
|
|
||||||
|
if (untaggedConnectionIds.length > 0) {
|
||||||
|
// 调用新的 action 批量添加标签
|
||||||
|
const assignSuccess = await connectionsStore.addTagToConnectionsAction(untaggedConnectionIds, newTag.id);
|
||||||
|
if (assignSuccess) {
|
||||||
|
uiNotificationsStore.addNotification({ message: t('workspaceConnectionList.allConnectionsTaggedSuccess'), type: 'success' });
|
||||||
|
}
|
||||||
|
// Assign failure notification is handled within the action
|
||||||
|
} else {
|
||||||
|
uiNotificationsStore.addNotification({ message: t('workspaceConnectionList.noConnectionsToTag'), type: 'info' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新展开状态 only if tag creation was successful
|
||||||
|
const untaggedGroupName = t('workspaceConnectionList.untagged');
|
||||||
|
if (expandedGroups.value[untaggedGroupName] !== undefined) {
|
||||||
|
const currentState = expandedGroups.value[untaggedGroupName];
|
||||||
|
delete expandedGroups.value[untaggedGroupName];
|
||||||
|
expandedGroups.value[newName] = currentState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If newTag is null, addTag failed (e.g., name exists), notification handled by store. operationSuccess remains false.
|
||||||
|
} else if (typeof currentEditingId === 'number') {
|
||||||
|
// --- 更新现有标签 ---
|
||||||
|
if (!originalTag) {
|
||||||
|
console.error(`Tag with ID ${currentEditingId} not found for update.`);
|
||||||
|
// Exit edit mode in finally block
|
||||||
|
} else if (originalTag.name === newName) {
|
||||||
|
operationSuccess = true; // No change needed, consider it success for UI state
|
||||||
|
} else {
|
||||||
|
// 名称已改变,尝试更新
|
||||||
|
const updateResult = await tagsStore.updateTag(currentEditingId, newName); // Returns boolean
|
||||||
|
if (updateResult) {
|
||||||
|
operationSuccess = true; // Core tag update succeeded
|
||||||
|
uiNotificationsStore.addNotification({ message: t('tags.updateSuccess'), type: 'success' });
|
||||||
|
// 更新展开状态 only if tag update was successful
|
||||||
|
if (expandedGroups.value[originalTag.name] !== undefined) {
|
||||||
|
const currentState = expandedGroups.value[originalTag.name];
|
||||||
|
delete expandedGroups.value[originalTag.name];
|
||||||
|
expandedGroups.value[newName] = currentState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If updateResult is false, updateTag failed (e.g., name exists), notification handled by store. operationSuccess remains false.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// 捕获这两个流程中未被 store action 捕获的意外错误
|
||||||
|
console.error("Error during finishEditingTag:", error);
|
||||||
|
uiNotificationsStore.addNotification({ message: t('common.unexpectedError'), type: 'error' });
|
||||||
|
// operationSuccess remains false
|
||||||
|
} finally {
|
||||||
|
// 无论核心操作成功与否,最终都退出编辑模式
|
||||||
|
// 这样即使用户输入了重复名称,收到通知后,输入框也会消失,恢复原状
|
||||||
|
editingTagId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// +++ 取消编辑(例如按 Esc 键) +++
|
||||||
|
const cancelEditingTag = () => {
|
||||||
|
editingTagId.value = null;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col overflow-hidden bg-background text-foreground">
|
<div class="h-full flex flex-col overflow-hidden bg-background text-foreground">
|
||||||
<!-- Loading State (Only show if loading AND no connections are available yet) -->
|
<!-- ... Loading/Error states ... -->
|
||||||
<div v-if="(connectionsLoading || tagsLoading) && connections.length === 0" class="flex items-center justify-center h-full text-text-secondary">
|
<div v-if="(connectionsLoading || tagsLoading) && connections.length === 0 && tags.length === 0" class="flex items-center justify-center h-full text-text-secondary">
|
||||||
<i class="fas fa-spinner fa-spin mr-2"></i> {{ t('common.loading') }}
|
<i class="fas fa-spinner fa-spin mr-2"></i> {{ t('common.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="connectionsError || tagsError" class="flex items-center justify-center h-full text-error px-4 text-center">
|
<div v-else-if="connectionsError || (tagsError && tags.length === 0)" class="flex items-center justify-center h-full text-error px-4 text-center">
|
||||||
<i class="fas fa-exclamation-triangle mr-2"></i> {{ connectionsError || tagsError }}
|
<i class="fas fa-exclamation-triangle mr-2"></i> {{ connectionsError || tagsError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -436,29 +562,59 @@ const scrollToHighlighted = async () => {
|
|||||||
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="mb-1 last:mb-0">
|
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="mb-1 last:mb-0">
|
||||||
<!-- Group Header -->
|
<!-- Group Header -->
|
||||||
<div
|
<div
|
||||||
class="group px-3 py-2 font-semibold cursor-pointer flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
||||||
@click="toggleGroup(groupData.groupName)"
|
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
||||||
|
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
||||||
>
|
>
|
||||||
<i :class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out', {'transform rotate-0': !expandedGroups[groupData.groupName]}]"></i>
|
<i
|
||||||
<span class="text-sm">{{ groupData.groupName }}</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]}]"
|
||||||
|
@click.stop="toggleGroup(groupData.groupName)"
|
||||||
|
class="cursor-pointer flex-shrink-0"
|
||||||
|
></i>
|
||||||
|
<!-- 编辑状态 -->
|
||||||
|
<input
|
||||||
|
v-if="editingTagId === (groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||||
|
:key="groupData.tagId === null ? 'untagged-input' : `tag-input-${groupData.tagId}`"
|
||||||
|
:ref="(el) => setTagInputRef(el, groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||||
|
type="text"
|
||||||
|
v-model="editedTagName"
|
||||||
|
class="text-sm bg-input border border-primary rounded px-1 py-0 w-full"
|
||||||
|
@blur="finishEditingTag"
|
||||||
|
@keydown.enter.prevent="finishEditingTag"
|
||||||
|
@keydown.esc.prevent="cancelEditingTag"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<!-- 显示状态 -->
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-sm inline-block overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
:class="{ 'cursor-pointer hover:underline': true }"
|
||||||
|
:title="t('workspaceConnectionList.clickToEditTag')"
|
||||||
|
@click.stop="startEditingTag(groupData.tagId, groupData.groupName)"
|
||||||
|
>
|
||||||
|
{{ groupData.groupName }}
|
||||||
|
</span>
|
||||||
|
<!-- 占位符,占据剩余空间 -->
|
||||||
|
<div class="flex-grow min-w-0"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Connection Items List -->
|
<!-- Connection Items List -->
|
||||||
<ul v-show="expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
|
<ul v-show="expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
|
||||||
<li
|
<!-- ... li v-for="conn in groupData.connections" ... -->
|
||||||
v-for="conn in groupData.connections"
|
<li
|
||||||
:key="conn.id"
|
v-for="conn in groupData.connections"
|
||||||
class="group my-0.5 py-2 pr-3 pl-4 cursor-pointer flex items-center rounded-md whitespace-nowrap overflow-hidden text-ellipsis text-foreground hover:bg-primary/10 transition-colors duration-150"
|
:key="conn.id"
|
||||||
:class="{ 'bg-primary/20 text-white font-medium': conn.id === highlightedConnectionId }"
|
class="group my-0.5 py-2 pr-3 pl-4 cursor-pointer flex items-center rounded-md whitespace-nowrap overflow-hidden text-ellipsis text-foreground hover:bg-primary/10 transition-colors duration-150"
|
||||||
:data-conn-id="conn.id"
|
:class="{ 'bg-primary/20 text-white font-medium': conn.id === highlightedConnectionId }"
|
||||||
@click.left="handleConnect(conn.id)"
|
:data-conn-id="conn.id"
|
||||||
@click.right.prevent
|
@click.left="handleConnect(conn.id)"
|
||||||
@contextmenu.prevent="showContextMenu($event, conn)"
|
@click.right.prevent
|
||||||
>
|
@contextmenu.prevent="showContextMenu($event, conn)"
|
||||||
<i :class="['fas', conn.type === 'RDP' ? 'fa-desktop' : 'fa-server', 'mr-2.5 w-4 text-center text-text-secondary group-hover:text-primary', { 'text-white': conn.id === highlightedConnectionId }]"></i>
|
>
|
||||||
<span class="overflow-hidden text-ellipsis whitespace-nowrap flex-grow text-sm" :title="conn.name || conn.host">
|
<i :class="['fas', conn.type === 'RDP' ? 'fa-desktop' : 'fa-server', 'mr-2.5 w-4 text-center text-text-secondary group-hover:text-primary', { 'text-white': conn.id === highlightedConnectionId }]"></i>
|
||||||
{{ conn.name || conn.host }}
|
<span class="overflow-hidden text-ellipsis whitespace-nowrap flex-grow text-sm" :title="conn.name || conn.host">
|
||||||
</span>
|
{{ conn.name || conn.host }}
|
||||||
</li>
|
</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -366,7 +366,9 @@
|
|||||||
},
|
},
|
||||||
"inputPlaceholder": "Type to search or create tags...",
|
"inputPlaceholder": "Type to search or create tags...",
|
||||||
"removeSelection": "Remove this tag selection",
|
"removeSelection": "Remove this tag selection",
|
||||||
"deleteTagGlobally": "Delete this tag globally"
|
"deleteTagGlobally": "Delete this tag globally",
|
||||||
|
"createSuccess": "Tag created successfully.",
|
||||||
|
"updateSuccess": "Tag updated successfully."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
@@ -797,7 +799,10 @@
|
|||||||
"workspaceConnectionList": {
|
"workspaceConnectionList": {
|
||||||
"untagged": "Untagged",
|
"untagged": "Untagged",
|
||||||
"searchPlaceholder": "Search name or host...",
|
"searchPlaceholder": "Search name or host...",
|
||||||
"noResults": "No connections found matching \"{searchTerm}\"."
|
"noResults": "No connections found matching \"{searchTerm}\".",
|
||||||
|
"allConnectionsTaggedSuccess": "All connections tagged successfully.",
|
||||||
|
"noConnectionsToTag": "No connections to tag.",
|
||||||
|
"clickToEditTag": "Click to edit tag name"
|
||||||
},
|
},
|
||||||
"remoteDesktopModal": {
|
"remoteDesktopModal": {
|
||||||
"title": "Remote Desktop",
|
"title": "Remote Desktop",
|
||||||
|
|||||||
@@ -977,9 +977,11 @@
|
|||||||
"confirmDelete": "\"{name}\"タグを削除しますか?この操作は元に戻せません。"
|
"confirmDelete": "\"{name}\"タグを削除しますか?この操作は元に戻せません。"
|
||||||
},
|
},
|
||||||
"removeSelection": "このタグの選択を解除",
|
"removeSelection": "このタグの選択を解除",
|
||||||
"title": "タグ管理"
|
"title": "タグ管理",
|
||||||
},
|
"createSuccess": "タグが正常に作成されました。",
|
||||||
"terminalTabBar": {
|
"updateSuccess": "タグが正常に更新されました。"
|
||||||
|
},
|
||||||
|
"terminalTabBar": {
|
||||||
"selectServerTitle": "接続するサーバーを選択"
|
"selectServerTitle": "接続するサーバーを選択"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
@@ -990,9 +992,12 @@
|
|||||||
"workspaceConnectionList": {
|
"workspaceConnectionList": {
|
||||||
"noResults": "\"{searchTerm}\"に一致する接続は見つかりませんでした。",
|
"noResults": "\"{searchTerm}\"に一致する接続は見つかりませんでした。",
|
||||||
"searchPlaceholder": "名前またはホストを検索...",
|
"searchPlaceholder": "名前またはホストを検索...",
|
||||||
"untagged": "タグなし"
|
"untagged": "タグなし",
|
||||||
},
|
"allConnectionsTaggedSuccess": "すべての接続にタグが正常に追加されました。",
|
||||||
"sshKeys": {
|
"noConnectionsToTag": "タグ付けする接続はありません。",
|
||||||
|
"clickToEditTag": "クリックしてタグ名を編集"
|
||||||
|
},
|
||||||
|
"sshKeys": {
|
||||||
"selector": {
|
"selector": {
|
||||||
"selectPlaceholder": "SSH キーを選択...",
|
"selectPlaceholder": "SSH キーを選択...",
|
||||||
"useDirectInput": "またはキーの内容を直接入力",
|
"useDirectInput": "またはキーの内容を直接入力",
|
||||||
|
|||||||
@@ -366,7 +366,9 @@
|
|||||||
},
|
},
|
||||||
"inputPlaceholder": "输入搜索或创建标签...",
|
"inputPlaceholder": "输入搜索或创建标签...",
|
||||||
"removeSelection": "移除此标签选择",
|
"removeSelection": "移除此标签选择",
|
||||||
"deleteTagGlobally": "全局删除此标签"
|
"deleteTagGlobally": "全局删除此标签",
|
||||||
|
"createSuccess": "标签创建成功。",
|
||||||
|
"updateSuccess": "标签更新成功。"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
@@ -800,7 +802,10 @@
|
|||||||
"workspaceConnectionList": {
|
"workspaceConnectionList": {
|
||||||
"untagged": "未标记",
|
"untagged": "未标记",
|
||||||
"searchPlaceholder": "搜索名称或主机...",
|
"searchPlaceholder": "搜索名称或主机...",
|
||||||
"noResults": "未找到匹配 \"{searchTerm}\" 的连接。"
|
"noResults": "未找到匹配 \"{searchTerm}\" 的连接。",
|
||||||
|
"allConnectionsTaggedSuccess": "所有连接已成功添加标签。",
|
||||||
|
"noConnectionsToTag": "没有需要添加标签的连接。",
|
||||||
|
"clickToEditTag": "点击编辑标签名称"
|
||||||
},
|
},
|
||||||
"remoteDesktopModal": {
|
"remoteDesktopModal": {
|
||||||
"title": "远程桌面",
|
"title": "远程桌面",
|
||||||
|
|||||||
@@ -233,5 +233,53 @@ export const useConnectionsStore = defineStore('connections', {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// +++ 新增:为多个连接添加一个标签 (调用新的后端 API) +++
|
||||||
|
async addTagToConnectionsAction(connectionIds: number[], tagId: number): Promise<boolean> {
|
||||||
|
if (connectionIds.length === 0) return true; // 没有连接需要更新,直接返回成功
|
||||||
|
|
||||||
|
this.isLoading = true; // 可以考虑为批量操作设置单独状态
|
||||||
|
this.error = null;
|
||||||
|
try {
|
||||||
|
// 调用新的后端 API POST /connections/add-tag
|
||||||
|
await apiClient.post('/connections/add-tag', {
|
||||||
|
connection_ids: connectionIds,
|
||||||
|
tag_id: tagId
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新成功后,清除缓存并重新获取以保证数据一致性
|
||||||
|
localStorage.removeItem('connectionsCache');
|
||||||
|
await this.fetchConnections();
|
||||||
|
return true; // 表示成功
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`为连接 ${connectionIds.join(', ')} 添加标签 ${tagId} 失败:`, err);
|
||||||
|
this.error = err.response?.data?.message || err.message || `为连接添加标签时发生未知错误。`;
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
console.warn('未授权,需要登录才能为连接添加标签。');
|
||||||
|
}
|
||||||
|
return false; // 表示失败
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// (保留) 更新单个连接的标签 (如果仍有需要)
|
||||||
|
async updateConnectionTags(connectionId: number, tagIds: number[]): Promise<boolean> {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.error = null;
|
||||||
|
try {
|
||||||
|
// 注意:此 API 端点可能已在后端移除或更改
|
||||||
|
await apiClient.put(`/connections/${connectionId}/tags`, { tag_ids: tagIds });
|
||||||
|
localStorage.removeItem('connectionsCache');
|
||||||
|
await this.fetchConnections();
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`更新连接 ${connectionId} 的标签失败:`, err);
|
||||||
|
this.error = err.response?.data?.message || err.message || `更新连接标签时发生未知错误。`;
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,19 +66,20 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加新标签 (添加后清除缓存)
|
// 添加新标签 (添加后清除缓存)
|
||||||
async function addTag(name: string): Promise<boolean> {
|
async function addTag(name: string): Promise<TagInfo | null> { // 修改返回类型
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<{ message: string, tag: TagInfo }>('/tags', { name }); // 使用 apiClient 并移除 base URL
|
const response = await apiClient.post<{ message: string, tag: TagInfo }>('/tags', { name }); // 假设后端返回新标签信息
|
||||||
// 添加成功后,清除缓存并重新获取
|
const newTag = response.data.tag;
|
||||||
|
// 添加成功后,清除缓存并重新获取 (fetchTags 会更新本地列表)
|
||||||
localStorage.removeItem('tagsCache');
|
localStorage.removeItem('tagsCache');
|
||||||
await fetchTags(); // fetchTags 会处理获取和缓存更新
|
await fetchTags(); // fetchTags 会处理获取和缓存更新
|
||||||
return true;
|
return newTag; // 返回新标签信息
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to add tag:', err);
|
console.error('Failed to add tag:', err);
|
||||||
error.value = err.response?.data?.message || err.message || '添加标签失败';
|
error.value = err.response?.data?.message || err.message || '添加标签失败';
|
||||||
return false;
|
return null; // 返回 null 表示失败
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user