From 4bc9a775244fe15e4fe434971b403bb7122e3676 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sun, 20 Apr 2025 07:56:18 +0800 Subject: [PATCH] update --- .gitignore | 3 +- .../backend/src/services/settings.service.ts | 43 ++++++- .../src/settings/settings.controller.ts | 104 +++++++++++----- .../backend/src/settings/settings.routes.ts | 6 + packages/frontend/src/stores/layout.store.ts | 113 ++++++++++++++---- 5 files changed, 210 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index 1262bef..91f91f4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ build/Release # Dependency directories node_modules/ jspm_packages/ - +scripts/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ @@ -137,3 +137,4 @@ dist *.db *.jpg /packages/backend/uploads/backgrounds +/temp_iterm_schemes diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index 8a104b8..e6010df 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -4,6 +4,7 @@ import { settingsRepository, Setting } from '../repositories/settings.repository const DEFAULT_FOCUS_SEQUENCE = ["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]; const FOCUS_SEQUENCE_KEY = 'focusSwitcherSequence'; // 焦点切换顺序设置键 const NAV_BAR_VISIBLE_KEY = 'navBarVisible'; // 导航栏可见性设置键 +const LAYOUT_TREE_KEY = 'layoutTree'; // 布局树设置键 export const settingsService = { /** @@ -163,5 +164,45 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${NAV_BAR_VISIBLE_KEY}:`, error); throw new Error('Failed to save nav bar visibility setting.'); } - } // *** 最后的方法后面不需要逗号 *** + }, // *** 确保这里有逗号 *** + + /** + * 获取布局树设置 + * @returns 返回存储的布局树 JSON 字符串,如果未设置则返回 null + */ + async getLayoutTree(): Promise { + console.log(`[Service] Attempting to get setting for key: ${LAYOUT_TREE_KEY}`); + try { + const layoutJson = await settingsRepository.getSetting(LAYOUT_TREE_KEY); + console.log(`[Service] Raw value from repository for ${LAYOUT_TREE_KEY}:`, layoutJson ? layoutJson.substring(0, 100) + '...' : null); // 只打印部分内容 + return layoutJson; // 直接返回 JSON 字符串或 null + } catch (error) { + console.error(`[Service] Error getting layout tree setting (key: ${LAYOUT_TREE_KEY}):`, error); + return null; // 出错时返回 null + } + }, // *** 确保这里有逗号 *** + + /** + * 设置布局树 + * @param layoutJson 布局树的 JSON 字符串 + */ + async setLayoutTree(layoutJson: string): Promise { + console.log(`[Service] setLayoutTree called with JSON (first 100 chars): ${layoutJson.substring(0, 100)}...`); + // 可选:在这里添加 JSON 格式验证 + try { + JSON.parse(layoutJson); // 尝试解析以验证格式 + } catch (e) { + console.error('[Service] Invalid JSON format provided for layout tree:', e); + throw new Error('Invalid layout tree JSON format.'); + } + + try { + console.log(`[Service] Attempting to save setting. Key: ${LAYOUT_TREE_KEY}`); + await settingsRepository.setSetting(LAYOUT_TREE_KEY, layoutJson); + console.log(`[Service] Successfully saved setting for key: ${LAYOUT_TREE_KEY}`); + } catch (error) { + console.error(`[Service] Error calling settingsRepository.setSetting for key ${LAYOUT_TREE_KEY}:`, error); + throw new Error('Failed to save layout tree setting.'); + } + } // *** 最后的方法后面不需要逗号 *** }; diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts index 23907a8..3e04040 100644 --- a/packages/backend/src/settings/settings.controller.ts +++ b/packages/backend/src/settings/settings.controller.ts @@ -6,8 +6,6 @@ import { ipBlacklistService } from '../services/ip-blacklist.service'; // 引入 const auditLogService = new AuditLogService(); // 实例化 AuditLogService export const settingsController = { - // ... (getAllSettings, updateSettings, getFocusSwitcherSequence 保持不变) ... - /** * 获取所有设置项 */ @@ -26,18 +24,15 @@ export const settingsController = { */ async updateSettings(req: Request, res: Response): Promise { try { - // TODO: 添加输入验证,确保 req.body 是 Record const settingsToUpdate: Record = req.body; if (typeof settingsToUpdate !== 'object' || settingsToUpdate === null) { res.status(400).json({ message: '无效的请求体,应为 JSON 对象' }); return; } - // --- 过滤掉外观设置和焦点切换顺序相关的键 --- const allowedSettingsKeys = [ 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', 'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled' - // 不在此处处理 'focusSwitcherSequence' ]; const filteredSettings: Record = {}; for (const key in settingsToUpdate) { @@ -45,14 +40,11 @@ export const settingsController = { filteredSettings[key] = settingsToUpdate[key]; } } - // --- 结束过滤 --- - // 只传递过滤后的设置给 service if (Object.keys(filteredSettings).length > 0) { await settingsService.setMultipleSettings(filteredSettings); } - // 记录审计日志 const updatedKeys = Object.keys(filteredSettings); if (updatedKeys.length > 0) { if (updatedKeys.includes('ipWhitelist') || updatedKeys.includes('ipWhitelistEnabled')) { @@ -68,52 +60,47 @@ export const settingsController = { } }, - // +++ 新增:获取焦点切换顺序 +++ /** * 获取焦点切换顺序 */ async getFocusSwitcherSequence(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get focus switcher sequence.'); // +++ 添加日志 +++ + console.log('[Controller] Received request to get focus switcher sequence.'); const sequence = await settingsService.getFocusSwitcherSequence(); - console.log('[Controller] Sending focus switcher sequence to client:', JSON.stringify(sequence)); // +++ 添加日志 +++ + console.log('[Controller] Sending focus switcher sequence to client:', JSON.stringify(sequence)); res.json(sequence); } catch (error: any) { - console.error('[Controller] 获取焦点切换顺序时出错:', error); // +++ 更新日志前缀 +++ + console.error('[Controller] 获取焦点切换顺序时出错:', error); res.status(500).json({ message: '获取焦点切换顺序失败', error: error.message }); } }, - // +++ 新增:设置焦点切换顺序 +++ /** * 设置焦点切换顺序 */ async setFocusSwitcherSequence(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set focus switcher sequence.'); // +++ 添加日志 +++ + console.log('[Controller] Received request to set focus switcher sequence.'); try { const { sequence } = req.body; - console.log('[Controller] Request body sequence:', JSON.stringify(sequence)); // +++ 添加日志 +++ + console.log('[Controller] Request body sequence:', JSON.stringify(sequence)); - // 输入验证 if (!Array.isArray(sequence) || !sequence.every(item => typeof item === 'string')) { - console.warn('[Controller] Invalid sequence format received:', sequence); // +++ 添加日志 +++ + console.warn('[Controller] Invalid sequence format received:', sequence); res.status(400).json({ message: '无效的请求体,"sequence" 必须是一个字符串数组' }); return; } - console.log('[Controller] Calling settingsService.setFocusSwitcherSequence...'); // +++ 添加日志 +++ + console.log('[Controller] Calling settingsService.setFocusSwitcherSequence...'); await settingsService.setFocusSwitcherSequence(sequence); - console.log('[Controller] settingsService.setFocusSwitcherSequence completed successfully.'); // +++ 添加日志 +++ + console.log('[Controller] settingsService.setFocusSwitcherSequence completed successfully.'); - // 记录审计日志 (可选) - console.log('[Controller] Logging audit action: FOCUS_SWITCHER_SEQUENCE_UPDATED'); // +++ 添加日志 +++ + console.log('[Controller] Logging audit action: FOCUS_SWITCHER_SEQUENCE_UPDATED'); auditLogService.logAction('FOCUS_SWITCHER_SEQUENCE_UPDATED', { sequence }); - console.log('[Controller] Sending success response.'); // +++ 添加日志 +++ + console.log('[Controller] Sending success response.'); res.status(200).json({ message: '焦点切换顺序已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置焦点切换顺序时出错:', error); // +++ 更新日志前缀 +++ - // 区分是服务层抛出的验证错误还是其他错误 + console.error('[Controller] 设置焦点切换顺序时出错:', error); if (error.message === 'Invalid sequence format provided.') { res.status(400).json({ message: '设置焦点切换顺序失败: 无效的格式', error: error.message }); } else { @@ -122,7 +109,6 @@ export const settingsController = { } }, - // +++ 新增:获取导航栏可见性 +++ /** * 获取导航栏可见性设置 */ @@ -131,14 +117,13 @@ export const settingsController = { console.log('[Controller] Received request to get nav bar visibility.'); const isVisible = await settingsService.getNavBarVisibility(); console.log(`[Controller] Sending nav bar visibility to client: ${isVisible}`); - res.json({ visible: isVisible }); // 返回包含 visible 键的对象 + res.json({ visible: isVisible }); } catch (error: any) { console.error('[Controller] 获取导航栏可见性时出错:', error); res.status(500).json({ message: '获取导航栏可见性失败', error: error.message }); } }, - // +++ 新增:设置导航栏可见性 +++ /** * 设置导航栏可见性 */ @@ -148,7 +133,6 @@ export const settingsController = { const { visible } = req.body; console.log('[Controller] Request body visible:', visible); - // 输入验证 if (typeof visible !== 'boolean') { console.warn('[Controller] Invalid visible format received:', visible); res.status(400).json({ message: '无效的请求体,"visible" 必须是一个布尔值' }); @@ -159,8 +143,6 @@ export const settingsController = { await settingsService.setNavBarVisibility(visible); console.log('[Controller] settingsService.setNavBarVisibility completed successfully.'); - // 记录审计日志 (可选) - // console.log('[Controller] Logging audit action: NAV_BAR_VISIBILITY_UPDATED'); // auditLogService.logAction('NAV_BAR_VISIBILITY_UPDATED', { visible }); console.log('[Controller] Sending success response.'); @@ -171,6 +153,66 @@ export const settingsController = { } }, + /** + * 获取布局树设置 + */ + async getLayoutTree(req: Request, res: Response): Promise { + try { + console.log('[Controller] Received request to get layout tree.'); + const layoutJson = await settingsService.getLayoutTree(); + if (layoutJson) { + try { + const layout = JSON.parse(layoutJson); + console.log('[Controller] Sending layout tree to client.'); + res.json(layout); + } catch (parseError) { + console.error('[Controller] Failed to parse layout tree JSON from DB:', parseError); + res.status(500).json({ message: '获取布局树失败:存储的数据格式无效' }); + } + } else { + console.log('[Controller] No layout tree found in settings, sending null.'); + res.json(null); + } + } catch (error: any) { + console.error('[Controller] 获取布局树时出错:', error); + res.status(500).json({ message: '获取布局树失败', error: error.message }); + } + }, + + /** + * 设置布局树 + */ + async setLayoutTree(req: Request, res: Response): Promise { + console.log('[Controller] Received request to set layout tree.'); + try { + const layoutTree = req.body; + + if (typeof layoutTree !== 'object' || layoutTree === null) { + console.warn('[Controller] Invalid layout tree format received (not an object):', layoutTree); + res.status(400).json({ message: '无效的请求体,应为 JSON 对象格式的布局树' }); + return; + } + + const layoutJson = JSON.stringify(layoutTree); + + console.log('[Controller] Calling settingsService.setLayoutTree...'); + await settingsService.setLayoutTree(layoutJson); + console.log('[Controller] settingsService.setLayoutTree completed successfully.'); + + // auditLogService.logAction('LAYOUT_TREE_UPDATED'); + + console.log('[Controller] Sending success response.'); + res.status(200).json({ message: '布局树已成功更新' }); + } catch (error: any) { + console.error('[Controller] 设置布局树时出错:', error); + if (error.message === 'Invalid layout tree JSON format.') { + res.status(400).json({ message: '设置布局树失败: 无效的 JSON 格式', error: error.message }); + } else { + res.status(500).json({ message: '设置布局树失败', error: error.message }); + } + } + }, + /** * 获取 IP 黑名单列表 (分页) */ @@ -196,9 +238,7 @@ export const settingsController = { res.status(400).json({ message: '缺少要删除的 IP 地址' }); return; } - // TODO: 可以添加对 IP 格式的验证 await ipBlacklistService.removeFromBlacklist(ipToDelete); - // 记录审计日志 (可选) // auditLogService.logAction('IP_BLACKLIST_REMOVED', { ip: ipToDelete }); res.status(200).json({ message: `IP 地址 ${ipToDelete} 已从黑名单中移除` }); } catch (error: any) { diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts index 26d4ac9..b441b98 100644 --- a/packages/backend/src/settings/settings.routes.ts +++ b/packages/backend/src/settings/settings.routes.ts @@ -23,6 +23,12 @@ router.get('/nav-bar-visibility', settingsController.getNavBarVisibility); // PUT /api/v1/settings/nav-bar-visibility - 更新导航栏可见性 router.put('/nav-bar-visibility', settingsController.setNavBarVisibility); +// +++ 新增:布局树路由 +++ +// GET /api/v1/settings/layout - 获取布局树 +router.get('/layout', settingsController.getLayoutTree); +// PUT /api/v1/settings/layout - 更新布局树 +router.put('/layout', settingsController.setLayoutTree); + // --- IP 黑名单管理路由 --- // GET /api/v1/settings/ip-blacklist - 获取 IP 黑名单列表 (需要认证) router.get('/ip-blacklist', settingsController.getIpBlacklist); diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index 8ed4152..91510f1 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -110,22 +110,51 @@ export const useLayoutStore = defineStore('layout', () => { }); // --- Actions --- - // 初始化布局:尝试从 localStorage 加载,否则使用默认布局 - function initializeLayout() { + // 初始化布局:优先尝试从后端加载,然后 localStorage,最后默认布局 + async function initializeLayout() { + let loadedFromBackend = false; + // 1. 尝试从后端加载 try { - const savedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY); - if (savedLayout) { - const parsedLayout = JSON.parse(savedLayout) as LayoutNode; - // 可选:添加验证逻辑确保加载的布局结构有效 - layoutTree.value = parsedLayout; - console.log('[Layout Store] 从 localStorage 加载布局成功。'); + console.log('[Layout Store] Attempting to load layout from backend...'); + const response = await axios.get('/api/v1/settings/layout'); + if (response.data) { + // TODO: 在这里添加对 response.data 的结构验证,确保它符合 LayoutNode 接口 + layoutTree.value = response.data; + loadedFromBackend = true; + console.log('[Layout Store] 从后端加载布局成功。'); + // 可选:如果后端加载成功,可以更新 localStorage + try { + localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(response.data)); + } catch (lsError) { + console.error('[Layout Store] 保存后端布局到 localStorage 失败:', lsError); + } } else { - layoutTree.value = getDefaultLayout(); - console.log('[Layout Store] 未找到保存的布局,使用默认布局。'); + console.log('[Layout Store] 后端未返回布局数据。'); } } catch (error) { - console.error('[Layout Store] 加载或解析布局失败:', error); - layoutTree.value = getDefaultLayout(); // 出错时回退到默认布局 + console.error('[Layout Store] 从后端加载布局失败:', error); + // 加载失败,继续尝试 localStorage + } + + // 2. 如果后端未加载成功,尝试从 localStorage 加载 + if (!loadedFromBackend) { + console.log('[Layout Store] Attempting to load layout from localStorage...'); + try { + const savedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY); + if (savedLayout) { + const parsedLayout = JSON.parse(savedLayout) as LayoutNode; + // TODO: 添加验证逻辑确保加载的布局结构有效 + layoutTree.value = parsedLayout; + console.log('[Layout Store] 从 localStorage 加载布局成功。'); + } else { + // 3. 如果 localStorage 也没有,使用默认布局 + layoutTree.value = getDefaultLayout(); + console.log('[Layout Store] 未找到保存的布局,使用默认布局。'); + } + } catch (error) { + console.error('[Layout Store] 从 localStorage 加载或解析布局失败:', error); + layoutTree.value = getDefaultLayout(); // 出错时回退到默认布局 + } } } @@ -220,25 +249,59 @@ export const useLayoutStore = defineStore('layout', () => { // alert('Failed to save preference.'); // 或者通知用户 } } + + // 新增 Action: 将当前布局树持久化到后端和 localStorage + async function persistLayoutTree() { + if (!layoutTree.value) { + console.warn('[Layout Store] persistLayoutTree: layoutTree is null, cannot persist.'); + // 可选:如果布局为空,是否也通知后端?或者删除后端的设置? + // await axios.delete('/api/v1/settings/layout'); // 示例:删除后端设置 + localStorage.removeItem(LAYOUT_STORAGE_KEY); // 保持移除本地存储 + return; + } + + const layoutToSave = JSON.stringify(layoutTree.value); + + // 1. 保存到后端 + try { + console.log('[Layout Store] Attempting to save layout to backend...'); + await axios.put('/api/v1/settings/layout', layoutTree.value); // 发送对象,后端会 stringify + console.log('[Layout Store] 布局已成功保存到后端。'); + } catch (error) { + console.error('[Layout Store] 保存布局到后端失败:', error); + // 可以考虑添加用户提示 + } + + // 2. 保存到 localStorage (作为备份或离线支持) + try { + localStorage.setItem(LAYOUT_STORAGE_KEY, layoutToSave); + console.log('[Layout Store] 布局已自动保存到 localStorage。'); + } catch (error) { + console.error('[Layout Store] 保存布局到 localStorage 失败:', error); + } + } + // --- 持久化 --- - // 监听 layoutTree 的变化,并自动保存到 localStorage + // 监听 layoutTree 的变化,并调用持久化方法 + // 添加防抖以避免过于频繁的 API 调用 + let debounceTimer: ReturnType | null = null; watch( layoutTree, - (newTree) => { - if (newTree) { - try { - localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(newTree)); - console.log('[Layout Store] 布局已自动保存到 localStorage。'); - } catch (error) { - console.error('[Layout Store] 保存布局到 localStorage 失败:', error); + (newTree, oldTree) => { + // 避免初始化时触发 (虽然 initializeLayout 已经是 async,但以防万一) + if (oldTree === undefined) return; + // 只有在实际发生变化时才触发持久化 + if (JSON.stringify(newTree) !== JSON.stringify(oldTree)) { + console.log('[Layout Store] Layout tree changed, scheduling persistence...'); + if (debounceTimer) { + clearTimeout(debounceTimer); } - } else { - // 如果布局被清空,也移除本地存储 - localStorage.removeItem(LAYOUT_STORAGE_KEY); - console.log('[Layout Store] 布局为空,已从 localStorage 移除。'); + debounceTimer = setTimeout(() => { + persistLayoutTree(); + }, 1000); // 1秒防抖 } }, - { deep: true } // 需要深度监听来捕获嵌套结构的变化 + { deep: true } ); // --- 初始化 ---