diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index acbb3f6..6ffc6a9 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -1,15 +1,16 @@ import { settingsRepository, Setting, getSidebarConfig as getSidebarConfigFromRepo, setSidebarConfig as setSidebarConfigInRepo } from '../repositories/settings.repository'; // Import specific repo functions import { SidebarConfig, PaneName, UpdateSidebarConfigDto } from '../types/settings.types'; -// +++ 定义焦点切换配置项接口 (与前端 store 保持一致) +++ -interface ConfigurableFocusableItem { - id: string; +// +++ 定义焦点切换完整配置接口 (与前端 store 保持一致) +++ +interface FocusItemConfig { // 单个项目的配置 shortcut?: string; } +interface FocusSwitcherFullConfig { // 完整配置结构 + sequence: string[]; + shortcuts: Record; +} -// --- 移除旧的默认字符串数组 --- -// const DEFAULT_FOCUS_SEQUENCE = ["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]; -const FOCUS_SEQUENCE_KEY = 'focusSwitcherSequence'; // 焦点切换顺序设置键 +const FOCUS_SEQUENCE_KEY = 'focusSwitcherSequence'; // 设置键保持不变 const NAV_BAR_VISIBLE_KEY = 'navBarVisible'; // 导航栏可见性设置键 const LAYOUT_TREE_KEY = 'layoutTree'; // 布局树设置键 const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制设置键 @@ -95,52 +96,60 @@ export const settingsService = { /** * 获取焦点切换顺序 - * @returns 返回存储的焦点切换顺序数组,如果未设置或无效则返回默认顺序 + * @returns 返回存储的完整焦点切换配置对象,如果未设置或无效则返回默认空配置 */ - async getFocusSwitcherSequence(): Promise { // +++ 更新返回类型 +++ + async getFocusSwitcherSequence(): Promise { // +++ 更新返回类型 +++ console.log(`[Service] Attempting to get setting for key: ${FOCUS_SEQUENCE_KEY}`); + const defaultConfig: FocusSwitcherFullConfig = { sequence: [], shortcuts: {} }; // 默认值 try { const configJson = await settingsRepository.getSetting(FOCUS_SEQUENCE_KEY); console.log(`[Service] Raw value from repository for ${FOCUS_SEQUENCE_KEY}:`, configJson); if (configJson) { const config = JSON.parse(configJson); - // +++ 验证新的数据结构 +++ - if (Array.isArray(config) && config.every(item => - typeof item === 'object' && item !== null && typeof item.id === 'string' && - (item.shortcut === undefined || typeof item.shortcut === 'string') - )) { - console.log('[Service] Fetched and validated focus switcher config:', JSON.stringify(config)); - return config as ConfigurableFocusableItem[]; + // +++ 验证 FocusSwitcherFullConfig 结构 +++ + if ( + typeof config === 'object' && config !== null && + Array.isArray(config.sequence) && config.sequence.every((item: any) => typeof item === 'string') && + typeof config.shortcuts === 'object' && config.shortcuts !== null && + Object.values(config.shortcuts).every((sc: any) => typeof sc === 'object' && sc !== null && (sc.shortcut === undefined || typeof sc.shortcut === 'string')) + ) { + console.log('[Service] Fetched and validated full focus switcher config:', JSON.stringify(config)); + // TODO: 可能需要进一步验证 sequence 中的 id 是否仍然有效 (存在于某个地方定义的可用 ID 列表) + // TODO: 可能需要进一步验证 shortcuts 中的 key 是否是有效的 ID + return config as FocusSwitcherFullConfig; } else { - console.warn('[Service] Invalid focus switcher config format found in settings. Returning empty array.'); + console.warn('[Service] Invalid full focus switcher config format found in settings. Returning default.'); } } else { - console.log('[Service] No focus switcher config found in settings. Returning empty array.'); + console.log('[Service] No focus switcher config found in settings. Returning default.'); } } catch (error) { - console.error(`[Service] Error parsing focus switcher config from settings (key: ${FOCUS_SEQUENCE_KEY}):`, error); + console.error(`[Service] Error parsing full focus switcher config from settings (key: ${FOCUS_SEQUENCE_KEY}):`, error); } - // +++ 返回空数组作为默认值 +++ - console.log('[Service] Returning empty array as default focus config.'); - return []; + console.log('[Service] Returning default focus config:', JSON.stringify(defaultConfig)); + return defaultConfig; }, /** - * 设置焦点切换顺序 - * @param sequence 要保存的焦点切换顺序数组 + * 设置完整的焦点切换配置 + * @param fullConfig 包含 sequence 和 shortcuts 的完整配置对象 */ - async setFocusSwitcherSequence(config: ConfigurableFocusableItem[]): Promise { // +++ 更新参数类型 +++ - console.log('[Service] setFocusSwitcherSequence called with new config format:', JSON.stringify(config)); - // +++ 验证新的数据结构 (虽然控制器层已验证,服务层再次验证更健壮) +++ - if (!Array.isArray(config) || !config.every(item => - typeof item === 'object' && item !== null && typeof item.id === 'string' && - (item.shortcut === undefined || typeof item.shortcut === 'string') - )) { - console.error('[Service] Attempted to save invalid focus switcher config format:', config); - throw new Error('Invalid config format provided.'); - } + async setFocusSwitcherSequence(fullConfig: FocusSwitcherFullConfig): Promise { // +++ 更新参数类型 +++ + console.log('[Service] setFocusSwitcherSequence called with full config:', JSON.stringify(fullConfig)); + // +++ 验证 FocusSwitcherFullConfig 结构 (控制器层已做基本验证) +++ + if ( + !(typeof fullConfig === 'object' && fullConfig !== null && + Array.isArray(fullConfig.sequence) && fullConfig.sequence.every((item: any) => typeof item === 'string') && + typeof fullConfig.shortcuts === 'object' && fullConfig.shortcuts !== null && + Object.values(fullConfig.shortcuts).every((sc: any) => typeof sc === 'object' && sc !== null && (sc.shortcut === undefined || typeof sc.shortcut === 'string'))) + ) { + console.error('[Service] Attempted to save invalid full focus switcher config format:', fullConfig); + throw new Error('Invalid full config format provided.'); + } + // TODO: 可能需要进一步验证 sequence 中的 id 和 shortcuts 中的 key 是否有效 + try { - const configJson = JSON.stringify(config); // +++ 序列化新的结构 +++ + const configJson = JSON.stringify(fullConfig); // +++ 序列化完整结构 +++ console.log(`[Service] Attempting to save setting. Key: ${FOCUS_SEQUENCE_KEY}, Value: ${configJson}`); await settingsRepository.setSetting(FOCUS_SEQUENCE_KEY, configJson); console.log(`[Service] Successfully saved setting for key: ${FOCUS_SEQUENCE_KEY}`); diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts index 1285a1a..c803399 100644 --- a/packages/backend/src/settings/settings.controller.ts +++ b/packages/backend/src/settings/settings.controller.ts @@ -87,27 +87,29 @@ export const settingsController = { async setFocusSwitcherSequence(req: Request, res: Response): Promise { console.log('[Controller] Received request to set focus switcher sequence.'); try { - // +++ 修改:直接获取请求体作为配置数组 +++ - const focusConfig = req.body; - console.log('[Controller] Request body focusConfig:', JSON.stringify(focusConfig)); + // +++ 修改:获取请求体并验证其是否符合 FocusSwitcherFullConfig 结构 +++ + const fullConfig = req.body; + console.log('[Controller] Request body fullConfig:', JSON.stringify(fullConfig)); - // +++ 修改:验证新的数据结构 (ConfigurableFocusableItem[]) +++ - if (!Array.isArray(focusConfig) || !focusConfig.every(item => - typeof item === 'object' && item !== null && typeof item.id === 'string' && - (item.shortcut === undefined || typeof item.shortcut === 'string') - )) { - console.warn('[Controller] Invalid focus config format received:', focusConfig); - res.status(400).json({ message: '无效的请求体,必须是包含 id (string) 和可选 shortcut (string) 的对象数组' }); + // +++ 验证 FocusSwitcherFullConfig 结构 +++ + if ( + !(typeof fullConfig === 'object' && fullConfig !== null && + Array.isArray(fullConfig.sequence) && fullConfig.sequence.every((item: any) => typeof item === 'string') && + typeof fullConfig.shortcuts === 'object' && fullConfig.shortcuts !== null && + Object.values(fullConfig.shortcuts).every((sc: any) => typeof sc === 'object' && sc !== null && (sc.shortcut === undefined || typeof sc.shortcut === 'string'))) + ) { + console.warn('[Controller] Invalid full focus config format received:', fullConfig); + res.status(400).json({ message: '无效的请求体,必须是包含 sequence (string[]) 和 shortcuts (Record) 的对象' }); return; } - console.log('[Controller] Calling settingsService.setFocusSwitcherSequence with new config format...'); - // +++ 修改:传递完整的配置数组给服务层 +++ - await settingsService.setFocusSwitcherSequence(focusConfig); + console.log('[Controller] Calling settingsService.setFocusSwitcherSequence with validated full config...'); + // +++ 传递验证后的 fullConfig 给服务层 +++ + await settingsService.setFocusSwitcherSequence(fullConfig); console.log('[Controller] settingsService.setFocusSwitcherSequence completed successfully.'); console.log('[Controller] Logging audit action: FOCUS_SWITCHER_SEQUENCE_UPDATED'); - auditLogService.logAction('FOCUS_SWITCHER_SEQUENCE_UPDATED', { config: focusConfig }); // +++ 修改审计日志内容 +++ + auditLogService.logAction('FOCUS_SWITCHER_SEQUENCE_UPDATED', { config: fullConfig }); // +++ 修改审计日志内容 +++ console.log('[Controller] Sending success response.'); res.status(200).json({ message: '焦点切换顺序已成功更新' }); diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index f95d2a0..92af541 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -177,14 +177,14 @@ const handleGlobalKeyUp = async (event: KeyboardEvent) => { } } - const configuredItems = focusSwitcherStore.configuredItems; - if (configuredItems.length === 0) { + const order = focusSwitcherStore.sequenceOrder; // ++ 使用新的 sequenceOrder state ++ + if (order.length === 0) { // ++ 检查新的 state ++ console.log('[App] No focus sequence configured.'); return; } let focused = false; - for (let i = 0; i < configuredItems.length; i++) { + for (let i = 0; i < order.length; i++) { // ++ Use order.length for loop condition ++ const nextFocusId = focusSwitcherStore.getNextFocusTargetId(currentFocusId); if (!nextFocusId) { console.warn('[App] Could not determine next focus target ID in sequence.'); diff --git a/packages/frontend/src/components/FocusSwitcherConfigurator.vue b/packages/frontend/src/components/FocusSwitcherConfigurator.vue index 29cd53b..520d63a 100644 --- a/packages/frontend/src/components/FocusSwitcherConfigurator.vue +++ b/packages/frontend/src/components/FocusSwitcherConfigurator.vue @@ -2,11 +2,11 @@ import { ref, computed, watch, reactive, type Ref } from 'vue'; // 添加 Ref import { useI18n } from 'vue-i18n'; import draggable from 'vuedraggable'; -import { useFocusSwitcherStore, type FocusableInput, type ConfiguredFocusableInput } from '../stores/focusSwitcher.store'; // +++ 导入新接口 +++ +import { useFocusSwitcherStore, type FocusableInput, type FocusItemConfig, type FocusSwitcherFullConfig } from '../stores/focusSwitcher.store'; // ++ 导入新接口 ++ import { storeToRefs } from 'pinia'; -// --- 移除本地类型定义 --- - +// 本地接口,仅用于右侧列表显示 +interface SequenceDisplayItem extends FocusableInput {} // --- Props --- @@ -36,27 +36,50 @@ const dialogStyle = reactive({ position: 'absolute' as 'absolute', }); const hasChanges = ref(false); -// 本地副本,用于在弹窗内编辑而不直接修改 store -const localSequence: Ref = ref([]); // +++ 使用导入的接口 +++ -// +++ 存储原始序列(包含 ID 和快捷键),用于比较 +++ -const originalSequence: Ref = ref([]); // +++ 使用导入的接口 +++ +// 本地副本,用于在弹窗内编辑 +const localSequence = ref([]); // 右侧列表,只关心顺序和基础信息 +const localItemConfigs = ref>({}); // 所有项目的配置 (快捷键) +const originalConfig = ref(null); // 存储原始完整配置用于比较 // --- Watchers --- -watch(() => props.isVisible, (newValue) => { +watch(() => props.isVisible, async (newValue) => { // ++ Make async for potential backend load ++ if (newValue) { - // 从 Store 加载当前配置到本地副本 - // 从 Store 加载当前配置到本地副本 - // !!! 注意:Store 现在也需要支持快捷键,这里暂时假设它返回的数据包含 shortcut !!! - // 假设 getConfiguredInputs 返回的是 LocalFocusableInput[] 或能转换的类型 - const loadedSequenceFromStore = focusSwitcherStore.getConfiguredInputs; // 这个 getter 可能需要修改 - console.log('[FocusSwitcherConfigurator] Loading sequence from store getter...'); - // 深拷贝,并确保每个项目都有 shortcut 属性(可能为 undefined) - // Store getter 现在返回正确的类型,可以直接深拷贝 - localSequence.value = JSON.parse(JSON.stringify(loadedSequenceFromStore)); - originalSequence.value = JSON.parse(JSON.stringify(loadedSequenceFromStore)); // 同样直接拷贝 + // --- 加载完整配置 --- + // 确保 Store 已初始化 (如果 Store 还没有加载完) + // await focusSwitcherStore.loadConfigurationFromBackend(); // 如果 Store 初始化时未加载,则在此加载 + + const currentSequenceOrder = focusSwitcherStore.sequenceOrder; + const currentItemConfigs = focusSwitcherStore.itemConfigs; + const allAvailableInputs = focusSwitcherStore.availableInputs; // 获取所有可用输入的基础信息 + const inputsMap = new Map(allAvailableInputs.map(input => [input.id, input])); + + console.log('[FocusSwitcherConfigurator] Loading full config from store...'); + console.log('[FocusSwitcherConfigurator] Store sequenceOrder:', JSON.stringify(currentSequenceOrder)); + console.log('[FocusSwitcherConfigurator] Store itemConfigs:', JSON.stringify(currentItemConfigs)); + + // 构建本地右侧列表 (localSequence) + localSequence.value = currentSequenceOrder + .map(id => inputsMap.get(id)) // 获取基础信息 + .filter((input): input is SequenceDisplayItem => input !== undefined); // 过滤掉无效 ID 并断言类型 + + // 构建本地所有项目配置 (localItemConfigs) - 深拷贝 + // 确保所有 availableInputs 都有一个条目,即使没有快捷键 + const initialConfigs: Record = {}; + allAvailableInputs.forEach(input => { + initialConfigs[input.id] = { ... (currentItemConfigs[input.id] || {}) }; // 复制 store 中的配置,或创建空对象 + }); + localItemConfigs.value = JSON.parse(JSON.stringify(initialConfigs)); + + // 存储原始完整配置用于比较 + originalConfig.value = JSON.parse(JSON.stringify({ + sequence: currentSequenceOrder, + shortcuts: currentItemConfigs + })); + hasChanges.value = false; - console.log('[FocusSwitcherConfigurator] Dialog opened. Loaded sequence to local copy:', localSequence.value); - console.log('[FocusSwitcherConfigurator] Original sequence stored:', originalSequence.value); + console.log('[FocusSwitcherConfigurator] Dialog opened. Loaded localSequence:', JSON.stringify(localSequence.value)); + console.log('[FocusSwitcherConfigurator] Loaded localItemConfigs:', JSON.stringify(localItemConfigs.value)); + console.log('[FocusSwitcherConfigurator] Original full config stored:', JSON.stringify(originalConfig.value)); // 重置/计算初始位置和大小 requestAnimationFrame(() => { if (dialogRef.value) { @@ -76,17 +99,30 @@ watch(() => props.isVisible, (newValue) => { }); // 监听本地序列(包括快捷键)变化,标记未保存更改 -watch(localSequence, (currentLocalSequence) => { - // 比较当前本地序列和原始序列的 JSON 字符串 - const hasChanged = JSON.stringify(currentLocalSequence) !== JSON.stringify(originalSequence.value); - if (hasChanged) { - // console.log('[FocusSwitcherConfigurator] Local sequence changed.'); // +++ Log: Changed +++ - hasChanges.value = true; - } else { - // console.log('[FocusSwitcherConfigurator] Local sequence reverted to original.'); // +++ Log: Reverted +++ - // 如果序列变回和原来一样,则标记为无更改 - hasChanges.value = false; +// --- 修改:监听 localSequence 和 localItemConfigs 的变化 --- +watch([localSequence, localItemConfigs], ([currentSequence, currentConfigs]) => { + if (!originalConfig.value) return; // 尚未加载完成 + + // 比较序列顺序 + const sequenceChanged = JSON.stringify(currentSequence.map(item => item.id)) !== JSON.stringify(originalConfig.value.sequence); + + // 比较快捷键配置 (需要过滤掉原始配置中不存在的键,以防初始化时加入) + const currentShortcuts: Record = {}; + for(const id in currentConfigs) { + // 只比较原始配置中存在的 ID 或当前序列中的 ID 的快捷键是否有变化 + if (originalConfig.value.shortcuts[id] !== undefined || currentSequence.some(item => item.id === id)) { + currentShortcuts[id] = { shortcut: currentConfigs[id].shortcut }; + } } + const originalShortcuts: Record = {}; + for(const id in originalConfig.value.shortcuts) { + originalShortcuts[id] = { shortcut: originalConfig.value.shortcuts[id].shortcut }; + } + const shortcutsChanged = JSON.stringify(currentShortcuts) !== JSON.stringify(originalShortcuts); + + hasChanges.value = sequenceChanged || shortcutsChanged; + // console.log(`[FocusSwitcherConfigurator] Changes detected: sequence=${sequenceChanged}, shortcuts=${shortcutsChanged}, hasChanges=${hasChanges.value}`); + }, { deep: true }); @@ -102,27 +138,41 @@ const closeDialog = () => { }; const saveConfiguration = () => { - // 提取仅包含 id 和 shortcut 的配置项数组 - const configToSave = localSequence.value.map(item => ({ - id: item.id, - shortcut: item.shortcut || undefined, // 空字符串视为未设置 - })); - console.log('[FocusSwitcherConfigurator] Saving configuration. Config to save:', configToSave); - // 调用 Store 中正确的更新函数 - focusSwitcherStore.updateConfiguration(configToSave); - console.log('[FocusSwitcherConfigurator] Configuration save process triggered via updateConfiguration.'); + // 构造 FocusSwitcherFullConfig 对象 + const newSequence = localSequence.value.map(item => item.id); + // 清理 shortcuts,移除没有快捷键的条目 (可选,取决于后端是否需要) + const newShortcuts: Record = {}; + for (const id in localItemConfigs.value) { + if (localItemConfigs.value[id]?.shortcut) { + newShortcuts[id] = { shortcut: localItemConfigs.value[id].shortcut }; + } + // 如果需要保存空快捷键的记录,则取消 if 条件 + // newShortcuts[id] = { shortcut: localItemConfigs.value[id]?.shortcut }; + } + + const fullConfigToSave: FocusSwitcherFullConfig = { + sequence: newSequence, + shortcuts: newShortcuts, + }; + + console.log('[FocusSwitcherConfigurator] Saving full configuration:', JSON.stringify(fullConfigToSave)); + focusSwitcherStore.updateConfiguration(fullConfigToSave); // 调用 Store 更新函数 + console.log('[FocusSwitcherConfigurator] Configuration save process triggered.'); hasChanges.value = false; emit('close'); // 保存后关闭 }; // --- Computed --- -// 新的计算属性:基于本地已配置列表动态计算可用输入框 +// ++ 修改:计算属性,获取不在右侧序列中的项目 (用于左侧列表) ++ const localAvailableInputs = computed(() => { - // 获取本地已配置项的 ID 集合 - const configuredIds = new Set(localSequence.value.map(item => item.id)); - // 从 store 的 availableInputs state 中过滤掉已在本地配置的项 - // 注意:直接访问 store 的 state ref - return focusSwitcherStore.availableInputs.filter(input => !configuredIds.has(input.id)); + const sequenceIds = new Set(localSequence.value.map(item => item.id)); + // 从所有可用输入中过滤掉已在序列中的,并合并本地快捷键配置 + return focusSwitcherStore.availableInputs + .filter(input => !sequenceIds.has(input.id)) + .map(input => ({ + ...input, + shortcut: localItemConfigs.value[input.id]?.shortcut // 从本地配置获取快捷键 + })); }); // 注意:已配置的列表直接使用 localSequence ref @@ -149,15 +199,23 @@ const localAvailableInputs = computed(() => { :group="{ name: 'focus-inputs', pull: true, put: false }" :sort="false" > -