diff --git a/packages/backend/src/database/schema.registry.ts b/packages/backend/src/database/schema.registry.ts index 2da886d..f140787 100644 --- a/packages/backend/src/database/schema.registry.ts +++ b/packages/backend/src/database/schema.registry.ts @@ -2,6 +2,7 @@ import { Database } from 'sqlite3'; import * as schemaSql from './schema'; import * as appearanceRepository from '../repositories/appearance.repository'; import * as terminalThemeRepository from '../repositories/terminal-theme.repository'; +import * as settingsRepository from '../repositories/settings.repository'; // <-- Import settings repository import { presetTerminalThemes } from '../config/preset-themes-definition'; import { runDb } from './connection'; // Import runDb for init functions @@ -16,20 +17,7 @@ export interface TableDefinition { // --- Initialization Functions --- -/** - * Initializes default settings in the settings table. - */ -const initSettingsTable = async (db: Database): Promise => { - const defaultSettings = [ - { key: 'ipWhitelistEnabled', value: 'false' }, - { key: 'ipWhitelist', value: '' } - ]; - for (const setting of defaultSettings) { - // Use INSERT OR IGNORE to avoid errors if settings already exist - await runDb(db, "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", [setting.key, setting.value]); - } - console.log('[DB Init] 默认 settings 初始化检查完成。'); -}; +// Remove the old initSettingsTable function, as the logic is now in the repository /** * Initializes preset terminal themes. @@ -66,7 +54,7 @@ export const tableDefinitions: TableDefinition[] = [ { name: 'settings', sql: schemaSql.createSettingsTableSQL, - init: initSettingsTable + init: settingsRepository.ensureDefaultSettingsExist // <-- Use the function from the repository }, { name: 'audit_logs', sql: schemaSql.createAuditLogsTableSQL }, { name: 'api_keys', sql: schemaSql.createApiKeysTableSQL }, diff --git a/packages/backend/src/repositories/settings.repository.ts b/packages/backend/src/repositories/settings.repository.ts index 27d41fd..e60cf1b 100644 --- a/packages/backend/src/repositories/settings.repository.ts +++ b/packages/backend/src/repositories/settings.repository.ts @@ -1,8 +1,10 @@ // packages/backend/src/repositories/settings.repository.ts -import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers +import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; +import { SidebarConfig } from '../types/settings.types'; // <-- Correct import path +import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint -// Remove top-level db instance -// const db = getDb(); +// Define keys for specific settings +const SIDEBAR_CONFIG_KEY = 'sidebarConfig'; export interface Setting { key: string; @@ -93,3 +95,94 @@ export const settingsRepository = { } }, }; + +// --- Specific Setting Getters/Setters --- + +/** + * 获取侧栏配置 + * @returns Promise - Returns the parsed config or default + */ +export const getSidebarConfig = async (): Promise => { + const defaultValue: SidebarConfig = { left: [], right: [] }; + try { + const jsonString = await settingsRepository.getSetting(SIDEBAR_CONFIG_KEY); + if (jsonString) { + try { + const config = JSON.parse(jsonString); + // Basic validation + if (config && Array.isArray(config.left) && Array.isArray(config.right)) { + // TODO: Add deeper validation if needed (e.g., check if items are valid PaneName) + return config as SidebarConfig; + } + console.warn(`[SettingsRepo] Invalid sidebarConfig format found in DB: ${jsonString}. Returning default.`); + } catch (parseError) { + console.error(`[SettingsRepo] Failed to parse sidebarConfig JSON from DB: ${jsonString}`, parseError); + } + } + } catch (error) { + console.error(`[SettingsRepo] Error fetching sidebar config setting (key: ${SIDEBAR_CONFIG_KEY}):`, error); + } + // Return default if not found, invalid, or error occurred + return defaultValue; +}; + +/** + * 设置侧栏配置 + * @param config - The sidebar configuration object + */ +export const setSidebarConfig = async (config: SidebarConfig): Promise => { + try { + // Basic validation before stringifying + if (!config || typeof config !== 'object' || !Array.isArray(config.left) || !Array.isArray(config.right)) { + throw new Error('Invalid sidebar config object provided.'); + } + // TODO: Add deeper validation if needed (e.g., check PaneName validity) + const jsonString = JSON.stringify(config); + await settingsRepository.setSetting(SIDEBAR_CONFIG_KEY, jsonString); + } catch (error) { + console.error(`[SettingsRepo] Error setting sidebar config (key: ${SIDEBAR_CONFIG_KEY}):`, error); + throw new Error('Failed to save sidebar configuration.'); + } +}; + + +// --- Initialization --- + +/** + * Ensures default settings exist in the settings table. + * This function should be called during database initialization. + * @param db - The active database instance + */ +export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise => { + const defaultSettings: Record = { + language: 'en', // Default language + ipWhitelistEnabled: 'false', + ipWhitelist: '', + maxLoginAttempts: '5', + loginBanDuration: '300', // 5 minutes in seconds + focusSwitcherSequence: JSON.stringify(["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]), // Default focus sequence + navBarVisible: 'true', // Default nav bar visibility + layoutTree: 'null', // Default layout tree (null initially) + autoCopyOnSelect: 'false', // Default auto copy setting + showPopupFileEditor: 'false', // Default popup editor setting + shareFileEditorTabs: 'true', // Default editor tab sharing + dockerStatusIntervalSeconds: '5', // Default Docker refresh interval + dockerDefaultExpand: 'false', // Default Docker expand state + statusMonitorIntervalSeconds: '3', // Default Status Monitor interval + [SIDEBAR_CONFIG_KEY]: JSON.stringify({ left: [], right: [] }), // Default sidebar config + // Add other default settings here + }; + const nowSeconds = Math.floor(Date.now() / 1000); + const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`; + + console.log('[SettingsRepo] Ensuring default settings exist...'); + try { + for (const [key, value] of Object.entries(defaultSettings)) { + await runDb(db, sqlInsertOrIgnore, [key, value, nowSeconds, nowSeconds]); + } + console.log('[SettingsRepo] Default settings check complete.'); + } catch (err: any) { + console.error(`[SettingsRepo] Error ensuring default settings:`, err.message); + throw new Error(`Failed to ensure default settings: ${err.message}`); + } +}; diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index 7883d43..6abeede 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -1,4 +1,5 @@ -import { settingsRepository, Setting } from '../repositories/settings.repository'; +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'; // <-- Correct import path // +++ 定义默认的焦点切换顺序 +++ const DEFAULT_FOCUS_SEQUENCE = ["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]; @@ -291,5 +292,68 @@ export const settingsService = { console.error(`[Service] Error calling settingsRepository.setSetting for key ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, error); throw new Error('Failed to save status monitor interval setting.'); } - } // *** 最后的方法后面不需要逗号 *** -}; + }, // *** 确保这里有逗号 *** + + // --- Sidebar Config Specific Functions --- + + /** + * 获取侧栏配置 + * @returns Promise + */ + async getSidebarConfig(): Promise { + console.log('[SettingsService] Getting sidebar config...'); + // Directly call the specific repository function + const config = await getSidebarConfigFromRepo(); + console.log('[SettingsService] Returning sidebar config:', config); + return config; + }, + + /** + * 设置侧栏配置 + * @param configDto - The sidebar configuration object from DTO + * @returns Promise + */ + async setSidebarConfig(configDto: UpdateSidebarConfigDto): Promise { + console.log('[SettingsService] Setting sidebar config:', configDto); + + // --- Validation --- + if (!configDto || typeof configDto !== 'object' || !Array.isArray(configDto.left) || !Array.isArray(configDto.right)) { + throw new Error('无效的侧栏配置格式。必须包含 left 和 right 数组。'); + } + + // Validate PaneName (using the type imported) + const validPaneNames: Set = new Set([ + 'connections', 'terminal', 'commandBar', 'fileManager', + 'editor', 'statusMonitor', 'commandHistory', 'quickCommands', + 'dockerManager' + ]); + + const validatePaneArray = (arr: any[], side: string) => { + if (!arr.every(item => typeof item === 'string' && validPaneNames.has(item as PaneName))) { + const invalidItems = arr.filter(item => typeof item !== 'string' || !validPaneNames.has(item as PaneName)); + throw new Error(`侧栏配置 (${side}) 包含无效的面板名称: ${invalidItems.join(', ')}`); + } + }; + + validatePaneArray(configDto.left, 'left'); + validatePaneArray(configDto.right, 'right'); + + // Prevent duplicates (optional, uncomment if needed) + // const allPanes = [...configDto.left, ...configDto.right]; + // const uniquePanes = new Set(allPanes); + // if (allPanes.length !== uniquePanes.size) { + // throw new Error('侧栏配置中不允许包含重复的面板。'); + // } + + // Prepare the data in the exact SidebarConfig format expected by the repo + const configToSave: SidebarConfig = { + left: configDto.left, + right: configDto.right, + }; + + // Directly call the specific repository function + await setSidebarConfigInRepo(configToSave); + console.log('[SettingsService] Sidebar config successfully set.'); + } // <-- No comma after the last method in the object + +}; // <-- End of settingsService object definition diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts index db863fd..f51820c 100644 --- a/packages/backend/src/settings/settings.controller.ts +++ b/packages/backend/src/settings/settings.controller.ts @@ -1,9 +1,10 @@ import { Request, Response } from 'express'; import { settingsService } from '../services/settings.service'; import { AuditLogService } from '../services/audit.service'; // 引入 AuditLogService -import { ipBlacklistService } from '../services/ip-blacklist.service'; // 引入 IP 黑名单服务 +import { ipBlacklistService } from '../services/ip-blacklist.service'; +import { UpdateSidebarConfigDto } from '../types/settings.types'; // <-- Correct import path -const auditLogService = new AuditLogService(); // 实例化 AuditLogService +const auditLogService = new AuditLogService(); export const settingsController = { /** @@ -291,5 +292,59 @@ export const settingsController = { console.error('[Controller] 设置终端选中自动复制时出错:', error); res.status(500).json({ message: '设置终端选中自动复制失败', error: error.message }); } - } // *** 最后的方法后面不需要逗号 *** -}; + }, // *** 确保这里有逗号 *** + + // --- Sidebar Config Controller Methods --- + + /** + * 获取侧栏配置 + */ + async getSidebarConfig(req: Request, res: Response): Promise { + try { + console.log('[Controller] Received request to get sidebar config.'); + const config = await settingsService.getSidebarConfig(); + console.log('[Controller] Sending sidebar config to client:', config); + res.json(config); + } catch (error: any) { + console.error('[Controller] 获取侧栏配置时出错:', error); + res.status(500).json({ message: '获取侧栏配置失败', error: error.message }); + } + }, + + /** + * 设置侧栏配置 + */ + async setSidebarConfig(req: Request, res: Response): Promise { + console.log('[Controller] Received request to set sidebar config.'); + try { + const configDto: UpdateSidebarConfigDto = req.body; + console.log('[Controller] Request body:', configDto); + + // --- DTO Validation (Basic) --- + // More specific validation happens in the service layer + if (!configDto || typeof configDto !== 'object' || !Array.isArray(configDto.left) || !Array.isArray(configDto.right)) { + console.warn('[Controller] Invalid sidebar config format received:', configDto); + res.status(400).json({ message: '无效的请求体,应为包含 left 和 right 数组的 JSON 对象' }); + return; + } + + console.log('[Controller] Calling settingsService.setSidebarConfig...'); + await settingsService.setSidebarConfig(configDto); + console.log('[Controller] settingsService.setSidebarConfig completed successfully.'); + + // auditLogService.logAction('SIDEBAR_CONFIG_UPDATED'); // Optional: Add audit log + + console.log('[Controller] Sending success response.'); + res.status(200).json({ message: '侧栏配置已成功更新' }); + } catch (error: any) { + console.error('[Controller] 设置侧栏配置时出错:', error); + // Handle specific validation errors from the service + if (error.message.includes('无效的面板名称') || error.message.includes('无效的侧栏配置格式')) { + res.status(400).json({ message: `设置侧栏配置失败: ${error.message}` }); + } else { + res.status(500).json({ message: '设置侧栏配置失败', error: error.message }); + } + } + } // <-- No comma after the last method + +}; // <-- End of settingsController object diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts index 48a0b49..4872308 100644 --- a/packages/backend/src/settings/settings.routes.ts +++ b/packages/backend/src/settings/settings.routes.ts @@ -43,4 +43,11 @@ router.get('/auto-copy-on-select', settingsController.getAutoCopyOnSelect); // PUT /api/v1/settings/auto-copy-on-select - 更新设置 router.put('/auto-copy-on-select', settingsController.setAutoCopyOnSelect); +// +++ 新增:侧栏配置路由 +++ +// GET /api/v1/settings/sidebar - 获取侧栏配置 +router.get('/sidebar', settingsController.getSidebarConfig); +// PUT /api/v1/settings/sidebar - 更新侧栏配置 +router.put('/sidebar', settingsController.setSidebarConfig); + + export default router; diff --git a/packages/backend/src/types/appearance.types.ts b/packages/backend/src/types/appearance.types.ts index e71a38f..7b866f3 100644 --- a/packages/backend/src/types/appearance.types.ts +++ b/packages/backend/src/types/appearance.types.ts @@ -1,5 +1,8 @@ import type { ITheme } from 'xterm'; +// 定义所有可用面板的名称 (后端独立定义) +export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager'; + /** * 外观设置数据结构 */ diff --git a/packages/backend/src/types/settings.types.ts b/packages/backend/src/types/settings.types.ts new file mode 100644 index 0000000..ce76996 --- /dev/null +++ b/packages/backend/src/types/settings.types.ts @@ -0,0 +1,19 @@ +// packages/backend/src/types/settings.types.ts + +// Define PaneName here as it's logically related to layout/sidebar settings +export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager'; + +/** + * 侧栏配置数据结构 (Managed by Settings Repository/Service) + */ +export interface SidebarConfig { + left: PaneName[]; + right: PaneName[]; +} + +/** + * 用于更新侧栏配置的 DTO + */ +export interface UpdateSidebarConfigDto extends SidebarConfig {} // Simple alias for now, can add validation later + +// You can add other settings-related types here if needed \ No newline at end of file diff --git a/packages/frontend/src/components/LayoutConfigurator.vue b/packages/frontend/src/components/LayoutConfigurator.vue index 85b41ac..10f6e0a 100644 --- a/packages/frontend/src/components/LayoutConfigurator.vue +++ b/packages/frontend/src/components/LayoutConfigurator.vue @@ -1,9 +1,9 @@