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

909 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Универсальный медиа-менеджер</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa;
overflow: hidden;
}
.media-manager {
height: 100vh;
display: flex;
flex-direction: column;
background: white;
}
/* Header */
.media-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.media-title {
font-size: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
/* Toolbar */
.media-toolbar {
background: white;
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.upload-zone {
position: relative;
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.upload-zone:hover {
background: #218838;
transform: translateY(-1px);
}
.upload-zone input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.search-box {
flex: 1;
max-width: 300px;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
}
.view-toggle {
display: flex;
gap: 5px;
}
.view-btn {
padding: 6px 10px;
border: 1px solid #ced4da;
background: white;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
}
.view-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
/* Content Area */
.media-content {
flex: 1;
display: flex;
overflow: hidden;
}
/* Sidebar */
.media-sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
padding: 20px;
overflow-y: auto;
}
.folder-tree {
list-style: none;
}
.folder-item {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 2px;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.folder-item:hover {
background: #e9ecef;
}
.folder-item.active {
background: #007bff;
color: white;
}
/* Gallery */
.media-gallery {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
}
.gallery-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.media-item {
background: white;
border: 2px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.media-item:hover {
border-color: #007bff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,123,255,0.15);
}
.media-item.selected {
border-color: #28a745;
background: #f8fff8;
}
.media-item.selected::after {
content: '✓';
position: absolute;
top: 5px;
right: 5px;
background: #28a745;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.media-preview {
width: 100%;
height: 120px;
object-fit: cover;
background: #f8f9fa;
}
.media-info {
padding: 10px;
}
.media-name {
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.media-size {
font-size: 11px;
color: #6c757d;
}
/* List View */
.list-item {
display: flex;
align-items: center;
padding: 12px;
background: white;
border: 1px solid #e9ecef;
border-radius: 6px;
}
.list-preview {
width: 60px;
height: 45px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.list-info {
flex: 1;
}
.list-actions {
display: flex;
gap: 8px;
}
/* Actions */
.action-btn {
padding: 4px 8px;
border: 1px solid #ced4da;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.action-btn:hover {
background: #f8f9fa;
}
.action-btn.delete {
color: #dc3545;
border-color: #dc3545;
}
.action-btn.delete:hover {
background: #dc3545;
color: white;
}
/* Drop Zone */
.drop-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,123,255,0.8);
color: white;
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
font-size: 24px;
font-weight: 600;
}
.drop-overlay.active {
display: flex;
}
/* Status Bar */
.media-status {
background: #f8f9fa;
padding: 10px 20px;
border-top: 1px solid #e9ecef;
font-size: 14px;
color: #6c757d;
display: flex;
justify-content: between;
align-items: center;
}
.status-left {
flex: 1;
}
.status-right {
display: flex;
gap: 15px;
}
/* Selection Actions */
.selection-actions {
background: #007bff;
color: white;
padding: 10px 20px;
display: none;
align-items: center;
gap: 15px;
}
.selection-actions.visible {
display: flex;
}
.select-btn {
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.select-btn:hover {
background: #218838;
}
/* Modal */
.media-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: white;
max-width: 90vw;
max-height: 90vh;
border-radius: 8px;
overflow: hidden;
}
.modal-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.modal-footer {
padding: 15px;
background: #f8f9fa;
text-align: center;
}
/* Loading */
.loading {
display: none;
text-align: center;
padding: 40px;
color: #6c757d;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.media-sidebar {
display: none;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.empty-icon {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
</style>
</head>
<body>
<div class="media-manager">
<!-- Header -->
<div class="media-header">
<div class="media-title">
<span>📁</span>
Медиа-менеджер
</div>
</div>
<!-- Toolbar -->
<div class="media-toolbar">
<div class="upload-zone">
<input type="file" id="fileInput" multiple accept="image/*">
<span>📤</span>
Загрузить файлы
</div>
<input type="text" class="search-box" id="searchBox" placeholder="🔍 Поиск файлов...">
<div class="view-toggle">
<button class="view-btn active" data-view="grid"></button>
<button class="view-btn" data-view="list"></button>
</div>
</div>
<!-- Selection Actions -->
<div class="selection-actions" id="selectionActions">
<span id="selectionCount">0 файлов выбрано</span>
<button class="select-btn" id="useSelectedBtn">Использовать выбранные</button>
<button class="action-btn delete" id="deleteSelectedBtn">Удалить выбранные</button>
</div>
<!-- Content -->
<div class="media-content">
<!-- Sidebar -->
<div class="media-sidebar">
<ul class="folder-tree" id="folderTree">
<li class="folder-item active" data-folder="all">
<span>📁</span> Все файлы
</li>
<li class="folder-item" data-folder="routes">
<span>📁</span> Маршруты
</li>
<li class="folder-item" data-folder="guides">
<span>👥</span> Гиды
</li>
<li class="folder-item" data-folder="articles">
<span>📰</span> Статьи
</li>
<li class="folder-item" data-folder="general">
<span>🗂️</span> Общие
</li>
</ul>
</div>
<!-- Gallery -->
<div class="media-gallery">
<div class="loading" id="loadingIndicator">
<div class="spinner"></div>
Загрузка файлов...
</div>
<div class="gallery-grid" id="mediaGrid"></div>
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">📷</div>
<h3>Нет изображений</h3>
<p>Загрузите изображения или выберите другую папку</p>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="media-status">
<div class="status-left" id="statusInfo">
Загрузка...
</div>
<div class="status-right">
<span id="totalFiles">0 файлов</span>
<span id="totalSize">0 KB</span>
</div>
</div>
<!-- Drop Overlay -->
<div class="drop-overlay" id="dropOverlay">
📤 Отпустите файлы для загрузки
</div>
<!-- Modal -->
<div class="media-modal" id="mediaModal">
<div class="modal-content">
<img class="modal-image" id="modalImage" src="" alt="">
<div class="modal-footer">
<button class="action-btn" onclick="closeModal()">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
class MediaManager {
constructor() {
this.selectedFiles = new Set();
this.currentFolder = 'all';
this.currentView = 'grid';
this.allFiles = [];
this.filteredFiles = [];
this.initializeEventListeners();
this.loadFiles();
}
initializeEventListeners() {
// File upload
document.getElementById('fileInput').addEventListener('change', (e) => {
this.handleFileUpload(e.target.files);
});
// Drag and drop
document.addEventListener('dragover', (e) => {
e.preventDefault();
document.getElementById('dropOverlay').classList.add('active');
});
document.addEventListener('dragleave', (e) => {
if (!e.relatedTarget) {
document.getElementById('dropOverlay').classList.remove('active');
}
});
document.addEventListener('drop', (e) => {
e.preventDefault();
document.getElementById('dropOverlay').classList.remove('active');
if (e.dataTransfer.files.length > 0) {
this.handleFileUpload(e.dataTransfer.files);
}
});
// Search
document.getElementById('searchBox').addEventListener('input', (e) => {
this.filterFiles(e.target.value);
});
// View toggle
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.currentView = btn.dataset.view;
this.renderFiles();
});
});
// Folder selection
document.querySelectorAll('.folder-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.folder-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
this.currentFolder = item.dataset.folder;
this.filterFiles();
});
});
// Selection actions
document.getElementById('useSelectedBtn').addEventListener('click', () => {
this.useSelectedFiles();
});
document.getElementById('deleteSelectedBtn').addEventListener('click', () => {
this.deleteSelectedFiles();
});
// Modal close
document.getElementById('mediaModal').addEventListener('click', (e) => {
if (e.target.id === 'mediaModal') {
this.closeModal();
}
});
}
async loadFiles() {
try {
this.showLoading(true);
const response = await fetch('/api/images/gallery');
const data = await response.json();
if (data.success) {
this.allFiles = data.data.map(file => ({
...file,
id: file.name,
url: file.path,
folder: file.folder || 'general'
}));
this.filterFiles();
} else {
throw new Error(data.message || 'Ошибка загрузки файлов');
}
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
this.showError('Ошибка загрузки файлов: ' + error.message);
} finally {
this.showLoading(false);
}
}
filterFiles(search = '') {
let files = this.allFiles;
// Фильтр по папке
if (this.currentFolder !== 'all') {
files = files.filter(file => file.folder === this.currentFolder);
}
// Фильтр по поиску
if (search) {
const searchTerm = search.toLowerCase();
files = files.filter(file =>
file.name.toLowerCase().includes(searchTerm)
);
}
this.filteredFiles = files;
this.renderFiles();
this.updateStatus();
}
renderFiles() {
const grid = document.getElementById('mediaGrid');
const empty = document.getElementById('emptyState');
if (this.filteredFiles.length === 0) {
grid.style.display = 'none';
empty.style.display = 'block';
return;
}
grid.style.display = this.currentView === 'grid' ? 'grid' : 'flex';
grid.className = this.currentView === 'grid' ? 'gallery-grid' : 'gallery-list';
empty.style.display = 'none';
grid.innerHTML = this.filteredFiles.map(file => {
const isSelected = this.selectedFiles.has(file.id);
const sizeText = this.formatFileSize(file.size);
if (this.currentView === 'grid') {
return `
<div class="media-item ${isSelected ? 'selected' : ''}"
data-id="${file.id}" onclick="mediaManager.selectFile('${file.id}')">
<img class="media-preview" src="${file.url}" alt="${file.name}"
onerror="this.src='/images/placeholders/no-image.png'">
<div class="media-info">
<div class="media-name" title="${file.name}">${file.name}</div>
<div class="media-size">${sizeText}</div>
</div>
</div>
`;
} else {
return `
<div class="media-item list-item ${isSelected ? 'selected' : ''}"
data-id="${file.id}">
<img class="list-preview" src="${file.url}" alt="${file.name}"
onerror="this.src='/images/placeholders/no-image.png'">
<div class="list-info">
<div class="media-name">${file.name}</div>
<div class="media-size">${sizeText}${file.folder}</div>
</div>
<div class="list-actions">
<button class="action-btn" onclick="mediaManager.selectFile('${file.id}')">
${isSelected ? 'Отменить' : 'Выбрать'}
</button>
<button class="action-btn" onclick="mediaManager.previewFile('${file.id}')">Просмотр</button>
<button class="action-btn delete" onclick="mediaManager.deleteFile('${file.id}')">Удалить</button>
</div>
</div>
`;
}
}).join('');
}
selectFile(fileId) {
if (this.selectedFiles.has(fileId)) {
this.selectedFiles.delete(fileId);
} else {
this.selectedFiles.add(fileId);
}
this.updateSelection();
this.renderFiles();
}
updateSelection() {
const count = this.selectedFiles.size;
const actions = document.getElementById('selectionActions');
const countEl = document.getElementById('selectionCount');
if (count > 0) {
actions.classList.add('visible');
countEl.textContent = `${count} файл${count > 1 ? (count > 4 ? 'ов' : 'а') : ''} выбрано`;
} else {
actions.classList.remove('visible');
}
}
previewFile(fileId) {
const file = this.allFiles.find(f => f.id === fileId);
if (file) {
document.getElementById('modalImage').src = file.url;
document.getElementById('mediaModal').style.display = 'flex';
}
}
closeModal() {
document.getElementById('mediaModal').style.display = 'none';
}
async deleteFile(fileId) {
if (!confirm('Удалить этот файл?')) return;
try {
const response = await fetch(`/api/images/delete/${fileId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
this.selectedFiles.delete(fileId);
await this.loadFiles();
this.showSuccess('Файл удален');
} else {
throw new Error(data.message);
}
} catch (error) {
this.showError('Ошибка удаления: ' + error.message);
}
}
async deleteSelectedFiles() {
if (this.selectedFiles.size === 0) return;
if (!confirm(`Удалить ${this.selectedFiles.size} файл(ов)?`)) return;
try {
const promises = Array.from(this.selectedFiles).map(fileId =>
fetch(`/api/images/delete/${fileId}`, { method: 'DELETE' })
);
await Promise.all(promises);
this.selectedFiles.clear();
await this.loadFiles();
this.showSuccess('Файлы удалены');
} catch (error) {
this.showError('Ошибка удаления: ' + error.message);
}
}
async handleFileUpload(files) {
const formData = new FormData();
Array.from(files).forEach(file => {
formData.append('images', file);
});
// Добавляем текущую папку
formData.append('folder', this.currentFolder === 'all' ? 'general' : this.currentFolder);
try {
this.showLoading(true, 'Загрузка файлов...');
const response = await fetch('/api/images/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
await this.loadFiles();
this.showSuccess(`Загружено ${files.length} файл(ов)`);
} else {
throw new Error(data.message);
}
} catch (error) {
this.showError('Ошибка загрузки: ' + error.message);
} finally {
this.showLoading(false);
document.getElementById('fileInput').value = '';
}
}
useSelectedFiles() {
const selectedData = Array.from(this.selectedFiles).map(fileId => {
const file = this.allFiles.find(f => f.id === fileId);
return {
id: file.id,
name: file.name,
url: file.url,
path: file.path
};
});
// Отправляем сообщение родительскому окну (если в iframe)
if (window.parent !== window) {
window.parent.postMessage({
type: 'media-manager-selection',
files: selectedData
}, '*');
}
// Или вызываем callback если определен
if (window.mediaManagerCallback) {
window.mediaManagerCallback(selectedData);
}
}
updateStatus() {
const total = this.filteredFiles.length;
const totalSize = this.filteredFiles.reduce((sum, file) => sum + (file.size || 0), 0);
document.getElementById('statusInfo').textContent =
`Папка: ${this.getFolderName(this.currentFolder)}`;
document.getElementById('totalFiles').textContent = `${total} файл${total !== 1 ? (total > 4 ? 'ов' : 'а') : ''}`;
document.getElementById('totalSize').textContent = this.formatFileSize(totalSize);
}
getFolderName(folder) {
const names = {
'all': 'Все файлы',
'routes': 'Маршруты',
'guides': 'Гиды',
'articles': 'Статьи',
'general': 'Общие'
};
return names[folder] || folder;
}
formatFileSize(bytes) {
if (!bytes) return '0 B';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
showLoading(show, text = 'Загрузка...') {
const loading = document.getElementById('loadingIndicator');
if (show) {
loading.style.display = 'block';
loading.querySelector('div:last-child').textContent = text;
} else {
loading.style.display = 'none';
}
}
showError(message) {
alert('Ошибка: ' + message);
}
showSuccess(message) {
// Можно заменить на toast уведомление
console.log('Успех:', message);
}
}
// Инициализация
const mediaManager = new MediaManager();
// Глобальные функции для обратной совместимости
window.selectFile = (id) => mediaManager.selectFile(id);
window.previewFile = (id) => mediaManager.previewFile(id);
window.deleteFile = (id) => mediaManager.deleteFile(id);
window.closeModal = () => mediaManager.closeModal();
</script>
</body>
</html>