feat(products): добавить загрузку изображений по URL для комплектов
Добавлена возможность загружать фотографии комплекта по прямой ссылке на формах создания и редактирования. JavaScript скачивает изображение и добавляет его как файл в форму для отправки на сервер. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,17 @@
|
|||||||
<div class="card border-0 shadow-sm mb-3">
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<h6 class="mb-2 text-muted"><i class="bi bi-images me-1"></i>Фотографии</h6>
|
<h6 class="mb-2 text-muted"><i class="bi bi-images me-1"></i>Фотографии</h6>
|
||||||
|
|
||||||
|
<!-- Загрузка по URL -->
|
||||||
|
<div class="input-group input-group-sm mb-2">
|
||||||
|
<input type="url" id="photoUrlInput" class="form-control"
|
||||||
|
placeholder="Вставьте ссылку на изображение (https://...)">
|
||||||
|
<button type="button" id="addPhotoByUrlBtn" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-download"></i> Загрузить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Загрузка с устройства -->
|
||||||
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm" id="id_photos">
|
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm" id="id_photos">
|
||||||
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
||||||
<div id="photoPreview" class="row g-1"></div>
|
<div id="photoPreview" class="row g-1"></div>
|
||||||
@@ -1221,6 +1232,85 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
photoInput.dispatchEvent(new Event('change'));
|
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 = '<span class="spinner-border spinner-border-sm"></span> Загрузка...';
|
||||||
|
|
||||||
|
// Скачиваем изображение
|
||||||
|
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 = '<i class="bi bi-download"></i> Загрузить';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]');
|
const kitForm = document.querySelector('form[method="post"]');
|
||||||
if (kitForm) {
|
if (kitForm) {
|
||||||
|
|||||||
@@ -66,15 +66,270 @@
|
|||||||
<!-- ФОТОГРАФИИ -->
|
<!-- ФОТОГРАФИИ -->
|
||||||
<div class="card border-0 shadow-sm mb-3">
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<h6 class="mb-2 text-muted"><i class="bi bi-images me-1"></i>Фотографии</h6>
|
<h6 class="mb-3 text-muted"><i class="bi bi-images me-1"></i>Фотографии комплекта</h6>
|
||||||
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm"
|
|
||||||
id="id_photos">
|
<!-- Контейнер для сообщений об операциях с фото -->
|
||||||
|
<div id="photos-messages-container"></div>
|
||||||
|
|
||||||
|
<!-- Существующие фотографии (только при редактировании) -->
|
||||||
|
{% if object and productkit_photos %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0 text-secondary small">
|
||||||
|
<i class="bi bi-collection"></i> Текущие фотографии
|
||||||
|
<span class="badge bg-primary rounded-pill" id="photos-count">{{ photos_count }}</span>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="delete-selected-btn" class="btn btn-danger btn-sm shadow-sm" style="display: none;">
|
||||||
|
<i class="bi bi-trash"></i> Удалить (<span id="selected-count">0</span>)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 mb-2" id="photos-grid">
|
||||||
|
{% for photo in productkit_photos %}
|
||||||
|
<div class="col-lg-3 col-md-4 col-sm-6 photo-card" data-photo-id="{{ photo.pk }}">
|
||||||
|
<div class="card h-100 border-0 shadow-sm hover-lift" style="transition: all 0.3s ease;">
|
||||||
|
<!-- Чекбокс для выбора -->
|
||||||
|
<div class="position-absolute" style="top: 6px; left: 6px; z-index: 10;">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input photo-checkbox shadow-sm"
|
||||||
|
data-photo-id="{{ photo.pk }}"
|
||||||
|
id="photo-check-{{ photo.pk }}"
|
||||||
|
style="width: 20px; height: 20px; cursor: pointer;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Бейдж главного фото -->
|
||||||
|
{% if photo.order == 0 %}
|
||||||
|
<div class="position-absolute" style="top: 6px; right: 6px; z-index: 10;">
|
||||||
|
<span class="badge bg-success shadow-sm" style="font-size: 0.7rem;">
|
||||||
|
<i class="bi bi-star-fill"></i> Главное
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Кликабельное фото -->
|
||||||
|
<div class="ratio ratio-1x1 bg-light rounded-top overflow-hidden"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#photoModal{{ photo.pk }}"
|
||||||
|
style="cursor: pointer;">
|
||||||
|
<img src="{{ photo.get_thumbnail_url }}"
|
||||||
|
alt="Фото комплекта"
|
||||||
|
class="object-fit-contain p-1"
|
||||||
|
style="transition: transform 0.3s ease;"
|
||||||
|
onmouseover="this.style.transform='scale(1.05)'"
|
||||||
|
onmouseout="this.style.transform='scale(1)'">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-2 bg-white">
|
||||||
|
<!-- Кнопка "Сделать главным" -->
|
||||||
|
{% if photo.order != 0 %}
|
||||||
|
<a href="{% url 'products:productkit-photo-set-main' photo.pk %}"
|
||||||
|
class="btn btn-outline-warning btn-sm w-100 mb-1"
|
||||||
|
style="font-size: 0.75rem; padding: 0.25rem 0.5rem;"
|
||||||
|
title="Сделать главным">
|
||||||
|
<i class="bi bi-star"></i> Главное
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Кнопки перемещения -->
|
||||||
|
<div class="btn-group w-100 mb-1" role="group">
|
||||||
|
<a href="{% url 'products:productkit-photo-move-up' photo.pk %}"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
style="font-size: 0.75rem; padding: 0.25rem 0.5rem;"
|
||||||
|
title="Переместить вверх">
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'products:productkit-photo-move-down' photo.pk %}"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
style="font-size: 0.75rem; padding: 0.25rem 0.5rem;"
|
||||||
|
title="Переместить вниз">
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка удаления -->
|
||||||
|
<a href="{% url 'products:productkit-photo-delete' photo.pk %}"
|
||||||
|
class="btn btn-outline-danger btn-sm w-100"
|
||||||
|
style="font-size: 0.75rem; padding: 0.25rem 0.5rem;"
|
||||||
|
onclick="return confirm('Удалить это фото?');">
|
||||||
|
<i class="bi bi-trash"></i> Удалить
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer bg-light text-center py-1">
|
||||||
|
<small class="text-muted" style="font-size: 0.7rem;">
|
||||||
|
<i class="bi bi-hash"></i> Позиция: {{ photo.order|add:1 }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно для просмотра фото -->
|
||||||
|
<div class="modal fade" id="photoModal{{ photo.pk }}" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||||
|
<div class="modal-content border-0 shadow">
|
||||||
|
<div class="modal-header bg-dark text-white">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-image"></i> Фото комплекта
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center bg-dark">
|
||||||
|
<img src="{{ photo.get_large_url }}" class="img-fluid rounded" alt="Фото комплекта" style="max-height: 75vh;">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="bi bi-x-circle"></i> Закрыть
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'products:productkit-photo-delete' photo.pk %}"
|
||||||
|
class="btn btn-danger"
|
||||||
|
onclick="return confirm('Удалить это фото?');">
|
||||||
|
<i class="bi bi-trash"></i> Удалить фото
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript для массового удаления фотографий -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const deleteBtn = document.getElementById('delete-selected-btn');
|
||||||
|
const selectedCount = document.getElementById('selected-count');
|
||||||
|
const photosCount = document.getElementById('photos-count');
|
||||||
|
const checkboxes = document.querySelectorAll('.photo-checkbox');
|
||||||
|
|
||||||
|
// Обновляем счётчик выбранных и видимость кнопки
|
||||||
|
function updateUI() {
|
||||||
|
const checked = document.querySelectorAll('.photo-checkbox:checked').length;
|
||||||
|
selectedCount.textContent = checked;
|
||||||
|
deleteBtn.style.display = checked > 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик для каждого чекбокса
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', updateUI);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик кнопки удаления
|
||||||
|
deleteBtn.addEventListener('click', function() {
|
||||||
|
const checked = document.querySelectorAll('.photo-checkbox:checked');
|
||||||
|
if (checked.length === 0) return;
|
||||||
|
|
||||||
|
const photoIds = Array.from(checked).map(cb => cb.dataset.photoId);
|
||||||
|
const count = photoIds.length;
|
||||||
|
|
||||||
|
if (!confirm(`Вы уверены, что хотите удалить ${count} фото?`)) return;
|
||||||
|
|
||||||
|
// Отключаем кнопку на время операции
|
||||||
|
deleteBtn.disabled = true;
|
||||||
|
deleteBtn.innerHTML = '<i class="bi bi-spinner spin"></i> Удаление...';
|
||||||
|
|
||||||
|
// Отправляем запрос на сервер
|
||||||
|
fetch('{% url "products:productkit-photos-delete-bulk" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ photo_ids: photoIds })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Удаляем карточки фотографий из DOM
|
||||||
|
photoIds.forEach(photoId => {
|
||||||
|
const card = document.querySelector(`[data-photo-id="${photoId}"]`);
|
||||||
|
if (card) card.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем счётчик фотографий
|
||||||
|
const newCount = parseInt(photosCount.textContent) - count;
|
||||||
|
photosCount.textContent = newCount;
|
||||||
|
|
||||||
|
// Скрываем блок если фотографий больше нет
|
||||||
|
if (newCount === 0) {
|
||||||
|
const photosSection = document.querySelector('#photos-grid')?.closest('.mb-3');
|
||||||
|
if (photosSection) {
|
||||||
|
photosSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем сообщение об успехе
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = 'alert alert-success alert-dismissible fade show';
|
||||||
|
alert.innerHTML = `✓ ${data.deleted} фото успешно удалено!
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
|
||||||
|
|
||||||
|
const messagesContainer = document.getElementById('photos-messages-container');
|
||||||
|
if (messagesContainer) {
|
||||||
|
messagesContainer.innerHTML = '';
|
||||||
|
messagesContainer.appendChild(alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скрываем кнопку
|
||||||
|
deleteBtn.style.display = 'none';
|
||||||
|
selectedCount.textContent = '0';
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'Неизвестная ошибка');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Ошибка при удалении: ' + error.message);
|
||||||
|
console.error(error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
deleteBtn.disabled = false;
|
||||||
|
deleteBtn.innerHTML = '<i class="bi bi-trash"></i> Удалить (<span id="selected-count">0</span>)';
|
||||||
|
document.getElementById('selected-count').textContent = '0';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Поле для загрузки новых фотографий -->
|
||||||
|
<div class="alert alert-info border-0 shadow-sm mb-0 p-2">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bi bi-cloud-upload me-2"></i>
|
||||||
|
<label for="id_photos" class="form-label fw-bold mb-0 small">
|
||||||
|
{% if object %}
|
||||||
|
<i class="bi bi-plus-circle"></i> Добавить новые фото
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-upload"></i> Загрузить фото
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Загрузка по URL -->
|
||||||
|
<div class="input-group input-group-sm mb-2">
|
||||||
|
<input type="url" id="photoUrlInput" class="form-control"
|
||||||
|
placeholder="Вставьте ссылку на изображение (https://...)">
|
||||||
|
<button type="button" id="addPhotoByUrlBtn" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-download"></i> Загрузить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm" id="id_photos">
|
||||||
|
<small class="form-text text-muted d-block mt-1">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
{% if object %}
|
||||||
|
Выберите фото для добавления к комплекту (можно выбрать несколько, до 5 штук всего)
|
||||||
|
{% else %}
|
||||||
|
Выберите фото для комплекта (можно выбрать несколько, до 5 штук)
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
||||||
<div id="photoPreview" class="row g-1"></div>
|
<div id="photoPreview" class="row g-1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ПРАВАЯ КОЛОНКА: Настройки -->
|
<!-- ПРАВАЯ КОЛОНКА: Настройки -->
|
||||||
<div class="col-lg-3">
|
<div class="col-lg-3">
|
||||||
@@ -364,6 +619,30 @@
|
|||||||
object-fit: cover;
|
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 компактный */
|
||||||
.alert-sm {
|
.alert-sm {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
@@ -1234,6 +1513,85 @@
|
|||||||
photoInput.dispatchEvent(new Event('change'));
|
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 = '<span class="spinner-border spinner-border-sm"></span> Загрузка...';
|
||||||
|
|
||||||
|
// Скачиваем изображение
|
||||||
|
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 = '<i class="bi bi-download"></i> Загрузить';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
setTimeout(async () => {
|
||||||
const currentAdjustmentType = adjustmentTypeInput.value;
|
const currentAdjustmentType = adjustmentTypeInput.value;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ urlpatterns = [
|
|||||||
path('kit/photo/<int:pk>/set-main/', views.productkit_photo_set_main, name='productkit-photo-set-main'),
|
path('kit/photo/<int:pk>/set-main/', views.productkit_photo_set_main, name='productkit-photo-set-main'),
|
||||||
path('kit/photo/<int:pk>/move-up/', views.productkit_photo_move_up, name='productkit-photo-move-up'),
|
path('kit/photo/<int:pk>/move-up/', views.productkit_photo_move_up, name='productkit-photo-move-up'),
|
||||||
path('kit/photo/<int:pk>/move-down/', views.productkit_photo_move_down, name='productkit-photo-move-down'),
|
path('kit/photo/<int:pk>/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
|
# API endpoints
|
||||||
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
|
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from .photo_management import (
|
|||||||
productkit_photo_set_main,
|
productkit_photo_set_main,
|
||||||
productkit_photo_move_up,
|
productkit_photo_move_up,
|
||||||
productkit_photo_move_down,
|
productkit_photo_move_down,
|
||||||
|
productkit_photos_delete_bulk,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Управление фотографиями (Category)
|
# Управление фотографиями (Category)
|
||||||
@@ -149,6 +150,7 @@ __all__ = [
|
|||||||
'productkit_photo_set_main',
|
'productkit_photo_set_main',
|
||||||
'productkit_photo_move_up',
|
'productkit_photo_move_up',
|
||||||
'productkit_photo_move_down',
|
'productkit_photo_move_down',
|
||||||
|
'productkit_photos_delete_bulk',
|
||||||
|
|
||||||
# Управление фотографиями Category
|
# Управление фотографиями Category
|
||||||
'category_photo_delete',
|
'category_photo_delete',
|
||||||
|
|||||||
@@ -380,3 +380,67 @@ def product_photos_delete_bulk(request):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Ошибка сервера: {str(e)}'
|
'error': f'Ошибка сервера: {str(e)}'
|
||||||
}, status=500)
|
}, 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user