🎨 Добавлен полный редактор стилей и поле image_url для туров

 Новые функции:
- Поле image_url в модели туров для изменения изображений через админ-панель
- Расширенная модель настроек сайта с категориями: colors, typography, images, effects, layout
- Динамический CSS генератор на основе настроек (/dynamic-styles.css)
- API для управления настройками сайта (/api/site-settings)

🎯 Редактор стилей:
- Управление цветами (основные, акцентные, текст, фон)
- Настройка типографики (шрифты, размеры, межстрочный интервал)
- Управление изображениями (фоны, логотипы, фавикон)
- Эффекты (прозрачность, тени, размытие, скругления)
- Макет (высота секций, размеры контейнеров)
- Пользовательский CSS код

🛠️ Техническая реализация:
- SiteSettingsHelper с кешированием для производительности
- CSS переменные для динамического изменения стилей
- Автоматическая миграция базы данных
- Интеграция с AdminJS для удобного управления
- Загрузка настроек в шаблоны для доступности

📊 База данных:
- Расширена таблица site_settings (добавлено поле category)
- Новые типы настроек: color, file
- 27 предустановленных настроек для полного контроля над дизайном
- Автоматическое применение миграций при старте приложения
This commit is contained in:
2025-11-29 22:03:00 +09:00
parent a461fea9d9
commit ed871fc4d1
8 changed files with 528 additions and 28 deletions

View File

@@ -0,0 +1,190 @@
import db from '../config/database.js';
class SiteSettingsHelper {
static cache = new Map();
static lastUpdate = null;
static cacheTimeout = 5 * 60 * 1000; // 5 минут
/**
* Получить все настройки сайта с кешированием
*/
static async getAllSettings() {
const now = Date.now();
// Проверяем кеш
if (this.lastUpdate && (now - this.lastUpdate) < this.cacheTimeout && this.cache.size > 0) {
return Object.fromEntries(this.cache);
}
try {
const result = await db.query('SELECT setting_key, setting_value, setting_type FROM site_settings ORDER BY category, setting_key');
// Очищаем кеш и заполняем новыми данными
this.cache.clear();
for (const row of result.rows) {
let value = row.setting_value;
// Преобразуем значение в зависимости от типа
switch (row.setting_type) {
case 'number':
value = parseFloat(value) || 0;
break;
case 'boolean':
value = value === 'true' || value === '1';
break;
case 'json':
try {
value = JSON.parse(value);
} catch {
value = {};
}
break;
}
this.cache.set(row.setting_key, value);
}
this.lastUpdate = now;
return Object.fromEntries(this.cache);
} catch (error) {
console.error('Error loading site settings:', error);
return {};
}
}
/**
* Получить настройку по ключу
*/
static async getSetting(key, defaultValue = null) {
const settings = await this.getAllSettings();
return settings[key] !== undefined ? settings[key] : defaultValue;
}
/**
* Установить настройку
*/
static async setSetting(key, value, type = 'text', description = '', category = 'general') {
try {
await db.query(`
INSERT INTO site_settings (setting_key, setting_value, setting_type, description, category, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (setting_key)
DO UPDATE SET
setting_value = EXCLUDED.setting_value,
setting_type = EXCLUDED.setting_type,
description = EXCLUDED.description,
category = EXCLUDED.category,
updated_at = NOW()
`, [key, String(value), type, description, category]);
// Обновляем кеш
this.cache.set(key, value);
return true;
} catch (error) {
console.error('Error setting site setting:', error);
return false;
}
}
/**
* Получить настройки по категории
*/
static async getSettingsByCategory(category) {
try {
const result = await db.query(
'SELECT setting_key, setting_value, setting_type FROM site_settings WHERE category = $1 ORDER BY setting_key',
[category]
);
const settings = {};
for (const row of result.rows) {
let value = row.setting_value;
switch (row.setting_type) {
case 'number':
value = parseFloat(value) || 0;
break;
case 'boolean':
value = value === 'true' || value === '1';
break;
case 'json':
try {
value = JSON.parse(value);
} catch {
value = {};
}
break;
}
settings[row.setting_key] = value;
}
return settings;
} catch (error) {
console.error('Error loading settings by category:', error);
return {};
}
}
/**
* Генерировать CSS переменные из настроек
*/
static async generateCSSVariables() {
const settings = await this.getAllSettings();
let css = ':root {\n';
// Цвета
if (settings.primary_color) css += ` --primary-color: ${settings.primary_color};\n`;
if (settings.secondary_color) css += ` --secondary-color: ${settings.secondary_color};\n`;
if (settings.accent_color) css += ` --accent-color: ${settings.accent_color};\n`;
if (settings.text_color) css += ` --text-color: ${settings.text_color};\n`;
if (settings.background_color) css += ` --background-color: ${settings.background_color};\n`;
if (settings.card_background) css += ` --card-background: ${settings.card_background};\n`;
// Типографика
if (settings.font_family_primary) css += ` --font-family-primary: ${settings.font_family_primary};\n`;
if (settings.font_family_display) css += ` --font-family-display: ${settings.font_family_display};\n`;
if (settings.font_size_base) css += ` --font-size-base: ${settings.font_size_base}px;\n`;
if (settings.line_height_base) css += ` --line-height-base: ${settings.line_height_base};\n`;
// Эффекты
if (settings.hero_overlay_opacity) css += ` --hero-overlay-opacity: ${settings.hero_overlay_opacity};\n`;
if (settings.hero_overlay_color) css += ` --hero-overlay-color: ${settings.hero_overlay_color};\n`;
if (settings.card_shadow) css += ` --card-shadow: ${settings.card_shadow};\n`;
if (settings.border_radius) css += ` --border-radius: ${settings.border_radius}px;\n`;
if (settings.blur_effect) css += ` --blur-effect: ${settings.blur_effect}px;\n`;
// Макет
if (settings.hero_height_desktop) css += ` --hero-height-desktop: ${settings.hero_height_desktop}vh;\n`;
if (settings.hero_height_mobile) css += ` --hero-height-mobile: ${settings.hero_height_mobile}vh;\n`;
if (settings.compact_hero_height) css += ` --compact-hero-height: ${settings.compact_hero_height}vh;\n`;
if (settings.container_max_width) css += ` --container-max-width: ${settings.container_max_width}px;\n`;
if (settings.navbar_height) css += ` --navbar-height: ${settings.navbar_height}px;\n`;
// Анимации
if (settings.animation_duration) css += ` --animation-duration: ${settings.animation_duration}s;\n`;
css += '}\n';
// Добавляем пользовательский CSS
if (settings.custom_css) {
css += '\n/* Пользовательский CSS */\n' + settings.custom_css + '\n';
}
return css;
}
/**
* Очистить кеш настроек
*/
static clearCache() {
this.cache.clear();
this.lastUpdate = null;
}
}
export default SiteSettingsHelper;