🚀 Korea Tourism Agency - Complete implementation
✨ Features: - Modern tourism website with responsive design - AdminJS admin panel with image editor integration - PostgreSQL database with comprehensive schema - Docker containerization - Image upload and gallery management 🛠 Tech Stack: - Backend: Node.js + Express.js - Database: PostgreSQL 13+ - Frontend: HTML/CSS/JS with responsive design - Admin: AdminJS with custom components - Deployment: Docker + Docker Compose - Image Processing: Sharp with optimization 📱 Admin Features: - Routes/Tours management (city, mountain, fishing) - Guides profiles with specializations - Articles and blog system - Image editor with upload/gallery/URL options - User management and authentication - Responsive admin interface 🎨 Design: - Korean tourism focused branding - Mobile-first responsive design - Custom CSS with modern aesthetics - Image optimization and gallery - SEO-friendly structure 🔒 Security: - Helmet.js security headers - bcrypt password hashing - Input validation and sanitization - CORS protection - Environment variables
This commit is contained in:
187
public/admin-image-editor-demo.html
Normal file
187
public/admin-image-editor-demo.html
Normal file
@@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Демонстрация редактора изображений в AdminJS</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-images me-2"></i>
|
||||
Редактор изображений для AdminJS - Готов к использованию!
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<strong>Редактор изображений успешно интегрирован в AdminJS!</strong>
|
||||
</div>
|
||||
|
||||
<h5>🎉 Что было реализовано:</h5>
|
||||
<ul class="list-group list-group-flush mb-4">
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
Полнофункциональный редактор изображений с галереей
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
API для загрузки и управления изображениями
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
Автоматическая интеграция с полями изображений в AdminJS
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
Превью изображений и организация по папкам
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
Оптимизация изображений через Sharp
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h5>📝 Как использовать в AdminJS:</h5>
|
||||
<ol class="mb-4">
|
||||
<li class="mb-2">
|
||||
<strong>Войдите в админ-панель:</strong>
|
||||
<a href="/admin" class="btn btn-primary btn-sm ms-2" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>
|
||||
Открыть AdminJS
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Данные для входа:</strong>
|
||||
<ul class="mt-1">
|
||||
<li>Username: <code>admin</code></li>
|
||||
<li>Password: Используйте существующий пароль администратора</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Редактируйте маршруты, гидов или статьи</strong>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Для полей изображений появится кнопка "📷 Выбрать"</strong>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Нажмите кнопку для открытия редактора в модальном окне</strong>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h5>🔧 Функции редактора:</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-info text-white">
|
||||
<i class="fas fa-upload me-2"></i>Загрузка
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Drag & Drop</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Формат: JPG, PNG, GIF</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Макс. размер: 5МБ</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Автооптимизация</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="fas fa-images me-2"></i>Галерея
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Все загруженные изображения</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Фильтр по папкам</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Превью с именами</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Поиск по категориям</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="fas fa-link me-2"></i>По URL
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Внешние изображения</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Прямые ссылки</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Предпросмотр</li>
|
||||
<li><i class="fas fa-check-circle text-success me-2"></i>Быстрая вставка</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>📂 Организация файлов:</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 border rounded">
|
||||
<i class="fas fa-route fa-2x text-primary mb-2"></i>
|
||||
<h6>/uploads/routes/</h6>
|
||||
<small class="text-muted">Изображения маршрутов</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 border rounded">
|
||||
<i class="fas fa-user-tie fa-2x text-info mb-2"></i>
|
||||
<h6>/uploads/guides/</h6>
|
||||
<small class="text-muted">Фотографии гидов</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 border rounded">
|
||||
<i class="fas fa-newspaper fa-2x text-warning mb-2"></i>
|
||||
<h6>/uploads/articles/</h6>
|
||||
<small class="text-muted">Изображения статей</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 border rounded">
|
||||
<i class="fas fa-folder fa-2x text-secondary mb-2"></i>
|
||||
<h6>/uploads/general/</h6>
|
||||
<small class="text-muted">Общие изображения</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Техническая информация:</strong>
|
||||
<br>
|
||||
Редактор автоматически определяет поля изображений по именам (image_url, photo, avatar) и добавляет к ним кнопку выбора.
|
||||
Изображения оптимизируются до разрешения 1200x800 с качеством 85% для оптимальной производительности.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="/admin" class="btn btn-primary btn-lg me-2" target="_blank">
|
||||
<i class="fas fa-cogs me-2"></i>
|
||||
Открыть админ-панель
|
||||
</a>
|
||||
<a href="/test-image-editor.html" class="btn btn-outline-secondary btn-lg" target="_blank">
|
||||
<i class="fas fa-test-tube me-2"></i>
|
||||
Тестовая страница
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -32,6 +32,35 @@
|
||||
margin: 0.125rem;
|
||||
}
|
||||
|
||||
/* Image Editor Button Styles */
|
||||
.image-editor-btn {
|
||||
background: #007bff !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 13px !important;
|
||||
cursor: pointer !important;
|
||||
white-space: nowrap !important;
|
||||
transition: background 0.2s ease !important;
|
||||
}
|
||||
|
||||
.image-editor-btn:hover {
|
||||
background: #0056b3 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Image Preview Styles */
|
||||
.image-preview {
|
||||
max-width: 180px !important;
|
||||
max-height: 120px !important;
|
||||
object-fit: cover !important;
|
||||
border: 1px solid #ddd !important;
|
||||
border-radius: 4px !important;
|
||||
margin-top: 8px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Cards Enhancement */
|
||||
.card {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
@@ -215,4 +244,61 @@
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Стили для редактора изображений в AdminJS */
|
||||
.image-editor-btn {
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px !important;
|
||||
padding: 6px 12px !important;
|
||||
}
|
||||
|
||||
.image-editor-btn:hover {
|
||||
background: #0056b3 !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Стили для превью изображений */
|
||||
.image-preview {
|
||||
border: 2px solid #e9ecef !important;
|
||||
transition: border-color 0.2s ease;
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.image-preview:hover {
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* Улучшенные стили для полей изображений */
|
||||
input[name*="image_url"]:focus,
|
||||
input[name*="photo"]:focus,
|
||||
input[name*="avatar"]:focus {
|
||||
border-left: 3px solid #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Анимация для успешного выбора изображения */
|
||||
@keyframes imageSelected {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.image-selected {
|
||||
animation: imageSelected 0.3s ease;
|
||||
}
|
||||
|
||||
/* Responsive стили для редактора */
|
||||
@media (max-width: 768px) {
|
||||
.image-editor-btn {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 10px !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
543
public/image-editor-compact.html
Normal file
543
public/image-editor-compact.html
Normal file
@@ -0,0 +1,543 @@
|
||||
<!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;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
min-height: 300px;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Загрузка */
|
||||
.upload-zone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.upload-zone:hover, .upload-zone.dragover {
|
||||
border-color: #007bff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
color: #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Галерея */
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.gallery-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.gallery-item:hover {
|
||||
border-color: #ddd;
|
||||
}
|
||||
.gallery-item.selected {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 10px rgba(0,123,255,0.3);
|
||||
}
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* URL ввод */
|
||||
.url-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Превью */
|
||||
.preview {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.preview img {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('upload')">📤 Загрузить</button>
|
||||
<button class="tab" onclick="switchTab('gallery')">🖼️ Галерея</button>
|
||||
<button class="tab" onclick="switchTab('url')">🔗 По ссылке</button>
|
||||
</div>
|
||||
|
||||
<!-- Загрузка -->
|
||||
<div id="upload-tab" class="tab-content active">
|
||||
<div class="upload-zone" onclick="document.getElementById('fileInput').click()">
|
||||
<div class="upload-icon">📁</div>
|
||||
<p><strong>Выберите файл</strong> или перетащите сюда</p>
|
||||
<p style="color: #666; font-size: 12px;">JPG, PNG, GIF (макс. 5МБ)</p>
|
||||
</div>
|
||||
<input type="file" id="fileInput" class="file-input" accept="image/*">
|
||||
</div>
|
||||
|
||||
<!-- Галерея -->
|
||||
<div id="gallery-tab" class="tab-content">
|
||||
<div id="galleryContent" class="loading">Загрузка галереи...</div>
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
<div id="url-tab" class="tab-content">
|
||||
<input type="url" id="urlInput" class="url-input" placeholder="Вставьте ссылку на изображение">
|
||||
<button class="btn" onclick="previewFromUrl()">Предварительный просмотр</button>
|
||||
</div>
|
||||
|
||||
<!-- Превью -->
|
||||
<div id="preview" class="preview" style="display: none;">
|
||||
<img id="previewImage" src="" alt="Превью">
|
||||
</div>
|
||||
|
||||
<!-- Действия -->
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="closeEditor()">Отмена</button>
|
||||
<button class="btn btn-primary" onclick="selectImage()">Выбрать</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedImageUrl = '';
|
||||
let currentField = null;
|
||||
|
||||
// Получаем параметры из URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const fieldName = urlParams.get('field') || 'image';
|
||||
const currentValue = urlParams.get('current') || '';
|
||||
|
||||
// Настройка обработчиков загрузки
|
||||
function setupUploadHandlers() {
|
||||
// Обработка загрузки файла
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
if (fileInput) {
|
||||
fileInput.removeEventListener('change', handleFileSelect); // Убираем предыдущий
|
||||
fileInput.addEventListener('change', handleFileSelect);
|
||||
}
|
||||
|
||||
// Drag & Drop
|
||||
const uploadZone = document.querySelector('.upload-zone');
|
||||
if (uploadZone) {
|
||||
// Убираем предыдущие обработчики
|
||||
uploadZone.removeEventListener('dragover', handleDragOver);
|
||||
uploadZone.removeEventListener('dragleave', handleDragLeave);
|
||||
uploadZone.removeEventListener('drop', handleDrop);
|
||||
|
||||
// Добавляем новые
|
||||
uploadZone.addEventListener('dragover', handleDragOver);
|
||||
uploadZone.addEventListener('dragleave', handleDragLeave);
|
||||
uploadZone.addEventListener('drop', handleDrop);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчики событий
|
||||
function handleFileSelect(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
showError('Файл слишком большой. Максимум 5МБ.');
|
||||
return;
|
||||
}
|
||||
uploadFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.add('dragover');
|
||||
}
|
||||
|
||||
function handleDragLeave(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.remove('dragover');
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.currentTarget.classList.remove('dragover');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type.startsWith('image/')) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
showError('Файл слишком большой. Максимум 5МБ.');
|
||||
return;
|
||||
}
|
||||
uploadFile(file);
|
||||
} else {
|
||||
showError('Пожалуйста, выберите изображение (JPG, PNG, GIF)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем текущее изображение если есть
|
||||
if (currentValue) {
|
||||
showPreview(currentValue);
|
||||
selectedImageUrl = currentValue;
|
||||
}
|
||||
|
||||
// Инициализируем обработчики загрузки
|
||||
setupUploadHandlers();
|
||||
|
||||
// Переключение табов
|
||||
function switchTab(tabName) {
|
||||
// Убираем активные классы
|
||||
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Добавляем активные классы
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
|
||||
// Загружаем галерею при первом открытии
|
||||
if (tabName === 'gallery') {
|
||||
loadGallery();
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка файла на сервер
|
||||
async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('folder', getFolderName());
|
||||
|
||||
try {
|
||||
showLoading('Загрузка изображения...');
|
||||
|
||||
const response = await fetch('/api/images/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Upload result:', result);
|
||||
|
||||
if (result.success) {
|
||||
selectedImageUrl = result.url;
|
||||
showPreview(result.url);
|
||||
hideLoading();
|
||||
showSuccess('Изображение успешно загружено!');
|
||||
} else {
|
||||
throw new Error(result.error || 'Ошибка загрузки');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
hideLoading();
|
||||
showError('Ошибка загрузки: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка галереи
|
||||
async function loadGallery() {
|
||||
const galleryContent = document.getElementById('galleryContent');
|
||||
galleryContent.innerHTML = '<div class="loading">Загрузка галереи...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/images/gallery');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Gallery result:', result);
|
||||
|
||||
if (result.success) {
|
||||
const gallery = document.createElement('div');
|
||||
gallery.className = 'gallery';
|
||||
|
||||
const images = result.data || result.images || [];
|
||||
if (images.length === 0) {
|
||||
gallery.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: #666;">Нет изображений в галерее</p>';
|
||||
} else {
|
||||
images.forEach(image => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'gallery-item';
|
||||
item.innerHTML = `<img src="${image.path}" alt="${image.name}" onerror="this.parentNode.style.display='none'">`;
|
||||
item.onclick = () => selectFromGallery(image.path, item);
|
||||
gallery.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
galleryContent.innerHTML = '';
|
||||
galleryContent.appendChild(gallery);
|
||||
} else {
|
||||
throw new Error(result.error || 'Ошибка получения галереи');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gallery error:', error);
|
||||
galleryContent.innerHTML = `<div class="error">Ошибка загрузки галереи: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Выбор из галереи
|
||||
function selectFromGallery(url, element) {
|
||||
// Убираем выделение с других элементов
|
||||
document.querySelectorAll('.gallery-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Выделяем выбранный элемент
|
||||
element.classList.add('selected');
|
||||
|
||||
selectedImageUrl = url;
|
||||
showPreview(url);
|
||||
}
|
||||
|
||||
// Предпросмотр по URL
|
||||
function previewFromUrl() {
|
||||
const url = document.getElementById('urlInput').value.trim();
|
||||
if (url) {
|
||||
// Проверяем, что это валидный URL
|
||||
try {
|
||||
new URL(url);
|
||||
selectedImageUrl = url;
|
||||
showPreview(url);
|
||||
showSuccess('URL изображения установлен!');
|
||||
} catch (e) {
|
||||
showError('Пожалуйста, введите корректный URL');
|
||||
}
|
||||
} else {
|
||||
showError('Пожалуйста, введите URL изображения');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать превью
|
||||
function showPreview(url) {
|
||||
const preview = document.getElementById('preview');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
|
||||
previewImage.src = url;
|
||||
preview.style.display = 'block';
|
||||
|
||||
previewImage.onload = function() {
|
||||
console.log('Image loaded successfully:', url);
|
||||
};
|
||||
|
||||
previewImage.onerror = function() {
|
||||
showError('Не удалось загрузить изображение по указанному URL');
|
||||
preview.style.display = 'none';
|
||||
selectedImageUrl = '';
|
||||
};
|
||||
}
|
||||
|
||||
// Выбрать изображение
|
||||
function selectImage() {
|
||||
if (selectedImageUrl) {
|
||||
// Отправляем выбранный URL родительскому окну
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'imageSelected',
|
||||
url: selectedImageUrl,
|
||||
field: fieldName
|
||||
}, '*');
|
||||
}
|
||||
closeEditor();
|
||||
} else {
|
||||
showError('Выберите изображение');
|
||||
}
|
||||
}
|
||||
|
||||
// Закрыть редактор
|
||||
function closeEditor() {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'editorClosed'
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Получить имя папки из поля
|
||||
function getFolderName() {
|
||||
if (fieldName.includes('route') || fieldName.includes('tour')) return 'routes';
|
||||
if (fieldName.includes('guide')) return 'guides';
|
||||
if (fieldName.includes('article')) return 'articles';
|
||||
return 'general';
|
||||
}
|
||||
|
||||
// Показать ошибку
|
||||
function showError(message) {
|
||||
hideLoading();
|
||||
removeMessages();
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'error';
|
||||
errorDiv.textContent = message;
|
||||
|
||||
document.querySelector('.container').insertBefore(errorDiv, document.querySelector('.actions'));
|
||||
setTimeout(() => errorDiv.remove(), 5000);
|
||||
}
|
||||
|
||||
// Показать успех
|
||||
function showSuccess(message) {
|
||||
removeMessages();
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'success';
|
||||
successDiv.style.cssText = `
|
||||
color: #155724;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
`;
|
||||
successDiv.textContent = message;
|
||||
|
||||
document.querySelector('.container').insertBefore(successDiv, document.querySelector('.actions'));
|
||||
setTimeout(() => successDiv.remove(), 3000);
|
||||
}
|
||||
|
||||
// Удалить все сообщения
|
||||
function removeMessages() {
|
||||
document.querySelectorAll('.error, .success').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// Показать загрузку
|
||||
function showLoading(message) {
|
||||
const uploadTab = document.getElementById('upload-tab');
|
||||
uploadTab.innerHTML = `<div class="loading">${message}</div>`;
|
||||
}
|
||||
|
||||
// Скрыть загрузку
|
||||
function hideLoading() {
|
||||
const uploadTab = document.getElementById('upload-tab');
|
||||
uploadTab.innerHTML = `
|
||||
<div class="upload-zone" onclick="document.getElementById('fileInput').click()">
|
||||
<div class="upload-icon">📁</div>
|
||||
<p><strong>Выберите файл</strong> или перетащите сюда</p>
|
||||
<p style="color: #666; font-size: 12px;">JPG, PNG, GIF (макс. 5МБ)</p>
|
||||
</div>
|
||||
<input type="file" id="fileInput" class="file-input" accept="image/*">
|
||||
`;
|
||||
|
||||
// Переподключаем обработчики
|
||||
setupUploadHandlers();
|
||||
}
|
||||
|
||||
// Enter в поле URL
|
||||
document.getElementById('urlInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
previewFromUrl();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
448
public/image-editor.html
Normal file
448
public/image-editor.html
Normal file
@@ -0,0 +1,448 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Редактор изображений - Korea Tourism</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.image-preview {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: cover;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.image-gallery {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.gallery-item {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.gallery-item:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.gallery-item.selected {
|
||||
border: 3px solid #007bff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.upload-area:hover, .upload-area.drag-over {
|
||||
border-color: #007bff;
|
||||
background: #e3f2fd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-images me-2"></i>
|
||||
Редактор изображений
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Вкладки -->
|
||||
<ul class="nav nav-tabs" id="imageEditorTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button">
|
||||
<i class="fas fa-upload me-2"></i>Загрузить
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="gallery-tab" data-bs-toggle="tab" data-bs-target="#gallery" type="button">
|
||||
<i class="fas fa-images me-2"></i>Галерея
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="url-tab" data-bs-toggle="tab" data-bs-target="#url" type="button">
|
||||
<i class="fas fa-link me-2"></i>По ссылке
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="imageEditorTabsContent">
|
||||
<!-- Вкладка загрузки -->
|
||||
<div class="tab-pane fade show active" id="upload" role="tabpanel">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
|
||||
<p class="mb-2">Перетащите файлы сюда или нажмите для выбора</p>
|
||||
<small class="text-muted">Поддерживаются: JPG, PNG, GIF (макс. 5МБ)</small>
|
||||
<input type="file" id="fileInput" class="d-none" multiple accept="image/*">
|
||||
</div>
|
||||
|
||||
<!-- Прогресс загрузки -->
|
||||
<div id="uploadProgress" class="mt-3" style="display: none;">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="text-center">
|
||||
<h6>Предварительный просмотр</h6>
|
||||
<img id="uploadPreview" class="image-preview" src="/images/placeholders/no-image.png" alt="Предварительный просмотр">
|
||||
<div class="mt-2">
|
||||
<small id="imageInfo" class="text-muted"></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка галереи -->
|
||||
<div class="tab-pane fade" id="gallery" role="tabpanel">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6>Загруженные изображения</h6>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-folder="routes">
|
||||
<i class="fas fa-route me-1"></i>Маршруты
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-folder="guides">
|
||||
<i class="fas fa-user-tie me-1"></i>Гиды
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-folder="articles">
|
||||
<i class="fas fa-newspaper me-1"></i>Статьи
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm active" data-folder="all">
|
||||
<i class="fas fa-images me-1"></i>Все
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-gallery">
|
||||
<div class="row" id="galleryImages">
|
||||
<!-- Изображения будут загружены динамически -->
|
||||
<div class="col-12 text-center py-4">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="mt-2 text-muted">Загрузка галереи...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Вкладка URL -->
|
||||
<div class="tab-pane fade" id="url" role="tabpanel">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8">
|
||||
<label for="imageUrl" class="form-label">URL изображения</label>
|
||||
<div class="input-group">
|
||||
<input type="url" class="form-control" id="imageUrl" placeholder="https://example.com/image.jpg">
|
||||
<button class="btn btn-primary" type="button" id="loadUrlImage">
|
||||
<i class="fas fa-download me-1"></i>Загрузить
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Введите прямую ссылку на изображение</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h6>Предварительный просмотр</h6>
|
||||
<img id="urlPreview" class="image-preview" src="/images/placeholders/no-image.png" alt="Предварительный просмотр">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<strong>Выбранное изображение:</strong>
|
||||
<span id="selectedImagePath" class="text-muted ms-2">Не выбрано</span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary me-2" onclick="closeImageEditor()">
|
||||
<i class="fas fa-times me-1"></i>Отмена
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmSelection" disabled>
|
||||
<i class="fas fa-check me-1"></i>Выбрать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
let selectedImagePath = null;
|
||||
let targetField = null;
|
||||
|
||||
// Инициализация редактора
|
||||
function initImageEditor() {
|
||||
setupUploadEvents();
|
||||
setupGalleryEvents();
|
||||
setupUrlEvents();
|
||||
loadGallery('all');
|
||||
}
|
||||
|
||||
// Настройка событий загрузки
|
||||
function setupUploadEvents() {
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
|
||||
uploadArea.addEventListener('click', () => fileInput.click());
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('drag-over');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('drag-over');
|
||||
handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFiles(e.target.files);
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка файлов
|
||||
function handleFiles(files) {
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
if (validateFile(file)) {
|
||||
showPreview(file);
|
||||
uploadFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация файла
|
||||
function validateFile(file) {
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert('Неподдерживаемый тип файла. Используйте JPG, PNG или GIF.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
alert('Файл слишком большой. Максимальный размер: 5МБ.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Показ предварительного просмотра
|
||||
function showPreview(file) {
|
||||
const reader = new FileReader();
|
||||
const preview = document.getElementById('uploadPreview');
|
||||
const info = document.getElementById('imageInfo');
|
||||
|
||||
reader.onload = (e) => {
|
||||
preview.src = e.target.result;
|
||||
info.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Загрузка файла на сервер
|
||||
async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const progress = document.getElementById('uploadProgress');
|
||||
const progressBar = progress.querySelector('.progress-bar');
|
||||
|
||||
progress.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/images/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
selectedImagePath = result.data.path;
|
||||
updateSelectedImage(selectedImagePath);
|
||||
showSuccessMessage('Изображение успешно загружено!');
|
||||
// Обновляем галерею
|
||||
setTimeout(() => loadGallery('all'), 500);
|
||||
} else {
|
||||
alert('Ошибка загрузки: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка загрузки: ' + error.message);
|
||||
} finally {
|
||||
progress.style.display = 'none';
|
||||
progressBar.style.width = '0%';
|
||||
}
|
||||
}
|
||||
|
||||
// Настройка событий галереи
|
||||
function setupGalleryEvents() {
|
||||
document.querySelectorAll('[data-folder]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('[data-folder]').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
loadGallery(e.target.dataset.folder);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка галереи
|
||||
async function loadGallery(folder) {
|
||||
const galleryContainer = document.getElementById('galleryImages');
|
||||
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-spinner fa-spin fa-2x text-muted"></i><p class="mt-2 text-muted">Загрузка галереи...</p></div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/images/gallery?folder=${folder}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderGallery(result.data);
|
||||
} else {
|
||||
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">Ошибка загрузки галереи</p></div>';
|
||||
}
|
||||
} catch (error) {
|
||||
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">Ошибка: ' + error.message + '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение галереи
|
||||
function renderGallery(images) {
|
||||
const galleryContainer = document.getElementById('galleryImages');
|
||||
|
||||
if (images.length === 0) {
|
||||
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">Изображения не найдены</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
galleryContainer.innerHTML = images.map(img => `
|
||||
<div class="col-md-2 col-sm-3 col-4 mb-3">
|
||||
<div class="gallery-item" data-path="${img.path}" onclick="selectGalleryImage('${img.path}')">
|
||||
<img src="${img.path}" class="img-fluid rounded" alt="${img.name}" title="${img.name}">
|
||||
<small class="d-block text-center text-muted mt-1">${img.name}</small>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Выбор изображения из галереи
|
||||
function selectGalleryImage(path) {
|
||||
document.querySelectorAll('.gallery-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
document.querySelector(`[data-path="${path}"]`).classList.add('selected');
|
||||
selectedImagePath = path;
|
||||
updateSelectedImage(path);
|
||||
}
|
||||
|
||||
// Настройка событий URL
|
||||
function setupUrlEvents() {
|
||||
const urlInput = document.getElementById('imageUrl');
|
||||
const loadBtn = document.getElementById('loadUrlImage');
|
||||
const urlPreview = document.getElementById('urlPreview');
|
||||
|
||||
loadBtn.addEventListener('click', () => {
|
||||
const url = urlInput.value.trim();
|
||||
if (url) {
|
||||
urlPreview.src = url;
|
||||
selectedImagePath = url;
|
||||
updateSelectedImage(url);
|
||||
}
|
||||
});
|
||||
|
||||
urlInput.addEventListener('input', (e) => {
|
||||
const url = e.target.value.trim();
|
||||
if (url && isValidUrl(url)) {
|
||||
urlPreview.src = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Проверка валидности URL
|
||||
function isValidUrl(string) {
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление выбранного изображения
|
||||
function updateSelectedImage(path) {
|
||||
document.getElementById('selectedImagePath').textContent = path;
|
||||
document.getElementById('confirmSelection').disabled = false;
|
||||
}
|
||||
|
||||
// Подтверждение выбора
|
||||
document.getElementById('confirmSelection').addEventListener('click', () => {
|
||||
if (selectedImagePath && window.opener) {
|
||||
// Передаем путь в родительское окно
|
||||
window.opener.postMessage({
|
||||
type: 'imageSelected',
|
||||
path: selectedImagePath,
|
||||
targetField: new URLSearchParams(window.location.search).get('field')
|
||||
}, '*');
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Закрытие редактора
|
||||
function closeImageEditor() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
// Показ сообщения об успехе
|
||||
function showSuccessMessage(message) {
|
||||
// Простое уведомление
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
alert.style.zIndex = '9999';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Инициализация при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', initImageEditor);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
275
public/image-system-docs.html
Normal file
275
public/image-system-docs.html
Normal file
@@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Система управления изображениями - Korea Tourism</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.hero-compact {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 0;
|
||||
}
|
||||
.feature-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.feature-card {
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.code-block {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
font-family: monospace;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.api-endpoint {
|
||||
background: #e3f2fd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.step-number {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигация -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">Korea Tourism</a>
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a class="nav-link" href="/">Главная</a>
|
||||
<a class="nav-link" href="/admin">Админ-панель</a>
|
||||
<a class="nav-link" href="/test-editor">Тест редактора</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-compact">
|
||||
<div class="container text-center">
|
||||
<h1 class="display-5 fw-bold mb-3">🖼️ Система управления изображениями</h1>
|
||||
<p class="lead">Полнофункциональный редактор с возможностями обрезки, поворота и оптимизации</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Функции -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card feature-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-primary text-white">
|
||||
<i class="fas fa-upload"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Загрузка и обработка</h5>
|
||||
<p class="card-text">Drag & Drop загрузка с автоматической оптимизацией и конвертацией в JPEG</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card feature-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-success text-white">
|
||||
<i class="fas fa-crop"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Редактирование</h5>
|
||||
<p class="card-text">Обрезка, поворот на 90°, отражение горизонтально и вертикально</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card feature-card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="feature-icon bg-info text-white">
|
||||
<i class="fas fa-cogs"></i>
|
||||
</div>
|
||||
<h5 class="card-title">API интеграция</h5>
|
||||
<p class="card-text">REST API для интеграции с любыми формами и компонентами</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Инструкции по использованию -->
|
||||
<section class="py-5 bg-light">
|
||||
<div class="container">
|
||||
<h2 class="text-center mb-5">📋 Инструкции по использованию</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fas fa-user-cog me-2"></i>Через AdminJS</h4>
|
||||
<div class="mb-3">
|
||||
<span class="step-number">1</span>
|
||||
Зайдите в <a href="/admin" target="_blank">админ-панель</a>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="step-number">2</span>
|
||||
Выберите "Туры" или "Гиды"
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="step-number">3</span>
|
||||
В поле "Image URL" укажите путь к изображению
|
||||
</div>
|
||||
<div class="code-block">
|
||||
Например: /uploads/routes/my-image.jpg
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fas fa-code me-2"></i>Через JavaScript</h4>
|
||||
<div class="mb-3">
|
||||
<span class="step-number">1</span>
|
||||
Подключите скрипт редактора
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<script src="/js/image-editor.js"></script>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="step-number">2</span>
|
||||
Откройте редактор
|
||||
</div>
|
||||
<div class="code-block">
|
||||
window.openImageEditor({
|
||||
targetFolder: 'routes', // routes, guides, articles
|
||||
}).then(url => {
|
||||
console.log('Сохранено:', url);
|
||||
});
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Документация -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<h2 class="text-center mb-5">🔌 API Эндпоинты</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-upload me-2 text-primary"></i>Загрузка изображения</h5>
|
||||
<div class="api-endpoint">
|
||||
<strong>POST</strong> /api/images/upload-image
|
||||
</div>
|
||||
<p>Загружает изображение во временную папку</p>
|
||||
<div class="code-block">
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
fetch('/api/images/upload-image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(r => r.json());
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-magic me-2 text-success"></i>Обработка изображения</h5>
|
||||
<div class="api-endpoint">
|
||||
<strong>POST</strong> /api/images/process-image
|
||||
</div>
|
||||
<p>Применяет трансформации и сохраняет финальный файл</p>
|
||||
<div class="code-block">
|
||||
fetch('/api/images/process-image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tempId: 'temp-123',
|
||||
rotation: 90,
|
||||
flipHorizontal: false,
|
||||
cropData: { x: 0, y: 0, width: 300, height: 200 },
|
||||
targetFolder: 'routes'
|
||||
})
|
||||
});
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-list me-2 text-info"></i>Список изображений</h5>
|
||||
<div class="api-endpoint">
|
||||
<strong>GET</strong> /api/images/images/{folder}
|
||||
</div>
|
||||
<p>Возвращает список всех изображений в папке</p>
|
||||
<div class="code-block">
|
||||
// Получить изображения туров
|
||||
fetch('/api/images/images/routes')
|
||||
.then(r => r.json())
|
||||
.then(data => console.log(data.images));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-trash me-2 text-danger"></i>Удаление изображения</h5>
|
||||
<div class="api-endpoint">
|
||||
<strong>DELETE</strong> /api/images/image
|
||||
</div>
|
||||
<p>Удаляет изображение с сервера</p>
|
||||
<div class="code-block">
|
||||
fetch('/api/images/image', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: '/uploads/routes/image.jpg'
|
||||
})
|
||||
});
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Тестирование -->
|
||||
<section class="py-5 bg-primary text-white">
|
||||
<div class="container text-center">
|
||||
<h2 class="mb-4">🧪 Тестирование системы</h2>
|
||||
<p class="lead mb-4">Попробуйте все возможности редактора изображений</p>
|
||||
<div class="d-flex gap-3 justify-content-center">
|
||||
<a href="/test-editor" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-play me-2"></i>Открыть тест-редактор
|
||||
</a>
|
||||
<a href="/admin" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-cog me-2"></i>Админ-панель
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-dark text-white py-3">
|
||||
<div class="container text-center">
|
||||
<p class="mb-0">© 2025 Korea Tourism Agency - Система управления изображениями</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
public/images/placeholders/no-image.svg
Normal file
13
public/images/placeholders/no-image.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- Placeholder для отсутствующих изображений -->
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/>
|
||||
<text x="100" y="90" text-anchor="middle" font-family="Arial" font-size="14" fill="#6c757d">
|
||||
Нет изображения
|
||||
</text>
|
||||
<text x="100" y="110" text-anchor="middle" font-family="Arial" font-size="12" fill="#adb5bd">
|
||||
No Image
|
||||
</text>
|
||||
<circle cx="100" cy="130" r="15" fill="#e9ecef"/>
|
||||
<rect x="90" y="125" width="20" height="10" fill="#adb5bd"/>
|
||||
<circle cx="95" cy="128" r="2" fill="#6c757d"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 657 B |
@@ -1,5 +1,96 @@
|
||||
/* Korea Tourism Agency Admin Panel Custom Scripts */
|
||||
|
||||
// Функция для открытия редактора изображений
|
||||
function openImageEditor(fieldName, currentValue) {
|
||||
const editorUrl = `/image-editor.html?field=${fieldName}¤t=${encodeURIComponent(currentValue || '')}`;
|
||||
const editorWindow = window.open(editorUrl, 'imageEditor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||
|
||||
// Слушаем сообщения от редактора
|
||||
const messageHandler = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) {
|
||||
const field = document.querySelector(`input[name="${fieldName}"], input[id="${fieldName}"]`);
|
||||
if (field) {
|
||||
field.value = event.data.path;
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
// Обновляем превью если есть
|
||||
updateImagePreview(fieldName, event.data.path);
|
||||
}
|
||||
|
||||
window.removeEventListener('message', messageHandler);
|
||||
editorWindow.close();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// Очистка обработчика при закрытии окна
|
||||
const checkClosed = setInterval(() => {
|
||||
if (editorWindow.closed) {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
clearInterval(checkClosed);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Функция обновления превью изображения
|
||||
function updateImagePreview(fieldName, imagePath) {
|
||||
const previewId = `${fieldName}_preview`;
|
||||
let preview = document.getElementById(previewId);
|
||||
|
||||
if (!preview) {
|
||||
// Создаем превью если его нет
|
||||
const field = document.querySelector(`input[name="${fieldName}"], input[id="${fieldName}"]`);
|
||||
if (field) {
|
||||
preview = document.createElement('img');
|
||||
preview.id = previewId;
|
||||
preview.className = 'img-thumbnail mt-2';
|
||||
preview.style.maxWidth = '200px';
|
||||
preview.style.maxHeight = '200px';
|
||||
field.parentNode.appendChild(preview);
|
||||
}
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
preview.src = imagePath || '/images/placeholders/no-image.png';
|
||||
preview.alt = 'Preview';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция добавления кнопки редактора к полю
|
||||
function addImageEditorButton(field) {
|
||||
const fieldName = field.name || field.id;
|
||||
if (!fieldName) return;
|
||||
|
||||
// Проверяем, не добавлена ли уже кнопка
|
||||
if (field.parentNode.querySelector('.image-editor-btn')) return;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'input-group';
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'btn btn-outline-secondary image-editor-btn';
|
||||
button.innerHTML = '<i class="fas fa-images"></i> Выбрать';
|
||||
button.onclick = () => openImageEditor(fieldName, field.value);
|
||||
|
||||
const buttonWrapper = document.createElement('div');
|
||||
buttonWrapper.className = 'input-group-append';
|
||||
buttonWrapper.appendChild(button);
|
||||
|
||||
// Перестраиваем структуру
|
||||
field.parentNode.insertBefore(wrapper, field);
|
||||
wrapper.appendChild(field);
|
||||
wrapper.appendChild(buttonWrapper);
|
||||
|
||||
// Добавляем превью если есть значение
|
||||
if (field.value) {
|
||||
updateImagePreview(fieldName, field.value);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initialize tooltips
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
@@ -11,6 +102,33 @@ $(document).ready(function() {
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut('slow');
|
||||
}, 5000);
|
||||
|
||||
// Добавляем кнопки редактора к полям изображений
|
||||
$('input[type="text"], input[type="url"]').each(function() {
|
||||
const field = this;
|
||||
const fieldName = field.name || field.id || '';
|
||||
|
||||
// Проверяем, относится ли поле к изображениям
|
||||
if (fieldName.includes('image') || fieldName.includes('photo') || fieldName.includes('avatar') ||
|
||||
fieldName.includes('picture') || fieldName.includes('thumbnail') || fieldName.includes('banner') ||
|
||||
$(field).closest('label').text().toLowerCase().includes('изображение') ||
|
||||
$(field).closest('label').text().toLowerCase().includes('картинка') ||
|
||||
$(field).closest('label').text().toLowerCase().includes('фото')) {
|
||||
addImageEditorButton(field);
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик для динамически добавляемых полей
|
||||
$(document).on('focus', 'input[type="text"], input[type="url"]', function() {
|
||||
const field = this;
|
||||
const fieldName = field.name || field.id || '';
|
||||
|
||||
if ((fieldName.includes('image') || fieldName.includes('photo') || fieldName.includes('avatar') ||
|
||||
fieldName.includes('picture') || fieldName.includes('thumbnail') || fieldName.includes('banner')) &&
|
||||
!field.parentNode.querySelector('.image-editor-btn')) {
|
||||
addImageEditorButton(field);
|
||||
}
|
||||
});
|
||||
|
||||
// Confirm delete actions
|
||||
$('.btn-delete').on('click', function(e) {
|
||||
|
||||
13
public/js/admin-image-loader.js
Normal file
13
public/js/admin-image-loader.js
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- Скрипт для AdminJS страниц -->
|
||||
<script>
|
||||
// Проверяем, находимся ли мы в админ-панели
|
||||
if (window.location.pathname.startsWith('/admin')) {
|
||||
// Загружаем редактор изображений для AdminJS
|
||||
const script = document.createElement('script');
|
||||
script.src = '/js/image-editor.js';
|
||||
script.onload = () => {
|
||||
console.log('Image Editor loaded for AdminJS');
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
</script>
|
||||
314
public/js/admin-image-selector-fixed.js
Normal file
314
public/js/admin-image-selector-fixed.js
Normal file
@@ -0,0 +1,314 @@
|
||||
// JavaScript для интеграции редактора изображений в AdminJS
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Функция для открытия редактора изображений
|
||||
function openImageEditor(inputField, fieldName) {
|
||||
const currentValue = inputField.value || '';
|
||||
const editorUrl = `/image-editor-compact.html?field=${fieldName}¤t=${encodeURIComponent(currentValue)}`;
|
||||
|
||||
// Убираем предыдущие модальные окна
|
||||
document.querySelectorAll('.image-editor-modal').forEach(modal => modal.remove());
|
||||
|
||||
// Создаем модальное окно
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'image-editor-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
height: 80%;
|
||||
max-height: 600px;
|
||||
position: relative;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
`;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '✕';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: #ff4757;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = editorUrl;
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
// Обработчик сообщений от iframe
|
||||
const messageHandler = function(event) {
|
||||
if (event.data.type === 'imageSelected' && event.data.field === fieldName) {
|
||||
console.log('🖼️ Изображение выбрано:', event.data.url);
|
||||
inputField.value = event.data.url;
|
||||
updateImagePreview(inputField, event.data.url);
|
||||
|
||||
// Триггерим событие change для обновления формы
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
inputField.dispatchEvent(changeEvent);
|
||||
|
||||
// Триггерим input событие
|
||||
const inputEvent = new Event('input', { bubbles: true });
|
||||
inputField.dispatchEvent(inputEvent);
|
||||
|
||||
modal.remove();
|
||||
window.removeEventListener('message', messageHandler);
|
||||
} else if (event.data.type === 'editorClosed') {
|
||||
modal.remove();
|
||||
window.removeEventListener('message', messageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
closeBtn.onclick = function() {
|
||||
modal.remove();
|
||||
window.removeEventListener('message', messageHandler);
|
||||
};
|
||||
|
||||
modal.onclick = function(e) {
|
||||
if (e.target === modal) {
|
||||
modal.remove();
|
||||
window.removeEventListener('message', messageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
content.appendChild(closeBtn);
|
||||
content.appendChild(iframe);
|
||||
modal.appendChild(content);
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Функция обновления превью изображения
|
||||
function updateImagePreview(inputField, imagePath) {
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||||
if (!fieldContainer) return;
|
||||
|
||||
// Находим или создаем превью
|
||||
let preview = fieldContainer.querySelector('.image-preview');
|
||||
|
||||
if (!preview) {
|
||||
preview = document.createElement('img');
|
||||
preview.className = 'image-preview';
|
||||
preview.style.cssText = `
|
||||
display: block;
|
||||
max-width: 180px;
|
||||
max-height: 120px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
// Вставляем превью после кнопки
|
||||
const button = fieldContainer.querySelector('.image-editor-btn');
|
||||
if (button) {
|
||||
const buttonContainer = button.parentNode;
|
||||
buttonContainer.parentNode.insertBefore(preview, buttonContainer.nextSibling);
|
||||
} else {
|
||||
fieldContainer.appendChild(preview);
|
||||
}
|
||||
}
|
||||
|
||||
if (imagePath && imagePath.trim()) {
|
||||
preview.src = imagePath + '?t=' + Date.now(); // Добавляем timestamp для обновления
|
||||
preview.style.display = 'block';
|
||||
preview.onerror = () => {
|
||||
preview.style.display = 'none';
|
||||
};
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция добавления кнопки редактора к полю
|
||||
function addImageEditorButton(inputField) {
|
||||
const fieldName = inputField.name || inputField.id || 'image';
|
||||
|
||||
// Проверяем, не добавлена ли уже кнопка
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||||
if (fieldContainer.querySelector('.image-editor-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем контейнер для кнопки
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.cssText = `
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
// Создаем кнопку
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'image-editor-btn';
|
||||
button.innerHTML = '📷 Выбрать';
|
||||
button.style.cssText = `
|
||||
padding: 6px 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openImageEditor(inputField, fieldName);
|
||||
};
|
||||
|
||||
buttonContainer.appendChild(button);
|
||||
|
||||
// Добавляем контейнер после поля ввода
|
||||
if (inputField.nextSibling) {
|
||||
inputField.parentNode.insertBefore(buttonContainer, inputField.nextSibling);
|
||||
} else {
|
||||
inputField.parentNode.appendChild(buttonContainer);
|
||||
}
|
||||
|
||||
// Добавляем превью если есть значение
|
||||
if (inputField.value && inputField.value.trim()) {
|
||||
updateImagePreview(inputField, inputField.value);
|
||||
}
|
||||
|
||||
// Слушаем изменения в поле для обновления превью
|
||||
inputField.addEventListener('input', () => {
|
||||
updateImagePreview(inputField, inputField.value);
|
||||
});
|
||||
|
||||
inputField.addEventListener('change', () => {
|
||||
updateImagePreview(inputField, inputField.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Функция проверки, является ли поле полем изображения
|
||||
function isImageField(inputField) {
|
||||
const fieldName = (inputField.name || inputField.id || '').toLowerCase();
|
||||
let labelText = '';
|
||||
|
||||
// Ищем label для поля
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]');
|
||||
if (fieldContainer) {
|
||||
const label = fieldContainer.querySelector('label, .property-label, h3');
|
||||
if (label) {
|
||||
labelText = label.textContent.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем по имени поля или тексту label
|
||||
return fieldName.includes('image') ||
|
||||
fieldName.includes('photo') ||
|
||||
fieldName.includes('avatar') ||
|
||||
fieldName.includes('picture') ||
|
||||
fieldName.includes('banner') ||
|
||||
fieldName.includes('thumbnail') ||
|
||||
(fieldName.includes('url') && (labelText.includes('image') || labelText.includes('изображение'))) ||
|
||||
labelText.includes('изображение') ||
|
||||
labelText.includes('картинка') ||
|
||||
labelText.includes('фото') ||
|
||||
labelText.includes('image') ||
|
||||
labelText.includes('picture');
|
||||
}
|
||||
|
||||
// Функция сканирования и добавления кнопок к полям изображений
|
||||
function scanAndAddImageButtons() {
|
||||
console.log('🔍 Сканирование полей для добавления кнопок редактора изображений...');
|
||||
|
||||
// Более широкий поиск полей ввода
|
||||
const inputFields = document.querySelectorAll('input[type="text"], input[type="url"], input:not([type="hidden"]):not([type="submit"]):not([type="button"])');
|
||||
|
||||
console.log(`📋 Найдено ${inputFields.length} полей ввода`);
|
||||
|
||||
inputFields.forEach((inputField, index) => {
|
||||
const fieldName = inputField.name || inputField.id || `field_${index}`;
|
||||
const isImage = isImageField(inputField);
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||||
const hasButton = fieldContainer.querySelector('.image-editor-btn');
|
||||
|
||||
console.log(`🔸 Поле "${fieldName}": isImage=${isImage}, hasButton=${!!hasButton}`);
|
||||
|
||||
if (isImage && !hasButton) {
|
||||
console.log(`➕ Добавляем кнопку для поля "${fieldName}"`);
|
||||
addImageEditorButton(inputField);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация при загрузке DOM
|
||||
function initialize() {
|
||||
console.log('🚀 Инициализация селектора изображений AdminJS');
|
||||
|
||||
scanAndAddImageButtons();
|
||||
|
||||
// Наблюдаем за изменениями в DOM для динамически добавляемых полей
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldScan = false;
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType === 1 && (node.tagName === 'INPUT' || node.querySelector('input'))) {
|
||||
shouldScan = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldScan) {
|
||||
setTimeout(scanAndAddImageButtons, 100);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// Ждем загрузки DOM
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
|
||||
// Также запускаем через задержки для AdminJS
|
||||
setTimeout(initialize, 1000);
|
||||
setTimeout(initialize, 3000);
|
||||
setTimeout(initialize, 5000);
|
||||
|
||||
})();
|
||||
300
public/js/admin-image-selector.js
Normal file
300
public/js/admin-image-selector.js
Normal file
@@ -0,0 +1,300 @@
|
||||
// JavaScript для интеграции редактора изображений в AdminJS
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Функция для открытия редактора изображений
|
||||
function openImageEditor(inputField, fieldName) {
|
||||
const currentValue = inputField.value || '';
|
||||
const editorUrl = `/image-editor-compact.html?field=${fieldName}¤t=${encodeURIComponent(currentValue)}`;
|
||||
|
||||
// Убираем предыдущие модальные окна
|
||||
document.querySelectorAll('.image-editor-modal').forEach(modal => modal.remove());
|
||||
|
||||
// Создаем модальное окно
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'image-editor-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
height: 80%;
|
||||
max-height: 600px;
|
||||
position: relative;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
`;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '✕';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: #ff4757;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = editorUrl;
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
// Обработчик закрытия
|
||||
const closeModal = () => {
|
||||
document.body.removeChild(modal);
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
|
||||
closeBtn.onclick = closeModal;
|
||||
|
||||
// Обработчик сообщений от редактора
|
||||
const handleMessage = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) {
|
||||
inputField.value = event.data.path;
|
||||
|
||||
// Триггерим события изменения
|
||||
inputField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
inputField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
// Обновляем превью если есть
|
||||
updateImagePreview(inputField, event.data.path);
|
||||
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
content.appendChild(closeBtn);
|
||||
content.appendChild(iframe);
|
||||
modal.appendChild(content);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Закрытие по клику на фон
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция обновления превью изображения
|
||||
function updateImagePreview(inputField, imagePath) {
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]');
|
||||
if (!fieldContainer) return;
|
||||
|
||||
// Находим или создаем превью
|
||||
let preview = fieldContainer.querySelector('.image-preview');
|
||||
|
||||
if (!preview) {
|
||||
preview = document.createElement('img');
|
||||
preview.className = 'image-preview';
|
||||
preview.style.cssText = `
|
||||
display: block;
|
||||
max-width: 180px;
|
||||
max-height: 120px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
// Вставляем превью после кнопки
|
||||
const button = fieldContainer.querySelector('.image-editor-btn');
|
||||
if (button && button.nextSibling) {
|
||||
button.parentNode.insertBefore(preview, button.nextSibling);
|
||||
} else {
|
||||
inputField.parentNode.appendChild(preview);
|
||||
}
|
||||
}
|
||||
|
||||
if (imagePath && imagePath.trim()) {
|
||||
preview.src = imagePath + '?t=' + Date.now(); // Добавляем timestamp для обновления
|
||||
preview.style.display = 'block';
|
||||
preview.onerror = () => {
|
||||
preview.style.display = 'none';
|
||||
};
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция добавления кнопки редактора к полю
|
||||
function addImageEditorButton(inputField) {
|
||||
const fieldName = inputField.name || inputField.id || 'image';
|
||||
|
||||
// Проверяем, не добавлена ли уже кнопка
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||||
if (fieldContainer.querySelector('.image-editor-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем контейнер для кнопки и превью
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.cssText = `
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
// Создаем кнопку
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'image-editor-btn';
|
||||
button.innerHTML = '📷 Выбрать';
|
||||
button.style.cssText = `
|
||||
padding: 6px 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
button.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openImageEditor(inputField, fieldName);
|
||||
};
|
||||
|
||||
buttonContainer.appendChild(button);
|
||||
|
||||
// Добавляем контейнер после поля ввода
|
||||
if (inputField.nextSibling) {
|
||||
inputField.parentNode.insertBefore(buttonContainer, inputField.nextSibling);
|
||||
} else {
|
||||
inputField.parentNode.appendChild(buttonContainer);
|
||||
}
|
||||
|
||||
// Добавляем превью если есть значение
|
||||
if (inputField.value && inputField.value.trim()) {
|
||||
updateImagePreview(inputField, inputField.value);
|
||||
}
|
||||
|
||||
// Слушаем изменения в поле для обновления превью
|
||||
inputField.addEventListener('input', () => {
|
||||
updateImagePreview(inputField, inputField.value);
|
||||
});
|
||||
|
||||
inputField.addEventListener('change', () => {
|
||||
updateImagePreview(inputField, inputField.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Функция проверки, является ли поле полем изображения
|
||||
function isImageField(inputField) {
|
||||
const fieldName = (inputField.name || inputField.id || '').toLowerCase();
|
||||
let labelText = '';
|
||||
|
||||
// Ищем label для поля
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]');
|
||||
if (fieldContainer) {
|
||||
const label = fieldContainer.querySelector('label, .property-label, h3');
|
||||
if (label) {
|
||||
labelText = label.textContent.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем по имени поля или тексту label
|
||||
return fieldName.includes('image') ||
|
||||
fieldName.includes('photo') ||
|
||||
fieldName.includes('avatar') ||
|
||||
fieldName.includes('picture') ||
|
||||
fieldName.includes('banner') ||
|
||||
fieldName.includes('thumbnail') ||
|
||||
fieldName.includes('url') && (labelText.includes('image') || labelText.includes('изображение')) ||
|
||||
labelText.includes('изображение') ||
|
||||
labelText.includes('картинка') ||
|
||||
labelText.includes('фото') ||
|
||||
labelText.includes('image') ||
|
||||
labelText.includes('picture');
|
||||
}
|
||||
|
||||
// Функция сканирования и добавления кнопок к полям изображений
|
||||
function scanAndAddImageButtons() {
|
||||
console.log('🔍 Сканирование полей для добавления кнопок редактора изображений...');
|
||||
|
||||
// Более широкий поиск полей ввода
|
||||
const inputFields = document.querySelectorAll('input[type="text"], input[type="url"], input:not([type="hidden"]):not([type="submit"]):not([type="button"])');
|
||||
|
||||
console.log(`📋 Найдено ${inputFields.length} полей ввода`);
|
||||
|
||||
inputFields.forEach((inputField, index) => {
|
||||
const fieldName = inputField.name || inputField.id || `field_${index}`;
|
||||
const isImage = isImageField(inputField);
|
||||
const hasButton = inputField.parentNode.querySelector('.image-editor-btn');
|
||||
|
||||
console.log(`🔸 Поле "${fieldName}": isImage=${isImage}, hasButton=${hasButton}`);
|
||||
|
||||
if (isImage && !hasButton) {
|
||||
console.log(`➕ Добавляем кнопку для поля "${fieldName}"`);
|
||||
addImageEditorButton(inputField);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация при загрузке DOM
|
||||
function initialize() {
|
||||
console.log('🚀 Инициализация селектора изображений AdminJS');
|
||||
|
||||
scanAndAddImageButtons();
|
||||
|
||||
// Наблюдаем за изменениями в DOM для динамически добавляемых полей
|
||||
const observer = new MutationObserver(() => {
|
||||
scanAndAddImageButtons();
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Периодическое сканирование для надежности
|
||||
setInterval(scanAndAddImageButtons, 2000);
|
||||
}
|
||||
|
||||
// Ждем загрузки DOM
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
|
||||
// Также запускаем через задержки для AdminJS
|
||||
setTimeout(initialize, 1000);
|
||||
setTimeout(initialize, 3000);
|
||||
setTimeout(initialize, 5000);
|
||||
|
||||
})();
|
||||
690
public/js/image-editor.js
Normal file
690
public/js/image-editor.js
Normal file
@@ -0,0 +1,690 @@
|
||||
/**
|
||||
* Image Editor Modal Component
|
||||
* Предоставляет интерфейс для загрузки, обрезки и редактирования изображений
|
||||
*/
|
||||
|
||||
class ImageEditor {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
targetFolder: 'routes',
|
||||
aspectRatio: null, // null = свободная обрезка, или например 16/9
|
||||
maxWidth: 1200,
|
||||
maxHeight: 800,
|
||||
...options
|
||||
};
|
||||
|
||||
this.modal = null;
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.image = null;
|
||||
this.imageData = null;
|
||||
this.cropBox = null;
|
||||
this.isDragging = false;
|
||||
this.lastMousePos = { x: 0, y: 0 };
|
||||
this.rotation = 0;
|
||||
this.flipHorizontal = false;
|
||||
this.flipVertical = false;
|
||||
|
||||
this.onSave = options.onSave || (() => {});
|
||||
this.onCancel = options.onCancel || (() => {});
|
||||
|
||||
this.createModal();
|
||||
}
|
||||
|
||||
createModal() {
|
||||
// Создаем модальное окно
|
||||
const modalHTML = `
|
||||
<div class="image-editor-overlay">
|
||||
<div class="image-editor-modal">
|
||||
<div class="image-editor-header">
|
||||
<h3>Редактор изображений</h3>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
|
||||
<div class="image-editor-body">
|
||||
<!-- Область загрузки -->
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<div class="upload-content">
|
||||
<div class="upload-icon">📷</div>
|
||||
<p>Перетащите изображение сюда или <button class="btn-link" id="selectFileBtn">выберите файл</button></p>
|
||||
<input type="file" id="fileInput" accept="image/*" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Область редактирования -->
|
||||
<div class="editor-area" id="editorArea" style="display: none;">
|
||||
<div class="editor-toolbar">
|
||||
<button class="tool-btn" id="rotateLeftBtn" title="Повернуть влево">↺</button>
|
||||
<button class="tool-btn" id="rotateRightBtn" title="Повернуть вправо">↻</button>
|
||||
<button class="tool-btn" id="flipHorizontalBtn" title="Отразить горизонтально">⟷</button>
|
||||
<button class="tool-btn" id="flipVerticalBtn" title="Отразить вертикально">↕</button>
|
||||
<button class="tool-btn" id="resetCropBtn" title="Сбросить обрезку">⌕</button>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas id="editorCanvas"></canvas>
|
||||
<div class="crop-overlay" id="cropOverlay">
|
||||
<div class="crop-box" id="cropBox">
|
||||
<div class="crop-handle nw"></div>
|
||||
<div class="crop-handle ne"></div>
|
||||
<div class="crop-handle sw"></div>
|
||||
<div class="crop-handle se"></div>
|
||||
<div class="crop-handle n"></div>
|
||||
<div class="crop-handle s"></div>
|
||||
<div class="crop-handle e"></div>
|
||||
<div class="crop-handle w"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-info">
|
||||
<span id="imageInfo">Размер: 0x0</span>
|
||||
<span id="cropInfo">Обрезка: не выбрана</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-editor-footer">
|
||||
<button class="btn btn-secondary" id="cancelBtn">Отмена</button>
|
||||
<button class="btn btn-primary" id="saveBtn" disabled>Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Добавляем стили
|
||||
if (!document.getElementById('image-editor-styles')) {
|
||||
const styles = document.createElement('style');
|
||||
styles.id = 'image-editor-styles';
|
||||
styles.textContent = `
|
||||
.image-editor-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.image-editor-modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 90vw;
|
||||
max-width: 900px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-editor-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-editor-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.image-editor-body {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background: #f9f9f9;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area.dragover {
|
||||
border-color: #007bff;
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.upload-content .upload-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.upload-content p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #007bff;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editor-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#editorCanvas {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.crop-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.crop-box {
|
||||
position: absolute;
|
||||
border: 2px solid #007bff;
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
pointer-events: all;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.crop-handle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #007bff;
|
||||
border: 1px solid white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.crop-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
|
||||
.crop-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
|
||||
.crop-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
|
||||
.crop-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
|
||||
.crop-handle.n { top: -5px; left: 50%; margin-left: -5px; cursor: n-resize; }
|
||||
.crop-handle.s { bottom: -5px; left: 50%; margin-left: -5px; cursor: s-resize; }
|
||||
.crop-handle.e { top: 50%; right: -5px; margin-top: -5px; cursor: e-resize; }
|
||||
.crop-handle.w { top: 50%; left: -5px; margin-top: -5px; cursor: w-resize; }
|
||||
|
||||
.image-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.image-editor-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #333;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styles);
|
||||
}
|
||||
|
||||
// Добавляем модальное окно в DOM
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
this.modal = document.querySelector('.image-editor-overlay');
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Закрытие модального окна
|
||||
this.modal.querySelector('.close-btn').addEventListener('click', () => this.close());
|
||||
this.modal.querySelector('#cancelBtn').addEventListener('click', () => this.close());
|
||||
|
||||
// Клик по overlay для закрытия
|
||||
this.modal.addEventListener('click', (e) => {
|
||||
if (e.target === this.modal) this.close();
|
||||
});
|
||||
|
||||
// Загрузка файла
|
||||
const fileInput = this.modal.querySelector('#fileInput');
|
||||
const selectFileBtn = this.modal.querySelector('#selectFileBtn');
|
||||
const uploadArea = this.modal.querySelector('#uploadArea');
|
||||
|
||||
selectFileBtn.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||||
|
||||
// Drag & Drop
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
this.loadImage(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Инструменты редактирования
|
||||
this.modal.querySelector('#rotateLeftBtn').addEventListener('click', () => this.rotate(-90));
|
||||
this.modal.querySelector('#rotateRightBtn').addEventListener('click', () => this.rotate(90));
|
||||
this.modal.querySelector('#flipHorizontalBtn').addEventListener('click', () => this.flipHorizontal = !this.flipHorizontal, this.redraw());
|
||||
this.modal.querySelector('#flipVerticalBtn').addEventListener('click', () => this.flipVertical = !this.flipVertical, this.redraw());
|
||||
this.modal.querySelector('#resetCropBtn').addEventListener('click', () => this.resetCrop());
|
||||
|
||||
// Сохранение
|
||||
this.modal.querySelector('#saveBtn').addEventListener('click', () => this.save());
|
||||
}
|
||||
|
||||
handleFileSelect(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
this.loadImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
async loadImage(file) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Пожалуйста, выберите изображение');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/images/upload-image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.imageData = result;
|
||||
this.showEditor();
|
||||
this.loadImageToCanvas(result.tempUrl);
|
||||
} else {
|
||||
alert(result.error || 'Ошибка загрузки изображения');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Ошибка загрузки изображения');
|
||||
}
|
||||
}
|
||||
|
||||
loadImageToCanvas(imageUrl) {
|
||||
this.image = new Image();
|
||||
this.image.onload = () => {
|
||||
this.initCanvas();
|
||||
this.initCropBox();
|
||||
this.redraw();
|
||||
this.updateInfo();
|
||||
this.modal.querySelector('#saveBtn').disabled = false;
|
||||
};
|
||||
this.image.src = imageUrl;
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.modal.querySelector('#uploadArea').style.display = 'none';
|
||||
this.modal.querySelector('#editorArea').style.display = 'block';
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
this.canvas = this.modal.querySelector('#editorCanvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// Вычисляем размеры canvas
|
||||
const containerWidth = 600; // максимальная ширина
|
||||
const containerHeight = 400; // максимальная высота
|
||||
|
||||
const imageRatio = this.image.width / this.image.height;
|
||||
const containerRatio = containerWidth / containerHeight;
|
||||
|
||||
if (imageRatio > containerRatio) {
|
||||
this.canvas.width = containerWidth;
|
||||
this.canvas.height = containerWidth / imageRatio;
|
||||
} else {
|
||||
this.canvas.width = containerHeight * imageRatio;
|
||||
this.canvas.height = containerHeight;
|
||||
}
|
||||
|
||||
this.scaleX = this.canvas.width / this.image.width;
|
||||
this.scaleY = this.canvas.height / this.image.height;
|
||||
}
|
||||
|
||||
initCropBox() {
|
||||
const overlay = this.modal.querySelector('#cropOverlay');
|
||||
const cropBox = this.modal.querySelector('#cropBox');
|
||||
|
||||
// Устанавливаем размеры overlay как у canvas
|
||||
const canvasRect = this.canvas.getBoundingClientRect();
|
||||
const containerRect = this.canvas.parentElement.getBoundingClientRect();
|
||||
|
||||
overlay.style.width = `${canvasRect.width}px`;
|
||||
overlay.style.height = `${canvasRect.height}px`;
|
||||
overlay.style.left = `${canvasRect.left - containerRect.left}px`;
|
||||
overlay.style.top = `${canvasRect.top - containerRect.top}px`;
|
||||
|
||||
// Инициализируем crop box (50% от центра)
|
||||
const boxWidth = canvasRect.width * 0.6;
|
||||
const boxHeight = canvasRect.height * 0.6;
|
||||
const boxLeft = (canvasRect.width - boxWidth) / 2;
|
||||
const boxTop = (canvasRect.height - boxHeight) / 2;
|
||||
|
||||
cropBox.style.width = `${boxWidth}px`;
|
||||
cropBox.style.height = `${boxHeight}px`;
|
||||
cropBox.style.left = `${boxLeft}px`;
|
||||
cropBox.style.top = `${boxTop}px`;
|
||||
|
||||
this.cropBox = {
|
||||
x: boxLeft / canvasRect.width,
|
||||
y: boxTop / canvasRect.height,
|
||||
width: boxWidth / canvasRect.width,
|
||||
height: boxHeight / canvasRect.height
|
||||
};
|
||||
|
||||
this.bindCropEvents(cropBox, overlay);
|
||||
}
|
||||
|
||||
bindCropEvents(cropBox, overlay) {
|
||||
let resizing = false;
|
||||
let moving = false;
|
||||
let startPos = { x: 0, y: 0 };
|
||||
let startBox = {};
|
||||
|
||||
// Обработчик для перемещения
|
||||
cropBox.addEventListener('mousedown', (e) => {
|
||||
if (e.target === cropBox) {
|
||||
moving = true;
|
||||
startPos = { x: e.clientX, y: e.clientY };
|
||||
startBox = { ...this.cropBox };
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик для изменения размера
|
||||
const handles = cropBox.querySelectorAll('.crop-handle');
|
||||
handles.forEach(handle => {
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
resizing = handle.className.replace('crop-handle ', '');
|
||||
startPos = { x: e.clientX, y: e.clientY };
|
||||
startBox = { ...this.cropBox };
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчики движения и отпускания мыши
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (moving || resizing) {
|
||||
this.updateCropBox(e, startPos, startBox, moving ? 'move' : resizing, overlay);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
moving = false;
|
||||
resizing = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateCropBox(e, startPos, startBox, action, overlay) {
|
||||
const overlayRect = overlay.getBoundingClientRect();
|
||||
const deltaX = (e.clientX - startPos.x) / overlayRect.width;
|
||||
const deltaY = (e.clientY - startPos.y) / overlayRect.height;
|
||||
|
||||
let newBox = { ...startBox };
|
||||
|
||||
if (action === 'move') {
|
||||
newBox.x = Math.max(0, Math.min(1 - startBox.width, startBox.x + deltaX));
|
||||
newBox.y = Math.max(0, Math.min(1 - startBox.height, startBox.y + deltaY));
|
||||
} else {
|
||||
// Изменение размера в зависимости от handle
|
||||
if (action.includes('n')) newBox.y += deltaY, newBox.height -= deltaY;
|
||||
if (action.includes('s')) newBox.height += deltaY;
|
||||
if (action.includes('w')) newBox.x += deltaX, newBox.width -= deltaX;
|
||||
if (action.includes('e')) newBox.width += deltaX;
|
||||
|
||||
// Ограничиваем минимальные размеры
|
||||
if (newBox.width < 0.1) newBox.width = 0.1;
|
||||
if (newBox.height < 0.1) newBox.height = 0.1;
|
||||
|
||||
// Ограничиваем границы
|
||||
if (newBox.x < 0) newBox.x = 0;
|
||||
if (newBox.y < 0) newBox.y = 0;
|
||||
if (newBox.x + newBox.width > 1) newBox.width = 1 - newBox.x;
|
||||
if (newBox.y + newBox.height > 1) newBox.height = 1 - newBox.y;
|
||||
}
|
||||
|
||||
this.cropBox = newBox;
|
||||
this.updateCropBoxDisplay(overlay);
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
updateCropBoxDisplay(overlay) {
|
||||
const cropBoxElement = this.modal.querySelector('#cropBox');
|
||||
const overlayRect = overlay.getBoundingClientRect();
|
||||
|
||||
cropBoxElement.style.left = `${this.cropBox.x * overlayRect.width}px`;
|
||||
cropBoxElement.style.top = `${this.cropBox.y * overlayRect.height}px`;
|
||||
cropBoxElement.style.width = `${this.cropBox.width * overlayRect.width}px`;
|
||||
cropBoxElement.style.height = `${this.cropBox.height * overlayRect.height}px`;
|
||||
}
|
||||
|
||||
rotate(degrees) {
|
||||
this.rotation = (this.rotation + degrees) % 360;
|
||||
this.redraw();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.ctx.save();
|
||||
|
||||
// Перемещаем к центру
|
||||
this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
|
||||
|
||||
// Поворот
|
||||
this.ctx.rotate(this.rotation * Math.PI / 180);
|
||||
|
||||
// Отражение
|
||||
this.ctx.scale(this.flipHorizontal ? -1 : 1, this.flipVertical ? -1 : 1);
|
||||
|
||||
// Рисуем изображение
|
||||
this.ctx.drawImage(
|
||||
this.image,
|
||||
-this.canvas.width / 2,
|
||||
-this.canvas.height / 2,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
resetCrop() {
|
||||
// Сброс crop box к исходному состоянию
|
||||
this.cropBox = { x: 0.2, y: 0.2, width: 0.6, height: 0.6 };
|
||||
this.updateCropBoxDisplay(this.modal.querySelector('#cropOverlay'));
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
updateInfo() {
|
||||
const imageInfo = this.modal.querySelector('#imageInfo');
|
||||
const cropInfo = this.modal.querySelector('#cropInfo');
|
||||
|
||||
imageInfo.textContent = `Размер: ${this.image.width}x${this.image.height}`;
|
||||
|
||||
const cropWidth = Math.round(this.cropBox.width * this.image.width);
|
||||
const cropHeight = Math.round(this.cropBox.height * this.image.height);
|
||||
cropInfo.textContent = `Обрезка: ${cropWidth}x${cropHeight}`;
|
||||
}
|
||||
|
||||
async save() {
|
||||
const saveBtn = this.modal.querySelector('#saveBtn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Сохранение...';
|
||||
|
||||
try {
|
||||
const cropData = {
|
||||
x: this.cropBox.x * this.image.width,
|
||||
y: this.cropBox.y * this.image.height,
|
||||
width: this.cropBox.width * this.image.width,
|
||||
height: this.cropBox.height * this.image.height
|
||||
};
|
||||
|
||||
const response = await fetch('/api/images/process-image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tempId: this.imageData.tempId,
|
||||
rotation: this.rotation,
|
||||
flipHorizontal: this.flipHorizontal,
|
||||
flipVertical: this.flipVertical,
|
||||
cropData,
|
||||
targetFolder: this.options.targetFolder
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.onSave(result.url);
|
||||
this.close();
|
||||
} else {
|
||||
alert(result.error || 'Ошибка сохранения');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Сохранить';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
alert('Ошибка сохранения изображения');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Сохранить';
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
// Удаляем временный файл
|
||||
if (this.imageData && this.imageData.tempId) {
|
||||
fetch(`/api/images/temp-image/${this.imageData.tempId}`, {
|
||||
method: 'DELETE'
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
this.onCancel();
|
||||
if (this.modal) {
|
||||
this.modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.modal) {
|
||||
this.modal.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Глобально доступная функция для открытия редактора
|
||||
window.openImageEditor = function(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const editor = new ImageEditor({
|
||||
...options,
|
||||
onSave: (url) => resolve(url),
|
||||
onCancel: () => reject(new Error('Canceled'))
|
||||
});
|
||||
editor.show();
|
||||
});
|
||||
};
|
||||
103
public/test-editor.html
Normal file
103
public/test-editor.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест редактора изображений</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-container {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.result-image {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
border: 1px solid #ddd;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Тестирование редактора изображений</h1>
|
||||
|
||||
<div class="test-container">
|
||||
<h3>Тест для туров (routes)</h3>
|
||||
<button class="btn" onclick="testImageEditor('routes')">Открыть редактор для туров</button>
|
||||
<div id="routes-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-container">
|
||||
<h3>Тест для гидов (guides)</h3>
|
||||
<button class="btn" onclick="testImageEditor('guides')">Открыть редактор для гидов</button>
|
||||
<div id="guides-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-container">
|
||||
<h3>Тест для статей (articles)</h3>
|
||||
<button class="btn" onclick="testImageEditor('articles')">Открыть редактор для статей</button>
|
||||
<div id="articles-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- Подключаем редактор изображений -->
|
||||
<script src="/js/image-editor.js"></script>
|
||||
|
||||
<script>
|
||||
function testImageEditor(targetFolder) {
|
||||
if (typeof window.openImageEditor === 'function') {
|
||||
window.openImageEditor({
|
||||
targetFolder: targetFolder,
|
||||
onSave: (url) => {
|
||||
console.log('Saved image:', url);
|
||||
const resultDiv = document.getElementById(targetFolder + '-result');
|
||||
resultDiv.innerHTML = `
|
||||
<p>Изображение сохранено: <strong>${url}</strong></p>
|
||||
<img src="${url}" alt="Result" class="result-image">
|
||||
`;
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.message !== 'Canceled') {
|
||||
console.error('Editor error:', error);
|
||||
alert('Ошибка редактора: ' + error.message);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Редактор изображений не загружен!');
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем доступность API
|
||||
async function checkAPI() {
|
||||
try {
|
||||
const response = await fetch('/api/images/images/routes');
|
||||
const result = await response.json();
|
||||
console.log('API check:', result);
|
||||
} catch (error) {
|
||||
console.error('API not available:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем при загрузке страницы
|
||||
checkAPI();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
191
public/test-image-editor.html
Normal file
191
public/test-image-editor.html
Normal file
@@ -0,0 +1,191 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест редактора изображений</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1>Тест интеграции редактора изображений</h1>
|
||||
<p>Этот файл демонстрирует, как редактор изображений будет работать с AdminJS.</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Поле изображения маршрута</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label for="route_image_url" class="form-label">Изображение маршрута</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="route_image_url" name="route_image_url" class="form-control"
|
||||
placeholder="/uploads/routes/example.jpg" value="/uploads/routes/seoul-city-tour.jpg">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openImageEditor('route_image_url', document.getElementById('route_image_url').value)">
|
||||
<i class="fas fa-images"></i> Выбрать
|
||||
</button>
|
||||
</div>
|
||||
<img id="route_image_url_preview" src="/uploads/routes/seoul-city-tour.jpg"
|
||||
class="img-thumbnail mt-2" style="max-width: 200px; max-height: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Поле изображения гида</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label for="guide_image_url" class="form-label">Фотография гида</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="guide_image_url" name="guide_image_url" class="form-control"
|
||||
placeholder="/uploads/guides/example.jpg" value="/uploads/guides/guide-profile.jpg">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openImageEditor('guide_image_url', document.getElementById('guide_image_url').value)">
|
||||
<i class="fas fa-images"></i> Выбрать
|
||||
</button>
|
||||
</div>
|
||||
<img id="guide_image_url_preview" src="/uploads/guides/guide-profile.jpg"
|
||||
class="img-thumbnail mt-2" style="max-width: 200px; max-height: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Доступные изображения</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="imageList" class="row">
|
||||
<!-- Будет заполнено динамически -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-info">
|
||||
<h6>Как использовать:</h6>
|
||||
<ol>
|
||||
<li>Нажмите кнопку "Выбрать" рядом с полем изображения</li>
|
||||
<li>Откроется редактор изображений в новом окне</li>
|
||||
<li>Выберите изображение из галереи, загрузите новое или укажите URL</li>
|
||||
<li>Нажмите "Выбрать" в редакторе</li>
|
||||
<li>Поле автоматически обновится с выбранным путем</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Функция для открытия редактора изображений
|
||||
function openImageEditor(fieldName, currentValue) {
|
||||
const editorUrl = `/image-editor.html?field=${fieldName}¤t=${encodeURIComponent(currentValue || '')}`;
|
||||
const editorWindow = window.open(editorUrl, 'imageEditor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||
|
||||
// Слушаем сообщения от редактора
|
||||
const messageHandler = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
const preview = document.getElementById(fieldName + '_preview');
|
||||
|
||||
if (field) {
|
||||
field.value = event.data.path;
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
preview.src = event.data.path;
|
||||
}
|
||||
|
||||
window.removeEventListener('message', messageHandler);
|
||||
editorWindow.close();
|
||||
|
||||
showSuccess(`Изображение обновлено: ${event.data.path}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// Очистка обработчика при закрытии окна
|
||||
const checkClosed = setInterval(() => {
|
||||
if (editorWindow.closed) {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
clearInterval(checkClosed);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Функция показа уведомления об успехе
|
||||
function showSuccess(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
alert.style.zIndex = '9999';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Загрузка списка изображений
|
||||
async function loadImageList() {
|
||||
try {
|
||||
const response = await fetch('/api/images/gallery?folder=all');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderImageList(result.data);
|
||||
} else {
|
||||
document.getElementById('imageList').innerHTML = '<p class="text-muted">Ошибка загрузки изображений</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('imageList').innerHTML = '<p class="text-muted">Ошибка: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение списка изображений
|
||||
function renderImageList(images) {
|
||||
const container = document.getElementById('imageList');
|
||||
|
||||
if (images.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted">Изображения не найдены</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = images.slice(0, 12).map(img => `
|
||||
<div class="col-md-2 col-sm-3 col-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<img src="${img.path}" class="card-img-top" style="height: 100px; object-fit: cover;" alt="${img.name}">
|
||||
<div class="card-body p-2">
|
||||
<small class="card-title text-truncate d-block">${img.name}</small>
|
||||
<small class="text-muted">${img.folder}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Загружаем изображения при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', loadImageList);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user