diff --git a/packages/backend/src/appearance/appearance.controller.ts b/packages/backend/src/appearance/appearance.controller.ts new file mode 100644 index 0000000..501b3c4 --- /dev/null +++ b/packages/backend/src/appearance/appearance.controller.ts @@ -0,0 +1,126 @@ +import { Request, Response } from 'express'; +import * as appearanceService from '../services/appearance.service'; +import { UpdateAppearanceDto } from '../types/appearance.types'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; + +// --- 背景图片上传配置 --- +const backgroundStorage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = path.join(__dirname, '../../uploads/backgrounds/'); + // 确保目录存在 + fs.mkdirSync(uploadPath, { recursive: true }); + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + // 使用时间戳和原始文件名(去除特殊字符)创建唯一文件名 + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const safeOriginalName = file.originalname.replace(/[^a-zA-Z0-9.]/g, '_'); + cb(null, uniqueSuffix + '-' + safeOriginalName); + } +}); + +const backgroundUpload = multer({ + storage: backgroundStorage, + fileFilter: (req, file, cb) => { + // 允许常见的图片格式 + const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; + if (allowedTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error('只允许上传图片文件 (JPEG, PNG, GIF, WebP, SVG)!')); + } + }, + limits: { fileSize: 5 * 1024 * 1024 } // 限制文件大小为 5MB +}); +// --- End Background Image Upload Config --- + + +/** + * 获取外观设置 + */ +export const getAppearanceSettingsController = async (req: Request, res: Response): Promise => { + try { + const settings = await appearanceService.getSettings(); + res.status(200).json(settings); + } catch (error: any) { + res.status(500).json({ message: '获取外观设置失败', error: error.message }); + } +}; + +/** + * 更新外观设置 + */ +export const updateAppearanceSettingsController = async (req: Request, res: Response): Promise => { + try { + const settingsDto: UpdateAppearanceDto = req.body; + // 注意:背景图片通常通过单独的上传接口处理,这里只更新文本类设置 + const success = await appearanceService.updateSettings(settingsDto); + if (success) { + // 获取更新后的设置并返回 + const updatedSettings = await appearanceService.getSettings(); + res.status(200).json(updatedSettings); + } else { + // 理论上更新单行设置总能找到 ID=1 的行,除非数据库有问题 + res.status(500).json({ message: '更新外观设置似乎失败了' }); + } + } catch (error: any) { + res.status(400).json({ message: '更新外观设置失败', error: error.message }); + } +}; + + +/** + * 上传页面背景图片 + */ +export const uploadPageBackgroundController = async (req: Request, res: Response): Promise => { + if (!req.file) { + res.status(400).json({ message: '没有上传文件' }); + return; + } + try { + // 文件已由 multer 保存到 uploads/backgrounds/ + // 返回相对于服务器根目录的文件路径,供前端使用 + const relativePath = `/uploads/backgrounds/${req.file.filename}`; + + // 更新数据库中的设置 + await appearanceService.updateSettings({ pageBackgroundImage: relativePath }); + + res.status(200).json({ message: '页面背景上传成功', filePath: relativePath }); + } catch (error: any) { + // 如果出错,尝试删除已上传的文件 + if (req.file && fs.existsSync(req.file.path)) { + fs.unlink(req.file.path, (err) => { + if (err) console.error("删除上传失败的背景文件时出错:", err); + }); + } + res.status(500).json({ message: '上传页面背景失败', error: error.message }); + } +}; + +/** + * 上传终端背景图片 + */ +export const uploadTerminalBackgroundController = async (req: Request, res: Response): Promise => { + if (!req.file) { + res.status(400).json({ message: '没有上传文件' }); + return; + } + try { + const relativePath = `/uploads/backgrounds/${req.file.filename}`; + await appearanceService.updateSettings({ terminalBackgroundImage: relativePath }); + res.status(200).json({ message: '终端背景上传成功', filePath: relativePath }); + } catch (error: any) { + if (req.file && fs.existsSync(req.file.path)) { + fs.unlink(req.file.path, (err) => { + if (err) console.error("删除上传失败的背景文件时出错:", err); + }); + } + res.status(500).json({ message: '上传终端背景失败', error: error.message }); + } +}; + +// 导出 multer 中间件以便在路由中使用 +export const uploadPageBackgroundMiddleware = backgroundUpload.single('pageBackgroundFile'); +export const uploadTerminalBackgroundMiddleware = backgroundUpload.single('terminalBackgroundFile'); diff --git a/packages/backend/src/appearance/appearance.routes.ts b/packages/backend/src/appearance/appearance.routes.ts new file mode 100644 index 0000000..4082bb5 --- /dev/null +++ b/packages/backend/src/appearance/appearance.routes.ts @@ -0,0 +1,32 @@ +import express from 'express'; +import * as appearanceController from './appearance.controller'; +import { isAuthenticated } from '../auth/auth.middleware'; + +const router = express.Router(); + +// 应用认证中间件 +router.use(isAuthenticated); + +// GET /api/v1/appearance - 获取所有外观设置 +router.get('/', appearanceController.getAppearanceSettingsController); + +// PUT /api/v1/appearance - 更新外观设置 (文本类) +router.put('/', appearanceController.updateAppearanceSettingsController); + +// POST /api/v1/appearance/background/page - 上传页面背景图片 +router.post( + '/background/page', + appearanceController.uploadPageBackgroundMiddleware, // 使用 multer 中间件 + appearanceController.uploadPageBackgroundController +); + +// POST /api/v1/appearance/background/terminal - 上传终端背景图片 +router.post( + '/background/terminal', + appearanceController.uploadTerminalBackgroundMiddleware, // 使用 multer 中间件 + appearanceController.uploadTerminalBackgroundController +); + +// TODO: 可能需要添加删除背景图片的路由 + +export default router; diff --git a/packages/backend/src/config/default-themes.ts b/packages/backend/src/config/default-themes.ts new file mode 100644 index 0000000..d1c8f4b --- /dev/null +++ b/packages/backend/src/config/default-themes.ts @@ -0,0 +1,48 @@ +import type { ITheme } from 'xterm'; + +// 默认 xterm 主题 +// (与 frontend/src/stores/settings.store.ts 中的定义保持一致) +export const defaultXtermTheme: ITheme = { + background: '#1e1e1e', + foreground: '#d4d4d4', + cursor: '#d4d4d4', + selectionBackground: '#264f78', // 使用 selectionBackground + black: '#000000', + red: '#cd3131', + green: '#0dbc79', + yellow: '#e5e510', + blue: '#2472c8', + magenta: '#bc3fbc', + cyan: '#11a8cd', + white: '#e5e5e5', + brightBlack: '#666666', + brightRed: '#f14c4c', + brightGreen: '#23d18b', + brightYellow: '#f5f543', + brightBlue: '#3b8eea', + brightMagenta: '#d670d6', + brightCyan: '#29b8db', + brightWhite: '#e5e5e5' +}; + +// 默认 UI 主题 (CSS 变量) +// (与 frontend/src/stores/settings.store.ts 中的定义保持一致) +export const defaultUiTheme: Record = { + '--app-bg-color': '#ffffff', + '--text-color': '#333333', + '--text-color-secondary': '#666666', + '--border-color': '#cccccc', + '--link-color': '#333', + '--link-hover-color': '#0056b3', + '--link-active-color': '#007bff', + '--header-bg-color': '#f0f0f0', + '--footer-bg-color': '#f0f0f0', + '--button-bg-color': '#007bff', + '--button-text-color': '#ffffff', + '--button-hover-bg-color': '#0056b3', + '--font-family-sans-serif': 'sans-serif', + '--base-padding': '1rem', + '--base-margin': '0.5rem', +}; + +// 未来可以在这里添加其他默认主题 diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7f58efa..4eba0f2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -18,6 +18,8 @@ import notificationRoutes from './notifications/notification.routes'; // 导入 import auditRoutes from './audit/audit.routes'; // 导入审计路由 import commandHistoryRoutes from './command-history/command-history.routes'; // 导入命令历史记录路由 import quickCommandsRoutes from './quick-commands/quick-commands.routes'; // 导入快捷指令路由 +import terminalThemeRoutes from './terminal-themes/terminal-theme.routes'; // 导入终端主题路由 +import appearanceRoutes from './appearance/appearance.routes'; // 导入外观设置路由 import { initializeWebSocket } from './websocket'; import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware'; // 导入 IP 白名单中间件 @@ -83,6 +85,13 @@ const sessionMiddleware = session({ }); app.use(sessionMiddleware); // 应用会话中间件 +// --- 静态文件服务 --- +// 提供上传的背景图片等静态资源 +const uploadsPath = path.join(__dirname, '../uploads'); // 指向 backend/uploads 目录 +app.use('/uploads', express.static(uploadsPath)); +console.log(`静态文件服务已启动,路径: ${uploadsPath}`); +// --- 结束静态文件服务 --- + // 扩展 Express Request 类型以包含 session 数据 (如果需要更明确的类型提示) declare module 'express-session' { @@ -106,6 +115,8 @@ app.use('/api/v1/notifications', notificationRoutes); // 挂载通知相关的 app.use('/api/v1/audit-logs', auditRoutes); // 挂载审计日志相关的路由 app.use('/api/v1/command-history', commandHistoryRoutes); // 挂载命令历史记录相关的路由 app.use('/api/v1/quick-commands', quickCommandsRoutes); // 挂载快捷指令相关的路由 +app.use('/api/v1/terminal-themes', terminalThemeRoutes); // 挂载终端主题路由 +app.use('/api/v1/appearance', appearanceRoutes); // 挂载外观设置路由 // 状态检查接口 app.get('/api/v1/status', (req: Request, res: Response) => { diff --git a/packages/backend/src/repositories/appearance.repository.ts b/packages/backend/src/repositories/appearance.repository.ts new file mode 100644 index 0000000..e53fe50 --- /dev/null +++ b/packages/backend/src/repositories/appearance.repository.ts @@ -0,0 +1,207 @@ +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 TABLE_NAME = 'appearance_settings'; +const SETTINGS_ID = 1; // Use a fixed ID for the single row of global settings + +/** + * 创建 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_background_image TEXT, + terminal_background_opacity REAL, -- Use REAL for floating point numbers + page_background_image TEXT, + page_background_opacity REAL, + updated_at INTEGER NOT NULL + ); + `; + db.run(sql, (err) => { + if (err) { + console.error(`创建 ${TABLE_NAME} 表失败:`, err.message); + } else { + // 确保默认设置行存在 + ensureDefaultSettingsExist(); + } + }); +}; + +// 辅助函数:将数据库行转换为 AppearanceSettings 对象 +const mapRowToAppearanceSettings = (row: any): AppearanceSettings => { + if (!row) return getDefaultAppearanceSettings(); // Return default if no row found + return { + _id: row.id.toString(), + customUiTheme: row.custom_ui_theme, + activeTerminalThemeId: row.active_terminal_theme_id, + terminalFontFamily: row.terminal_font_family, + terminalBackgroundImage: row.terminal_background_image, + terminalBackgroundOpacity: row.terminal_background_opacity, + pageBackgroundImage: row.page_background_image, + pageBackgroundOpacity: row.page_background_opacity, + updatedAt: row.updated_at, + }; +}; + +// 获取默认外观设置 +const getDefaultAppearanceSettings = (): AppearanceSettings => { + // TODO: Find the ID of the default preset theme from terminal_themes table later + // For now, leave activeTerminalThemeId null or undefined + return { + _id: SETTINGS_ID.toString(), + customUiTheme: JSON.stringify(defaultUiTheme), // Use default UI theme + activeTerminalThemeId: undefined, // Needs to be set after querying default theme ID + terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"', // Default font + terminalBackgroundImage: undefined, + terminalBackgroundOpacity: 1.0, + pageBackgroundImage: undefined, + pageBackgroundOpacity: 1.0, + updatedAt: Date.now(), + }; +}; + + +/** + * 确保默认设置行存在 + */ +const ensureDefaultSettingsExist = () => { + const defaults = getDefaultAppearanceSettings(); + const sqlSelect = `SELECT id FROM ${TABLE_NAME} WHERE id = ?`; + db.get(sqlSelect, [SETTINGS_ID], (err, row) => { + if (err) { + console.error(`检查默认外观设置时出错:`, err.message); + return; + } + if (!row) { + const sqlInsert = ` + INSERT INTO ${TABLE_NAME} ( + id, custom_ui_theme, active_terminal_theme_id, terminal_font_family, + terminal_background_image, terminal_background_opacity, + page_background_image, page_background_opacity, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + db.run(sqlInsert, [ + SETTINGS_ID, + defaults.customUiTheme, + defaults.activeTerminalThemeId, // Initially undefined + defaults.terminalFontFamily, + defaults.terminalBackgroundImage, + defaults.terminalBackgroundOpacity, + defaults.pageBackgroundImage, + defaults.pageBackgroundOpacity, + defaults.updatedAt + ], (insertErr) => { + if (insertErr) { + console.error('插入默认外观设置失败:', insertErr.message); + } else { + console.log('默认外观设置已初始化。'); + // Now try to find and set the default theme ID + findAndSetDefaultThemeId(); + } + }); + } else { + // If row exists, still check if default theme ID needs setting + findAndSetDefaultThemeId(); + } + }); +}; + +/** + * 查找默认终端主题 ID 并更新外观设置 + */ +const findAndSetDefaultThemeId = async () => { + try { + // 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) => { + if (err) { + console.error("查找默认终端主题 ID 失败:", err.message); + return; + } + if (defaultThemeRow) { + const defaultThemeId = defaultThemeRow.id.toString(); + // 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 }); + } + } else { + console.warn("未找到系统默认终端主题,无法设置 activeTerminalThemeId。"); + } + }); + } catch (error) { + console.error("设置默认终端主题 ID 时出错:", error); + } +}; + + +/** + * 获取外观设置 + * @returns Promise + */ +export const getAppearanceSettings = async (): Promise => { + return new Promise((resolve, reject) => { + db.get(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [SETTINGS_ID], (err, row) => { + if (err) { + console.error('获取外观设置失败:', err.message); + reject(new Error('获取外观设置失败')); + } else { + resolve(mapRowToAppearanceSettings(row)); + } + }); + }); +}; + +/** + * 更新外观设置 + * @param settingsDto 更新的数据 + * @returns Promise 是否成功更新 + */ +export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise => { + const now = Date.now(); + let sql = `UPDATE ${TABLE_NAME} SET updated_at = ?`; + const params: any[] = [now]; + + // 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 + if (['custom_ui_theme', 'active_terminal_theme_id', 'terminal_font_family', 'terminal_background_image', 'terminal_background_opacity', 'page_background_image', 'page_background_opacity'].includes(dbKey)) { + updates.push(`${dbKey} = ?`); + params.push((settingsDto as any)[key]); + } + } + } + + if (updates.length === 0) { + return true; // Nothing to update + } + + sql += `, ${updates.join(', ')} WHERE id = ?`; + params.push(SETTINGS_ID); + + return new Promise((resolve, reject) => { + db.run(sql, params, function (err) { + if (err) { + console.error('更新外观设置失败:', err.message); + reject(new Error('更新外观设置失败')); + } else { + resolve(this.changes > 0); + } + }); + }); +}; + +// 初始化时创建表 +createTableIfNotExists(); diff --git a/packages/backend/src/repositories/terminal-theme.repository.ts b/packages/backend/src/repositories/terminal-theme.repository.ts new file mode 100644 index 0000000..3a1d573 --- /dev/null +++ b/packages/backend/src/repositories/terminal-theme.repository.ts @@ -0,0 +1,203 @@ +import { getDb } from '../database'; +import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types'; +import { defaultXtermTheme } from '../config/default-themes'; // 假设默认主题配置在此 + +const db = getDb(); + +/** + * 创建 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) => { + if (err) { + console.error('创建 terminal_themes 表失败:', err.message); + } else { + // 表创建成功后,初始化预设主题 + initializePresetThemes(); + } + }); +}; + +// 辅助函数:将数据库行转换为 TerminalTheme 对象 +const mapRowToTerminalTheme = (row: any): TerminalTheme => { + return { + _id: row.id.toString(), // SQLite ID 是数字,转换为字符串以匹配 NeDB 风格 + name: row.name, + themeData: JSON.parse(row.theme_data), // 解析 JSON 字符串 + isPreset: !!row.is_preset, // 转换为布尔值 + isSystemDefault: !!row.is_system_default, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +}; + +/** + * 查找所有终端主题 + * @returns Promise + */ +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) => { + if (err) { + console.error('查询所有终端主题失败:', err.message); + reject(new Error('查询终端主题失败')); + } else { + resolve(rows.map(mapRowToTerminalTheme)); + } + }); + }); +}; + +/** + * 根据 ID 查找终端主题 + * @param id 主题 ID (注意:这里是 SQLite 的数字 ID) + * @returns Promise + */ +export const findThemeById = async (id: number): Promise => { + return new Promise((resolve, reject) => { + db.get('SELECT * FROM terminal_themes WHERE id = ?', [id], (err, row) => { + if (err) { + console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message); + reject(new Error('查询终端主题失败')); + } else { + resolve(row ? mapRowToTerminalTheme(row) : null); + } + }); + }); +}; + +/** + * 创建一个新的终端主题 + * @param themeDto 创建主题所需的数据 + * @returns Promise 新创建的主题 + */ +export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise => { + const now = Date.now(); + const themeDataJson = JSON.stringify(themeDto.themeData); // 将 ITheme 转换为 JSON 字符串 + const sql = ` + INSERT INTO terminal_themes (name, theme_data, is_preset, created_at, updated_at) + VALUES (?, ?, 0, ?, ?) + `; + return new Promise((resolve, reject) => { + db.run(sql, [themeDto.name, themeDataJson, now, now], function (err) { + if (err) { + console.error('创建新终端主题失败:', err.message); + // 特别处理唯一约束错误 + if (err.message.includes('UNIQUE constraint failed')) { + reject(new Error(`主题名称 "${themeDto.name}" 已存在。`)); + } else { + reject(new Error('创建终端主题失败')); + } + } else { + // 获取新插入行的 ID 并查询返回完整对象 + findThemeById(this.lastID) + .then(newTheme => { + if (newTheme) { + resolve(newTheme); + } else { + // 理论上不应该发生,但作为回退 + reject(new Error('创建主题后未能检索到该主题')); + } + }) + .catch(reject); + } + }); + }); +}; + +/** + * 更新一个终端主题 + * @param id 要更新的主题 ID (SQLite 数字 ID) + * @param themeDto 更新的数据 + * @returns Promise 是否成功更新 + */ +export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto): Promise => { + const now = Date.now(); + const themeDataJson = JSON.stringify(themeDto.themeData); + // 只允许更新非预设主题的 name 和 theme_data + const sql = ` + UPDATE terminal_themes + SET name = ?, theme_data = ?, updated_at = ? + WHERE id = ? AND is_preset = 0 + `; + return new Promise((resolve, reject) => { + db.run(sql, [themeDto.name, themeDataJson, now, id], function (err) { + if (err) { + console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message); + if (err.message.includes('UNIQUE constraint failed')) { + reject(new Error(`主题名称 "${themeDto.name}" 已存在。`)); + } else { + reject(new Error('更新终端主题失败')); + } + } else { + resolve(this.changes > 0); // 如果有行被改变,则更新成功 + } + }); + }); +}; + +/** + * 删除一个终端主题 + * @param id 要删除的主题 ID (SQLite 数字 ID) + * @returns Promise 是否成功删除 + */ +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) { + if (err) { + console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message); + reject(new Error('删除终端主题失败')); + } else { + resolve(this.changes > 0); // 如果有行被改变,则删除成功 + } + }); + }); +}; + +/** + * 初始化预设主题 (如果不存在) + */ +export const initializePresetThemes = async () => { + const defaultPresetName = '默认暗色'; // Default Dark + const themeDataJson = JSON.stringify(defaultXtermTheme); + 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; + } + 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}" 已初始化。`); + } + }); + } + // 在这里可以添加更多预设主题的初始化逻辑 + }); +}; + +// 初始化时创建表 +createTableIfNotExists(); diff --git a/packages/backend/src/services/appearance.service.ts b/packages/backend/src/services/appearance.service.ts new file mode 100644 index 0000000..c77c5b2 --- /dev/null +++ b/packages/backend/src/services/appearance.service.ts @@ -0,0 +1,55 @@ +import * as appearanceRepository from '../repositories/appearance.repository'; +import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types'; +import * as terminalThemeRepository from '../repositories/terminal-theme.repository'; // 需要验证 activeTerminalThemeId + +/** + * 获取外观设置 + * @returns Promise + */ +export const getSettings = async (): Promise => { + return appearanceRepository.getAppearanceSettings(); +}; + +/** + * 更新外观设置 + * @param settingsDto 更新数据 + * @returns 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}`); + } + try { + const themeExists = await terminalThemeRepository.findThemeById(themeIdNum); + if (!themeExists) { + throw new Error(`指定的终端主题 ID 不存在: ${settingsDto.activeTerminalThemeId}`); + } + } 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}`); + } + } 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. + } + + + // 验证透明度值 (如果提供了) + if (settingsDto.terminalBackgroundOpacity !== undefined && (settingsDto.terminalBackgroundOpacity < 0 || settingsDto.terminalBackgroundOpacity > 1)) { + throw new Error('终端背景透明度必须在 0 和 1 之间'); + } + if (settingsDto.pageBackgroundOpacity !== undefined && (settingsDto.pageBackgroundOpacity < 0 || settingsDto.pageBackgroundOpacity > 1)) { + throw new Error('页面背景透明度必须在 0 和 1 之间'); + } + + // TODO: 如果实现了背景图片上传,这里需要处理文件路径或 URL 的验证/保存逻辑 + + return appearanceRepository.updateAppearanceSettings(settingsDto); +}; + +// 注意:背景图片上传/处理逻辑需要根据最终决定(URL vs 上传)来添加。 diff --git a/packages/backend/src/services/terminal-theme.service.ts b/packages/backend/src/services/terminal-theme.service.ts new file mode 100644 index 0000000..8160747 --- /dev/null +++ b/packages/backend/src/services/terminal-theme.service.ts @@ -0,0 +1,88 @@ +import * as terminalThemeRepository from '../repositories/terminal-theme.repository'; +import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types'; +// import { validate } from 'class-validator'; // 移除导入 +import type { ITheme } from 'xterm'; + +/** + * 获取所有终端主题 + * @returns Promise + */ +export const getAllThemes = async (): Promise => { + return terminalThemeRepository.findAllThemes(); +}; + +/** + * 根据 ID 获取单个终端主题 + * @param id 主题 ID (SQLite 数字 ID) + * @returns Promise + */ +export const getThemeById = async (id: number): Promise => { + if (isNaN(id)) { + throw new Error('无效的主题 ID'); + } + return terminalThemeRepository.findThemeById(id); +}; + +/** + * 创建新终端主题 + * @param themeDto 创建数据 + * @returns Promise + */ +export const createNewTheme = async (themeDto: CreateTerminalThemeDto): Promise => { + // 移除验证相关的注释 + // 简单验证 themeData 结构 (确保基本字段存在) + if (!themeDto.themeData || typeof themeDto.themeData.background !== 'string' || typeof themeDto.themeData.foreground !== 'string') { + throw new Error('无效的主题数据格式'); + } + + return terminalThemeRepository.createTheme(themeDto); +}; + +/** + * 更新终端主题 + * @param id 主题 ID (SQLite 数字 ID) + * @param themeDto 更新数据 + * @returns Promise 是否成功更新 + */ +export const updateExistingTheme = async (id: number, themeDto: UpdateTerminalThemeDto): Promise => { + if (isNaN(id)) { + throw new Error('无效的主题 ID'); + } + // 可选:验证 themeDto + if (!themeDto.name || !themeDto.themeData || typeof themeDto.themeData.background !== 'string' || typeof themeDto.themeData.foreground !== 'string') { + throw new Error('无效的主题更新数据'); + } + return terminalThemeRepository.updateTheme(id, themeDto); +}; + +/** + * 删除终端主题 + * @param id 主题 ID (SQLite 数字 ID) + * @returns Promise 是否成功删除 + */ +export const deleteExistingTheme = async (id: number): Promise => { + if (isNaN(id)) { + throw new Error('无效的主题 ID'); + } + return terminalThemeRepository.deleteTheme(id); +}; + +/** + * 导入终端主题 + * @param themeData 主题数据对象 (ITheme) + * @param name 主题名称 + * @returns Promise + */ +export const importTheme = async (themeData: ITheme, name: string): Promise => { + if (!name) { + throw new Error('导入主题时必须提供名称'); + } + // 验证导入的数据结构是否符合 ITheme (简化验证) + if (typeof themeData.background !== 'string' || typeof themeData.foreground !== 'string') { + throw new Error('导入的主题数据格式无效'); + } + const dto: CreateTerminalThemeDto = { name, themeData }; + return createNewTheme(dto); +}; + +// 注意:导出功能通常在 Controller 层处理,根据 ID 获取主题数据后,设置响应头并发送 JSON 文件。 diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts index 2212062..5254afa 100644 --- a/packages/backend/src/settings/settings.controller.ts +++ b/packages/backend/src/settings/settings.controller.ts @@ -30,18 +30,38 @@ export const settingsController = { res.status(400).json({ message: '无效的请求体,应为 JSON 对象' }); return; } - // 可以在这里添加更严格的验证,例如检查值的类型等 - await settingsService.setMultipleSettings(settingsToUpdate); + // --- 过滤掉外观设置相关的键 --- + const allowedSettingsKeys = [ + 'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration', + 'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled' // 添加 ipWhitelistEnabled + // 在这里添加其他允许的通用设置键 + ]; + const filteredSettings: Record = {}; + for (const key in settingsToUpdate) { + if (allowedSettingsKeys.includes(key)) { + filteredSettings[key] = settingsToUpdate[key]; + } + } + // --- 结束过滤 --- + + // 只传递过滤后的设置给 service + if (Object.keys(filteredSettings).length > 0) { + await settingsService.setMultipleSettings(filteredSettings); + } + // 记录审计日志 // 区分 IP 白名单更新和其他设置更新 - const updatedKeys = Object.keys(settingsToUpdate); - if (updatedKeys.includes('ipWhitelist')) { - auditLogService.logAction('IP_WHITELIST_UPDATED', { updatedKeys }); - } else { - auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys }); + // 注意:现在审计日志可能需要更精细的逻辑,因为它只记录了实际更新的通用设置 + const updatedKeys = Object.keys(filteredSettings); // 使用过滤后的键 + if (updatedKeys.length > 0) { // 只有实际更新了才记录 + if (updatedKeys.includes('ipWhitelist') || updatedKeys.includes('ipWhitelistEnabled')) { + auditLogService.logAction('IP_WHITELIST_UPDATED', { updatedKeys }); + } else { + auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys }); + } } - res.status(200).json({ message: '设置已成功更新' }); + res.status(200).json({ message: '设置已成功更新' }); // 即使没有更新通用设置也返回成功 } catch (error: any) { console.error('更新设置时出错:', error); res.status(500).json({ message: '更新设置失败', error: error.message }); diff --git a/packages/backend/src/terminal-themes/terminal-theme.controller.ts b/packages/backend/src/terminal-themes/terminal-theme.controller.ts new file mode 100644 index 0000000..ba3e25f --- /dev/null +++ b/packages/backend/src/terminal-themes/terminal-theme.controller.ts @@ -0,0 +1,203 @@ +import { Request, Response } from 'express'; +import * as terminalThemeService from '../services/terminal-theme.service'; +import { CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types'; +import type { ITheme } from 'xterm'; +import multer from 'multer'; // 用于处理文件上传 +import fs from 'fs'; +import path from 'path'; + +// 配置 multer 用于处理 JSON 文件上传 (导入) +const upload = multer({ + dest: path.join(__dirname, '../../temp-uploads/'), // 临时存储目录 + fileFilter: (req, file, cb) => { + if (file.mimetype === 'application/json' || file.originalname.endsWith('.json')) { + cb(null, true); + } else { + cb(new Error('只允许上传 JSON 文件!')); + } + }, + limits: { fileSize: 1024 * 1024 } // 限制文件大小为 1MB +}); + +/** + * 获取所有终端主题 + */ +export const getAllThemesController = async (req: Request, res: Response): Promise => { + try { + const themes = await terminalThemeService.getAllThemes(); + res.status(200).json(themes); + } catch (error: any) { + res.status(500).json({ message: '获取终端主题列表失败', error: error.message }); + } +}; + +/** + * 根据 ID 获取单个终端主题 + */ +export const getThemeByIdController = async (req: Request, res: Response): Promise => { + try { + const id = parseInt(req.params.id, 10); + if (isNaN(id)) { + res.status(400).json({ message: '无效的主题 ID' }); + return; + } + const theme = await terminalThemeService.getThemeById(id); + if (theme) { + res.status(200).json(theme); + } else { + res.status(404).json({ message: '未找到指定的主题' }); + } + } catch (error: any) { + res.status(500).json({ message: '获取终端主题失败', error: error.message }); + } +}; + +/** + * 创建新终端主题 + */ +export const createThemeController = async (req: Request, res: Response): Promise => { + try { + const themeDto: CreateTerminalThemeDto = req.body; + // 基本验证 + if (!themeDto.name || !themeDto.themeData) { + res.status(400).json({ message: '缺少主题名称或主题数据' }); + return; + } + const newTheme = await terminalThemeService.createNewTheme(themeDto); + res.status(201).json(newTheme); + } catch (error: any) { + // 检查是否是名称重复错误 + if (error.message.includes('已存在')) { + res.status(409).json({ message: error.message }); // 409 Conflict + } else { + res.status(400).json({ message: '创建终端主题失败', error: error.message }); + } + } +}; + +/** + * 更新终端主题 + */ +export const updateThemeController = async (req: Request, res: Response): Promise => { + try { + const id = parseInt(req.params.id, 10); + if (isNaN(id)) { + res.status(400).json({ message: '无效的主题 ID' }); + return; + } + const themeDto: UpdateTerminalThemeDto = req.body; + // 基本验证 + if (!themeDto.name || !themeDto.themeData) { + res.status(400).json({ message: '缺少主题名称或主题数据' }); + return; + } + const success = await terminalThemeService.updateExistingTheme(id, themeDto); + if (success) { + res.status(200).json({ message: '主题更新成功' }); + } else { + // 可能因为 ID 不存在或主题是预设主题而更新失败 + res.status(404).json({ message: '未找到可更新的主题或该主题为预设主题' }); + } + } catch (error: any) { + if (error.message.includes('已存在')) { + res.status(409).json({ message: error.message }); // 409 Conflict + } else { + res.status(400).json({ message: '更新终端主题失败', error: error.message }); + } + } +}; + +/** + * 删除终端主题 + */ +export const deleteThemeController = async (req: Request, res: Response): Promise => { + try { + const id = parseInt(req.params.id, 10); + if (isNaN(id)) { + res.status(400).json({ message: '无效的主题 ID' }); + return; + } + const success = await terminalThemeService.deleteExistingTheme(id); + if (success) { + res.status(200).json({ message: '主题删除成功' }); + } else { + // 可能因为 ID 不存在或主题是预设主题而删除失败 + res.status(404).json({ message: '未找到可删除的主题或该主题为预设主题' }); + } + } catch (error: any) { + res.status(500).json({ message: '删除终端主题失败', error: error.message }); + } +}; + +/** + * 导入终端主题 (处理文件上传) + */ +export const importThemeController = async (req: Request, res: Response): Promise => { + if (!req.file) { + res.status(400).json({ message: '没有上传文件' }); + return; + } + + const filePath = req.file.path; + const originalName = req.file.originalname; + // 尝试从文件名中提取名称 (去除 .json 后缀) + const defaultName = originalName.endsWith('.json') ? originalName.slice(0, -5) : originalName; + // 允许用户通过 body 传递 name,否则使用文件名 + const themeName = req.body.name || defaultName; + + try { + const fileContent = await fs.promises.readFile(filePath, 'utf-8'); + const themeData: ITheme = JSON.parse(fileContent); + + // 调用 service 进行导入 + const importedTheme = await terminalThemeService.importTheme(themeData, themeName); + + // 删除临时文件 + await fs.promises.unlink(filePath); + + res.status(201).json(importedTheme); + + } catch (error: any) { + // 确保即使出错也删除临时文件 + if (fs.existsSync(filePath)) { + await fs.promises.unlink(filePath).catch(unlinkErr => console.error("删除临时导入文件失败:", unlinkErr)); + } + + if (error instanceof SyntaxError) { + res.status(400).json({ message: '导入失败:文件不是有效的 JSON 格式', error: error.message }); + } else if (error.message.includes('已存在')) { + res.status(409).json({ message: `导入失败: ${error.message}` }); // 409 Conflict + } else { + res.status(400).json({ message: '导入终端主题失败', error: error.message }); + } + } +}; + +/** + * 导出终端主题 + */ +export const exportThemeController = async (req: Request, res: Response): Promise => { + try { + const id = parseInt(req.params.id, 10); + if (isNaN(id)) { + res.status(400).json({ message: '无效的主题 ID' }); + return; + } + const theme = await terminalThemeService.getThemeById(id); + if (theme) { + const themeJson = JSON.stringify(theme.themeData, null, 2); // 格式化 JSON 输出 + const fileName = `${theme.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`; // 创建安全的文件名 + + res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`); + res.setHeader('Content-Type', 'application/json'); + res.status(200).send(themeJson); + } else { + res.status(404).json({ message: '未找到指定的主题' }); + } + } catch (error: any) { + res.status(500).json({ message: '导出终端主题失败', error: error.message }); + } +}; + +// 将 upload 中间件导出,以便在路由中使用 +export const uploadMiddleware = upload; diff --git a/packages/backend/src/terminal-themes/terminal-theme.routes.ts b/packages/backend/src/terminal-themes/terminal-theme.routes.ts new file mode 100644 index 0000000..cd8ac42 --- /dev/null +++ b/packages/backend/src/terminal-themes/terminal-theme.routes.ts @@ -0,0 +1,32 @@ +import express from 'express'; +import * as themeController from './terminal-theme.controller'; +import { isAuthenticated } from '../auth/auth.middleware'; // 修正导入名称 + +const router = express.Router(); + +// 应用认证中间件到所有主题路由 +router.use(isAuthenticated); // 修正使用的中间件名称 + +// GET /api/v1/terminal-themes - 获取所有主题 +router.get('/', themeController.getAllThemesController); + +// POST /api/v1/terminal-themes - 创建新主题 +router.post('/', themeController.createThemeController); + +// GET /api/v1/terminal-themes/:id - 获取单个主题 +router.get('/:id', themeController.getThemeByIdController); + +// PUT /api/v1/terminal-themes/:id - 更新主题 +router.put('/:id', themeController.updateThemeController); + +// DELETE /api/v1/terminal-themes/:id - 删除主题 +router.delete('/:id', themeController.deleteThemeController); + +// POST /api/v1/terminal-themes/import - 导入主题 (使用 multer 中间件处理文件) +router.post('/import', themeController.uploadMiddleware.single('themeFile'), themeController.importThemeController); + +// GET /api/v1/terminal-themes/:id/export - 导出主题 +router.get('/:id/export', themeController.exportThemeController); + + +export default router; diff --git a/packages/backend/src/types/appearance.types.ts b/packages/backend/src/types/appearance.types.ts new file mode 100644 index 0000000..72ff0c2 --- /dev/null +++ b/packages/backend/src/types/appearance.types.ts @@ -0,0 +1,22 @@ +import type { ITheme } from 'xterm'; + +/** + * 外观设置数据结构 + */ +export interface AppearanceSettings { + _id?: string; // 通常只有一个文档,ID 固定或不使用 + userId?: string; // 如果需要区分用户设置 (当前假设为全局) + customUiTheme?: string; // UI 主题 (CSS 变量 JSON 字符串) + activeTerminalThemeId?: string; // 当前激活的终端主题 ID (对应 terminal_themes 表的 _id) + terminalFontFamily?: string; // 终端字体列表字符串 + terminalBackgroundImage?: string; // 终端背景图片 URL 或路径 + terminalBackgroundOpacity?: number; // 终端背景透明度 (0-1) + pageBackgroundImage?: string; // 页面背景图片 URL 或路径 + pageBackgroundOpacity?: number; // 页面背景透明度 (0-1) + updatedAt?: number; +} + +/** + * 用于更新外观设置的数据结构 (所有字段可选) + */ +export type UpdateAppearanceDto = Partial>; diff --git a/packages/backend/src/types/terminal-theme.types.ts b/packages/backend/src/types/terminal-theme.types.ts new file mode 100644 index 0000000..c96f215 --- /dev/null +++ b/packages/backend/src/types/terminal-theme.types.ts @@ -0,0 +1,24 @@ +import type { ITheme } from 'xterm'; + +/** + * 终端主题数据结构 + */ +export interface TerminalTheme { + _id?: string; // NeDB 自动生成的 ID + name: string; // 主题名称,例如 "默认暗色", "Solarized Light" + themeData: ITheme; // xterm.js 的 ITheme 对象 + isPreset: boolean; // 是否为系统预设主题 + isSystemDefault?: boolean; // (可选) 是否为系统默认主题 + createdAt?: number; // 创建时间戳 + updatedAt?: number; // 更新时间戳 +} + +/** + * 用于创建新主题的数据结构 (可能不需要 _id, isPreset 等) + */ +export type CreateTerminalThemeDto = Omit; + +/** + * 用于更新主题的数据结构 (所有字段可选) + */ +export type UpdateTerminalThemeDto = Partial>; diff --git a/packages/backend/uploads/backgrounds/1744891110818-458210923-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744891110818-458210923-photo_2025_02_21_18_19_37.jpg new file mode 100644 index 0000000..d5e1b56 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891110818-458210923-photo_2025_02_21_18_19_37.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744891131360-900833841-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891131360-900833841-electerm_theme_termius_bg.png new file mode 100644 index 0000000..3d8da0e Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891131360-900833841-electerm_theme_termius_bg.png differ diff --git a/packages/backend/uploads/backgrounds/1744891372507-129203109-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891372507-129203109-electerm_theme_termius_bg.png new file mode 100644 index 0000000..3d8da0e Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891372507-129203109-electerm_theme_termius_bg.png differ diff --git a/packages/backend/uploads/backgrounds/1744891516681-853695262-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891516681-853695262-electerm_theme_termius_bg.png new file mode 100644 index 0000000..3d8da0e Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891516681-853695262-electerm_theme_termius_bg.png differ diff --git a/packages/backend/uploads/backgrounds/1744891586352-246836344-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891586352-246836344-electerm_theme_termius_bg.png new file mode 100644 index 0000000..3d8da0e Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891586352-246836344-electerm_theme_termius_bg.png differ diff --git a/packages/backend/uploads/backgrounds/1744891636121-95985054-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744891636121-95985054-photo_2025_02_21_18_19_37.jpg new file mode 100644 index 0000000..d5e1b56 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891636121-95985054-photo_2025_02_21_18_19_37.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744891704175-821520443-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744891704175-821520443-photo_2025_02_21_18_19_37.jpg new file mode 100644 index 0000000..d5e1b56 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891704175-821520443-photo_2025_02_21_18_19_37.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744891708952-69455130-photo_2024_07_23_16_27_29.jpg b/packages/backend/uploads/backgrounds/1744891708952-69455130-photo_2024_07_23_16_27_29.jpg new file mode 100644 index 0000000..041a952 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891708952-69455130-photo_2024_07_23_16_27_29.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744891768744-518616352-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891768744-518616352-electerm_theme_termius_bg.png new file mode 100644 index 0000000..3d8da0e Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891768744-518616352-electerm_theme_termius_bg.png differ diff --git a/packages/backend/uploads/backgrounds/1744891781051-54616880-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744891781051-54616880-20200806_225831.jpg new file mode 100644 index 0000000..b355043 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891781051-54616880-20200806_225831.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744891865390-270625090-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744891865390-270625090-20200806_225831.jpg new file mode 100644 index 0000000..b355043 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891865390-270625090-20200806_225831.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744891972263-47439011-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744891972263-47439011-20200806_225831.jpg new file mode 100644 index 0000000..b355043 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744891972263-47439011-20200806_225831.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744892074360-955625026-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744892074360-955625026-20200806_225831.jpg new file mode 100644 index 0000000..b355043 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744892074360-955625026-20200806_225831.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744892189492-579083255-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744892189492-579083255-photo_2025_02_21_18_19_37.jpg new file mode 100644 index 0000000..d5e1b56 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744892189492-579083255-photo_2025_02_21_18_19_37.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744892411731-109400587-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744892411731-109400587-20200806_225831.jpg new file mode 100644 index 0000000..b355043 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744892411731-109400587-20200806_225831.jpg differ diff --git a/packages/backend/uploads/backgrounds/1744892761635-658966873-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744892761635-658966873-20200806_225831.jpg new file mode 100644 index 0000000..b355043 Binary files /dev/null and b/packages/backend/uploads/backgrounds/1744892761635-658966873-20200806_225831.jpg differ diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index e695254..6954a4c 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -2,8 +2,8 @@ import { RouterLink, RouterView } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useAuthStore } from './stores/auth.store'; -import { useSettingsStore } from './stores/settings.store'; // 导入设置 Store -import { ref } from 'vue'; // 导入 ref +import { useSettingsStore } from './stores/settings.store'; +import { useAppearanceStore } from './stores/appearance.store'; // 导入外观 Store import { storeToRefs } from 'pinia'; // 导入通知显示组件 import UINotificationDisplay from './components/UINotificationDisplay.vue'; @@ -14,25 +14,24 @@ import StyleCustomizer from './components/StyleCustomizer.vue'; const { t } = useI18n(); const authStore = useAuthStore(); -const settingsStore = useSettingsStore(); // 实例化设置 Store -const { isAuthenticated } = storeToRefs(authStore); // 获取登录状态 -const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); // 获取弹窗编辑器设置 - -// 控制样式自定义器可见性的状态 -const isStyleCustomizerVisible = ref(false); +const settingsStore = useSettingsStore(); +const appearanceStore = useAppearanceStore(); // 实例化外观 Store +const { isAuthenticated } = storeToRefs(authStore); +const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); +const { isStyleCustomizerVisible } = storeToRefs(appearanceStore); // 从外观 store 获取可见性状态 const handleLogout = () => { authStore.logout(); }; -// 打开样式自定义器 +// 打开样式自定义器的方法现在直接调用 store action const openStyleCustomizer = () => { - isStyleCustomizerVisible.value = true; + appearanceStore.toggleStyleCustomizer(true); }; -// 关闭样式自定义器 (由子组件触发) +// 关闭样式自定义器的方法现在也调用 store action const closeStyleCustomizer = () => { - isStyleCustomizerVisible.value = false; + appearanceStore.toggleStyleCustomizer(false); }; @@ -48,7 +47,7 @@ const closeStyleCustomizer = () => { {{ t('nav.notifications') }} | {{ t('nav.auditLogs') }} | {{ t('nav.settings') }} | - 🎨 | + 🎨 | {{ t('nav.login') }} {{ t('nav.logout') }} @@ -64,7 +63,7 @@ const closeStyleCustomizer = () => { - +