@@ -1221,6 +1232,85 @@ document.addEventListener('DOMContentLoaded', function() {
photoInput.dispatchEvent(new Event('change'));
};
+ // ========== ЗАГРУЗКА ФОТО ПО URL ==========
+ const photoUrlInput = document.getElementById('photoUrlInput');
+ const addPhotoByUrlBtn = document.getElementById('addPhotoByUrlBtn');
+
+ async function addPhotoFromUrl(imageUrl) {
+ // Валидация URL
+ try {
+ new URL(imageUrl);
+ } catch {
+ alert('Пожалуйста, введите корректный URL (начинающийся с https://)');
+ return;
+ }
+
+ if (!imageUrl.match(/\.(jpg|jpeg|png|gif|webp|heic|heif|svg)$/i) && !imageUrl.includes('/')) {
+ alert('URL должен указывать на изображение (jpg, png, webp, etc.)');
+ return;
+ }
+
+ try {
+ // Показываем индикатор загрузки
+ addPhotoByUrlBtn.disabled = true;
+ addPhotoByUrlBtn.innerHTML = '
Загрузка...';
+
+ // Скачиваем изображение
+ const response = await fetch(imageUrl, { mode: 'cors' });
+ if (!response.ok) throw new Error('Не удалось скачать изображение');
+
+ const blob = await response.blob();
+
+ // Проверяем, что это изображение
+ if (!blob.type.startsWith('image/')) {
+ throw new Error('Файл не является изображением');
+ }
+
+ // Создаем File объект
+ const filename = imageUrl.split('/').pop().split('?')[0] || 'image.jpg';
+ const file = new File([blob], filename, { type: blob.type });
+
+ // Добавляем в selectedFiles и обновляем input
+ selectedFiles.push(file);
+ const dataTransfer = new DataTransfer();
+ selectedFiles.forEach(f => dataTransfer.items.add(f));
+ photoInput.files = dataTransfer.files;
+
+ // Триггерим событие change для обновления превью
+ photoInput.dispatchEvent(new Event('change'));
+
+ // Очищаем поле ввода
+ photoUrlInput.value = '';
+
+ } catch (error) {
+ console.error('Error loading image from URL:', error);
+ alert('Ошибка загрузки изображения: ' + error.message + '\n\nУбедитесь, что сервер изображений поддерживает CORS.');
+ } finally {
+ addPhotoByUrlBtn.disabled = false;
+ addPhotoByUrlBtn.innerHTML = '
Загрузить';
+ }
+ }
+
+ if (addPhotoByUrlBtn) {
+ addPhotoByUrlBtn.addEventListener('click', () => {
+ const url = photoUrlInput.value.trim();
+ if (url) {
+ addPhotoFromUrl(url);
+ }
+ });
+
+ // Загрузка по Enter
+ photoUrlInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ const url = photoUrlInput.value.trim();
+ if (url) {
+ addPhotoFromUrl(url);
+ }
+ }
+ });
+ }
+
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
const kitForm = document.querySelector('form[method="post"]');
if (kitForm) {
diff --git a/myproject/products/templates/products/productkit_edit.html b/myproject/products/templates/products/productkit_edit.html
index b69a4b6..ca7a439 100644
--- a/myproject/products/templates/products/productkit_edit.html
+++ b/myproject/products/templates/products/productkit_edit.html
@@ -66,11 +66,266 @@
-
Фотографии
-
-
-
+
Фотографии комплекта
+
+
+
+
+
+ {% if object and productkit_photos %}
+
+
+
+
+ Текущие фотографии
+ {{ photos_count }}
+
+
+
+
+
+ {% for photo in productkit_photos %}
+
+
+
+
+
+
+ {% if photo.order == 0 %}
+
+
+ Главное
+
+
+ {% endif %}
+
+
+
+

+
+
+
+
+ {% if photo.order != 0 %}
+
+ Главное
+
+ {% endif %}
+
+
+
+
+
+
+ Удалить
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if object %}
+ Выберите фото для добавления к комплекту (можно выбрать несколько, до 5 штук всего)
+ {% else %}
+ Выберите фото для комплекта (можно выбрать несколько, до 5 штук)
+ {% endif %}
+
+
@@ -364,6 +619,30 @@
object-fit: cover;
}
+ /* Hover эффект для карточек фото */
+ .hover-lift:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+ }
+
+ .photo-card .card {
+ border: 1px solid #e0e0e0;
+ }
+
+ .photo-card:hover .card {
+ border-color: #667eea;
+ }
+
+ /* Спиннер для загрузки */
+ .spin {
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+
/* Alert компактный */
.alert-sm {
padding: 0.5rem 0.75rem;
@@ -1234,6 +1513,85 @@
photoInput.dispatchEvent(new Event('change'));
};
+ // ========== ЗАГРУЗКА ФОТО ПО URL ==========
+ const photoUrlInput = document.getElementById('photoUrlInput');
+ const addPhotoByUrlBtn = document.getElementById('addPhotoByUrlBtn');
+
+ async function addPhotoFromUrl(imageUrl) {
+ // Валидация URL
+ try {
+ new URL(imageUrl);
+ } catch {
+ alert('Пожалуйста, введите корректный URL (начинающийся с https://)');
+ return;
+ }
+
+ if (!imageUrl.match(/\.(jpg|jpeg|png|gif|webp|heic|heif|svg)$/i) && !imageUrl.includes('/')) {
+ alert('URL должен указывать на изображение (jpg, png, webp, etc.)');
+ return;
+ }
+
+ try {
+ // Показываем индикатор загрузки
+ addPhotoByUrlBtn.disabled = true;
+ addPhotoByUrlBtn.innerHTML = '
Загрузка...';
+
+ // Скачиваем изображение
+ const response = await fetch(imageUrl, { mode: 'cors' });
+ if (!response.ok) throw new Error('Не удалось скачать изображение');
+
+ const blob = await response.blob();
+
+ // Проверяем, что это изображение
+ if (!blob.type.startsWith('image/')) {
+ throw new Error('Файл не является изображением');
+ }
+
+ // Создаем File объект
+ const filename = imageUrl.split('/').pop().split('?')[0] || 'image.jpg';
+ const file = new File([blob], filename, { type: blob.type });
+
+ // Добавляем в selectedFiles и обновляем input
+ selectedFiles.push(file);
+ const dataTransfer = new DataTransfer();
+ selectedFiles.forEach(f => dataTransfer.items.add(f));
+ photoInput.files = dataTransfer.files;
+
+ // Триггерим событие change для обновления превью
+ photoInput.dispatchEvent(new Event('change'));
+
+ // Очищаем поле ввода
+ photoUrlInput.value = '';
+
+ } catch (error) {
+ console.error('Error loading image from URL:', error);
+ alert('Ошибка загрузки изображения: ' + error.message + '\n\nУбедитесь, что сервер изображений поддерживает CORS.');
+ } finally {
+ addPhotoByUrlBtn.disabled = false;
+ addPhotoByUrlBtn.innerHTML = '
Загрузить';
+ }
+ }
+
+ if (addPhotoByUrlBtn) {
+ addPhotoByUrlBtn.addEventListener('click', () => {
+ const url = photoUrlInput.value.trim();
+ if (url) {
+ addPhotoFromUrl(url);
+ }
+ });
+
+ // Загрузка по Enter
+ photoUrlInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ const url = photoUrlInput.value.trim();
+ if (url) {
+ addPhotoFromUrl(url);
+ }
+ }
+ });
+ }
+
// ========== ЗАГРУЗКА СОХРАНЁННЫХ ЗНАЧЕНИЙ КОРРЕКТИРОВКИ ==========
setTimeout(async () => {
const currentAdjustmentType = adjustmentTypeInput.value;
diff --git a/myproject/products/urls.py b/myproject/products/urls.py
index 102c572..82f5022 100644
--- a/myproject/products/urls.py
+++ b/myproject/products/urls.py
@@ -42,6 +42,7 @@ urlpatterns = [
path('kit/photo/
/set-main/', views.productkit_photo_set_main, name='productkit-photo-set-main'),
path('kit/photo//move-up/', views.productkit_photo_move_up, name='productkit-photo-move-up'),
path('kit/photo//move-down/', views.productkit_photo_move_down, name='productkit-photo-move-down'),
+ path('kit/photos/delete-bulk/', views.productkit_photos_delete_bulk, name='productkit-photos-delete-bulk'),
# API endpoints
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
diff --git a/myproject/products/views/__init__.py b/myproject/products/views/__init__.py
index c4fff94..b372e4c 100644
--- a/myproject/products/views/__init__.py
+++ b/myproject/products/views/__init__.py
@@ -21,6 +21,7 @@ from .photo_management import (
productkit_photo_set_main,
productkit_photo_move_up,
productkit_photo_move_down,
+ productkit_photos_delete_bulk,
)
# Управление фотографиями (Category)
@@ -149,6 +150,7 @@ __all__ = [
'productkit_photo_set_main',
'productkit_photo_move_up',
'productkit_photo_move_down',
+ 'productkit_photos_delete_bulk',
# Управление фотографиями Category
'category_photo_delete',
diff --git a/myproject/products/views/photo_management.py b/myproject/products/views/photo_management.py
index ccc4f97..03d028e 100644
--- a/myproject/products/views/photo_management.py
+++ b/myproject/products/views/photo_management.py
@@ -380,3 +380,67 @@ def product_photos_delete_bulk(request):
'success': False,
'error': f'Ошибка сервера: {str(e)}'
}, status=500)
+
+
+@require_http_methods(["POST"])
+@login_required
+def productkit_photos_delete_bulk(request):
+ """
+ AJAX endpoint для массового удаления фотографий комплекта.
+
+ Ожидает JSON: {photo_ids: [1, 2, 3]}
+ Возвращает JSON: {success: true, deleted: 3} или {success: false, error: "..."}
+ """
+ # Проверка прав доступа
+ if not request.user.has_perm('products.change_productkit'):
+ return JsonResponse({
+ 'success': False,
+ 'error': 'У вас нет прав для удаления фотографий'
+ }, status=403)
+
+ try:
+ # Получаем список photo_ids из JSON тела запроса
+ data = json.loads(request.body)
+ photo_ids = data.get('photo_ids', [])
+
+ if not photo_ids or not isinstance(photo_ids, list):
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Неверный формат: требуется список photo_ids'
+ }, status=400)
+
+ # Удаляем фотографии
+ deleted_count = 0
+ for photo_id in photo_ids:
+ try:
+ photo = ProductKitPhoto.objects.get(pk=photo_id)
+ photo.delete() # Это вызовет ImageProcessor.delete_all_versions()
+ deleted_count += 1
+ except ProductKitPhoto.DoesNotExist:
+ # Если фото не найдена, просто пропускаем
+ continue
+ except Exception as e:
+ # Логируем ошибку но продолжаем удаление остальных
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.error(f"Error deleting kit photo {photo_id}: {str(e)}", exc_info=True)
+ continue
+
+ return JsonResponse({
+ 'success': True,
+ 'deleted': deleted_count
+ })
+
+ except json.JSONDecodeError:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Неверный JSON формат'
+ }, status=400)
+ except Exception as e:
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.error(f"Bulk kit photo deletion error: {str(e)}", exc_info=True)
+ return JsonResponse({
+ 'success': False,
+ 'error': f'Ошибка сервера: {str(e)}'
+ }, status=500)