Улучшение генератора названий для букетов
- Добавлена функциональность для кнопок 'ВЗЯТЬ' и 'УДАЛИТЬ' - Реализовано получение и удаление названий из базы данных - Исправлена фильтрация названий - Исправлена проблема с обработчиками событий
This commit is contained in:
@@ -100,41 +100,38 @@
|
||||
<br><span class="badge bg-secondary mt-1">В базе: <span id="bouquetNamesCount">{{ bouquet_names_count }}</span> названий</span>
|
||||
</p>
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="populateNamesBtn">
|
||||
<i class="bi bi-magic"></i> Пополнить базу названиями
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="getThreeNamesBtn">
|
||||
<i class="bi bi-refresh"></i> Дать три варианта
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Предложения названий -->
|
||||
<div class="name-suggestions">
|
||||
<!-- Строка 1 -->
|
||||
<div class="d-flex justify-content-between align-items-center py-2 border-bottom">
|
||||
<span class="text-muted small">Романтический букет роз</span>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-success btn-xs">Взять</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-xs">Потом</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs">Убрать</button>
|
||||
<div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row" data-name-id="">
|
||||
<span class="text-muted small name-text">-</span>
|
||||
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
||||
<button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Строка 2 -->
|
||||
<div class="d-flex justify-content-between align-items-center py-2 border-bottom">
|
||||
<span class="text-muted small">Солнечный букет подсолнухов</span>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-success btn-xs">Взять</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-xs">Потом</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs">Убрать</button>
|
||||
<div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row" data-name-id="">
|
||||
<span class="text-muted small name-text">-</span>
|
||||
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
||||
<button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Строка 3 -->
|
||||
<div class="d-flex justify-content-between align-items-center py-2">
|
||||
<span class="text-muted small">Элегантный букет лотосов</span>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-success btn-xs">Взять</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-xs">Потом</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs">Убрать</button>
|
||||
<div class="d-flex justify-content-between align-items-center py-2 name-row" data-name-id="">
|
||||
<span class="text-muted small name-text">-</span>
|
||||
<div class="d-flex gap-1 name-buttons" style="display: none;">
|
||||
<button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1412,57 +1409,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// ========== ГЕНЕРАТОР НАЗВАНИЙ ==========
|
||||
// Обработчик для кнопок "Взять"
|
||||
document.querySelectorAll('.name-suggestions .btn-success').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const suggestionText = this.closest('.d-flex').querySelector('.text-muted').textContent;
|
||||
const nameInput = document.getElementById('id_name');
|
||||
if (nameInput) {
|
||||
nameInput.value = suggestionText;
|
||||
// Улучшаем визуальный эффект
|
||||
nameInput.style.borderColor = '#198754';
|
||||
nameInput.style.boxShadow = '0 0 0 0.25rem rgba(25, 135, 84, 0.15)';
|
||||
setTimeout(() => {
|
||||
nameInput.style.borderColor = '';
|
||||
nameInput.style.boxShadow = '';
|
||||
}, 2000);
|
||||
}
|
||||
// Закрываем collapse
|
||||
const collapse = document.getElementById('nameGeneratorCollapse');
|
||||
const bsCollapse = new bootstrap.Collapse(collapse, { toggle: false });
|
||||
bsCollapse.hide();
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчик для кнопок "Убрать"
|
||||
document.querySelectorAll('.name-suggestions .btn-outline-danger').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
this.closest('.d-flex').remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчик для кнопок "Потом"
|
||||
document.querySelectorAll('.name-suggestions .btn-outline-secondary').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const row = this.closest('.d-flex');
|
||||
row.style.opacity = '0.5';
|
||||
row.style.textDecoration = 'line-through';
|
||||
setTimeout(() => {
|
||||
row.style.opacity = '1';
|
||||
row.style.textDecoration = 'none';
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== ГЕНЕРАТОР НАЗВАНИЙ - НОВЫЕ ОБРАБОТЧИКИ ==========
|
||||
|
||||
// Обработчик для кнопки "Сгенерировать" (LLM)
|
||||
const generateBtn = document.querySelector('#nameGeneratorCollapse .btn-outline-primary');
|
||||
if (generateBtn) {
|
||||
generateBtn.addEventListener('click', async function() {
|
||||
const originalHTML = generateBtn.innerHTML;
|
||||
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Генерация...';
|
||||
generateBtn.disabled = true;
|
||||
// Обработчик для кнопки "Пополнить базу названиям<D18F><D0BC>"
|
||||
const populateNamesBtn = document.getElementById('populateNamesBtn');
|
||||
if (populateNamesBtn) {
|
||||
populateNamesBtn.addEventListener('click', async function() {
|
||||
const originalHTML = populateNamesBtn.innerHTML;
|
||||
populateNamesBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Пополнение...';
|
||||
populateNamesBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch("{% url 'products:api-generate-bouquet-names' %}", {
|
||||
@@ -1478,105 +1432,211 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (data.success) {
|
||||
// Обновляем счётчик
|
||||
updateBouquetNamesCount();
|
||||
// Загружаем случайные 3
|
||||
await loadRandomNames();
|
||||
alert(data.message);
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при генерации названий. Проверьте, что настроена AI-интеграция.');
|
||||
alert('Ошибка при пополнении базы названий. Проверьте, что настроена AI-интеграция.');
|
||||
} finally {
|
||||
generateBtn.innerHTML = originalHTML;
|
||||
generateBtn.disabled = false;
|
||||
populateNamesBtn.innerHTML = originalHTML;
|
||||
populateNamesBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обработчик для кнопки "Случайное"
|
||||
const randomBtn = document.querySelector('#nameGeneratorCollapse .btn-outline-secondary');
|
||||
if (randomBtn) {
|
||||
randomBtn.addEventListener('click', loadRandomNames);
|
||||
// Обработчик для кнопки "Дать три варианта"
|
||||
const getThreeNamesBtn = document.getElementById('getThreeNamesBtn');
|
||||
if (getThreeNamesBtn) {
|
||||
getThreeNamesBtn.addEventListener('click', loadThreeRandomNames);
|
||||
}
|
||||
|
||||
async function loadRandomNames() {
|
||||
const originalHTML = randomBtn.innerHTML;
|
||||
randomBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Загрузка...';
|
||||
randomBtn.disabled = true;
|
||||
// Функция для загрузки трёх случайных названий
|
||||
async function loadThreeRandomNames() {
|
||||
const originalHTML = getThreeNamesBtn.innerHTML;
|
||||
getThreeNamesBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Загрузка...';
|
||||
getThreeNamesBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=3");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.names && data.names.length > 0) {
|
||||
updateNameSuggestions(data.names);
|
||||
updateNameRows(data.names);
|
||||
} else {
|
||||
alert('В базе пока нет названий. Сначала запустите загрузку из JSON или нажмите "Сгенерировать"');
|
||||
alert('В базе пока нет названий. Сначала запустите загрузку из JSON или нажмите "Пополнить базу названиями"');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при загрузке названий');
|
||||
} finally {
|
||||
randomBtn.innerHTML = originalHTML;
|
||||
randomBtn.disabled = false;
|
||||
getThreeNamesBtn.innerHTML = originalHTML;
|
||||
getThreeNamesBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для обновления строк с названиями
|
||||
function updateNameRows(names) {
|
||||
const rows = document.querySelectorAll('.name-row');
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
const nameTextElement = row.querySelector('.name-text');
|
||||
const buttonsElement = row.querySelector('.name-buttons');
|
||||
|
||||
if (index < names.length) {
|
||||
// Если есть название для этой строки
|
||||
const nameObj = names[index];
|
||||
nameTextElement.textContent = nameObj.name;
|
||||
row.setAttribute('data-name-id', nameObj.id);
|
||||
|
||||
// Показываем кнопки
|
||||
buttonsElement.style.display = 'flex';
|
||||
} else {
|
||||
// Если нет названия для этой строки
|
||||
nameTextElement.textContent = '-';
|
||||
row.setAttribute('data-name-id', '');
|
||||
|
||||
// Скрываем кнопки
|
||||
buttonsElement.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Устанавливаем обработчики событий для новых кнопок
|
||||
attachNameRowHandlers();
|
||||
}
|
||||
|
||||
// Обработчики для кнопок "Взять" и "Удалить"
|
||||
function attachNameRowHandlers() {
|
||||
// Обработчик для кнопки "Взять"
|
||||
document.querySelectorAll('.btn-take-name').forEach(button => {
|
||||
// Проверяем, был ли уже добавлен обработчик
|
||||
if (!button.dataset.handlerAttached) {
|
||||
button.addEventListener('click', async function() {
|
||||
const row = this.closest('.name-row');
|
||||
const nameText = row.querySelector('.name-text').textContent;
|
||||
const nameId = row.getAttribute('data-name-id');
|
||||
const nameInput = document.getElementById('id_name');
|
||||
|
||||
if (nameInput) {
|
||||
nameInput.value = nameText;
|
||||
nameInput.style.borderColor = '#198754';
|
||||
nameInput.style.boxShadow = '0 0 0 0.25rem rgba(25, 135, 84, 0.15)';
|
||||
setTimeout(() => {
|
||||
nameInput.style.borderColor = '';
|
||||
nameInput.style.boxShadow = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Удаляем название из базы данных
|
||||
if (nameId) {
|
||||
await removeNameFromDatabase(nameId);
|
||||
|
||||
// Заменяем название новым из базы данных
|
||||
await replaceNameInRow(row);
|
||||
}
|
||||
});
|
||||
|
||||
// Отмечаем, что обработчик уже добавлен
|
||||
button.dataset.handlerAttached = 'true';
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик для кнопки "Удалить"
|
||||
document.querySelectorAll('.btn-delete-name').forEach(button => {
|
||||
// Проверяем, был ли уже добавлен обработчик
|
||||
if (!button.dataset.handlerAttached) {
|
||||
button.addEventListener('click', async function() {
|
||||
const row = this.closest('.name-row');
|
||||
const nameId = row.getAttribute('data-name-id');
|
||||
|
||||
// Удаляем название из базы данных
|
||||
if (nameId) {
|
||||
await removeNameFromDatabase(nameId);
|
||||
|
||||
// Заменяем название новым из базы данных
|
||||
await replaceNameInRow(row);
|
||||
}
|
||||
});
|
||||
|
||||
// Отмечаем, что обработчик уже добавлен
|
||||
button.dataset.handlerAttached = 'true';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для удаления названия из базы данных
|
||||
async function removeNameFromDatabase(nameId) {
|
||||
try {
|
||||
const response = await fetch(`{% url 'products:api-delete-bouquet-name' 0 %}`.replace('/0/', `/${nameId}/`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Обновляем счётчик
|
||||
updateBouquetNamesCount();
|
||||
} else {
|
||||
console.error('Failed to delete name from database');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting name:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для замены названия в строке новым из базы данных
|
||||
async function replaceNameInRow(row) {
|
||||
try {
|
||||
const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=1");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.names && data.names.length > 0) {
|
||||
const newName = data.names[0];
|
||||
const nameTextElement = row.querySelector('.name-text');
|
||||
const buttonsElement = row.querySelector('.name-buttons');
|
||||
|
||||
nameTextElement.textContent = newName.name;
|
||||
row.setAttribute('data-name-id', newName.id);
|
||||
|
||||
// Показываем кнопки
|
||||
buttonsElement.style.display = 'flex';
|
||||
} else {
|
||||
// Если в базе больше нет названий
|
||||
const nameTextElement = row.querySelector('.name-text');
|
||||
const buttonsElement = row.querySelector('.name-buttons');
|
||||
|
||||
nameTextElement.textContent = '-';
|
||||
row.setAttribute('data-name-id', '');
|
||||
|
||||
// Скрываем кнопки
|
||||
buttonsElement.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error replacing name:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для обновления счётчика названий в базе
|
||||
async function updateBouquetNamesCount() {
|
||||
try {
|
||||
const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=999");
|
||||
const response = await fetch("{% url 'products:api-get-bouquet-names-count' %}");
|
||||
const data = await response.json();
|
||||
const countEl = document.getElementById('bouquetNamesCount');
|
||||
if (countEl) countEl.textContent = data.names.length;
|
||||
if (countEl) countEl.textContent = data.count;
|
||||
} catch (error) {
|
||||
console.error('Error updating count:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateNameSuggestions(names) {
|
||||
const container = document.querySelector('.name-suggestions');
|
||||
container.innerHTML = '';
|
||||
|
||||
names.forEach((name, index) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'd-flex justify-content-between align-items-center py-2' +
|
||||
(index < names.length - 1 ? ' border-bottom' : '');
|
||||
row.innerHTML = `
|
||||
<span class="text-muted small">${name}</span>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-success btn-xs btn-apply-name">Взять</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-xs btn-remove-name">Убрать</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
attachNameButtonHandlers();
|
||||
}
|
||||
|
||||
function attachNameButtonHandlers() {
|
||||
document.querySelectorAll('.btn-apply-name').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const name = this.closest('.d-flex').querySelector('.text-muted').textContent;
|
||||
const nameInput = document.getElementById('id_name');
|
||||
if (nameInput) {
|
||||
nameInput.value = name;
|
||||
nameInput.style.borderColor = '#198754';
|
||||
nameInput.style.boxShadow = '0 0 0 0.25rem rgba(25, 135, 84, 0.15)';
|
||||
setTimeout(() => {
|
||||
nameInput.style.borderColor = '';
|
||||
nameInput.style.boxShadow = '';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-remove-name').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
this.closest('.d-flex').remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Инициализация обработчиков кнопок
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('btn-take-name') || e.target.classList.contains('btn-delete-name')) {
|
||||
attachNameRowHandlers();
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
|
||||
const kitForm = document.querySelector('form[method="post"]');
|
||||
|
||||
@@ -58,6 +58,8 @@ urlpatterns = [
|
||||
path('api/bulk-update-categories/', api_views.bulk_update_categories, name='api-bulk-update-categories'),
|
||||
path('api/bouquet-names/random/', api_views.RandomBouquetNamesView.as_view(), name='api-random-bouquet-names'),
|
||||
path('api/bouquet-names/generate/', api_views.GenerateBouquetNamesView.as_view(), name='api-generate-bouquet-names'),
|
||||
path('api/bouquet-names/<int:pk>/delete/', api_views.DeleteBouquetNameView.as_view(), name='api-delete-bouquet-name'),
|
||||
path('api/bouquet-names/count/', api_views.GetBouquetNamesCountView.as_view(), name='api-get-bouquet-names-count'),
|
||||
|
||||
# Photo processing status API (for AJAX polling)
|
||||
path('api/photos/status/<str:task_id>/', photo_status_api.photo_processing_status, name='api-photo-status'),
|
||||
|
||||
@@ -1816,8 +1816,12 @@ class RandomBouquetNamesView(View):
|
||||
count = int(request.GET.get('count', 3))
|
||||
# Ограничиваем максимум до 100
|
||||
count = min(count, 100)
|
||||
names = list(BouquetName.objects.order_by('?')[:count].values_list('name', flat=True))
|
||||
return JsonResponse({'names': names})
|
||||
|
||||
# Получаем случайные названия с ID (любые, не только одобренные)
|
||||
queryset = BouquetName.objects.order_by('?')[:count]
|
||||
names_data = [{'id': obj.id, 'name': obj.name} for obj in queryset]
|
||||
|
||||
return JsonResponse({'names': names_data})
|
||||
|
||||
|
||||
class GenerateBouquetNamesView(View):
|
||||
@@ -1843,3 +1847,25 @@ class GenerateBouquetNamesView(View):
|
||||
})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': msg}, status=400)
|
||||
|
||||
|
||||
class DeleteBouquetNameView(View):
|
||||
"""Удаляет конкретное название из базы"""
|
||||
|
||||
def delete(self, request, pk):
|
||||
try:
|
||||
name_obj = BouquetName.objects.get(pk=pk)
|
||||
name_obj.delete()
|
||||
return JsonResponse({'success': True})
|
||||
except BouquetName.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Название не найдено'}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
||||
|
||||
|
||||
class GetBouquetNamesCountView(View):
|
||||
"""Возвращает количество названий в базе"""
|
||||
|
||||
def get(self, request):
|
||||
count = BouquetName.objects.count()
|
||||
return JsonResponse({'count': count})
|
||||
|
||||
Reference in New Issue
Block a user