Добавлено быстрое создание тегов на странице списка
API функционал: - Создан endpoint /api/tags/create/ для AJAX создания тегов - Валидация: пустое имя, длина, уникальность (регистронезависимо) - Автоматическая генерация slug через модель - Возврат JSON с данными созданного тега UI функционал на странице списка тегов: - Панель быстрого создания с крупным полем ввода - Автофокус на поле при загрузке страницы - Создание тега по нажатию Enter или клику на кнопку - Индикатор загрузки (спиннер) во время создания - Блокировка поля/кнопки во время запроса - Автоматическая перезагрузка страницы после создания - Возврат фокуса в поле при ошибке - Красивые alert-сообщения об успехе/ошибке - CSRF защита Workflow: ввёл название → Enter → тег создан → фокус снова в поле → можно вводить следующий 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,24 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель быстрого создания тега -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3">
|
||||||
|
<i class="bi bi-lightning-charge"></i> Быстрое создание тега
|
||||||
|
</h5>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="quick-tag-input" class="form-control form-control-lg"
|
||||||
|
placeholder="Введите название тега и нажмите Enter..."
|
||||||
|
autocomplete="off">
|
||||||
|
<button class="btn btn-success" id="quick-tag-btn" type="button">
|
||||||
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="quick-tag-message" class="mt-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Панель поиска и фильтров -->
|
<!-- Панель поиска и фильтров -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -144,4 +162,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const input = document.getElementById('quick-tag-input');
|
||||||
|
const btn = document.getElementById('quick-tag-btn');
|
||||||
|
const messageDiv = document.getElementById('quick-tag-message');
|
||||||
|
|
||||||
|
// Автофокус на поле при загрузке страницы
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
function createTag() {
|
||||||
|
const name = input.value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
showMessage('Введите название тега', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Блокируем кнопку и поле во время создания
|
||||||
|
btn.disabled = true;
|
||||||
|
input.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
|
||||||
|
|
||||||
|
fetch('{% url "products:api-tag-create" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({name: name})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showMessage(`✓ Тег "${data.tag.name}" успешно создан!`, 'success');
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
// Перезагружаем страницу через 1 секунду для обновления списка
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showMessage('Ошибка: ' + data.error, 'danger');
|
||||||
|
// Разблокируем поле и кнопку при ошибке
|
||||||
|
btn.disabled = false;
|
||||||
|
input.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-plus-circle"></i> Создать';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showMessage('Ошибка сети при создании тега', 'danger');
|
||||||
|
// Разблокируем поле и кнопку при ошибке
|
||||||
|
btn.disabled = false;
|
||||||
|
input.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-plus-circle"></i> Создать';
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(text, type) {
|
||||||
|
messageDiv.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show mb-0" role="alert">
|
||||||
|
${text}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик клика по кнопке
|
||||||
|
btn.addEventListener('click', createTag);
|
||||||
|
|
||||||
|
// Обработчик нажатия Enter
|
||||||
|
input.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
createTag();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ urlpatterns = [
|
|||||||
# 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'),
|
||||||
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
|
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
|
||||||
|
path('api/tags/create/', views.create_tag_api, name='api-tag-create'),
|
||||||
|
|
||||||
# CRUD URLs for ProductVariantGroup (Варианты товаров)
|
# CRUD URLs for ProductVariantGroup (Варианты товаров)
|
||||||
path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'),
|
path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'),
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ from .tag_views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# API представления
|
# API представления
|
||||||
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api
|
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api, create_tag_api
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -149,4 +149,5 @@ __all__ = [
|
|||||||
'search_products_and_variants',
|
'search_products_and_variants',
|
||||||
'validate_kit_cost',
|
'validate_kit_cost',
|
||||||
'create_temporary_kit_api',
|
'create_temporary_kit_api',
|
||||||
|
'create_tag_api',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -620,3 +620,94 @@ def create_temporary_kit_api(request):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Ошибка при создании комплекта: {str(e)}'
|
'error': f'Ошибка при создании комплекта: {str(e)}'
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
def create_tag_api(request):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для быстрого создания тега из списка тегов.
|
||||||
|
|
||||||
|
Принимает JSON:
|
||||||
|
{
|
||||||
|
"name": "Новый тег"
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает JSON:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"tag": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Новый тег",
|
||||||
|
"slug": "novyj-teg",
|
||||||
|
"is_active": true,
|
||||||
|
"products_count": 0,
|
||||||
|
"kits_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Или при ошибке:
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Описание ошибки"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Метод не поддерживается'
|
||||||
|
}, status=405)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
from ..models import ProductTag
|
||||||
|
|
||||||
|
data = json.loads(request.body)
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
|
||||||
|
# Валидация
|
||||||
|
if not name:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Название тега не может быть пустым'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
if len(name) > 100:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Название тега слишком длинное (максимум 100 символов)'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Проверка уникальности (регистронезависимо)
|
||||||
|
if ProductTag.objects.filter(name__iexact=name).exists():
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Тег "{name}" уже существует'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Создание тега (slug будет сгенерирован автоматически в модели)
|
||||||
|
tag = ProductTag.objects.create(
|
||||||
|
name=name,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'tag': {
|
||||||
|
'id': tag.id,
|
||||||
|
'name': tag.name,
|
||||||
|
'slug': tag.slug,
|
||||||
|
'is_active': tag.is_active,
|
||||||
|
'products_count': 0,
|
||||||
|
'kits_count': 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Некорректный JSON'
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Ошибка при создании тега: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
|
|||||||
Reference in New Issue
Block a user