✨ Новые функции: - Поле 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 предустановленных настроек для полного контроля над дизайном - Автоматическое применение миграций при старте приложения
190 lines
6.5 KiB
JavaScript
190 lines
6.5 KiB
JavaScript
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; |