update
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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');
|
||||
@@ -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;
|
||||
@@ -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<string, string> = {
|
||||
'--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',
|
||||
};
|
||||
|
||||
// 未来可以在这里添加其他默认主题
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<AppearanceSettings>
|
||||
*/
|
||||
export const getAppearanceSettings = async (): Promise<AppearanceSettings> => {
|
||||
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<boolean> 是否成功更新
|
||||
*/
|
||||
export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise<boolean> => {
|
||||
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();
|
||||
@@ -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<TerminalTheme[]>
|
||||
*/
|
||||
export const findAllThemes = async (): Promise<TerminalTheme[]> => {
|
||||
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<TerminalTheme | null>
|
||||
*/
|
||||
export const findThemeById = async (id: number): Promise<TerminalTheme | null> => {
|
||||
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<TerminalTheme> 新创建的主题
|
||||
*/
|
||||
export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<TerminalTheme> => {
|
||||
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<boolean> 是否成功更新
|
||||
*/
|
||||
export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto): Promise<boolean> => {
|
||||
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<boolean> 是否成功删除
|
||||
*/
|
||||
export const deleteTheme = async (id: number): Promise<boolean> => {
|
||||
// 只允许删除非预设主题
|
||||
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();
|
||||
@@ -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<AppearanceSettings>
|
||||
*/
|
||||
export const getSettings = async (): Promise<AppearanceSettings> => {
|
||||
return appearanceRepository.getAppearanceSettings();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新外观设置
|
||||
* @param settingsDto 更新数据
|
||||
* @returns Promise<boolean> 是否成功更新
|
||||
*/
|
||||
export const updateSettings = async (settingsDto: UpdateAppearanceDto): Promise<boolean> => {
|
||||
// 验证 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 上传)来添加。
|
||||
@@ -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<TerminalTheme[]>
|
||||
*/
|
||||
export const getAllThemes = async (): Promise<TerminalTheme[]> => {
|
||||
return terminalThemeRepository.findAllThemes();
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个终端主题
|
||||
* @param id 主题 ID (SQLite 数字 ID)
|
||||
* @returns Promise<TerminalTheme | null>
|
||||
*/
|
||||
export const getThemeById = async (id: number): Promise<TerminalTheme | null> => {
|
||||
if (isNaN(id)) {
|
||||
throw new Error('无效的主题 ID');
|
||||
}
|
||||
return terminalThemeRepository.findThemeById(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建新终端主题
|
||||
* @param themeDto 创建数据
|
||||
* @returns Promise<TerminalTheme>
|
||||
*/
|
||||
export const createNewTheme = async (themeDto: CreateTerminalThemeDto): Promise<TerminalTheme> => {
|
||||
// 移除验证相关的注释
|
||||
// 简单验证 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<boolean> 是否成功更新
|
||||
*/
|
||||
export const updateExistingTheme = async (id: number, themeDto: UpdateTerminalThemeDto): Promise<boolean> => {
|
||||
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<boolean> 是否成功删除
|
||||
*/
|
||||
export const deleteExistingTheme = async (id: number): Promise<boolean> => {
|
||||
if (isNaN(id)) {
|
||||
throw new Error('无效的主题 ID');
|
||||
}
|
||||
return terminalThemeRepository.deleteTheme(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* 导入终端主题
|
||||
* @param themeData 主题数据对象 (ITheme)
|
||||
* @param name 主题名称
|
||||
* @returns Promise<TerminalTheme>
|
||||
*/
|
||||
export const importTheme = async (themeData: ITheme, name: string): Promise<TerminalTheme> => {
|
||||
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 文件。
|
||||
@@ -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<string, string> = {};
|
||||
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 });
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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<Omit<AppearanceSettings, '_id' | 'userId' | 'updatedAt'>>;
|
||||
@@ -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<TerminalTheme, '_id' | 'isPreset' | 'isSystemDefault' | 'createdAt' | 'updatedAt'>;
|
||||
|
||||
/**
|
||||
* 用于更新主题的数据结构 (所有字段可选)
|
||||
*/
|
||||
export type UpdateTerminalThemeDto = Partial<Omit<TerminalTheme, '_id' | 'isPreset' | 'isSystemDefault' | 'createdAt' | 'updatedAt'>>;
|
||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 950 KiB |
|
After Width: | Height: | Size: 950 KiB |
|
After Width: | Height: | Size: 950 KiB |
|
After Width: | Height: | Size: 950 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 950 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |