🎨 Добавлен полный редактор стилей и поле 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:
190
src/helpers/site-settings.js
Normal file
190
src/helpers/site-settings.js
Normal 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;
|
||||
Reference in New Issue
Block a user