From 6f403834ac8f92bb17114cf0a0bb9cfbf87b8449 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sat, 19 Apr 2025 14:05:26 +0800 Subject: [PATCH] update --- .../src/config/preset-themes-definition.ts | 110 +++++++++++++ packages/backend/src/database.ts | 92 ++++++++--- .../src/repositories/appearance.repository.ts | 119 ++++++++------ .../repositories/terminal-theme.repository.ts | 131 +++++++++------ .../src/services/appearance.service.ts | 28 ++-- .../backend/src/types/appearance.types.ts | 2 +- .../src/components/StyleCustomizer.vue | 63 +++++--- packages/frontend/src/components/Terminal.vue | 9 ++ .../frontend/src/stores/appearance.store.ts | 150 ++++++++---------- 9 files changed, 471 insertions(+), 233 deletions(-) create mode 100644 packages/backend/src/config/preset-themes-definition.ts diff --git a/packages/backend/src/config/preset-themes-definition.ts b/packages/backend/src/config/preset-themes-definition.ts new file mode 100644 index 0000000..584d1c0 --- /dev/null +++ b/packages/backend/src/config/preset-themes-definition.ts @@ -0,0 +1,110 @@ +import type { ITheme } from 'xterm'; +import type { TerminalTheme } from '../types/terminal-theme.types'; + +// 定义基础的 ITheme 结构,用于类型提示 +interface PresetITheme extends ITheme { + name?: string; // itermcolors 文件可能包含 name +} + +// 定义预设主题数组的类型,确保包含 preset_key +type PresetThemeDefinition = Omit & { preset_key: string }; + +// 默认主题 (可以从 default-themes.ts 导入或直接定义) +const defaultThemeData: PresetITheme = { + background: '#ffffff', + foreground: '#000000', + cursor: '#000000', + cursorAccent: '#ffffff', + selectionBackground: '#a7a7a7', // 使用更柔和的灰色作为选择背景 + black: '#000000', + red: '#c50f1f', + green: '#13a10e', + yellow: '#c19c00', + blue: '#0037da', + magenta: '#881798', + cyan: '#3a96dd', + white: '#cccccc', + brightBlack: '#767676', + brightRed: '#e74856', + brightGreen: '#16c60c', + brightYellow: '#f9f1a5', + brightBlue: '#3b78ff', + brightMagenta: '#b4009e', + brightCyan: '#61d6d6', + brightWhite: '#ffffff', +}; + +// 其他预设主题 (从 iterm-themes.ts 迁移并添加 preset_key) +// 注意:需要将原 iterm-themes.ts 中的主题数据转换为 ITheme 格式 +// 这里仅作示例,你需要将实际的主题数据填充进来 + +const solarizedDark: PresetITheme = { + background: '#002b36', + foreground: '#839496', + cursor: '#839496', + selectionBackground: '#073642', + black: '#073642', + red: '#dc322f', + green: '#859900', + yellow: '#b58900', + blue: '#268bd2', + magenta: '#d33682', + cyan: '#2aa198', + white: '#eee8d5', + brightBlack: '#002b36', + brightRed: '#cb4b16', + brightGreen: '#586e75', + brightYellow: '#657b83', + brightBlue: '#839496', + brightMagenta: '#6c71c4', // Note: Original Solarized might use a different magenta + brightCyan: '#93a1a1', + brightWhite: '#fdf6e3', +}; + +const oneDark: PresetITheme = { + foreground: '#abb2bf', + background: '#282c34', + cursor: '#abb2bf', + selectionBackground: '#3e4451', // A slightly lighter background for selection + black: '#282c34', // Often same as background + red: '#e06c75', + green: '#98c379', + yellow: '#e5c07b', + blue: '#61afef', + magenta: '#c678dd', + cyan: '#56b6c2', + white: '#abb2bf', // Often same as foreground + brightBlack: '#5c6370', // A darker grey + brightRed: '#e06c75', // Often same as normal red + brightGreen: '#98c379', // Often same as normal green + brightYellow: '#e5c07b', // Often same as normal yellow + brightBlue: '#61afef', // Often same as normal blue + brightMagenta: '#c678dd', // Often same as normal magenta + brightCyan: '#56b6c2', // Often same as normal cyan + brightWhite: '#ffffff', // Pure white for bright white +}; + +// ... (添加其他所有需要的预设主题) + +// 导出预设主题数组 +export const presetTerminalThemes: PresetThemeDefinition[] = [ + { + preset_key: 'default', // 指定一个唯一的 key + name: '默认', // 显示名称 + themeData: defaultThemeData, + isPreset: true, + }, + { + preset_key: 'solarized-dark', + name: 'Solarized Dark', + themeData: solarizedDark, + isPreset: true, + }, + { + preset_key: 'one-dark', + name: 'One Dark', + themeData: oneDark, + isPreset: true, + }, + // ... (添加其他所有预设主题对象) +]; \ No newline at end of file diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 59c4837..1b97653 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -1,9 +1,14 @@ import sqlite3 from 'sqlite3'; import path from 'path'; import fs from 'fs'; +// 导入 Repository 模块以调用其初始化相关函数 +import * as appearanceRepository from './repositories/appearance.repository'; +import * as terminalThemeRepository from './repositories/terminal-theme.repository'; +// 导入预设主题定义 +import { presetTerminalThemes } from './config/preset-themes-definition'; -// 数据库文件路径 (相对于 backend 项目根目录) -const dbDir = path.resolve(__dirname, '../../data'); // 使用 '../../data' 定位到 monorepo 根目录下的 data 文件夹 +// 数据库文件路径 +const dbDir = path.resolve(__dirname, '../../data'); const dbPath = path.join(dbDir, 'nexus-terminal.db'); // 确保数据库目录存在 @@ -12,44 +17,88 @@ if (!fs.existsSync(dbDir)) { console.log(`数据库目录已创建: ${dbDir}`); } -// 使用详细模式以获得更丰富的错误信息 const verboseSqlite3 = sqlite3.verbose(); - -// 创建并连接数据库 -// 使用单例 (singleton) 模式确保只有一个数据库连接实例 let dbInstance: sqlite3.Database | null = null; +/** + * 执行数据库初始化序列:创建表、插入预设主题、设置默认外观。 + * @param db 数据库实例 + */ +const runDatabaseInitializations = (db: sqlite3.Database) => { + db.serialize(() => { + console.log('[DB Init] 开始数据库初始化序列...'); + + // 1. 启用外键约束 (必须在事务之外或序列化块的开始处) + db.run('PRAGMA foreign_keys = ON;', (pragmaErr) => { + if (pragmaErr) { + console.error('[DB Init] 启用外键约束失败:', pragmaErr.message); + // 根据需要处理错误,可能需要阻止后续初始化 + } else { + console.log('[DB Init] 外键约束已启用。'); + } + }); + + // 2. 创建 terminal_themes 表 + db.run(terminalThemeRepository.SQL_CREATE_TABLE, (err) => { + if (err) { + console.error('[DB Init] 创建 terminal_themes 表失败:', err.message); + } else { + console.log('[DB Init] terminal_themes 表已存在或已创建。'); + // 3. 初始化预设主题 (只有在表创建成功或已存在后才执行) + terminalThemeRepository.initializePresetThemes(presetTerminalThemes) + .then(() => console.log('[DB Init] 预设主题初始化检查完成。')) + .catch(initErr => console.error('[DB Init] 初始化预设主题时出错:', initErr)); + } + }); + + // 4. 创建 appearance_settings 表 + db.run(appearanceRepository.SQL_CREATE_TABLE, (err) => { + if (err) { + console.error('[DB Init] 创建 appearance_settings 表失败:', err.message); + } else { + console.log('[DB Init] appearance_settings 表已存在或已创建。'); + // 5. 确保默认设置存在并设置默认激活主题 (只有在表创建成功或已存在后才执行) + // 注意:这需要等待预设主题初始化完成,但 serialize 会保证顺序 + appearanceRepository.ensureDefaultSettingsExist() + .then(() => console.log('[DB Init] 外观设置初始化检查完成。')) + .catch(initErr => console.error('[DB Init] 初始化外观设置时出错:', initErr)); + } + }); + + // TODO: 在这里添加其他表的创建和初始化逻辑... + + console.log('[DB Init] 数据库初始化序列提交。'); + }); +}; + export const getDb = (): sqlite3.Database => { if (!dbInstance) { + console.log(`[DB] 尝试连接到数据库: ${dbPath}`); dbInstance = new verboseSqlite3.Database(dbPath, (err) => { if (err) { - console.error('打开数据库时出错:', err.message); - // 在实际应用中,这里可能需要更健壮的错误处理,例如直接退出进程 + console.error('[DB] 打开数据库时出错:', err.message); process.exit(1); } else { - console.log(`已连接到 SQLite 数据库: ${dbPath}`); - // 可选:启用外键约束 (如果数据库设计中使用了外键) - // dbInstance.run('PRAGMA foreign_keys = ON;', (pragmaErr) => { - // if (pragmaErr) { - // console.error('启用外键约束失败:', pragmaErr.message); - // } - // }); + console.log(`[DB] 已连接到 SQLite 数据库: ${dbPath}`); + // 连接成功后运行初始化逻辑 + runDatabaseInitializations(dbInstance as sqlite3.Database); } }); } return dbInstance; }; -// 优雅停机:在应用接收到中断信号 (如 Ctrl+C) 时关闭数据库连接 +// 优雅停机 process.on('SIGINT', () => { if (dbInstance) { + console.log('[DB] 收到 SIGINT,正在关闭数据库连接...'); dbInstance.close((err) => { if (err) { - console.error('关闭数据库时出错:', err.message); + console.error('[DB] 关闭数据库时出错:', err.message); } else { - console.log('数据库连接已关闭。'); + console.log('[DB] 数据库连接已关闭。'); } - process.exit(0); + process.exit(err ? 1 : 0); }); } else { process.exit(0); @@ -57,3 +106,8 @@ process.on('SIGINT', () => { }); export default getDb; + +// 注意:为了让这个文件工作,需要修改 repository 文件: +// 1. 从 repository 文件中移除顶层的 createTableIfNotExists() 调用。 +// 2. 从 repository 文件中导出 SQL_CREATE_TABLE 字符串常量。 +// 3. 从 appearance.repository.ts 导出 ensureDefaultSettingsExist 函数。 diff --git a/packages/backend/src/repositories/appearance.repository.ts b/packages/backend/src/repositories/appearance.repository.ts index 974a8bf..a223c6f 100644 --- a/packages/backend/src/repositories/appearance.repository.ts +++ b/packages/backend/src/repositories/appearance.repository.ts @@ -2,33 +2,40 @@ import { getDb } from '../database'; import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types'; import { defaultUiTheme } from '../config/default-themes'; // Assuming default UI theme is here too -const db = getDb(); +// const db = getDb(); // Removed top-level call to avoid circular dependency issues const TABLE_NAME = 'appearance_settings'; const SETTINGS_ID = 1; // Use a fixed ID for the single row of global settings /** - * 创建 appearance_settings 表 (如果不存在) + * SQL语句:创建 appearance_settings 表 + */ +export const SQL_CREATE_TABLE = ` + CREATE TABLE IF NOT EXISTS ${TABLE_NAME} ( + id INTEGER PRIMARY KEY, -- Fixed ID for the single settings row + custom_ui_theme TEXT, + active_terminal_theme_id INTEGER NULL, -- 修改为 INTEGER NULL + terminal_font_family TEXT, + terminal_font_size INTEGER, + editor_font_size INTEGER, -- 新增:编辑器字体大小 + terminal_background_image TEXT, + page_background_image TEXT, + updated_at INTEGER NOT NULL, + FOREIGN KEY(active_terminal_theme_id) REFERENCES terminal_themes(id) -- 添加外键约束 + ); +`; + +/** + * 创建 appearance_settings 表 (如果不存在) - 不再自动调用 */ const createTableIfNotExists = () => { - const sql = ` - CREATE TABLE IF NOT EXISTS ${TABLE_NAME} ( - id INTEGER PRIMARY KEY, -- Fixed ID for the single settings row - custom_ui_theme TEXT, - active_terminal_theme_id TEXT, - terminal_font_family TEXT, - terminal_font_size INTEGER, - editor_font_size INTEGER, -- 新增:编辑器字体大小 - terminal_background_image TEXT, - page_background_image TEXT, - updated_at INTEGER NOT NULL - ); - `; - db.run(sql, (err) => { + // This function is no longer called automatically, initialization is handled in database.ts + getDb().run(SQL_CREATE_TABLE, (err) => { if (err) { console.error(`创建 ${TABLE_NAME} 表失败:`, err.message); } else { - // 确保默认设置行存在 - ensureDefaultSettingsExist(); + console.log(`${TABLE_NAME} 表已存在或已创建。`); + // 确保默认设置行存在 - 这个调用也应该移到 database.ts + // ensureDefaultSettingsExist(); } }); }; @@ -39,7 +46,7 @@ const mapRowToAppearanceSettings = (row: any): AppearanceSettings => { return { _id: row.id.toString(), customUiTheme: row.custom_ui_theme, - activeTerminalThemeId: row.active_terminal_theme_id, + activeTerminalThemeId: row.active_terminal_theme_id, // 直接返回数字或 null terminalFontFamily: row.terminal_font_family, terminalFontSize: row.terminal_font_size, editorFontSize: row.editor_font_size, // 新增:编辑器字体大小映射 @@ -58,7 +65,7 @@ const getDefaultAppearanceSettings = (): AppearanceSettings => { return { _id: SETTINGS_ID.toString(), customUiTheme: JSON.stringify(defaultUiTheme), // Use default UI theme - activeTerminalThemeId: undefined, // Needs to be set after querying default theme ID + activeTerminalThemeId: null, // 初始应为 null,待 findAndSetDefaultThemeId 设置 terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"', // Default font terminalFontSize: 14, editorFontSize: 14, // 新增:默认编辑器字体大小 @@ -72,12 +79,14 @@ const getDefaultAppearanceSettings = (): AppearanceSettings => { /** - * 确保默认设置行存在 + * 确保默认设置行存在,并在需要时设置默认激活主题 ID。 + * 这个函数应该在数据库初始化时,在预设主题初始化之后调用。 */ -const ensureDefaultSettingsExist = () => { +export const ensureDefaultSettingsExist = async () => { // 改为 async 以便内部 await const defaults = getDefaultAppearanceSettings(); - const sqlSelect = `SELECT id FROM ${TABLE_NAME} WHERE id = ?`; - db.get(sqlSelect, [SETTINGS_ID], (err, row) => { + const sqlSelect = `SELECT id, active_terminal_theme_id FROM ${TABLE_NAME} WHERE id = ?`; // 同时查询当前 ID + // 将回调函数改为 async + getDb().get(sqlSelect, [SETTINGS_ID], async (err, row) => { if (err) { console.error(`检查默认外观设置时出错:`, err.message); return; @@ -91,10 +100,10 @@ const ensureDefaultSettingsExist = () => { updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) -- 调整占位符数量 `; - db.run(sqlInsert, [ - SETTINGS_ID, - defaults.customUiTheme, - defaults.activeTerminalThemeId, // Initially undefined + getDb().run(sqlInsert, [ + SETTINGS_ID, + defaults.customUiTheme, + defaults.activeTerminalThemeId, // Initially null defaults.terminalFontFamily, defaults.terminalFontSize, defaults.editorFontSize, // 添加 editor_font_size 默认值参数 @@ -113,8 +122,8 @@ const ensureDefaultSettingsExist = () => { } }); } else { - // If row exists, still check if default theme ID needs setting - findAndSetDefaultThemeId(); + // 如果行已存在,直接调用 findAndSetDefaultThemeId 检查并设置默认 ID + await findAndSetDefaultThemeId(); // 使用 await } }); }; @@ -127,19 +136,22 @@ const findAndSetDefaultThemeId = async () => { // Find the default theme from the other table const defaultThemeSql = `SELECT id FROM terminal_themes WHERE is_system_default = 1 LIMIT 1`; // Explicitly type the row or use type assertion - db.get(defaultThemeSql, [], async (err, defaultThemeRow: { id: number } | undefined) => { + getDb().get(defaultThemeSql, [], async (err, defaultThemeRow: { id: number } | undefined) => { if (err) { console.error("查找默认终端主题 ID 失败:", err.message); return; } if (defaultThemeRow) { - const defaultThemeId = defaultThemeRow.id.toString(); + const defaultThemeIdNum = defaultThemeRow.id; // 直接使用数字 ID // Check current appearance settings const currentSettings = await getAppearanceSettings(); - if (currentSettings && currentSettings.activeTerminalThemeId !== defaultThemeId) { - // Update only if the active ID is not already the default - console.log(`设置默认激活终端主题 ID 为: ${defaultThemeId}`); - await updateAppearanceSettings({ activeTerminalThemeId: defaultThemeId }); + // Only set the default theme ID if no active theme ID is currently set (i.e., it's null in the DB) + if (currentSettings && currentSettings.activeTerminalThemeId === null) { + console.log(`数据库中未设置激活终端主题,设置为默认数字 ID: ${defaultThemeIdNum}`); + // 更新时传递数字 ID + await updateAppearanceSettings({ activeTerminalThemeId: defaultThemeIdNum }); + } else { + console.log(`数据库中已设置激活终端主题数字 ID (${currentSettings?.activeTerminalThemeId}) 或未找到默认主题,跳过设置默认 ID。`); } } else { console.warn("未找到系统默认终端主题,无法设置 activeTerminalThemeId。"); @@ -157,7 +169,7 @@ const findAndSetDefaultThemeId = async () => { */ export const getAppearanceSettings = async (): Promise => { return new Promise((resolve, reject) => { - db.get(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [SETTINGS_ID], (err, row) => { + getDb().get(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [SETTINGS_ID], (err, row) => { if (err) { console.error('获取外观设置失败:', err.message); reject(new Error('获取外观设置失败')); @@ -180,14 +192,22 @@ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto) // Dynamically build the SET part of the query const updates: string[] = []; - for (const key in settingsDto) { - if (Object.prototype.hasOwnProperty.call(settingsDto, key)) { - const dbKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); // Convert camelCase to snake_case - // Ensure only valid keys are updated (Added editor_font_size) - if (['custom_ui_theme', 'active_terminal_theme_id', 'terminal_font_family', 'terminal_font_size', 'editor_font_size', 'terminal_background_image', 'page_background_image'].includes(dbKey)) { - updates.push(`${dbKey} = ?`); - params.push((settingsDto as any)[key]); + const validDbKeys = ['custom_ui_theme', 'active_terminal_theme_id', 'terminal_font_family', 'terminal_font_size', 'editor_font_size', 'terminal_background_image', 'page_background_image']; + + // Iterate over potential keys to update + for (const key of Object.keys(settingsDto) as Array) { + const dbKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); // Convert camelCase to snake_case + + if (validDbKeys.includes(dbKey)) { + const value = settingsDto[key]; + // active_terminal_theme_id 应该是数字或 null + if (dbKey === 'active_terminal_theme_id' && typeof value !== 'number' && value !== null) { + console.error(`[AppearanceRepo] 更新 active_terminal_theme_id 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`); + continue; // 跳过无效类型 } + updates.push(`${dbKey} = ?`); + // 直接推入值 (数字或 null) + params.push(value); } } @@ -199,16 +219,21 @@ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto) params.push(SETTINGS_ID); return new Promise((resolve, reject) => { - db.run(sql, params, function (err) { + // --- 增加详细日志 --- + console.log(`[AppearanceRepo] Executing SQL: ${sql}`); + console.log(`[AppearanceRepo] With Params: ${JSON.stringify(params)}`); + // --- 日志结束 --- + getDb().run(sql, params, function (err) { if (err) { console.error('更新外观设置失败:', err.message); reject(new Error('更新外观设置失败')); } else { + console.log(`[AppearanceRepo] 更新外观设置成功,影响行数: ${this.changes}`); resolve(this.changes > 0); } }); }); }; -// 初始化时创建表 -createTableIfNotExists(); +// 初始化时创建表 - Removed: Initialization is now handled in database.ts +// createTableIfNotExists(); diff --git a/packages/backend/src/repositories/terminal-theme.repository.ts b/packages/backend/src/repositories/terminal-theme.repository.ts index cff5b6e..125ae52 100644 --- a/packages/backend/src/repositories/terminal-theme.repository.ts +++ b/packages/backend/src/repositories/terminal-theme.repository.ts @@ -2,29 +2,34 @@ import { getDb } from '../database'; import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types'; import { defaultXtermTheme } from '../config/default-themes'; // 假设默认主题配置在此 -const db = getDb(); +// const db = getDb(); // Removed top-level call to avoid circular dependency issues /** - * 创建 terminal_themes 表 (如果不存在) + * SQL语句:创建 terminal_themes 表 + */ +export const SQL_CREATE_TABLE = ` + CREATE TABLE IF NOT EXISTS terminal_themes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + theme_data TEXT NOT NULL, -- Store ITheme as JSON string + is_preset BOOLEAN NOT NULL DEFAULT 0, + preset_key TEXT NULL UNIQUE, -- 可选,用于识别预设主题 + is_system_default BOOLEAN NOT NULL DEFAULT 0, -- 新增:标记是否为系统默认主题 + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); +`; + +/** + * 创建 terminal_themes 表 (如果不存在) - 不再自动调用 */ const createTableIfNotExists = () => { - const sql = ` - CREATE TABLE IF NOT EXISTS terminal_themes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - theme_data TEXT NOT NULL, -- Store ITheme as JSON string - is_preset BOOLEAN NOT NULL DEFAULT 0, - is_system_default BOOLEAN DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - `; - db.run(sql, (err) => { + // This function is no longer called automatically, initialization is handled in database.ts + getDb().run(SQL_CREATE_TABLE, (err) => { if (err) { console.error('创建 terminal_themes 表失败:', err.message); } else { - // 表创建成功后,初始化预设主题 - initializePresetThemes(); + console.log('terminal_themes 表已存在或已创建。'); } }); }; @@ -36,7 +41,7 @@ const mapRowToTerminalTheme = (row: any): TerminalTheme => { name: row.name, themeData: JSON.parse(row.theme_data), // 解析 JSON 字符串 isPreset: !!row.is_preset, // 转换为布尔值 - isSystemDefault: !!row.is_system_default, + isSystemDefault: !!row.is_system_default, // 映射新增的列 createdAt: row.created_at, updatedAt: row.updated_at, }; @@ -48,7 +53,7 @@ const mapRowToTerminalTheme = (row: any): TerminalTheme => { */ export const findAllThemes = async (): Promise => { return new Promise((resolve, reject) => { - db.all('SELECT * FROM terminal_themes ORDER BY is_preset DESC, name ASC', [], (err, rows) => { + getDb().all('SELECT * FROM terminal_themes ORDER BY is_preset DESC, name ASC', [], (err, rows) => { if (err) { console.error('查询所有终端主题失败:', err.message); reject(new Error('查询终端主题失败')); @@ -66,7 +71,7 @@ export const findAllThemes = async (): Promise => { */ export const findThemeById = async (id: number): Promise => { return new Promise((resolve, reject) => { - db.get('SELECT * FROM terminal_themes WHERE id = ?', [id], (err, row) => { + getDb().get('SELECT * FROM terminal_themes WHERE id = ?', [id], (err, row) => { if (err) { console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message); reject(new Error('查询终端主题失败')); @@ -90,7 +95,7 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise { - db.run(sql, [themeDto.name, themeDataJson, now, now], function (err) { + getDb().run(sql, [themeDto.name, themeDataJson, now, now], function (err) { if (err) { console.error('创建新终端主题失败:', err.message); // 特别处理唯一约束错误 @@ -132,7 +137,7 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto): WHERE id = ? AND is_preset = 0 `; return new Promise((resolve, reject) => { - db.run(sql, [themeDto.name, themeDataJson, now, id], function (err) { + getDb().run(sql, [themeDto.name, themeDataJson, now, id], function (err) { if (err) { console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message); if (err.message.includes('UNIQUE constraint failed')) { @@ -156,7 +161,7 @@ export const deleteTheme = async (id: number): Promise => { // 只允许删除非预设主题 const sql = 'DELETE FROM terminal_themes WHERE id = ? AND is_preset = 0'; return new Promise((resolve, reject) => { - db.run(sql, [id], function (err) { + getDb().run(sql, [id], function (err) { if (err) { console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message); reject(new Error('删除终端主题失败')); @@ -168,36 +173,62 @@ export const deleteTheme = async (id: number): Promise => { }; /** - * 初始化预设主题 (如果不存在) + * 初始化预设主题到数据库 (如果不存在) + * 这个函数应该在数据库连接成功后,由应用初始化逻辑调用。 + * @param presets 预设主题定义数组 (包含 name, themeData, isPreset=true, 可选 preset_key) */ -export const initializePresetThemes = async () => { - const defaultPresetName = '默认'; // Default Light - const themeDataJson = JSON.stringify(defaultXtermTheme); - const now = Date.now(); +export const initializePresetThemes = async (presets: Array & { preset_key?: string }>) => { + console.log('[DB Init] 开始检查并初始化预设主题...'); + const now = Date.now(); - // 检查默认预设是否存在 - db.get('SELECT id FROM terminal_themes WHERE name = ? AND is_preset = 1', [defaultPresetName], (err, row) => { - if (err) { - console.error('检查预设主题时出错:', err.message); - return; + // 使用 for...of 循环确保顺序执行检查和插入(避免并发 UNIQUE 约束问题) + for (const preset of presets) { + await new Promise((resolve, reject) => { + // 优先使用 preset_key 检查,如果提供了的话 + const checkColumn = preset.preset_key ? 'preset_key' : 'name'; + const checkValue = preset.preset_key ?? preset.name; + + getDb().get(`SELECT id FROM terminal_themes WHERE ${checkColumn} = ? AND is_preset = 1`, [checkValue], (err, row) => { + if (err) { + console.error(`[DB Init] 检查预设主题 "${preset.name}" (Key: ${checkValue}) 时出错:`, err.message); + return reject(err); + } + if (!row) { + const themeDataJson = JSON.stringify(preset.themeData); + const isDefault = preset.preset_key === 'default' ? 1 : 0; + // 始终包含 preset_key 列,如果不存在则插入 NULL + const columns = ['name', 'theme_data', 'is_preset', 'is_system_default', 'preset_key', 'created_at', 'updated_at']; // 7 columns + const values = [preset.name, themeDataJson, 1, isDefault, preset.preset_key ?? null, now, now]; // 7 values + const placeholders = ['?', '?', '?', '?', '?', '?', '?']; // 7 placeholders + + // 移除动态添加 preset_key 的逻辑 + // if (preset.preset_key) { + // values.push(preset.preset_key); + // placeholders.push('?'); + // } + + const insertSql = ` + INSERT INTO terminal_themes (${columns.join(', ')}) + VALUES (${placeholders.join(', ')}) + `; + + getDb().run(insertSql, values, (insertErr) => { + if (insertErr) { + console.error(`[DB Init] 初始化预设主题 "${preset.name}" (Key: ${preset.preset_key ?? 'N/A'}) 失败:`, insertErr.message); // 调整日志输出 + return reject(insertErr); + } else { + console.log(`[DB Init] 预设主题 "${preset.name}" (Key: ${checkValue}) 已初始化到数据库。`); + resolve(); + } + }); + } else { + // console.log(`[DB Init] 预设主题 "${preset.name}" (Key: ${checkValue}) 已存在,跳过初始化。`); + resolve(); + } + }); + }); } - if (!row) { - // 如果不存在,则插入 - const insertSql = ` - INSERT INTO terminal_themes (name, theme_data, is_preset, is_system_default, created_at, updated_at) - VALUES (?, ?, 1, 1, ?, ?) - `; - db.run(insertSql, [defaultPresetName, themeDataJson, now, now], (insertErr) => { - if (insertErr) { - console.error(`初始化预设主题 "${defaultPresetName}" 失败:`, insertErr.message); - } else { - console.log(`预设主题 "${defaultPresetName}" 已初始化。`); - } - }); - } - // 在这里可以添加更多预设主题的初始化逻辑 - }); + console.log('[DB Init] 预设主题检查和初始化完成。'); }; -// 初始化时创建表 -createTableIfNotExists(); +// 移除所有在此文件中的初始化调用和相关导入,它们应该在 database.ts 或 app.ts 中进行 diff --git a/packages/backend/src/services/appearance.service.ts b/packages/backend/src/services/appearance.service.ts index 74582d7..b50abc8 100644 --- a/packages/backend/src/services/appearance.service.ts +++ b/packages/backend/src/services/appearance.service.ts @@ -18,24 +18,28 @@ export const getSettings = async (): Promise => { export const updateSettings = async (settingsDto: UpdateAppearanceDto): Promise => { // 验证 activeTerminalThemeId (如果提供了) if (settingsDto.activeTerminalThemeId !== undefined && settingsDto.activeTerminalThemeId !== null) { - const themeIdNum = parseInt(settingsDto.activeTerminalThemeId, 10); - if (isNaN(themeIdNum)) { - throw new Error(`无效的终端主题 ID 格式: ${settingsDto.activeTerminalThemeId}`); + const themeIdNum = settingsDto.activeTerminalThemeId; // ID is now number | null + // 验证 ID 是否为有效的数字 + if (typeof themeIdNum !== 'number') { + console.error(`[AppearanceService] 收到的 activeTerminalThemeId 不是有效的数字: ${themeIdNum}`); + throw new Error(`无效的终端主题 ID 类型,应为数字。`); } try { + // 直接使用数字 ID 调用 findThemeById 进行验证 const themeExists = await terminalThemeRepository.findThemeById(themeIdNum); if (!themeExists) { - throw new Error(`指定的终端主题 ID 不存在: ${settingsDto.activeTerminalThemeId}`); + console.warn(`[AppearanceService] 尝试更新为不存在的终端主题数字 ID: ${themeIdNum}`); + throw new Error(`指定的终端主题 ID 不存在: ${themeIdNum}`); } - } catch (e: any) { // Catch potential errors from findThemeById as well - console.error(`验证终端主题 ID (${settingsDto.activeTerminalThemeId}) 时出错:`, e.message); - // Rethrow a more specific error or the original one - throw new Error(`验证终端主题 ID 时出错: ${e.message || settingsDto.activeTerminalThemeId}`); + console.log(`[AppearanceService] 终端主题数字 ID ${themeIdNum} 验证通过。`); + } catch (e: any) { + console.error(`[AppearanceService] 验证终端主题数字 ID (${themeIdNum}) 时出错:`, e.message); + throw new Error(`验证终端主题 ID 时出错: ${e.message || themeIdNum}`); } - } else if (settingsDto.hasOwnProperty('activeTerminalThemeId')) { - // Handle explicit setting to null/undefined (meaning reset to default/no theme) - // The repository update logic handles null/undefined correctly, so no specific validation needed here. - // We just need to ensure the key exists in the DTO if it's meant to be cleared. + } else if (settingsDto.hasOwnProperty('activeTerminalThemeId') && settingsDto.activeTerminalThemeId === null) { + // 处理显式设置为 null (表示重置为默认/无用户主题) + console.log(`[AppearanceService] 接收到将 activeTerminalThemeId 设置为 null 的请求。`); + // 仓库层会处理 null } // 验证 terminalFontSize (如果提供了) diff --git a/packages/backend/src/types/appearance.types.ts b/packages/backend/src/types/appearance.types.ts index aaa2406..e71a38f 100644 --- a/packages/backend/src/types/appearance.types.ts +++ b/packages/backend/src/types/appearance.types.ts @@ -7,7 +7,7 @@ export interface AppearanceSettings { _id?: string; // 通常只有一个文档,ID 固定或不使用 userId?: string; // 如果需要区分用户设置 (当前假设为全局) customUiTheme?: string; // UI 主题 (CSS 变量 JSON 字符串) - activeTerminalThemeId?: string; // 当前激活的终端主题 ID (对应 terminal_themes 表的 _id) + activeTerminalThemeId?: number | null; // 修改为数字 ID 或 null terminalFontFamily?: string; // 终端字体列表字符串 terminalFontSize?: number; // 终端字体大小 (px) terminalBackgroundImage?: string; // 终端背景图片 URL 或路径 diff --git a/packages/frontend/src/components/StyleCustomizer.vue b/packages/frontend/src/components/StyleCustomizer.vue index f5cd452..e3b11d1 100644 --- a/packages/frontend/src/components/StyleCustomizer.vue +++ b/packages/frontend/src/components/StyleCustomizer.vue @@ -13,8 +13,8 @@ const { appearanceSettings, // <-- 添加这个 ref currentUiTheme, // currentTerminalTheme, // 这个是计算属性,只读,在编辑时不需要直接用 - activeTerminalThemeId, - availableTerminalThemes, + activeTerminalThemeId, // 现在是 number | null | undefined + allTerminalThemes, // 使用重命名后的变量 currentTerminalFontFamily, currentTerminalFontSize, currentEditorFontSize, // <-- 新增 @@ -359,16 +359,23 @@ const handleSaveEditorFontSize = async () => { // 应用选定的终端主题 const handleApplyTheme = async (theme: TerminalTheme) => { - // 确保 theme._id 存在且不等于当前激活的 ID - // setActiveTerminalTheme 期望 string | null,而 theme._id 是 string | undefined - // 如果 theme._id 是 undefined (理论上不应发生在列表项上),传递 null - const themeIdToApply = theme._id ?? null; - if (themeIdToApply === null || themeIdToApply === activeTerminalThemeId.value) return; + // theme._id 是字符串 ID + if (!theme._id) return; // 防御性检查 + + // 将字符串 ID 转换为数字进行比较 + const themeIdNum = parseInt(theme._id, 10); + if (isNaN(themeIdNum)) { + console.error(`无效的主题 ID 格式: ${theme._id}`); + return; + } + + // activeTerminalThemeId.value 是 number | null + if (themeIdNum === activeTerminalThemeId.value) return; // 如果已经是激活的,则不操作 try { - await appearanceStore.setActiveTerminalTheme(themeIdToApply); + // setActiveTerminalTheme action 现在需要字符串 ID + await appearanceStore.setActiveTerminalTheme(theme._id); // 成功后 activeTerminalThemeId 会自动更新 - // alert(`Theme '${theme.name}' applied successfully`); // 移除成功提示 } catch (error: any) { console.error("应用终端主题失败:", error); alert(t('styleCustomizer.setActiveThemeFailed', { message: error.message })); @@ -601,16 +608,19 @@ const handleImportThemeFile = async (event: Event) => { // 处理主题导出 (导出当前激活的主题) const handleExportActiveTheme = async () => { - const currentId = activeTerminalThemeId.value; // activeTerminalThemeId 是 Ref - if (currentId) { // 检查 currentId 是否为真值 (不是 undefined 或空字符串) + const currentIdNum = activeTerminalThemeId.value; // 现在是 number | null | undefined + // 必须同时检查 null 和 undefined + if (currentIdNum !== null && currentIdNum !== undefined) { try { - await appearanceStore.exportTerminalTheme(currentId); + // exportTerminalTheme action 需要字符串 ID + await appearanceStore.exportTerminalTheme(currentIdNum.toString()); } catch (error: any) { console.error("导出主题失败:", error); alert(t('styleCustomizer.exportFailed', { message: error.message })); } } else { - alert(t('styleCustomizer.noActiveThemeToExport')); // 需要添加翻译 + console.warn("尝试导出主题,但 activeTerminalThemeId 为 null 或 undefined"); + alert(t('styleCustomizer.noActiveThemeToExport')); } }; @@ -698,24 +708,31 @@ const formatXtermLabel = (key: keyof ITheme): string => { // 获取当前激活主题的名称 const activeThemeName = computed(() => { - const theme = availableTerminalThemes.value.find(t => t._id === activeTerminalThemeId.value); - // 如果找不到主题 (例如 ID 无效或列表为空),则显示提示 - return theme ? theme.name : 'No active theme selected'; + const currentIdNum = activeTerminalThemeId.value; // number | null | undefined + // 同时检查 null 和 undefined + if (currentIdNum === null || currentIdNum === undefined) { + return '未选择主题'; // 或者返回默认主题名称? + } + // 在 allTerminalThemes 中查找 + // 将数字 ID 转为字符串进行比较 + const theme = allTerminalThemes.value.find((t: TerminalTheme) => t._id === currentIdNum.toString()); + return theme ? theme.name : '未知主题'; }); -// 过滤和排序主题列表 +// 过滤和排序主题列表 (使用 allTerminalThemes) const filteredAndSortedThemes = computed(() => { - const searchTerm = themeSearchTerm.value.toLowerCase().trim(); // 转小写并去除首尾空格 - let themes = [...availableTerminalThemes.value]; // 创建副本以进行排序 + const searchTerm = themeSearchTerm.value.toLowerCase().trim(); + // 使用 allTerminalThemes + let themes = [...allTerminalThemes.value]; // 过滤 if (searchTerm) { - themes = themes.filter(theme => theme.name.toLowerCase().includes(searchTerm)); + // 显式指定 theme 类型 + themes = themes.filter((theme: TerminalTheme) => theme.name.toLowerCase().includes(searchTerm)); } - // 排序逻辑已移除 - // 默认按名称升序排序 - themes.sort((a, b) => a.name.localeCompare(b.name)); + // 按名称升序排序 + themes.sort((a: TerminalTheme, b: TerminalTheme) => a.name.localeCompare(b.name)); return themes; }); diff --git a/packages/frontend/src/components/Terminal.vue b/packages/frontend/src/components/Terminal.vue index 3877bc3..d2f4f8b 100644 --- a/packages/frontend/src/components/Terminal.vue +++ b/packages/frontend/src/components/Terminal.vue @@ -214,7 +214,16 @@ onMounted(() => { watch(currentTerminalTheme, (newTheme) => { if (terminal) { console.log(`[Terminal ${props.sessionId}] 应用新终端主题。`); + // 直接修改 options 对象 terminal.options.theme = newTheme; + // 修改选项后需要刷新终端才能生效 + try { + // 刷新整个视口 + terminal.refresh(0, terminal.rows - 1); + console.log(`[Terminal ${props.sessionId}] 终端已刷新以应用新主题。`); + } catch (e) { + console.warn(`[Terminal ${props.sessionId}] 刷新终端以应用主题时出错:`, e); + } } }, { deep: true }); diff --git a/packages/frontend/src/stores/appearance.store.ts b/packages/frontend/src/stores/appearance.store.ts index 08e6dcf..fc02b2e 100644 --- a/packages/frontend/src/stores/appearance.store.ts +++ b/packages/frontend/src/stores/appearance.store.ts @@ -26,15 +26,11 @@ export const useAppearanceStore = defineStore('appearance', () => { // Appearance Settings State const appearanceSettings = ref>({}); // 从 API 获取的原始设置 - const userTerminalThemes = ref([]); // <-- 新增:只存储用户自定义主题 + const allTerminalThemes = ref([]); // 重命名: 存储从后端获取的所有主题 // --- Computed Properties (Getters) --- - // 合并后的可用终端主题列表 (包括预设和用户自定义) - const availableTerminalThemes = computed(() => { - // 确保预设主题在前,用户主题在后 - return [...presetTerminalThemes, ...userTerminalThemes.value]; - }); + // 移除 availableTerminalThemes 计算属性,直接使用 allTerminalThemes // 当前应用的 UI 主题 (CSS 变量对象) const currentUiTheme = computed>(() => { return safeJsonParse(appearanceSettings.value.customUiTheme, defaultUiTheme); @@ -45,11 +41,16 @@ export const useAppearanceStore = defineStore('appearance', () => { // 当前应用的终端主题对象 (ITheme) const currentTerminalTheme = computed(() => { - if (!activeTerminalThemeId.value || availableTerminalThemes.value.length === 0) { - return defaultXtermTheme; // 回退到默认 + const activeId = activeTerminalThemeId.value; // number | null | undefined + if (activeId === null || activeId === undefined || allTerminalThemes.value.length === 0) { + // 如果没有激活 ID 或列表为空,查找默认主题 + // TODO: 需要确认默认主题的识别方式 (preset_key='default' 或 name='默认') + const defaultTheme = allTerminalThemes.value.find(t => t.name === '默认'); // 假设按名称查找 + return defaultTheme ? defaultTheme.themeData : defaultXtermTheme; } - const activeTheme = availableTerminalThemes.value.find(t => t._id === activeTerminalThemeId.value); - return activeTheme ? activeTheme.themeData : defaultXtermTheme; // 找不到也回退 + // 根据数字 ID 查找 (需要将 theme._id 转回数字比较) + const activeTheme = allTerminalThemes.value.find(t => parseInt(t._id ?? '-1', 10) === activeId); + return activeTheme ? activeTheme.themeData : defaultXtermTheme; // 找不到也回退到 xterm 默认 }); // 当前终端字体设置 @@ -89,25 +90,29 @@ export const useAppearanceStore = defineStore('appearance', () => { // 并行加载外观设置和主题列表 const [settingsResponse, themesResponse] = await Promise.all([ axios.get('/api/v1/appearance'), - axios.get('/api/v1/terminal-themes') + axios.get('/api/v1/terminal-themes') // 获取所有主题 ]); appearanceSettings.value = settingsResponse.data; - userTerminalThemes.value = themesResponse.data; // <-- 更新 userTerminalThemes + allTerminalThemes.value = themesResponse.data; // 更新 allTerminalThemes console.log('[AppearanceStore] 外观设置已加载:', appearanceSettings.value); - console.log('[AppearanceStore] 用户终端主题列表已加载:', userTerminalThemes.value); // <-- 修改日志 + console.log('[AppearanceStore] 所有终端主题列表已加载:', allTerminalThemes.value); + + // --- 后端返回的 activeTerminalThemeId 已经是 number | null --- + // 前端不再需要设置默认主题 ID 的逻辑,后端初始化时会保证它不为 NULL + // 如果后端返回 null (理论上不应发生,除非初始化失败),则 currentTerminalTheme 计算属性会回退到 defaultXtermTheme // 应用加载的 UI 主题 applyUiTheme(currentUiTheme.value); // 应用背景 applyPageBackground(); - // 终端背景和主题将在 Terminal 组件中应用 + // 终端主题将由 Terminal 组件根据 activeTerminalThemeId 自动应用 } catch (err: any) { console.error('加载外观数据失败:', err); error.value = err.response?.data?.message || err.message || '加载外观数据失败'; // 出错时应用默认值 appearanceSettings.value = {}; // 清空可能不完整的设置 - userTerminalThemes.value = []; // <-- 清空 userTerminalThemes + allTerminalThemes.value = []; // 清空 allTerminalThemes applyUiTheme(defaultUiTheme); applyPageBackground(); // 应用默认背景(可能为空) } finally { @@ -127,30 +132,16 @@ export const useAppearanceStore = defineStore('appearance', () => { /** * 更新外观设置 (不包括主题列表管理) - * @param updates 要更新的设置项 + * @param updates 要更新的设置项 (activeTerminalThemeId 应为 number | null) */ async function updateAppearanceSettings(updates: UpdateAppearanceDto) { try { - // --- 修复预设主题闪烁问题 --- - // 检查本次更新是否是为了清除后端 activeTerminalThemeId (因为选择了预设) - const isClearingThemeForPreset = updates.activeTerminalThemeId === undefined && - appearanceSettings.value.activeTerminalThemeId?.startsWith('preset-'); - const presetThemeIdToRestore = isClearingThemeForPreset ? appearanceSettings.value.activeTerminalThemeId : null; - // --- 修复结束 --- - + // 移除预设主题闪烁修复逻辑,不再需要 const response = await axios.put('/api/v1/appearance', updates); // 使用后端返回的最新设置更新本地状态 appearanceSettings.value = response.data; console.log('[AppearanceStore] 外观设置已更新:', appearanceSettings.value); - // --- 修复预设主题闪烁问题 --- - // 如果之前是为了清除预设主题而更新的,现在恢复前端的预设 ID - if (presetThemeIdToRestore) { - appearanceSettings.value.activeTerminalThemeId = presetThemeIdToRestore; - console.log(`[AppearanceStore] Restored preset theme ID after backend update: ${presetThemeIdToRestore}`); - } - // --- 修复结束 --- - // 如果 UI 主题或背景更新,重新应用 if (updates.customUiTheme !== undefined) applyUiTheme(currentUiTheme.value); if (updates.pageBackgroundImage !== undefined) applyPageBackground(); // 移除 pageBackgroundOpacity 检查 @@ -179,28 +170,31 @@ export const useAppearanceStore = defineStore('appearance', () => { /** * 设置激活的终端主题 - * @param themeId 主题 ID + * @param themeId 主题的字符串 ID (来自 UI) 或 null (用于重置,但新逻辑下不直接使用 null) */ - async function setActiveTerminalTheme(themeId: string | null) { - // 检查是否为预设主题 ID - if (themeId && themeId.startsWith('preset-')) { - // 1. 直接更新本地状态以立即反映前端选择 - appearanceSettings.value.activeTerminalThemeId = themeId; - console.log(`[AppearanceStore] Applied preset theme locally: ${themeId}`); - // 2. 通知后端没有激活的用户主题 (发送 undefined) - try { - await updateAppearanceSettings({ activeTerminalThemeId: undefined }); // <-- 使用 undefined 替代 null - console.log('[AppearanceStore] Notified backend: No user theme active (preset selected).'); - } catch (error) { - // 即使通知后端失败,前端也已应用,记录错误但可能不需要回滚前端状态 - console.error('[AppearanceStore] Failed to notify backend about preset theme selection:', error); - // 可选:如果希望严格同步,可以在这里抛出错误或回滚本地状态 - // appearanceSettings.value.activeTerminalThemeId = // previous value? 需要额外逻辑记录 - // throw error; // 重新抛出错误,让调用者处理 - } - } else { - // 对于用户主题或 null,按原逻辑更新后端 - await updateAppearanceSettings({ activeTerminalThemeId: themeId ?? undefined }); + async function setActiveTerminalTheme(themeId: string) { // 参数改为 string,不允许 null + const previousActiveId = appearanceSettings.value.activeTerminalThemeId; // 记录之前的数字 ID 或 null + + // 1. 将传入的字符串 ID 转换为数字 + const idNum = parseInt(themeId, 10); + if (isNaN(idNum)) { + console.error(`[AppearanceStore] setActiveTerminalTheme 接收到无效的数字 ID 字符串: ${themeId}`); + throw new Error(`无效的主题 ID: ${themeId}`); + } + + // 2. 立即更新前端本地状态 (使用数字 ID) + appearanceSettings.value.activeTerminalThemeId = idNum; + console.log(`[AppearanceStore] Applied theme locally (ID): ${idNum}`); + + // 3. 更新后端 (发送数字 ID) + try { + await updateAppearanceSettings({ activeTerminalThemeId: idNum }); + console.log(`[AppearanceStore] Notified backend. Sent activeTerminalThemeId: ${idNum}`); + } catch (error) { + // 如果更新后端失败,回滚前端状态 + console.error('[AppearanceStore] Failed to update backend activeTerminalThemeId:', error); + appearanceSettings.value.activeTerminalThemeId = previousActiveId; // 回滚到之前的数字 ID 或 null + throw new Error(`应用主题失败: ${error instanceof Error ? error.message : String(error)}`); } } @@ -231,18 +225,7 @@ export const useAppearanceStore = defineStore('appearance', () => { // --- 终端主题列表管理 Actions --- /** - * 重新加载终端主题列表 - */ - async function reloadTerminalThemes() { - try { - const response = await axios.get('/api/v1/terminal-themes'); - userTerminalThemes.value = response.data; // <-- 更新 userTerminalThemes - console.log('[AppearanceStore] 用户终端主题列表已重新加载:', userTerminalThemes.value); // <-- 添加日志 - } catch (err: any) { - console.error('重新加载终端主题列表失败:', err); - // 可以选择抛出错误或显示通知 - } - } + // 移除 reloadTerminalThemes,统一由 loadInitialAppearanceData 处理加载 /** * 创建新的终端主题 @@ -252,7 +235,7 @@ export const useAppearanceStore = defineStore('appearance', () => { async function createTerminalTheme(name: string, themeData: ITheme) { try { await axios.post('/api/v1/terminal-themes', { name, themeData }); - await reloadTerminalThemes(); // 重新加载列表 + await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('创建终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '创建终端主题失败'); @@ -267,11 +250,11 @@ export const useAppearanceStore = defineStore('appearance', () => { */ async function updateTerminalTheme(id: string, name: string, themeData: ITheme) { try { - await axios.put(`/api/v1/terminal-themes/${id}`, { name, themeData }); - await reloadTerminalThemes(); // 重新加载列表 - } catch (err: any) { - console.error('更新终端主题失败:', err); - throw new Error(err.response?.data?.message || err.message || '更新终端主题失败'); + await axios.put(`/api/v1/terminal-themes/${id}`, { name, themeData }); + await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 + } catch (err: any) { + console.error('更新终端主题失败:', err); + throw new Error(err.response?.data?.message || err.message || '更新终端主题失败'); } } @@ -282,11 +265,17 @@ export const useAppearanceStore = defineStore('appearance', () => { async function deleteTerminalTheme(id: string) { try { await axios.delete(`/api/v1/terminal-themes/${id}`); - // 如果删除的是当前激活的主题,则切换回默认 - if (activeTerminalThemeId.value === id) { - await setActiveTerminalTheme(null); // 或者设置为默认主题的 ID + // 如果删除的是当前激活的主题,则切换回默认主题 ID + // 需要将字符串 id 转换为数字进行比较 + const idNum = parseInt(id, 10); + if (!isNaN(idNum) && activeTerminalThemeId.value === idNum) { + // 查找默认主题的数字 ID (这里假设默认主题 ID 为 1,实际应从配置或查询获取) + // TODO: 需要一种可靠的方式获取默认主题的数字 ID + const defaultThemeIdNum = 1; // 临时硬编码,需要改进 + console.log(`[AppearanceStore] 删除的主题是当前激活主题,尝试切换到默认主题 ID: ${defaultThemeIdNum}`); + await setActiveTerminalTheme(defaultThemeIdNum.toString()); // setActiveTerminalTheme 需要字符串 ID } - await reloadTerminalThemes(); // 重新加载列表 + await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('删除终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '删除终端主题失败'); @@ -308,7 +297,7 @@ export const useAppearanceStore = defineStore('appearance', () => { await axios.post('/api/v1/terminal-themes/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); - await reloadTerminalThemes(); + await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('导入终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '导入终端主题失败'); @@ -494,7 +483,7 @@ export const useAppearanceStore = defineStore('appearance', () => { error, // State refs (原始数据) appearanceSettings, - availableTerminalThemes, + allTerminalThemes, // 导出重命名后的 ref // Computed Getters currentUiTheme, activeTerminalThemeId, @@ -515,11 +504,10 @@ export const useAppearanceStore = defineStore('appearance', () => { setTerminalFontFamily, setTerminalFontSize, setEditorFontSize, // <-- 新增 - reloadTerminalThemes, - createTerminalTheme, - updateTerminalTheme, - deleteTerminalTheme, - importTerminalTheme, + createTerminalTheme, // 保留 + updateTerminalTheme, // 保留 + deleteTerminalTheme, // 保留 + importTerminalTheme, // 保留 exportTerminalTheme, uploadPageBackground, uploadTerminalBackground,