Files
tourrism_site/public/professional-style-editor.html
Andrey K. Choi 13c752b93a feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
2025-11-30 21:57:58 +09:00

1010 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Профессиональный редактор стилей</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa;
overflow: hidden;
}
.editor-container {
height: 100vh;
display: flex;
}
/* Sidebar */
.sidebar {
width: 320px;
background: white;
border-right: 1px solid #e9ecef;
overflow-y: auto;
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
}
.sidebar-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
position: sticky;
top: 0;
z-index: 10;
}
.sidebar-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 5px;
}
.sidebar-subtitle {
font-size: 14px;
opacity: 0.9;
}
/* Control Sections */
.control-section {
border-bottom: 1px solid #e9ecef;
}
.section-header {
background: #f8f9fa;
padding: 12px 20px;
font-weight: 600;
font-size: 14px;
color: #495057;
cursor: pointer;
display: flex;
align-items: center;
justify-content: between;
border-bottom: 1px solid #e9ecef;
}
.section-header:hover {
background: #e9ecef;
}
.section-content {
padding: 20px;
}
.section-content.collapsed {
display: none;
}
.section-toggle {
font-size: 12px;
margin-left: auto;
}
/* Form Controls */
.control-group {
margin-bottom: 20px;
}
.control-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
color: #495057;
}
.control-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.control-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
}
/* Color Controls */
.color-control {
display: flex;
gap: 10px;
align-items: center;
}
.color-preview {
width: 40px;
height: 32px;
border: 1px solid #ced4da;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.color-input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.color-value {
flex: 1;
font-family: 'Courier New', monospace;
}
/* Slider Controls */
.slider-control {
display: flex;
align-items: center;
gap: 10px;
}
.slider {
flex: 1;
height: 6px;
border-radius: 3px;
background: #e9ecef;
outline: none;
cursor: pointer;
}
.slider::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.slider-value {
min-width: 50px;
text-align: right;
font-family: 'Courier New', monospace;
font-size: 12px;
}
/* Image Controls */
.image-control {
border: 2px dashed #ced4da;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.image-control:hover {
border-color: #007bff;
background: #f8f9fa;
}
.image-control.has-image {
border-style: solid;
border-color: #28a745;
padding: 0;
overflow: hidden;
}
.image-preview {
max-width: 100%;
max-height: 120px;
object-fit: cover;
border-radius: 6px;
}
.image-placeholder {
color: #6c757d;
font-size: 14px;
}
.image-actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
.btn-small {
padding: 4px 8px;
font-size: 12px;
border: 1px solid #ced4da;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-small:hover {
background: #f8f9fa;
}
.btn-small.primary {
background: #007bff;
color: white;
border-color: #007bff;
}
.btn-small.primary:hover {
background: #0056b3;
}
.btn-small.danger {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.btn-small.danger:hover {
background: #c82333;
}
/* Action Buttons */
.actions-bar {
position: sticky;
bottom: 0;
background: white;
border-top: 1px solid #e9ecef;
padding: 15px 20px;
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
}
.btn.primary {
background: #28a745;
color: white;
}
.btn.primary:hover {
background: #218838;
transform: translateY(-1px);
}
.btn.secondary {
background: #6c757d;
color: white;
}
.btn.secondary:hover {
background: #5a6268;
}
/* Preview Panel */
.preview-panel {
flex: 1;
background: white;
overflow: hidden;
position: relative;
}
.preview-header {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: between;
}
.preview-title {
font-weight: 600;
color: #495057;
}
.preview-controls {
display: flex;
gap: 10px;
align-items: center;
}
.preview-frame {
width: 100%;
height: calc(100vh - 80px);
border: none;
}
/* Media Manager Modal */
.media-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.media-modal.visible {
display: flex;
}
.media-modal-content {
width: 90vw;
height: 90vh;
background: white;
border-radius: 12px;
overflow: hidden;
}
.media-modal iframe {
width: 100%;
height: 100%;
border: none;
}
/* Status */
.status-message {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 6px;
color: white;
font-weight: 500;
z-index: 2000;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.status-message.visible {
transform: translateX(0);
}
.status-message.success {
background: #28a745;
}
.status-message.error {
background: #dc3545;
}
/* Loading */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.9);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.loading-overlay.visible {
display: flex;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="editor-container">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">🎨 Редактор стилей</div>
<div class="sidebar-subtitle">Настройка дизайна сайта</div>
</div>
<!-- Colors Section -->
<div class="control-section">
<div class="section-header" onclick="toggleSection(this)">
🎨 Цвета
<span class="section-toggle"></span>
</div>
<div class="section-content">
<div class="control-group">
<label class="control-label">Основной цвет</label>
<div class="color-control">
<div class="color-preview">
<input type="color" class="color-input" id="primaryColor" value="#007bff">
</div>
<input type="text" class="control-input color-value" id="primaryColorValue" value="#007bff">
</div>
</div>
<div class="control-group">
<label class="control-label">Вторичный цвет</label>
<div class="color-control">
<div class="color-preview">
<input type="color" class="color-input" id="secondaryColor" value="#6c757d">
</div>
<input type="text" class="control-input color-value" id="secondaryColorValue" value="#6c757d">
</div>
</div>
<div class="control-group">
<label class="control-label">Цвет фона</label>
<div class="color-control">
<div class="color-preview">
<input type="color" class="color-input" id="backgroundColor" value="#f8f9fa">
</div>
<input type="text" class="control-input color-value" id="backgroundColorValue" value="#f8f9fa">
</div>
</div>
<div class="control-group">
<label class="control-label">Цвет текста</label>
<div class="color-control">
<div class="color-preview">
<input type="color" class="color-input" id="textColor" value="#333333">
</div>
<input type="text" class="control-input color-value" id="textColorValue" value="#333333">
</div>
</div>
</div>
</div>
<!-- Typography Section -->
<div class="control-section">
<div class="section-header" onclick="toggleSection(this)">
📝 Типографика
<span class="section-toggle"></span>
</div>
<div class="section-content">
<div class="control-group">
<label class="control-label">Основной шрифт</label>
<select class="control-input" id="primaryFont">
<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="'Lato', sans-serif">Lato</option>
<option value="'Montserrat', sans-serif">Montserrat</option>
<option value="'Poppins', sans-serif">Poppins</option>
<option value="Arial, sans-serif">Arial</option>
<option value="Georgia, serif">Georgia</option>
</select>
</div>
<div class="control-group">
<label class="control-label">Размер шрифта</label>
<div class="slider-control">
<input type="range" class="slider" id="fontSize" min="12" max="24" value="16">
<span class="slider-value">16px</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Межстрочный интервал</label>
<div class="slider-control">
<input type="range" class="slider" id="lineHeight" min="1.0" max="2.0" step="0.1" value="1.6">
<span class="slider-value">1.6</span>
</div>
</div>
</div>
</div>
<!-- Layout Section -->
<div class="control-section">
<div class="section-header" onclick="toggleSection(this)">
📐 Макет
<span class="section-toggle"></span>
</div>
<div class="section-content">
<div class="control-group">
<label class="control-label">Максимальная ширина</label>
<div class="slider-control">
<input type="range" class="slider" id="maxWidth" min="960" max="1600" step="40" value="1200">
<span class="slider-value">1200px</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Внутренние отступы</label>
<div class="slider-control">
<input type="range" class="slider" id="padding" min="10" max="50" step="5" value="20">
<span class="slider-value">20px</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Радиус скругления</label>
<div class="slider-control">
<input type="range" class="slider" id="borderRadius" min="0" max="20" step="2" value="6">
<span class="slider-value">6px</span>
</div>
</div>
</div>
</div>
<!-- Background Images Section -->
<div class="control-section">
<div class="section-header" onclick="toggleSection(this)">
🖼️ Фоновые изображения
<span class="section-toggle"></span>
</div>
<div class="section-content">
<div class="control-group">
<label class="control-label">Фон заголовка</label>
<div class="image-control" id="headerBgControl" onclick="openMediaManager('headerBg')">
<div class="image-placeholder">
📷 Выбрать изображение
</div>
</div>
<input type="hidden" id="headerBg">
</div>
<div class="control-group">
<label class="control-label">Фон страницы</label>
<div class="image-control" id="pageBgControl" onclick="openMediaManager('pageBg')">
<div class="image-placeholder">
📷 Выбрать изображение
</div>
</div>
<input type="hidden" id="pageBg">
</div>
</div>
</div>
<!-- Actions Bar -->
<div class="actions-bar">
<button class="btn secondary" onclick="resetToDefaults()">Сброс</button>
<button class="btn primary" onclick="saveSettings()">💾 Сохранить</button>
</div>
</div>
<!-- Preview Panel -->
<div class="preview-panel">
<div class="preview-header">
<div class="preview-title">📱 Предварительный просмотр</div>
<div class="preview-controls">
<button class="btn-small" onclick="refreshPreview()">🔄 Обновить</button>
<button class="btn-small primary" onclick="openPreviewInNewTab()">🔗 Открыть в новой вкладке</button>
</div>
</div>
<iframe class="preview-frame" id="previewFrame" src="/"></iframe>
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- Media Manager Modal -->
<div class="media-modal" id="mediaModal">
<div class="media-modal-content">
<iframe id="mediaManagerFrame" src="/universal-media-manager.html"></iframe>
</div>
</div>
<!-- Status Messages -->
<div class="status-message" id="statusMessage"></div>
<script>
let currentSettings = {};
let currentImageField = null;
// Инициализация
document.addEventListener('DOMContentLoaded', function() {
initializeControls();
loadSettings();
setupPreviewUpdates();
});
// Инициализация контролов
function initializeControls() {
// Color controls
setupColorControl('primaryColor', 'primaryColorValue');
setupColorControl('secondaryColor', 'secondaryColorValue');
setupColorControl('backgroundColor', 'backgroundColorValue');
setupColorControl('textColor', 'textColorValue');
// Slider controls
setupSliderControl('fontSize', 'px');
setupSliderControl('lineHeight', '');
setupSliderControl('maxWidth', 'px');
setupSliderControl('padding', 'px');
setupSliderControl('borderRadius', 'px');
// Other controls
document.getElementById('primaryFont').addEventListener('change', updatePreview);
}
function setupColorControl(colorId, valueId) {
const colorInput = document.getElementById(colorId);
const valueInput = document.getElementById(valueId);
colorInput.addEventListener('input', function() {
valueInput.value = this.value;
currentSettings[colorId.replace('Color', '-color')] = this.value;
updatePreview();
});
valueInput.addEventListener('input', function() {
if (/^#[0-9A-F]{6}$/i.test(this.value)) {
colorInput.value = this.value;
currentSettings[colorId.replace('Color', '-color')] = this.value;
updatePreview();
}
});
}
function setupSliderControl(sliderId, unit) {
const slider = document.getElementById(sliderId);
const valueSpan = slider.parentElement.querySelector('.slider-value');
slider.addEventListener('input', function() {
valueSpan.textContent = this.value + unit;
currentSettings[sliderId.replace(/([A-Z])/g, '-$1').toLowerCase()] = this.value + unit;
updatePreview();
});
}
// Media Manager
function openMediaManager(fieldName) {
currentImageField = fieldName;
document.getElementById('mediaModal').classList.add('visible');
}
// Получение сообщений от медиа-менеджера
window.addEventListener('message', function(event) {
if (event.data.type === 'media-manager-selection' && event.data.files.length > 0) {
const file = event.data.files[0]; // Берем первый выбранный файл
setImageForField(currentImageField, file);
document.getElementById('mediaModal').classList.remove('visible');
}
});
function setImageForField(fieldName, file) {
const control = document.getElementById(fieldName + 'Control');
const input = document.getElementById(fieldName);
// Обновляем скрытый input
input.value = file.url;
// Обновляем визуал
control.classList.add('has-image');
control.innerHTML = `
<img class="image-preview" src="${file.url}" alt="${file.name}">
<div class="image-actions">
<button class="btn-small primary" onclick="openMediaManager('${fieldName}')">Заменить</button>
<button class="btn-small danger" onclick="removeImage('${fieldName}')">Удалить</button>
</div>
`;
// Обновляем настройки
currentSettings[fieldName.replace(/([A-Z])/g, '-$1').toLowerCase()] = file.url;
updatePreview();
}
function removeImage(fieldName) {
const control = document.getElementById(fieldName + 'Control');
const input = document.getElementById(fieldName);
input.value = '';
control.classList.remove('has-image');
control.innerHTML = '<div class="image-placeholder">📷 Выбрать изображение</div>';
delete currentSettings[fieldName.replace(/([A-Z])/g, '-$1').toLowerCase()];
updatePreview();
}
// Загрузка настроек
async function loadSettings() {
try {
showLoading(true);
const response = await fetch('/api/settings/styles');
const data = await response.json();
if (data.success) {
currentSettings = data.styles;
applySettingsToControls(currentSettings);
updatePreview();
} else {
throw new Error(data.message || 'Ошибка загрузки настроек');
}
} catch (error) {
console.error('Ошибка загрузки настроек:', error);
showStatus('Ошибка загрузки настроек', 'error');
} finally {
showLoading(false);
}
}
function applySettingsToControls(settings) {
// Colors
if (settings['primary-color']) {
document.getElementById('primaryColor').value = settings['primary-color'];
document.getElementById('primaryColorValue').value = settings['primary-color'];
}
if (settings['secondary-color']) {
document.getElementById('secondaryColor').value = settings['secondary-color'];
document.getElementById('secondaryColorValue').value = settings['secondary-color'];
}
if (settings['background-color']) {
document.getElementById('backgroundColor').value = settings['background-color'];
document.getElementById('backgroundColorValue').value = settings['background-color'];
}
if (settings['text-color']) {
document.getElementById('textColor').value = settings['text-color'];
document.getElementById('textColorValue').value = settings['text-color'];
}
// Typography
if (settings['primary-font']) {
document.getElementById('primaryFont').value = settings['primary-font'];
}
if (settings['base-font-size']) {
const size = parseInt(settings['base-font-size']);
document.getElementById('fontSize').value = size;
document.querySelector('#fontSize').parentElement.querySelector('.slider-value').textContent = size + 'px';
}
if (settings['line-height']) {
const height = parseFloat(settings['line-height']);
document.getElementById('lineHeight').value = height;
document.querySelector('#lineHeight').parentElement.querySelector('.slider-value').textContent = height;
}
// Layout
if (settings['max-width']) {
const width = parseInt(settings['max-width']);
document.getElementById('maxWidth').value = width;
document.querySelector('#maxWidth').parentElement.querySelector('.slider-value').textContent = width + 'px';
}
if (settings['padding']) {
const padding = parseInt(settings['padding']);
document.getElementById('padding').value = padding;
document.querySelector('#padding').parentElement.querySelector('.slider-value').textContent = padding + 'px';
}
if (settings['border-radius']) {
const radius = parseInt(settings['border-radius']);
document.getElementById('borderRadius').value = radius;
document.querySelector('#borderRadius').parentElement.querySelector('.slider-value').textContent = radius + 'px';
}
// Background images
if (settings['header-bg']) {
setImageForField('headerBg', { url: settings['header-bg'], name: 'Header Background' });
}
if (settings['page-bg']) {
setImageForField('pageBg', { url: settings['page-bg'], name: 'Page Background' });
}
}
// Обновление превью
function updatePreview() {
const css = generateCSS();
// Отправляем CSS в iframe (если возможно)
try {
const iframe = document.getElementById('previewFrame');
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
let styleEl = iframeDoc.getElementById('custom-preview-styles');
if (!styleEl) {
styleEl = iframeDoc.createElement('style');
styleEl.id = 'custom-preview-styles';
iframeDoc.head.appendChild(styleEl);
}
styleEl.textContent = css;
} catch (e) {
// Ошибка доступа к iframe (возможно, другой домен)
console.log('Не удается обновить превью напрямую');
}
}
function generateCSS() {
return `
:root {
--primary-color: ${currentSettings['primary-color'] || '#007bff'};
--secondary-color: ${currentSettings['secondary-color'] || '#6c757d'};
--background-color: ${currentSettings['background-color'] || '#f8f9fa'};
--text-color: ${currentSettings['text-color'] || '#333333'};
--primary-font: ${currentSettings['primary-font'] || "'Inter', sans-serif"};
--base-font-size: ${currentSettings['base-font-size'] || '16px'};
--line-height: ${currentSettings['line-height'] || '1.6'};
--max-width: ${currentSettings['max-width'] || '1200px'};
--padding: ${currentSettings['padding'] || '20px'};
--border-radius: ${currentSettings['border-radius'] || '6px'};
}
body {
font-family: var(--primary-font);
font-size: var(--base-font-size);
line-height: var(--line-height);
color: var(--text-color);
background-color: var(--background-color);
${currentSettings['page-bg'] ? `background-image: url('${currentSettings['page-bg']}');` : ''}
}
.container {
max-width: var(--max-width);
padding: var(--padding);
}
.header, .hero {
${currentSettings['header-bg'] ? `background-image: url('${currentSettings['header-bg']}');` : ''}
background-size: cover;
background-position: center;
}
.btn-primary, .button, .btn {
background-color: var(--primary-color);
border-radius: var(--border-radius);
}
.btn-secondary {
background-color: var(--secondary-color);
border-radius: var(--border-radius);
}
.card, .section {
border-radius: var(--border-radius);
padding: var(--padding);
}
`;
}
// Сохранение настроек
async function saveSettings() {
try {
showLoading(true);
const css = generateCSS();
const response = await fetch('/api/settings/styles', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
styles: currentSettings,
css: css
})
});
const data = await response.json();
if (data.success) {
showStatus('Настройки успешно сохранены!', 'success');
refreshPreview();
} else {
throw new Error(data.error || 'Ошибка сохранения');
}
} catch (error) {
console.error('Ошибка сохранения:', error);
showStatus('Ошибка сохранения настроек', 'error');
} finally {
showLoading(false);
}
}
// Сброс к defaults
function resetToDefaults() {
if (!confirm('Сбросить все настройки к значениям по умолчанию?')) return;
currentSettings = {
'primary-color': '#007bff',
'secondary-color': '#6c757d',
'background-color': '#f8f9fa',
'text-color': '#333333',
'primary-font': "'Inter', sans-serif",
'base-font-size': '16px',
'line-height': '1.6',
'max-width': '1200px',
'padding': '20px',
'border-radius': '6px'
};
applySettingsToControls(currentSettings);
updatePreview();
showStatus('Настройки сброшены', 'success');
}
// Обновление превью
function refreshPreview() {
document.getElementById('previewFrame').src = document.getElementById('previewFrame').src;
}
function openPreviewInNewTab() {
window.open('/', '_blank');
}
// Утилиты
function toggleSection(header) {
const content = header.nextElementSibling;
const toggle = header.querySelector('.section-toggle');
content.classList.toggle('collapsed');
toggle.textContent = content.classList.contains('collapsed') ? '▶' : '▼';
}
function showLoading(show) {
document.getElementById('loadingOverlay').classList.toggle('visible', show);
}
function showStatus(message, type) {
const statusEl = document.getElementById('statusMessage');
statusEl.textContent = message;
statusEl.className = `status-message ${type} visible`;
setTimeout(() => {
statusEl.classList.remove('visible');
}, 3000);
}
function setupPreviewUpdates() {
// Обновляем превью при изменении размера окна
window.addEventListener('resize', updatePreview);
}
// Закрытие модальных окон при клике вне их
document.addEventListener('click', function(event) {
if (event.target.classList.contains('media-modal')) {
event.target.classList.remove('visible');
}
});
// Горячие клавиши
document.addEventListener('keydown', function(event) {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case 's':
event.preventDefault();
saveSettings();
break;
case 'r':
event.preventDefault();
refreshPreview();
break;
}
}
if (event.key === 'Escape') {
document.querySelectorAll('.media-modal').forEach(modal => {
modal.classList.remove('visible');
});
}
});
</script>
</body>
</html>