From 6144633a5e9065b690a2fbeef80fa05daa37ec0b Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sat, 3 May 2025 20:34:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=98=BE=E9=9A=90?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E7=9A=84=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/services/settings.service.ts | 56 +++++ .../src/settings/settings.controller.ts | 82 ++++++- .../backend/src/settings/settings.routes.ts | 12 + .../components/WorkspaceConnectionList.vue | 141 ++++++++--- packages/frontend/src/locales/en-US.json | 8 +- packages/frontend/src/locales/ja-JP.json | 8 +- packages/frontend/src/locales/zh-CN.json | 8 +- .../frontend/src/stores/settings.store.ts | 98 ++++++-- .../frontend/src/views/QuickCommandsView.vue | 224 +++++++++++------- packages/frontend/src/views/SettingsView.vue | 92 +++++++ 10 files changed, 592 insertions(+), 137 deletions(-) diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index a5fae56..2e9df9f 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -31,6 +31,8 @@ const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制 const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键 const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔 const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键 +const SHOW_CONNECTION_TAGS_KEY = 'showConnectionTags'; // 连接标签显示设置键 +const SHOW_QUICK_COMMAND_TAGS_KEY = 'showQuickCommandTags'; // 快捷指令标签显示设置键 export const settingsService = { /** @@ -479,6 +481,60 @@ export const settingsService = { // Directly call the specific repository function with the full, validated config await setCaptchaConfigInRepo(configToSave); console.log('[SettingsService] CAPTCHA config successfully set.'); + }, // <-- Add comma here + + // --- Show Connection Tags --- + async getShowConnectionTags(): Promise { + console.log(`[Service] Attempting to get setting for key: ${SHOW_CONNECTION_TAGS_KEY}`); + try { + const valueStr = await settingsRepository.getSetting(SHOW_CONNECTION_TAGS_KEY); + console.log(`[Service] Raw value from repository for ${SHOW_CONNECTION_TAGS_KEY}:`, valueStr); + // 默认显示,所以只有当值为 'false' 时才返回 false + return valueStr !== 'false'; + } catch (error) { + console.error(`[Service] Error getting show connection tags setting (key: ${SHOW_CONNECTION_TAGS_KEY}):`, error); + return true; // 默认返回 true + } + }, // *** 确保这里有逗号 *** + + async setShowConnectionTags(enabled: boolean): Promise { + console.log(`[Service] setShowConnectionTags called with: ${enabled}`); + try { + const valueStr = String(enabled); + console.log(`[Service] Attempting to save setting. Key: ${SHOW_CONNECTION_TAGS_KEY}, Value: ${valueStr}`); + await settingsRepository.setSetting(SHOW_CONNECTION_TAGS_KEY, valueStr); + console.log(`[Service] Successfully saved setting for key: ${SHOW_CONNECTION_TAGS_KEY}`); + } catch (error) { + console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_CONNECTION_TAGS_KEY}:`, error); + throw new Error('Failed to save show connection tags setting.'); + } + }, // *** 确保这里有逗号 *** + + // --- Show Quick Command Tags --- + async getShowQuickCommandTags(): Promise { + console.log(`[Service] Attempting to get setting for key: ${SHOW_QUICK_COMMAND_TAGS_KEY}`); + try { + const valueStr = await settingsRepository.getSetting(SHOW_QUICK_COMMAND_TAGS_KEY); + console.log(`[Service] Raw value from repository for ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, valueStr); + // 默认显示,所以只有当值为 'false' 时才返回 false + return valueStr !== 'false'; + } catch (error) { + console.error(`[Service] Error getting show quick command tags setting (key: ${SHOW_QUICK_COMMAND_TAGS_KEY}):`, error); + return true; // 默认返回 true + } + }, // *** 确保这里有逗号 *** + + async setShowQuickCommandTags(enabled: boolean): Promise { + console.log(`[Service] setShowQuickCommandTags called with: ${enabled}`); + try { + const valueStr = String(enabled); + console.log(`[Service] Attempting to save setting. Key: ${SHOW_QUICK_COMMAND_TAGS_KEY}, Value: ${valueStr}`); + await settingsRepository.setSetting(SHOW_QUICK_COMMAND_TAGS_KEY, valueStr); + console.log(`[Service] Successfully saved setting for key: ${SHOW_QUICK_COMMAND_TAGS_KEY}`); + } catch (error) { + console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, error); + throw new Error('Failed to save show quick command tags setting.'); + } } // <-- No comma after the last method }; // <-- End of settingsService object definition diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts index f1fa468..2c417fd 100644 --- a/packages/backend/src/settings/settings.controller.ts +++ b/packages/backend/src/settings/settings.controller.ts @@ -417,6 +417,86 @@ async setCaptchaConfig(req: Request, res: Response): Promise { res.status(500).json({ message: '设置 CAPTCHA 配置失败', error: error.message }); } } -} +}, // <-- Add comma here + + // --- Show Connection Tags --- + async getShowConnectionTags(req: Request, res: Response): Promise { + try { + console.log('[控制器] 收到获取“显示连接标签”设置的请求。'); + const isEnabled = await settingsService.getShowConnectionTags(); + console.log(`[控制器] 向客户端发送“显示连接标签”设置: ${isEnabled}`); + res.json({ enabled: isEnabled }); + } catch (error: any) { + console.error('[控制器] 获取“显示连接标签”设置时出错:', error); + res.status(500).json({ message: '获取“显示连接标签”设置失败', error: error.message }); + } + }, // *** 确保这里有逗号 *** + + async setShowConnectionTags(req: Request, res: Response): Promise { + console.log('[控制器] 收到设置“显示连接标签”设置的请求。'); + try { + const { enabled } = req.body; + console.log('[控制器] 请求体 enabled:', enabled); + + if (typeof enabled !== 'boolean') { + console.warn('[控制器] 收到无效的 enabled 格式:', enabled); + res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' }); + return; + } + + console.log('[控制器] 调用 settingsService.setShowConnectionTags...'); + await settingsService.setShowConnectionTags(enabled); + console.log('[控制器] settingsService.setShowConnectionTags 成功完成。'); + + auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] }); + notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] }); + + console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '“显示连接标签”设置已成功更新' }); + } catch (error: any) { + console.error('[控制器] 设置“显示连接标签”时出错:', error); + res.status(500).json({ message: '设置“显示连接标签”失败', error: error.message }); + } + }, // *** 确保这里有逗号 *** + + // --- Show Quick Command Tags --- + async getShowQuickCommandTags(req: Request, res: Response): Promise { + try { + console.log('[控制器] 收到获取“显示快捷指令标签”设置的请求。'); + const isEnabled = await settingsService.getShowQuickCommandTags(); + console.log(`[控制器] 向客户端发送“显示快捷指令标签”设置: ${isEnabled}`); + res.json({ enabled: isEnabled }); + } catch (error: any) { + console.error('[控制器] 获取“显示快捷指令标签”设置时出错:', error); + res.status(500).json({ message: '获取“显示快捷指令标签”设置失败', error: error.message }); + } + }, // *** 确保这里有逗号 *** + + async setShowQuickCommandTags(req: Request, res: Response): Promise { + console.log('[控制器] 收到设置“显示快捷指令标签”设置的请求。'); + try { + const { enabled } = req.body; + console.log('[控制器] 请求体 enabled:', enabled); + + if (typeof enabled !== 'boolean') { + console.warn('[控制器] 收到无效的 enabled 格式:', enabled); + res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' }); + return; + } + + console.log('[控制器] 调用 settingsService.setShowQuickCommandTags...'); + await settingsService.setShowQuickCommandTags(enabled); + console.log('[控制器] settingsService.setShowQuickCommandTags 成功完成。'); + + auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] }); + notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] }); + + console.log('[控制器] 发送成功响应。'); + res.status(200).json({ message: '“显示快捷指令标签”设置已成功更新' }); + } catch (error: any) { + console.error('[控制器] 设置“显示快捷指令标签”时出错:', error); + res.status(500).json({ message: '设置“显示快捷指令标签”失败', error: error.message }); + } + } // <-- No comma after the last method }; diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts index 2527f8a..91cf9a0 100644 --- a/packages/backend/src/settings/settings.routes.ts +++ b/packages/backend/src/settings/settings.routes.ts @@ -53,6 +53,18 @@ router.get('/sidebar', settingsController.getSidebarConfig); // PUT /api/v1/settings/sidebar - 更新侧栏配置 router.put('/sidebar', settingsController.setSidebarConfig); +// +++ 新增:显示连接标签路由 +++ +// GET /api/v1/settings/show-connection-tags - 获取设置 +router.get('/show-connection-tags', settingsController.getShowConnectionTags); +// PUT /api/v1/settings/show-connection-tags - 更新设置 +router.put('/show-connection-tags', settingsController.setShowConnectionTags); + +// +++ 新增:显示快捷指令标签路由 +++ +// GET /api/v1/settings/show-quick-command-tags - 获取设置 +router.get('/show-quick-command-tags', settingsController.getShowQuickCommandTags); +// PUT /api/v1/settings/show-quick-command-tags - 更新设置 +router.put('/show-quick-command-tags', settingsController.setShowQuickCommandTags); + export default router; diff --git a/packages/frontend/src/components/WorkspaceConnectionList.vue b/packages/frontend/src/components/WorkspaceConnectionList.vue index 3386692..4f4a711 100644 --- a/packages/frontend/src/components/WorkspaceConnectionList.vue +++ b/packages/frontend/src/components/WorkspaceConnectionList.vue @@ -9,6 +9,7 @@ import { useTagsStore, TagInfo } from '../stores/tags.store'; // 确保 TagInfo import { useSessionStore } from '../stores/session.store'; import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // +++ 修正导入大小写 +++ +import { useSettingsStore } from '../stores/settings.store'; // 新增:导入设置 store // 定义事件 const emit = defineEmits([ @@ -26,9 +27,11 @@ const tagsStore = useTagsStore(); const sessionStore = useSessionStore(); // 获取 session store 实例 const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++ const uiNotificationsStore = useUiNotificationsStore(); // +++ 修正实例化大小写 +++ +const settingsStore = useSettingsStore(); // 新增:实例化设置 store const { connections, isLoading: connectionsLoading, error: connectionsError } = storeToRefs(connectionsStore); const { tags, isLoading: tagsLoading, error: tagsError } = storeToRefs(tagsStore); +const { showConnectionTagsBoolean } = storeToRefs(settingsStore); // 新增:获取设置项 // 搜索词 const searchTerm = ref(''); @@ -73,17 +76,25 @@ const highlightedIndex = ref(-1); // -1 表示没有高亮项 const listAreaRef = ref(null); // 列表容器的 ref // 计算属性:扁平化的、当前可见的连接列表(用于键盘导航) +// 注意:这个 flatVisibleConnections 依赖于 filteredAndGroupedConnections 和 expandedGroups +// 当 showConnectionTagsBoolean 为 false 时,它不会被直接使用,但键盘导航逻辑依赖它 const flatVisibleConnections = computed(() => { const flatList: ConnectionInfo[] = []; - filteredAndGroupedConnections.value.forEach(group => { - // 只添加展开分组中的连接 - if (expandedGroups.value[group.groupName]) { - flatList.push(...group.connections); - } - }); + // 如果显示标签,则只包含展开分组的连接 + if (showConnectionTagsBoolean.value) { + filteredAndGroupedConnections.value.forEach(group => { + if (expandedGroups.value[group.groupName]) { + flatList.push(...group.connections); + } + }); + } else { + // 如果不显示标签,则包含所有过滤后的连接 + flatList.push(...flatFilteredConnections.value); // 使用下面定义的 flatFilteredConnections + } return flatList; }); + // 计算属性:当前高亮连接的 ID const highlightedConnectionId = computed(() => { if (highlightedIndex.value >= 0 && highlightedIndex.value < flatVisibleConnections.value.length) { @@ -110,8 +121,7 @@ const setTagInputRef = (el: any, id: string | number) => { } }; -// 计算属性:过滤并按标签分组连接 -// 需要修改 filteredAndGroupedConnections,使其包含 tagId +// 计算属性:过滤并按标签分组连接 (仅在 showConnectionTagsBoolean 为 true 时使用) const filteredAndGroupedConnections = computed(() => { const groups: Record = {}; // 修改:添加 tagId const untagged: ConnectionInfo[] = []; @@ -128,7 +138,7 @@ const filteredAndGroupedConnections = computed(() => { if (conn.host.toLowerCase().includes(lowerSearchTerm)) { return true; } - // Check associated tag names + // Check associated tag names (Always check tags for filtering, regardless of display setting) 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 @@ -151,21 +161,27 @@ const filteredAndGroupedConnections = computed(() => { const groupName = tag.name; if (!groups[groupName]) { groups[groupName] = { connections: [], tagId: tag.id }; // 修改:存储 tagId + // Initialize expanded state only if not already set if (expandedGroups.value[groupName] === undefined) { - expandedGroups.value[groupName] = true; + expandedGroups.value[groupName] = true; // Default to expanded } } + // Avoid duplicates if a connection has multiple tags matching the search if (!groups[groupName].connections.some(c => c.id === conn.id)) { groups[groupName].connections.push(conn); } tagged = true; } }); - if (!tagged) { + // If none of the tags were found in the tagMap (e.g., stale data), treat as untagged + if (!tagged && !untagged.some(c => c.id === conn.id)) { untagged.push(conn); } } else { - untagged.push(conn); + // Ensure untagged connections are not duplicated + if (!untagged.some(c => c.id === conn.id)) { + untagged.push(conn); + } } }); @@ -185,8 +201,9 @@ const filteredAndGroupedConnections = computed(() => { if (untagged.length > 0) { const untaggedGroupName = t('workspaceConnectionList.untagged'); + // Initialize expanded state only if not already set if (expandedGroups.value[untaggedGroupName] === undefined) { - expandedGroups.value[untaggedGroupName] = true; + expandedGroups.value[untaggedGroupName] = true; // Default to expanded } // 未标记的分组没有 tagId result.push({ groupName: untaggedGroupName, connections: untagged, tagId: null }); @@ -195,24 +212,65 @@ const filteredAndGroupedConnections = computed(() => { return result; }); +// 新增:计算属性,仅过滤,不分组 (用于 showConnectionTagsBoolean 为 false 时) +const flatFilteredConnections = computed(() => { + const lowerSearchTerm = searchTerm.value.toLowerCase(); + const tagMap = new Map(tags.value.map(tag => [tag.id, tag.name])); // 创建 tagMap 用于搜索 + + const filtered = connections.value.filter(conn => { + // Check connection name + if (conn.name && conn.name.toLowerCase().includes(lowerSearchTerm)) { + return true; + } + // Check connection host + if (conn.host.toLowerCase().includes(lowerSearchTerm)) { + return true; + } + // Check associated tag names (Always check tags for filtering) + if (conn.tag_ids && conn.tag_ids.length > 0) { + for (const tagId of conn.tag_ids) { + const tagName = tagMap.get(tagId); + if (tagName && tagName.toLowerCase().includes(lowerSearchTerm)) { + return true; // Match found in tag name + } + } + } + // No match found + return false; + }); + + // Sort the flat list + return filtered.sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host)); +}); + + // +++ 监听分组状态变化并保存到 localStorage +++ watch(expandedGroups, (newState) => { - try { - localStorage.setItem(EXPANDED_GROUPS_STORAGE_KEY, JSON.stringify(newState)); - } catch (e) { - console.error('Failed to save expanded groups state to localStorage:', e); + // Only save if tags are shown + if (showConnectionTagsBoolean.value) { + try { + localStorage.setItem(EXPANDED_GROUPS_STORAGE_KEY, JSON.stringify(newState)); + } catch (e) { + console.error('Failed to save expanded groups state to localStorage:', e); + } } }, { deep: true }); - + // 监听搜索词变化,重置高亮索引 watch(searchTerm, () => { highlightedIndex.value = -1; }); - + // 监听分组展开状态变化,重置高亮索引 (这个 watch 保留,用于重置高亮) watch(expandedGroups, () => { highlightedIndex.value = -1; }, { deep: true }); + + // 监听显示模式变化,重置高亮索引 + watch(showConnectionTagsBoolean, () => { + highlightedIndex.value = -1; + }); + // +++ 监听编辑状态,自动聚焦输入框 +++ watch(editingTagId, async (newId) => { if (newId !== null) { @@ -226,13 +284,13 @@ const filteredAndGroupedConnections = computed(() => { } } }); - + // 切换分组展开/折叠 const toggleGroup = (groupName: string) => { // 状态现在总是 boolean,直接切换 expandedGroups.value[groupName] = !expandedGroups.value[groupName]; }; - + // 处理单击连接 (左键/Enter) - 使用 session store 处理连接请求 const handleConnect = (connectionId: number, event?: MouseEvent | KeyboardEvent) => { if (event instanceof MouseEvent && event.button !== 0) { @@ -364,6 +422,8 @@ onMounted(() => { unregisterFocusAction = focusSwitcherStore.registerFocusAction('connectionListSearch', focusSearchInput); connectionsStore.fetchConnections(); // 移到 onMounted tagsStore.fetchTags(); // 移到 onMounted + // Load initial expanded state after fetching tags/connections + expandedGroups.value = loadInitialExpandedGroups(); }); onBeforeUnmount(() => { @@ -390,7 +450,7 @@ defineExpose({ focusSearchInput }); // --- 键盘导航和确认 --- const handleKeyDown = (event: KeyboardEvent) => { - const list = flatVisibleConnections.value; + const list = flatVisibleConnections.value; // Always navigate the potentially flat list if (!list.length) return; switch (event.key) { @@ -418,7 +478,8 @@ const scrollToHighlighted = async () => { await nextTick(); // 等待 DOM 更新 if (!listAreaRef.value || highlightedConnectionId.value === null) return; - const highlightedElement = listAreaRef.value.querySelector(`.connection-item[data-conn-id="${highlightedConnectionId.value}"]`); + // Query selector needs to work for both grouped and flat lists + const highlightedElement = listAreaRef.value.querySelector(`li[data-conn-id="${highlightedConnectionId.value}"]`); if (highlightedElement) { highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } @@ -547,7 +608,7 @@ const cancelEditingTag = () => { @blur="handleBlur" /> - +
-
- -
+
+
+ +
{ +
+ +
    +
  • + + + {{ conn.name || conn.host }} + +
  • +
diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 35cb911..ab9b734 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -625,7 +625,13 @@ }, "error": { "sidebarPersistentSaveFailed": "Failed to save sidebar setting." - } + }, + "showConnectionTagsTitle": "Show Connection Tags", + "showConnectionTagsLabel": "Show tags in connection list", + "showConnectionTagsDescription": "Disable to hide tags in the connection list and exclude them from search.", + "showQuickCommandTagsTitle": "Show Quick Command Tags", + "showQuickCommandTagsLabel": "Show tags in quick command list", + "showQuickCommandTagsDescription": "Disable to hide tags in the quick command list and exclude them from search." }, "ipBlacklist": { "title": "IP Blacklist Management", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 8a9d6ad..aed7195 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -857,7 +857,13 @@ "success": { "sidebarPersistentSaved": "サイドバーの設定を保存しました。" }, - "title": "ワークスペースとターミナル" + "title": "ワークスペースとターミナル", + "showConnectionTagsTitle": "接続タグを表示", + "showConnectionTagsLabel": "接続リストにタグを表示", + "showConnectionTagsDescription": "無効にすると、接続リストのタグが非表示になり、検索から除外されます。", + "showQuickCommandTagsTitle": "クイックコマンドタグを表示", + "showQuickCommandTagsLabel": "クイックコマンドリストにタグを表示", + "showQuickCommandTagsDescription": "無効にすると、クイックコマンドリストのタグが非表示になり、検索から除外されます。" }, "about": { "version": "バージョン", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 9bf4137..da4e3ab 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -625,7 +625,13 @@ }, "error": { "sidebarPersistentSaveFailed": "保存侧边栏设置失败。" - } + }, + "showConnectionTagsTitle": "显示连接标签", + "showConnectionTagsLabel": "在连接列表中显示标签", + "showConnectionTagsDescription": "关闭后将隐藏连接列表中的标签,并从搜索中排除标签。", + "showQuickCommandTagsTitle": "显示快捷指令标签", + "showQuickCommandTagsLabel": "在快捷指令列表中显示标签", + "showQuickCommandTagsDescription": "关闭后将隐藏快捷指令列表中的标签,并从搜索中排除标签。" }, "ipBlacklist": { "title": "IP 黑名单管理", diff --git a/packages/frontend/src/stores/settings.store.ts b/packages/frontend/src/stores/settings.store.ts index 76344fe..9318cb0 100644 --- a/packages/frontend/src/stores/settings.store.ts +++ b/packages/frontend/src/stores/settings.store.ts @@ -53,6 +53,8 @@ interface SettingsState { ipBlacklistEnabled?: string; dashboardSortBy?: SortField; dashboardSortOrder?: SortOrder; + showConnectionTags?: string; // 'true' or 'false' + showQuickCommandTags?: string; // 'true' or 'false' [key: string]: string | undefined; } @@ -82,8 +84,23 @@ export const useSettingsStore = defineStore('settings', () => { try { console.log('[SettingsStore] 加载通用设置...'); - const response = await apiClient.get>('/settings'); // 使用 apiClient - settings.value = response.data; // Store fetched general settings + // Fetch all settings, including the new ones + const [ + generalSettingsResponse, + showConnectionTagsResponse, + showQuickCommandTagsResponse + ] = await Promise.all([ + apiClient.get>('/settings'), + apiClient.get<{ enabled: boolean }>('/settings/show-connection-tags'), + apiClient.get<{ enabled: boolean }>('/settings/show-quick-command-tags') + ]); + + settings.value = generalSettingsResponse.data; // Store fetched general settings + + // Store the specific boolean settings + settings.value.showConnectionTags = String(showConnectionTagsResponse.data.enabled); + settings.value.showQuickCommandTags = String(showQuickCommandTagsResponse.data.enabled); + // --- 更详细的日志 --- console.log('[SettingsStore] Fetched settings from backend:', JSON.stringify(settings.value)); @@ -238,6 +255,15 @@ export const useSettingsStore = defineStore('settings', () => { settings.value.dashboardSortOrder = 'desc'; } + // NEW: Tag visibility defaults + if (settings.value.showConnectionTags === undefined) { + settings.value.showConnectionTags = 'true'; // 默认显示 + } + if (settings.value.showQuickCommandTags === undefined) { + settings.value.showQuickCommandTags = 'true'; // 默认显示 + } + + // --- 语言设置 --- const langFromSettings = settings.value.language; console.log(`[SettingsStore] Language from fetched settings: ${langFromSettings}`); // <-- 添加日志 @@ -299,10 +325,11 @@ export const useSettingsStore = defineStore('settings', () => { /** * Updates a single general setting value both locally and on the backend. + * Uses specific endpoints for boolean settings where available. * @param key The setting key to update. - * @param value The new value for the setting. + * @param value The new value for the setting (string for general, boolean for specific). */ - async function updateSetting(key: keyof SettingsState, value: string) { + async function updateSetting(key: keyof SettingsState, value: string | boolean) { // 移除外观相关的键检查 const allowedKeys: Array = [ 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', @@ -319,26 +346,48 @@ export const useSettingsStore = defineStore('settings', () => { 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键 'ipBlacklistEnabled', 'dashboardSortBy', - 'dashboardSortOrder' + 'dashboardSortOrder', + 'showConnectionTags', // NEW + 'showQuickCommandTags' // NEW ]; if (!allowedKeys.includes(key)) { console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`); throw new Error(`不允许更新设置项 '${key}'`); } + // Use specific endpoints for boolean settings + const booleanEndpoints: Partial> = { + showConnectionTags: '/settings/show-connection-tags', + showQuickCommandTags: '/settings/show-quick-command-tags', + autoCopyOnSelect: '/settings/auto-copy-on-select', + // Add other boolean settings with specific endpoints here if needed + }; + try { - console.log(`[SettingsStore] Attempting to update setting - Key: ${key}, Value: ${value}`); // +++ Add log +++ - // 注意:后端 controller 现在会过滤,但前端也做一层检查更好 - const payload = { [key]: value }; - console.log('[SettingsStore] Sending PUT request to /settings with payload:', payload); // +++ Add log +++ - await apiClient.put('/settings', payload); // 使用 apiClient - console.log(`[SettingsStore] Successfully updated setting via API - Key: ${key}`); // +++ Add log +++ - // Update store state *after* successful API call - settings.value = { ...settings.value, [key]: value }; + let apiPromise: Promise; + const endpoint = booleanEndpoints[key]; + + if (endpoint && typeof value === 'boolean') { + console.log(`[SettingsStore] Attempting to update boolean setting via specific endpoint - Key: ${key}, Value: ${value}, Endpoint: ${endpoint}`); + apiPromise = apiClient.put(endpoint, { enabled: value }); + } else if (typeof value === 'string') { + console.log(`[SettingsStore] Attempting to update general setting - Key: ${key}, Value: ${value}`); + const payload = { [key]: value }; + console.log('[SettingsStore] Sending PUT request to /settings with payload:', payload); + apiPromise = apiClient.put('/settings', payload); + } else { + throw new Error(`Invalid value type for setting '${key}': expected boolean for specific endpoint or string for general.`); + } + + await apiPromise; + console.log(`[SettingsStore] Successfully updated setting via API - Key: ${key}`); + + // Update store state *after* successful API call + settings.value = { ...settings.value, [key]: String(value) }; // Store as string internally // If updating language, check if it's valid and update i18n - if (key === 'language' && availableLocales.includes(value)) { - console.log(`[SettingsStore] updateSetting: Language updated to ${value}. Calling setLocale...`); // <-- 添加日志 + if (key === 'language' && typeof value === 'string' && availableLocales.includes(value)) { + console.log(`[SettingsStore] updateSetting: Language updated to ${value}. Calling setLocale...`); setLocale(value); } else if (key === 'language') { console.warn(`[SettingsStore] updateSetting: Attempted to set invalid language '${value}'. Ignoring i18n update.`); @@ -381,7 +430,9 @@ export const useSettingsStore = defineStore('settings', () => { 'rdpModalHeight', // NEW: 添加 RDP 模态框高度键 'ipBlacklistEnabled', 'dashboardSortBy', - 'dashboardSortOrder' + 'dashboardSortOrder', + 'showConnectionTags', // NEW + 'showQuickCommandTags' // NEW ]; const filteredUpdates: Partial = {}; let languageUpdate: string | undefined = undefined; @@ -435,7 +486,8 @@ export const useSettingsStore = defineStore('settings', () => { const newWidths = { ...parsedSidebarPaneWidths.value, [paneName]: width }; parsedSidebarPaneWidths.value = newWidths; // Update local reactive state first try { - await updateSetting('sidebarPaneWidths', JSON.stringify(newWidths)); + // Use updateMultipleSettings for consistency, even for one setting + await updateMultipleSettings({ sidebarPaneWidths: JSON.stringify(newWidths) }); } catch (error) { console.error(`[SettingsStore] Failed to save sidebarPaneWidths after updating ${paneName}:`, error); // Optionally revert local state or show error to user @@ -642,6 +694,15 @@ export const useSettingsStore = defineStore('settings', () => { const recaptchaSiteKey = computed(() => captchaSettings.value?.recaptchaSiteKey ?? ''); // DO NOT expose secret keys via getters + // NEW: Getters for tag visibility + const showConnectionTagsBoolean = computed(() => { + return settings.value.showConnectionTags !== 'false'; // Default to true + }); + const showQuickCommandTagsBoolean = computed(() => { + return settings.value.showQuickCommandTags !== 'false'; // Default to true + }); + + return { settings, // 只包含通用设置 isLoading, @@ -677,5 +738,8 @@ export const useSettingsStore = defineStore('settings', () => { dashboardSortBy, dashboardSortOrder, saveDashboardSortPreference, + // NEW: Expose tag visibility getters + showConnectionTagsBoolean, + showQuickCommandTagsBoolean, }; }); diff --git a/packages/frontend/src/views/QuickCommandsView.vue b/packages/frontend/src/views/QuickCommandsView.vue index dfa1e45..514e789 100644 --- a/packages/frontend/src/views/QuickCommandsView.vue +++ b/packages/frontend/src/views/QuickCommandsView.vue @@ -26,88 +26,123 @@
- -
+ +

{{ t('common.loading', '加载中...') }}

- -
+ +

{{ $t('quickCommands.empty', '没有快捷指令。') }}

- + +
+ +

{{ t('quickCommands.noResults', '没有找到匹配的指令') }} "{{ searchTerm }}"

+
+ +
-
- - -
- - - - - - {{ groupData.groupName }} - - - -
- -
    -
  • - -
    - {{ cmd.name }} - {{ cmd.command }} -
    - -
    - {{ cmd.usage_count }} - - -
    -
  • -
-
+ +
+
+ +
+ + + + + + {{ groupData.groupName }} + + + +
+ +
    +
  • + +
    + {{ cmd.name }} + {{ cmd.command }} +
    + +
    + {{ cmd.usage_count }} + + +
    +
  • +
+
+
+ +
    +
  • + +
    + {{ cmd.name }} + {{ cmd.command }} +
    + +
    + {{ cmd.usage_count }} + + +
    +
  • +
@@ -130,12 +165,14 @@ import { useUiNotificationsStore } from '../stores/uiNotifications.store'; import { useI18n } from 'vue-i18n'; import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; // 导入表单组件 import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++ +import { useSettingsStore } from '../stores/settings.store'; // 新增:导入设置 store const quickCommandsStore = useQuickCommandsStore(); const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate the new tag store +++ const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通知 const { t } = useI18n(); const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++ +const settingsStore = useSettingsStore(); // 新增:实例化设置 store const hoveredItemId = ref(null); const isFormVisible = ref(false); @@ -159,9 +196,17 @@ const isLoading = computed(() => quickCommandsStore.isLoading); // selectedIndex now refers to the index within the flatVisibleCommands list // Also get expandedGroups reactively for the template const { selectedIndex: storeSelectedIndex, flatVisibleCommands, expandedGroups } = storeToRefs(quickCommandsStore); +const { showQuickCommandTagsBoolean } = storeToRefs(settingsStore); // 新增:获取设置项 + +// 新增:计算属性,仅过滤和排序,不分组 +const flatFilteredCommands = computed(() => { + // 直接使用 store 中的 flatVisibleCommands,因为它已经处理了过滤和排序 + return quickCommandsStore.flatVisibleCommands; +}); // --- Helper function for selection check --- const isCommandSelected = (commandId: number): boolean => { + // 使用 store 的 flatVisibleCommands 和 storeSelectedIndex if (storeSelectedIndex.value < 0 || !flatVisibleCommands.value[storeSelectedIndex.value]) { return false; } @@ -181,8 +226,18 @@ onMounted(async () => { // Make onMounted async await quickCommandsStore.fetchQuickCommands(); // Also fetch the quick command tags using the correct store instance await quickCommandTagsStore.fetchTags(); + // +++ 注册自定义聚焦动作 +++ + unregisterFocus = focusSwitcherStore.registerFocusAction('quickCommandsSearch', focusSearchInput); }); +onBeforeUnmount(() => { + // +++ 调用保存的注销函数 +++ + if (unregisterFocus) { + unregisterFocus(); + } +}); + + // +++ Watcher to focus input when editing starts +++ watch(editingTagId, async (newId) => { if (newId !== null) { @@ -197,18 +252,11 @@ watch(editingTagId, async (newId) => { } }); +// 新增:监听显示模式变化,重置选择 +watch(showQuickCommandTagsBoolean, () => { + quickCommandsStore.resetSelection(); +}); -// +++ 注册/注销自定义聚焦动作 +++ -onMounted(() => { - // +++ 保存返回的注销函数 +++ - unregisterFocus = focusSwitcherStore.registerFocusAction('quickCommandsSearch', focusSearchInput); -}); -onBeforeUnmount(() => { - // +++ 调用保存的注销函数 +++ - if (unregisterFocus) { - unregisterFocus(); - } -}); // --- 事件处理 --- @@ -221,12 +269,13 @@ const updateSearchTerm = (event: Event) => { // +++ 重构滚动逻辑 +++ const scrollToSelected = async (index: number) => { await nextTick(); // 等待 DOM 更新 + // 使用 store 的 flatVisibleCommands if (index < 0 || !commandListContainerRef.value || !flatVisibleCommands.value[index]) return; const selectedCommandId = flatVisibleCommands.value[index].id; const listContainer = commandListContainerRef.value; - // Find the element using the data attribute + // Find the element using the data attribute (works for both views) const selectedElement = listContainer.querySelector(`li[data-command-id="${selectedCommandId}"]`) as HTMLLIElement; if (selectedElement) { @@ -244,9 +293,9 @@ watch(storeSelectedIndex, (newIndex) => { scrollToSelected(newIndex); }); -// Keyboard navigation now operates on the flat visible list +// Keyboard navigation now operates on the flat visible list from the store const handleSearchInputKeydown = (event: KeyboardEvent) => { - // Use flatVisibleCommands for navigation logic + // 使用 store 的 flatVisibleCommands const commands = flatVisibleCommands.value; if (!commands.length) return; @@ -263,6 +312,7 @@ const handleSearchInputKeydown = (event: KeyboardEvent) => { break; case 'Enter': event.preventDefault(); + // 使用 store 的 storeSelectedIndex if (storeSelectedIndex.value >= 0 && storeSelectedIndex.value < commands.length) { executeCommand(commands[storeSelectedIndex.value]); } @@ -294,6 +344,7 @@ const toggleGroup = (groupName: string) => { // 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 + // 使用 store 的 flatVisibleCommands 和 storeSelectedIndex const selectedCmdId = storeSelectedIndex.value >= 0 && flatVisibleCommands.value[storeSelectedIndex.value] ? flatVisibleCommands.value[storeSelectedIndex.value].id : null; @@ -476,4 +527,3 @@ const cancelEditingTag = () => { }; - diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index 1dc63c8..e68e8f4 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -444,6 +444,46 @@
+
+ +
+

{{ $t('settings.workspace.showConnectionTagsTitle', '显示连接标签') }}

+
+
+ + +
+

{{ $t('settings.workspace.showConnectionTagsDescription', '关闭后将隐藏连接列表中的标签,并从搜索中排除标签。') }}

+
+ +

{{ showConnectionTagsMessage }}

+
+
+
+
+ +
+

{{ $t('settings.workspace.showQuickCommandTagsTitle', '显示快捷指令标签') }}

+
+
+ + +
+

{{ $t('settings.workspace.showQuickCommandTagsDescription', '关闭后将隐藏快捷指令列表中的标签,并从搜索中排除标签。') }}

+
+ +

{{ showQuickCommandTagsMessage }}

+
+
+
@@ -624,6 +664,8 @@ const { captchaSettings, // <-- Import CAPTCHA settings state commandInputSyncTarget, // NEW: Import command input sync target getter ipBlacklistEnabledBoolean, // <-- Import IP Blacklist enabled getter + showConnectionTagsBoolean, // NEW: Import connection tag visibility getter + showQuickCommandTagsBoolean, // NEW: Import quick command tag visibility getter } = storeToRefs(settingsStore); // Removed Passkey state import from authStore @@ -648,6 +690,8 @@ const popupEditorEnabled = ref(true); // 本地状态,用于 v-model const workspaceSidebarPersistentEnabled = ref(false); // 新增:侧边栏固定设置的本地状态 const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none'); // NEW: Local state for command input sync target const ipBlacklistEnabled = ref(true); // <-- Local state for IP Blacklist switch +const showConnectionTagsLocal = ref(true); // NEW: Local state for connection tags switch +const showQuickCommandTagsLocal = ref(true); // NEW: Local state for quick command tags switch // --- Local UI feedback state --- const ipWhitelistLoading = ref(false); @@ -692,6 +736,12 @@ const selectedTimezone = ref('UTC'); // 本地状态,用于时区 v-model const timezoneLoading = ref(false); const timezoneMessage = ref(''); const timezoneSuccess = ref(false); +const showConnectionTagsLoading = ref(false); // NEW +const showConnectionTagsMessage = ref(''); // NEW +const showConnectionTagsSuccess = ref(false); // NEW +const showQuickCommandTagsLoading = ref(false); // NEW +const showQuickCommandTagsMessage = ref(''); // NEW +const showQuickCommandTagsSuccess = ref(false); // NEW // CAPTCHA Form State const captchaForm = reactive({ // Use reactive for the form object enabled: false, @@ -740,6 +790,8 @@ watch(settings, (newSettings, oldSettings) => { commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target selectedTimezone.value = newSettings.timezone || 'UTC'; // 同步时区设置 ipBlacklistEnabled.value = ipBlacklistEnabledBoolean.value; // <-- Sync IP Blacklist enabled state + showConnectionTagsLocal.value = showConnectionTagsBoolean.value; // NEW: Sync connection tags state + showQuickCommandTagsLocal.value = showQuickCommandTagsBoolean.value; // NEW: Sync quick command tags state }, { deep: true, immediate: true }); // immediate: true to run on initial load @@ -928,6 +980,46 @@ const handleUpdateTimezone = async () => { } }; +// --- Show Connection Tags setting method --- +const handleUpdateShowConnectionTags = async () => { + showConnectionTagsLoading.value = true; + showConnectionTagsMessage.value = ''; + showConnectionTagsSuccess.value = false; + try { + await settingsStore.updateSetting('showConnectionTags', showConnectionTagsLocal.value); + showConnectionTagsMessage.value = t('settings.workspace.success.showConnectionTagsSaved', '连接标签显示设置已保存'); // 需要添加翻译 + showConnectionTagsSuccess.value = true; + } catch (error: any) { + console.error('更新显示连接标签设置失败:', error); + showConnectionTagsMessage.value = error.message || t('settings.workspace.error.showConnectionTagsSaveFailed', '保存连接标签显示设置失败'); // 需要添加翻译 + showConnectionTagsSuccess.value = false; + // No need to revert local state on failure with explicit save button + } finally { + showConnectionTagsLoading.value = false; + // Keep message visible until next save attempt + } +}; + +// --- Show Quick Command Tags setting method --- +const handleUpdateShowQuickCommandTags = async () => { + showQuickCommandTagsLoading.value = true; + showQuickCommandTagsMessage.value = ''; + showQuickCommandTagsSuccess.value = false; + try { + await settingsStore.updateSetting('showQuickCommandTags', showQuickCommandTagsLocal.value); + showQuickCommandTagsMessage.value = t('settings.workspace.success.showQuickCommandTagsSaved', '快捷指令标签显示设置已保存'); // 需要添加翻译 + showQuickCommandTagsSuccess.value = true; + } catch (error: any) { + console.error('更新显示快捷指令标签设置失败:', error); + showQuickCommandTagsMessage.value = error.message || t('settings.workspace.error.showQuickCommandTagsSaveFailed', '保存快捷指令标签显示设置失败'); // 需要添加翻译 + showQuickCommandTagsSuccess.value = false; + // No need to revert local state on failure with explicit save button + } finally { + showQuickCommandTagsLoading.value = false; + // Keep message visible until next save attempt + } +}; + // --- 外观设置 --- const openStyleCustomizer = () => { appearanceStore.toggleStyleCustomizer(true);