- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
1013 lines
38 KiB
HTML
1013 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Редактор стилей сайта</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: #f5f5f5;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.editor-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
/* Боковая панель */
|
||
.sidebar {
|
||
width: 350px;
|
||
background: white;
|
||
border-right: 1px solid #e0e0e0;
|
||
overflow-y: auto;
|
||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.sidebar-header h1 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sidebar-header p {
|
||
opacity: 0.9;
|
||
font-size: 14px;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
/* Группы настроек */
|
||
.settings-group {
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.group-header {
|
||
padding: 15px 20px;
|
||
background: #fafafa;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: #333;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.group-header:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.group-header .toggle {
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.group-header.collapsed .toggle {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.group-content {
|
||
padding: 15px 20px;
|
||
}
|
||
|
||
.group-content.collapsed {
|
||
display: none;
|
||
}
|
||
|
||
/* Элементы формы */
|
||
.form-row {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #555;
|
||
margin-bottom: 5px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
/* Цветовые контролы */
|
||
.color-control {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.color-preview {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 6px;
|
||
border: 1px solid #ddd;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.color-input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.color-text {
|
||
flex: 1;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Файловые контролы */
|
||
.file-control {
|
||
position: relative;
|
||
}
|
||
|
||
.file-preview {
|
||
width: 100%;
|
||
height: 80px;
|
||
border: 2px dashed #ddd;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #888;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
background-size: cover;
|
||
background-position: center;
|
||
}
|
||
|
||
.file-preview:hover {
|
||
border-color: #667eea;
|
||
color: #667eea;
|
||
}
|
||
|
||
.file-preview.has-image {
|
||
border-style: solid;
|
||
color: white;
|
||
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.file-input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Селекты */
|
||
.select-control {
|
||
position: relative;
|
||
}
|
||
|
||
.form-select {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Превью область */
|
||
.preview-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.preview-header {
|
||
padding: 15px 20px;
|
||
background: white;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.preview-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #5a6fd8;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f8f9fa;
|
||
color: #6c757d;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #e9ecef;
|
||
}
|
||
|
||
.preview-content {
|
||
flex: 1;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.preview-iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
background: white;
|
||
}
|
||
|
||
/* Уведомления */
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 12px 20px;
|
||
border-radius: 6px;
|
||
color: white;
|
||
font-weight: 500;
|
||
z-index: 1000;
|
||
transform: translateX(100%);
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.notification.show {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.notification.success {
|
||
background: #28a745;
|
||
}
|
||
|
||
.notification.error {
|
||
background: #dc3545;
|
||
}
|
||
|
||
/* Резайзер */
|
||
.resizer {
|
||
width: 5px;
|
||
background: transparent;
|
||
cursor: col-resize;
|
||
position: relative;
|
||
}
|
||
|
||
.resizer:hover {
|
||
background: #667eea;
|
||
}
|
||
|
||
/* Мобильная адаптация */
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
width: 100%;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 100;
|
||
transform: translateX(-100%);
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.sidebar.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.mobile-toggle {
|
||
display: block;
|
||
position: fixed;
|
||
top: 20px;
|
||
left: 20px;
|
||
z-index: 101;
|
||
background: #667eea;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
|
||
.mobile-toggle {
|
||
display: none;
|
||
}
|
||
|
||
/* Загрузка */
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: #888;
|
||
}
|
||
|
||
.spinner {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid #f3f3f3;
|
||
border-top: 2px solid #667eea;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="mobile-toggle" onclick="toggleSidebar()">☰</button>
|
||
|
||
<div class="editor-container">
|
||
<!-- Боковая панель с настройками -->
|
||
<div class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<h1>🎨 Редактор стилей</h1>
|
||
<p>Настройки дизайна сайта</p>
|
||
</div>
|
||
|
||
<!-- Цвета -->
|
||
<div class="settings-group">
|
||
<div class="group-header" onclick="toggleGroup('colors')">
|
||
<span>🎨 Цветовая схема</span>
|
||
<span class="toggle">▼</span>
|
||
</div>
|
||
<div class="group-content" id="colors-content">
|
||
<div class="form-row">
|
||
<label class="form-label">Основной цвет</label>
|
||
<div class="color-control">
|
||
<div class="color-preview" style="background-color: #ff6b6b;">
|
||
<input type="color" class="color-input" value="#ff6b6b" onchange="updateColor('primary', this.value)">
|
||
</div>
|
||
<input type="text" class="color-text form-input" value="#ff6b6b" oninput="updateColorText('primary', this.value)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Вторичный цвет</label>
|
||
<div class="color-control">
|
||
<div class="color-preview" style="background-color: #38C172;">
|
||
<input type="color" class="color-input" value="#38C172" onchange="updateColor('secondary', this.value)">
|
||
</div>
|
||
<input type="text" class="color-text form-input" value="#38C172" oninput="updateColorText('secondary', this.value)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Цвет фона</label>
|
||
<div class="color-control">
|
||
<div class="color-preview" style="background-color: #f8f9fa;">
|
||
<input type="color" class="color-input" value="#f8f9fa" onchange="updateColor('background', this.value)">
|
||
</div>
|
||
<input type="text" class="color-text form-input" value="#f8f9fa" oninput="updateColorText('background', this.value)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Цвет текста</label>
|
||
<div class="color-control">
|
||
<div class="color-preview" style="background-color: #333333;">
|
||
<input type="color" class="color-input" value="#333333" onchange="updateColor('text', this.value)">
|
||
</div>
|
||
<input type="text" class="color-text form-input" value="#333333" oninput="updateColorText('text', this.value)">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Типографика -->
|
||
<div class="settings-group">
|
||
<div class="group-header" onclick="toggleGroup('typography')">
|
||
<span>📝 Типографика</span>
|
||
<span class="toggle">▼</span>
|
||
</div>
|
||
<div class="group-content" id="typography-content">
|
||
<div class="form-row">
|
||
<label class="form-label">Основной шрифт</label>
|
||
<select class="form-select" onchange="updateFont('primary', this.value)">
|
||
<option value="'Inter', sans-serif">Inter</option>
|
||
<option value="'Roboto', sans-serif">Roboto</option>
|
||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||
<option value="'Montserrat', sans-serif">Montserrat</option>
|
||
<option value="'Poppins', sans-serif">Poppins</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Размер базового шрифта</label>
|
||
<input type="range" class="form-input" min="14" max="20" value="16"
|
||
oninput="updateFontSize('base', this.value); this.nextElementSibling.textContent = this.value + 'px'">
|
||
<span>16px</span>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Высота строки</label>
|
||
<input type="range" class="form-input" min="1.2" max="2.0" step="0.1" value="1.6"
|
||
oninput="updateLineHeight(this.value); this.nextElementSibling.textContent = this.value">
|
||
<span>1.6</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Изображения -->
|
||
<div class="settings-group">
|
||
<div class="group-header" onclick="toggleGroup('images')">
|
||
<span>🖼️ Изображения</span>
|
||
<span class="toggle">▼</span>
|
||
</div>
|
||
<div class="group-content" id="images-content">
|
||
<div class="form-row">
|
||
<label class="form-label">Логотип</label>
|
||
<div class="file-control">
|
||
<div class="file-preview" onclick="document.getElementById('logo-input').click()">
|
||
<span>Выберите логотип</span>
|
||
</div>
|
||
<input type="file" id="logo-input" class="file-input" accept="image/*" onchange="uploadImage('logo', this)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Фоновое изображение героя</label>
|
||
<div class="file-control">
|
||
<div class="file-preview" onclick="document.getElementById('hero-bg-input').click()">
|
||
<span>Выберите фон</span>
|
||
</div>
|
||
<input type="file" id="hero-bg-input" class="file-input" accept="image/*" onchange="uploadImage('hero-bg', this)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Фавикон</label>
|
||
<div class="file-control">
|
||
<div class="file-preview" onclick="document.getElementById('favicon-input').click()">
|
||
<span>Выберите фавикон</span>
|
||
</div>
|
||
<input type="file" id="favicon-input" class="file-input" accept="image/x-icon,image/png" onchange="uploadImage('favicon', this)">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Макет -->
|
||
<div class="settings-group">
|
||
<div class="group-header" onclick="toggleGroup('layout')">
|
||
<span>📐 Макет</span>
|
||
<span class="toggle">▼</span>
|
||
</div>
|
||
<div class="group-content" id="layout-content">
|
||
<div class="form-row">
|
||
<label class="form-label">Максимальная ширина контента</label>
|
||
<input type="range" class="form-input" min="1000" max="1400" value="1200"
|
||
oninput="updateMaxWidth(this.value); this.nextElementSibling.textContent = this.value + 'px'">
|
||
<span>1200px</span>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Отступы секций</label>
|
||
<input type="range" class="form-input" min="20" max="100" value="60"
|
||
oninput="updateSectionPadding(this.value); this.nextElementSibling.textContent = this.value + 'px'">
|
||
<span>60px</span>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Скругление углов</label>
|
||
<input type="range" class="form-input" min="0" max="20" value="8"
|
||
oninput="updateBorderRadius(this.value); this.nextElementSibling.textContent = this.value + 'px'">
|
||
<span>8px</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Анимации -->
|
||
<div class="settings-group">
|
||
<div class="group-header" onclick="toggleGroup('animations')">
|
||
<span>✨ Анимации</span>
|
||
<span class="toggle">▼</span>
|
||
</div>
|
||
<div class="group-content" id="animations-content">
|
||
<div class="form-row">
|
||
<label class="form-label">Скорость переходов</label>
|
||
<select class="form-select" onchange="updateTransitionSpeed(this.value)">
|
||
<option value="0.2s">Быстро (0.2s)</option>
|
||
<option value="0.3s" selected>Нормально (0.3s)</option>
|
||
<option value="0.5s">Медленно (0.5s)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="form-label">Эффект при наведении</label>
|
||
<select class="form-select" onchange="updateHoverEffect(this.value)">
|
||
<option value="scale">Масштабирование</option>
|
||
<option value="lift" selected>Поднятие</option>
|
||
<option value="fade">Изменение прозрачности</option>
|
||
<option value="none">Без эффекта</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Разделитель -->
|
||
<div class="resizer" onmousedown="startResize(event)"></div>
|
||
|
||
<!-- Область превью -->
|
||
<div class="preview-area">
|
||
<div class="preview-header">
|
||
<h2 class="preview-title">Превью сайта</h2>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-secondary" onclick="resetStyles()">Сбросить</button>
|
||
<button class="btn btn-secondary" onclick="exportStyles()">Экспорт</button>
|
||
<button class="btn btn-primary" onclick="saveStyles()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
<div class="preview-content">
|
||
<iframe id="preview-iframe" class="preview-iframe" src="http://localhost:3000"></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Уведомления -->
|
||
<div id="notification" class="notification"></div>
|
||
|
||
<script>
|
||
let currentStyles = {};
|
||
let isResizing = false;
|
||
|
||
// Инициализация
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadCurrentStyles();
|
||
initializeIframe();
|
||
});
|
||
|
||
// Переключение групп настроек
|
||
function toggleGroup(groupName) {
|
||
const content = document.getElementById(groupName + '-content');
|
||
const header = content.previousElementSibling;
|
||
|
||
if (content.classList.contains('collapsed')) {
|
||
content.classList.remove('collapsed');
|
||
header.classList.remove('collapsed');
|
||
} else {
|
||
content.classList.add('collapsed');
|
||
header.classList.add('collapsed');
|
||
}
|
||
}
|
||
|
||
// Мобильное меню
|
||
function toggleSidebar() {
|
||
const sidebar = document.getElementById('sidebar');
|
||
sidebar.classList.toggle('open');
|
||
}
|
||
|
||
// Обновление цветов
|
||
function updateColor(type, color) {
|
||
currentStyles[type + '-color'] = color;
|
||
|
||
// Обновляем превью цвета
|
||
const preview = document.querySelector(`input[onchange*="${type}"]`).parentElement;
|
||
preview.style.backgroundColor = color;
|
||
|
||
// Обновляем текстовое поле
|
||
const textInput = preview.nextElementSibling;
|
||
textInput.value = color;
|
||
|
||
applyStyleChanges();
|
||
}
|
||
|
||
function updateColorText(type, color) {
|
||
if (isValidColor(color)) {
|
||
updateColor(type, color);
|
||
}
|
||
}
|
||
|
||
function isValidColor(color) {
|
||
const s = new Option().style;
|
||
s.color = color;
|
||
return s.color !== '';
|
||
}
|
||
|
||
// Обновление шрифтов
|
||
function updateFont(type, font) {
|
||
currentStyles[type + '-font'] = font;
|
||
applyStyleChanges();
|
||
}
|
||
|
||
function updateFontSize(type, size) {
|
||
currentStyles[type + '-font-size'] = size + 'px';
|
||
applyStyleChanges();
|
||
}
|
||
|
||
function updateLineHeight(height) {
|
||
currentStyles['line-height'] = height;
|
||
applyStyleChanges();
|
||
}
|
||
|
||
// Загрузка изображений
|
||
async function uploadImage(type, input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('image', file);
|
||
|
||
try {
|
||
showNotification('Загрузка изображения...', 'info');
|
||
|
||
const response = await fetch('/api/images/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
currentStyles[type + '-image'] = result.url;
|
||
|
||
// Обновляем превью
|
||
const preview = input.parentElement.querySelector('.file-preview');
|
||
preview.style.backgroundImage = `url(${result.url})`;
|
||
preview.classList.add('has-image');
|
||
preview.innerHTML = '<span>Изображение загружено</span>';
|
||
|
||
applyStyleChanges();
|
||
showNotification('Изображение успешно загружено!', 'success');
|
||
} else {
|
||
throw new Error(result.error || 'Ошибка загрузки');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки изображения:', error);
|
||
showNotification('Ошибка загрузки изображения', 'error');
|
||
}
|
||
}
|
||
|
||
// Обновление макета
|
||
function updateMaxWidth(width) {
|
||
currentStyles['max-width'] = width + 'px';
|
||
applyStyleChanges();
|
||
}
|
||
|
||
function updateSectionPadding(padding) {
|
||
currentStyles['section-padding'] = padding + 'px';
|
||
applyStyleChanges();
|
||
}
|
||
|
||
function updateBorderRadius(radius) {
|
||
currentStyles['border-radius'] = radius + 'px';
|
||
applyStyleChanges();
|
||
}
|
||
|
||
// Обновление анимаций
|
||
function updateTransitionSpeed(speed) {
|
||
currentStyles['transition-speed'] = speed;
|
||
applyStyleChanges();
|
||
}
|
||
|
||
function updateHoverEffect(effect) {
|
||
currentStyles['hover-effect'] = effect;
|
||
applyStyleChanges();
|
||
}
|
||
|
||
// Применение изменений стилей
|
||
function applyStyleChanges() {
|
||
const iframe = document.getElementById('preview-iframe');
|
||
if (!iframe.contentDocument) return;
|
||
|
||
let customCSS = generateCustomCSS();
|
||
|
||
// Удаляем старые стили
|
||
const oldStyle = iframe.contentDocument.querySelector('#custom-styles');
|
||
if (oldStyle) oldStyle.remove();
|
||
|
||
// Добавляем новые стили
|
||
const style = iframe.contentDocument.createElement('style');
|
||
style.id = 'custom-styles';
|
||
style.textContent = customCSS;
|
||
iframe.contentDocument.head.appendChild(style);
|
||
}
|
||
|
||
// Генерация CSS
|
||
function generateCustomCSS() {
|
||
let css = ':root {\n';
|
||
|
||
// Цвета
|
||
if (currentStyles['primary-color']) {
|
||
css += ` --primary-color: ${currentStyles['primary-color']};\n`;
|
||
css += ` --primary-rgb: ${hexToRgb(currentStyles['primary-color'])};\n`;
|
||
}
|
||
if (currentStyles['secondary-color']) {
|
||
css += ` --secondary-color: ${currentStyles['secondary-color']};\n`;
|
||
css += ` --secondary-rgb: ${hexToRgb(currentStyles['secondary-color'])};\n`;
|
||
}
|
||
if (currentStyles['background-color']) {
|
||
css += ` --background-color: ${currentStyles['background-color']};\n`;
|
||
}
|
||
if (currentStyles['text-color']) {
|
||
css += ` --text-color: ${currentStyles['text-color']};\n`;
|
||
}
|
||
|
||
css += '}\n\n';
|
||
|
||
// Основные стили
|
||
css += 'body {\n';
|
||
if (currentStyles['primary-font']) {
|
||
css += ` font-family: ${currentStyles['primary-font']} !important;\n`;
|
||
}
|
||
if (currentStyles['base-font-size']) {
|
||
css += ` font-size: ${currentStyles['base-font-size']} !important;\n`;
|
||
}
|
||
if (currentStyles['line-height']) {
|
||
css += ` line-height: ${currentStyles['line-height']} !important;\n`;
|
||
}
|
||
if (currentStyles['text-color']) {
|
||
css += ` color: ${currentStyles['text-color']} !important;\n`;
|
||
}
|
||
css += '}\n\n';
|
||
|
||
// Контейнеры
|
||
if (currentStyles['max-width']) {
|
||
css += `.container, .main-content {\n max-width: ${currentStyles['max-width']} !important;\n margin: 0 auto;\n}\n\n`;
|
||
}
|
||
|
||
// Секции
|
||
if (currentStyles['section-padding']) {
|
||
css += `.section, section {\n padding: ${currentStyles['section-padding']} 0 !important;\n}\n\n`;
|
||
}
|
||
|
||
// Цветовые схемы для компонентов
|
||
if (currentStyles['primary-color']) {
|
||
css += `.btn-primary, .bg-primary, .text-primary {\n background-color: ${currentStyles['primary-color']} !important;\n border-color: ${currentStyles['primary-color']} !important;\n}\n\n`;
|
||
css += `.btn-primary:hover {\n background-color: ${darkenColor(currentStyles['primary-color'], 10)} !important;\n border-color: ${darkenColor(currentStyles['primary-color'], 10)} !important;\n}\n\n`;
|
||
css += `.navbar-brand, .nav-link:hover {\n color: ${currentStyles['primary-color']} !important;\n}\n\n`;
|
||
}
|
||
|
||
if (currentStyles['secondary-color']) {
|
||
css += `.btn-secondary, .bg-secondary {\n background-color: ${currentStyles['secondary-color']} !important;\n border-color: ${currentStyles['secondary-color']} !important;\n}\n\n`;
|
||
css += `.btn-secondary:hover {\n background-color: ${darkenColor(currentStyles['secondary-color'], 10)} !important;\n border-color: ${darkenColor(currentStyles['secondary-color'], 10)} !important;\n}\n\n`;
|
||
}
|
||
|
||
// Фон страницы
|
||
if (currentStyles['background-color']) {
|
||
css += `body, .bg-light {\n background-color: ${currentStyles['background-color']} !important;\n}\n\n`;
|
||
}
|
||
|
||
// Скругление углов
|
||
if (currentStyles['border-radius']) {
|
||
css += `.card, .btn, .form-control, .rounded, .modal-content {\n border-radius: ${currentStyles['border-radius']} !important;\n}\n\n`;
|
||
css += `.card-img-top {\n border-top-left-radius: ${currentStyles['border-radius']} !important;\n border-top-right-radius: ${currentStyles['border-radius']} !important;\n}\n\n`;
|
||
}
|
||
|
||
// Переходы
|
||
if (currentStyles['transition-speed']) {
|
||
css += `*, *::before, *::after {\n transition: all ${currentStyles['transition-speed']} ease !important;\n}\n\n`;
|
||
}
|
||
|
||
// Эффекты при наведении
|
||
if (currentStyles['hover-effect']) {
|
||
switch(currentStyles['hover-effect']) {
|
||
case 'scale':
|
||
css += `.card:hover, .btn:hover {\n transform: scale(1.05) !important;\n}\n\n`;
|
||
break;
|
||
case 'lift':
|
||
css += `.card:hover {\n transform: translateY(-10px) !important;\n box-shadow: 0 20px 40px rgba(0,0,0,0.15) !important;\n}\n\n`;
|
||
css += `.btn:hover {\n transform: translateY(-2px) !important;\n box-shadow: 0 8px 20px rgba(0,0,0,0.15) !important;\n}\n\n`;
|
||
break;
|
||
case 'fade':
|
||
css += `.card:hover, .btn:hover {\n opacity: 0.85 !important;\n}\n\n`;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Фоновые изображения
|
||
if (currentStyles['hero-bg-image']) {
|
||
css += `.hero, .jumbotron, .bg-hero {\n background-image: url('${currentStyles['hero-bg-image']}') !important;\n background-size: cover;\n background-position: center;\n background-attachment: fixed;\n}\n\n`;
|
||
}
|
||
|
||
// Логотип
|
||
if (currentStyles['logo-image']) {
|
||
css += `.navbar-brand img, .logo {\n content: url('${currentStyles['logo-image']}') !important;\n max-height: 50px;\n width: auto;\n}\n\n`;
|
||
}
|
||
|
||
return css;
|
||
}
|
||
|
||
// Вспомогательные функции для работы с цветами
|
||
function hexToRgb(hex) {
|
||
const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
||
return result ?
|
||
\`\${parseInt(result[1], 16)}, \${parseInt(result[2], 16)}, \${parseInt(result[3], 16)}\` :
|
||
'0, 0, 0';
|
||
}
|
||
|
||
function darkenColor(hex, amount) {
|
||
const num = parseInt(hex.replace('#', ''), 16);
|
||
const r = Math.max(0, (num >> 16) - amount);
|
||
const g = Math.max(0, (num >> 8 & 0x00FF) - amount);
|
||
const b = Math.max(0, (num & 0x0000FF) - amount);
|
||
return '#' + (r << 16 | g << 8 | b).toString(16).padStart(6, '0');
|
||
}
|
||
|
||
// Инициализация iframe
|
||
function initializeIframe() {
|
||
const iframe = document.getElementById('preview-iframe');
|
||
|
||
iframe.onload = function() {
|
||
// Применяем текущие стили после загрузки
|
||
setTimeout(() => {
|
||
applyStyleChanges();
|
||
}, 100);
|
||
};
|
||
}
|
||
|
||
// Загрузка текущих стилей
|
||
async function loadCurrentStyles() {
|
||
try {
|
||
const response = await fetch('/api/settings/styles');
|
||
const data = await response.json();
|
||
currentStyles = data.styles || {};
|
||
updateUIFromStyles();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки стилей:', error);
|
||
}
|
||
}
|
||
|
||
// Обновление интерфейса из стилей
|
||
function updateUIFromStyles() {
|
||
// Обновляем все контролы на основе загруженных стилей
|
||
Object.keys(currentStyles).forEach(key => {
|
||
const value = currentStyles[key];
|
||
|
||
// Находим соответствующий контрол и обновляем его
|
||
const control = document.querySelector(`[onchange*="${key}"], [oninput*="${key}"]`);
|
||
if (control) {
|
||
control.value = value;
|
||
|
||
// Особая обработка для цветовых контролов
|
||
if (key.includes('color')) {
|
||
const preview = control.parentElement;
|
||
if (preview.classList.contains('color-preview')) {
|
||
preview.style.backgroundColor = value;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Сохранение стилей
|
||
async function saveStyles() {
|
||
try {
|
||
showNotification('Сохранение стилей...', 'info');
|
||
|
||
const response = await fetch('/api/settings/styles', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
styles: currentStyles,
|
||
css: generateCustomCSS()
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showNotification('Стили успешно сохранены!', 'success');
|
||
} else {
|
||
throw new Error(result.error || 'Ошибка сохранения');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка сохранения стилей:', error);
|
||
showNotification('Ошибка сохранения стилей', 'error');
|
||
}
|
||
}
|
||
|
||
// Сброс стилей
|
||
function resetStyles() {
|
||
if (confirm('Вы уверены, что хотите сбросить все настройки?')) {
|
||
currentStyles = {};
|
||
updateUIFromStyles();
|
||
applyStyleChanges();
|
||
showNotification('Стили сброшены', 'success');
|
||
}
|
||
}
|
||
|
||
// Экспорт стилей
|
||
function exportStyles() {
|
||
const css = generateCustomCSS();
|
||
const blob = new Blob([css], { type: 'text/css' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'custom-styles.css';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
showNotification('CSS файл экспортирован', 'success');
|
||
}
|
||
|
||
// Уведомления
|
||
function showNotification(message, type = 'info') {
|
||
const notification = document.getElementById('notification');
|
||
notification.textContent = message;
|
||
notification.className = `notification ${type} show`;
|
||
|
||
setTimeout(() => {
|
||
notification.classList.remove('show');
|
||
}, 3000);
|
||
}
|
||
|
||
// Изменение размера панели
|
||
function startResize(e) {
|
||
isResizing = true;
|
||
document.addEventListener('mousemove', resize);
|
||
document.addEventListener('mouseup', stopResize);
|
||
}
|
||
|
||
function resize(e) {
|
||
if (!isResizing) return;
|
||
|
||
const sidebar = document.getElementById('sidebar');
|
||
const newWidth = Math.min(Math.max(e.clientX, 250), window.innerWidth * 0.7);
|
||
sidebar.style.width = newWidth + 'px';
|
||
}
|
||
|
||
function stopResize() {
|
||
isResizing = false;
|
||
document.removeEventListener('mousemove', resize);
|
||
document.removeEventListener('mouseup', stopResize);
|
||
}
|
||
|
||
// Горячие клавиши
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.ctrlKey || e.metaKey) {
|
||
switch(e.key) {
|
||
case 's':
|
||
e.preventDefault();
|
||
saveStyles();
|
||
break;
|
||
case 'r':
|
||
e.preventDefault();
|
||
resetStyles();
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |