some fixes

This commit is contained in:
2025-10-22 21:22:44 +09:00
parent 46fad7ecc2
commit 6ff35e26f4
514 changed files with 156165 additions and 0 deletions

View File

@@ -0,0 +1,601 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Редактор Баннеров - SmartSolTech Admin</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/fixes.css">
<style>
.upload-zone {
border: 2px dashed #d1d5db;
transition: all 0.3s ease;
}
.upload-zone.dragover {
border-color: #3b82f6;
background-color: #eff6ff;
}
.image-preview {
position: relative;
overflow: hidden;
border-radius: 8px;
}
.image-preview:hover .overlay {
opacity: 1;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900">
<div class="flex">
<!-- Sidebar -->
<div class="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen fixed">
<div class="p-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6">Админ Панель</h2>
<nav class="space-y-2">
<a href="/admin" class="flex items-center px-4 py-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<i class="fas fa-tachometer-alt mr-3"></i>
Дашборд
</a>
<a href="/admin/banner-editor" class="flex items-center px-4 py-2 bg-blue-500 text-white rounded">
<i class="fas fa-image mr-3"></i>
Редактор Баннеров
</a>
<a href="/admin/portfolio" class="flex items-center px-4 py-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<i class="fas fa-briefcase mr-3"></i>
Портфолио
</a>
</nav>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 ml-64 p-8">
<div class="max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
<i class="fas fa-image text-blue-500 mr-3"></i>
Редактор Баннеров
</h1>
<div class="flex space-x-4">
<button id="refresh-images" class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors">
<i class="fas fa-sync-alt mr-2"></i>
Обновить
</button>
<button id="upload-modal-btn" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
<i class="fas fa-plus mr-2"></i>
Загрузить Изображения
</button>
</div>
</div>
<!-- Banner Types Tabs -->
<div class="mb-8">
<div class="border-b border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex space-x-8">
<button class="banner-tab active py-2 px-1 border-b-2 border-blue-500 font-medium text-sm text-blue-600" data-page="home">
Главная страница
</button>
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="about">
О нас
</button>
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="services">
Услуги
</button>
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="portfolio">
Портфолио
</button>
</nav>
</div>
</div>
<!-- Current Banner Display -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Текущий баннер: <span id="current-page">Главная страница</span>
</h2>
<div id="current-banner" class="relative">
<div class="w-full h-64 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 rounded-lg flex items-center justify-center">
<div class="text-center text-white">
<h3 class="text-4xl font-bold mb-2">Текущий Баннер</h3>
<p class="text-xl opacity-90">Нажмите на изображение ниже, чтобы заменить</p>
</div>
</div>
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span id="banner-info">Используется CSS градиент</span>
</div>
<button id="remove-banner" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm" style="display: none;">
<i class="fas fa-trash mr-1"></i>
Удалить
</button>
</div>
</div>
</div>
<!-- Image Gallery -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Галерея изображений
</h2>
<div id="loading" class="text-center py-8" style="display: none;">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<p class="mt-2 text-gray-600 dark:text-gray-400">Загрузка...</p>
</div>
<div id="images-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- Изображения будут загружены динамически -->
</div>
<div id="no-images" class="text-center py-8" style="display: none;">
<i class="fas fa-images text-4xl text-gray-400 mb-4"></i>
<p class="text-gray-600 dark:text-gray-400">Нет загруженных изображений</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors" onclick="document.getElementById('upload-modal-btn').click()">
Загрузить первое изображение
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Modal -->
<div id="upload-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Загрузить изображения</h3>
<button id="close-modal" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Upload Zone -->
<div id="upload-zone" class="upload-zone rounded-lg p-8 text-center mb-6">
<div class="mb-4">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
<p class="text-lg text-gray-600 dark:text-gray-400 mb-2">
Перетащите изображения сюда или нажмите для выбора
</p>
<p class="text-sm text-gray-500">
Поддерживаются: JPG, PNG, GIF, WebP (максимум 10MB каждое)
</p>
</div>
<input type="file" id="file-input" multiple accept="image/*" class="hidden">
<button type="button" onclick="document.getElementById('file-input').click()" class="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
Выбрать файлы
</button>
</div>
<!-- Upload Progress -->
<div id="upload-progress" class="hidden mb-6">
<div class="bg-gray-200 rounded-full h-2 mb-2">
<div id="progress-bar" class="bg-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<p id="progress-text" class="text-sm text-gray-600 dark:text-gray-400 text-center">Загрузка...</p>
</div>
<!-- Preview Area -->
<div id="preview-area" class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6" style="display: none;">
<!-- Previews will be added here -->
</div>
<!-- Upload Button -->
<div class="flex justify-end">
<button id="upload-btn" class="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-upload mr-2"></i>
Загрузить
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script>
class BannerEditor {
constructor() {
this.currentPage = 'home';
this.selectedFiles = [];
this.bannerSettings = {
home: { type: 'gradient', image: null },
about: { type: 'gradient', image: null },
services: { type: 'gradient', image: null },
portfolio: { type: 'gradient', image: null }
};
this.init();
}
init() {
this.loadImages();
this.setupEventListeners();
this.loadBannerSettings();
}
setupEventListeners() {
// Tab switching
document.querySelectorAll('.banner-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const page = e.target.dataset.page;
this.switchPage(page);
});
});
// Upload modal
document.getElementById('upload-modal-btn').addEventListener('click', () => {
document.getElementById('upload-modal').classList.remove('hidden');
});
document.getElementById('close-modal').addEventListener('click', () => {
this.closeModal();
});
// File upload
const fileInput = document.getElementById('file-input');
const uploadZone = document.getElementById('upload-zone');
fileInput.addEventListener('change', (e) => {
this.handleFiles(e.target.files);
});
// Drag and drop
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
this.handleFiles(e.dataTransfer.files);
});
// Upload button
document.getElementById('upload-btn').addEventListener('click', () => {
this.uploadFiles();
});
// Refresh images
document.getElementById('refresh-images').addEventListener('click', () => {
this.loadImages();
});
}
switchPage(page) {
this.currentPage = page;
// Update active tab
document.querySelectorAll('.banner-tab').forEach(tab => {
tab.classList.remove('active', 'border-blue-500', 'text-blue-600');
tab.classList.add('border-transparent', 'text-gray-500');
});
document.querySelector(`[data-page="${page}"]`).classList.add('active', 'border-blue-500', 'text-blue-600');
document.querySelector(`[data-page="${page}"]`).classList.remove('border-transparent', 'text-gray-500');
// Update current page display
const pageNames = {
home: 'Главная страница',
about: 'О нас',
services: 'Услуги',
portfolio: 'Портфолио'
};
document.getElementById('current-page').textContent = pageNames[page];
this.updateCurrentBanner();
}
updateCurrentBanner() {
const banner = this.bannerSettings[this.currentPage];
const bannerElement = document.getElementById('current-banner').querySelector('div');
const infoElement = document.getElementById('banner-info');
const removeBtn = document.getElementById('remove-banner');
if (banner.image) {
bannerElement.style.backgroundImage = `url(${banner.image})`;
bannerElement.style.backgroundSize = 'cover';
bannerElement.style.backgroundPosition = 'center';
bannerElement.innerHTML = '';
infoElement.textContent = `Изображение: ${banner.image.split('/').pop()}`;
removeBtn.style.display = 'block';
} else {
bannerElement.style.backgroundImage = '';
bannerElement.className = 'w-full h-64 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 rounded-lg flex items-center justify-center';
bannerElement.innerHTML = `
<div class="text-center text-white">
<h3 class="text-4xl font-bold mb-2">Текущий Баннер</h3>
<p class="text-xl opacity-90">Нажмите на изображение ниже, чтобы заменить</p>
</div>
`;
infoElement.textContent = 'Используется CSS градиент';
removeBtn.style.display = 'none';
}
}
async loadImages() {
const loading = document.getElementById('loading');
const grid = document.getElementById('images-grid');
const noImages = document.getElementById('no-images');
loading.style.display = 'block';
grid.innerHTML = '';
noImages.style.display = 'none';
try {
const response = await fetch('/media/list');
const data = await response.json();
if (data.success && data.images.length > 0) {
grid.innerHTML = '';
data.images.forEach(image => {
this.addImageToGrid(image);
});
} else {
noImages.style.display = 'block';
}
} catch (error) {
console.error('Error loading images:', error);
this.showNotification('Ошибка загрузки изображений', 'error');
} finally {
loading.style.display = 'none';
}
}
addImageToGrid(image) {
const grid = document.getElementById('images-grid');
const imageElement = document.createElement('div');
imageElement.className = 'image-preview bg-gray-200 rounded-lg cursor-pointer';
imageElement.innerHTML = `
<img src="${image.url}" alt="${image.filename}" class="w-full h-32 object-cover rounded-lg">
<div class="overlay">
<div class="flex space-x-2">
<button class="use-image px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm" data-url="${image.url}">
<i class="fas fa-check mr-1"></i>
Использовать
</button>
<button class="delete-image px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm" data-filename="${image.filename}">
<i class="fas fa-trash mr-1"></i>
Удалить
</button>
</div>
</div>
<div class="p-2">
<p class="text-xs text-gray-600 truncate">${image.filename}</p>
<p class="text-xs text-gray-500">${this.formatFileSize(image.size)}</p>
</div>
`;
// Add event listeners
imageElement.querySelector('.use-image').addEventListener('click', (e) => {
e.stopPropagation();
this.setImageAsBanner(image.url);
});
imageElement.querySelector('.delete-image').addEventListener('click', (e) => {
e.stopPropagation();
this.deleteImage(image.filename);
});
grid.appendChild(imageElement);
}
setImageAsBanner(imageUrl) {
this.bannerSettings[this.currentPage] = {
type: 'image',
image: imageUrl
};
this.updateCurrentBanner();
this.saveBannerSettings();
this.showNotification('Баннер обновлен!', 'success');
}
async deleteImage(filename) {
if (!confirm('Вы уверены, что хотите удалить это изображение?')) {
return;
}
try {
const response = await fetch(`/media/${filename}`, {
method: 'DELETE'
});
if (response.ok) {
this.loadImages();
this.showNotification('Изображение удалено', 'success');
} else {
throw new Error('Failed to delete image');
}
} catch (error) {
console.error('Error deleting image:', error);
this.showNotification('Ошибка удаления изображения', 'error');
}
}
handleFiles(files) {
this.selectedFiles = Array.from(files).filter(file => {
if (file.type.startsWith('image/')) {
if (file.size <= 10 * 1024 * 1024) { // 10MB
return true;
} else {
this.showNotification(`Файл ${file.name} слишком большой (максимум 10MB)`, 'error');
}
} else {
this.showNotification(`Файл ${file.name} не является изображением`, 'error');
}
return false;
});
this.showPreviews();
document.getElementById('upload-btn').disabled = this.selectedFiles.length === 0;
}
showPreviews() {
const previewArea = document.getElementById('preview-area');
previewArea.innerHTML = '';
previewArea.style.display = this.selectedFiles.length > 0 ? 'grid' : 'none';
this.selectedFiles.forEach((file, index) => {
const reader = new FileReader();
reader.onload = (e) => {
const preview = document.createElement('div');
preview.className = 'relative';
preview.innerHTML = `
<img src="${e.target.result}" alt="${file.name}" class="w-full h-24 object-cover rounded">
<button class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs hover:bg-red-600" onclick="bannerEditor.removePreview(${index})">
<i class="fas fa-times"></i>
</button>
<p class="text-xs text-gray-600 mt-1 truncate">${file.name}</p>
`;
previewArea.appendChild(preview);
};
reader.readAsDataURL(file);
});
}
removePreview(index) {
this.selectedFiles.splice(index, 1);
this.showPreviews();
document.getElementById('upload-btn').disabled = this.selectedFiles.length === 0;
}
async uploadFiles() {
if (this.selectedFiles.length === 0) return;
const progressContainer = document.getElementById('upload-progress');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const uploadBtn = document.getElementById('upload-btn');
progressContainer.classList.remove('hidden');
uploadBtn.disabled = true;
const formData = new FormData();
this.selectedFiles.forEach(file => {
formData.append('images', file);
});
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
progressText.textContent = `Загрузка... ${Math.round(percentComplete)}%`;
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
this.showNotification('Изображения загружены успешно!', 'success');
this.closeModal();
this.loadImages();
} else {
throw new Error(response.message);
}
} else {
throw new Error('Upload failed');
}
});
xhr.addEventListener('error', () => {
throw new Error('Network error');
});
xhr.open('POST', '/media/upload-multiple');
xhr.send(formData);
} catch (error) {
console.error('Upload error:', error);
this.showNotification('Ошибка загрузки изображений', 'error');
} finally {
progressContainer.classList.add('hidden');
uploadBtn.disabled = false;
progressBar.style.width = '0%';
}
}
closeModal() {
document.getElementById('upload-modal').classList.add('hidden');
this.selectedFiles = [];
document.getElementById('preview-area').style.display = 'none';
document.getElementById('upload-btn').disabled = true;
document.getElementById('upload-progress').classList.add('hidden');
}
loadBannerSettings() {
const saved = localStorage.getItem('bannerSettings');
if (saved) {
this.bannerSettings = JSON.parse(saved);
}
this.updateCurrentBanner();
}
saveBannerSettings() {
localStorage.setItem('bannerSettings', JSON.stringify(this.bannerSettings));
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showNotification(message, type = 'info') {
// Simple notification - you can enhance this with a proper notification system
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
// Initialize when page loads
let bannerEditor;
document.addEventListener('DOMContentLoaded', () => {
bannerEditor = new BannerEditor();
});
</script>
</body>
</html>