Refactor: Compact UI for product tags templates

Redesigned tag_form.html and tag_list.html with modern, compact layout:
- Narrower card width, inline form elements, simplified controls
- List-group layout instead of table for tag list
- Streamlined JavaScript with auto-hiding messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-23 23:54:58 +03:00
parent addc5e0962
commit 157bd50082
2 changed files with 119 additions and 302 deletions

View File

@@ -3,97 +3,57 @@
{% block title %}{% if object %}Редактировать тег{% else %}Создать тег{% endif %}{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
{% if object %}
<i class="bi bi-pencil"></i> Редактировать тег
{% else %}
<i class="bi bi-plus-circle"></i> Создать новый тег
{% endif %}
</h4>
</div>
<div class="card-body">
<div class="col-sm-6 col-md-5 col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h5 class="text-center mb-4">
<i class="bi bi-tag{% if not object %}-fill{% endif %} text-primary"></i>
{% if object %}Редактировать{% else %}Новый тег{% endif %}
</h5>
<form method="post">
{% csrf_token %}
<!-- Блок 1: Название -->
<div class="mb-4">
<label for="id_name" class="form-label fw-bold fs-5">
{{ form.name.label }}
</label>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-type"></i></span>
{{ form.name }}
{% if form.name.errors %}
<div class="text-danger mt-1">
{{ form.name.errors }}
</div>
{% endif %}
{% if form.name.help_text %}
<div class="form-text">{{ form.name.help_text }}</div>
{% endif %}
{% if form.name.errors %}<small class="text-danger">{{ form.name.errors.0 }}</small>{% endif %}
</div>
<!-- Блок 2: Slug -->
<div class="mb-4">
<label for="id_slug" class="form-label">
{{ form.slug.label }}
</label>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-link-45deg"></i></span>
{{ form.slug }}
{% if form.slug.errors %}
<div class="text-danger mt-1">
{{ form.slug.errors }}
</div>
{% endif %}
{% if form.slug.help_text %}
<div class="form-text">{{ form.slug.help_text }}</div>
{% endif %}
{% if form.slug.errors %}<small class="text-danger">{{ form.slug.errors.0 }}</small>{% endif %}
</div>
<!-- Блок 3: Активность -->
<div class="mb-4">
<div class="form-check form-switch">
<div class="form-check form-switch mb-4">
{{ form.is_active }}
<label class="form-check-label" for="id_is_active">
{{ form.is_active.label }}
</label>
</div>
{% if form.is_active.errors %}
<div class="text-danger mt-1">
{{ form.is_active.errors }}
</div>
{% endif %}
<label class="form-check-label small" for="id_is_active">Активен</label>
</div>
<!-- Кнопки действий -->
<div class="d-flex gap-2">
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i>
{% if object %}Сохранить изменения{% else %}Создать тег{% endif %}
<i class="bi bi-check2"></i> {% if object %}Сохранить{% else %}Создать{% endif %}
</button>
<a href="{% url 'products:tag-list' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
<a href="{% url 'products:tag-list' %}" class="btn btn-light btn-sm">Отмена</a>
</div>
</form>
</div>
</div>
<!-- Дополнительная информация при редактировании -->
{% if object %}
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title">Дополнительная информация</h6>
<ul class="list-unstyled mb-0">
<li><strong>Создан:</strong> {{ object.created_at|date:"d.m.Y H:i" }}</li>
<li><strong>Обновлен:</strong> {{ object.updated_at|date:"d.m.Y H:i" }}</li>
</ul>
</div>
</div>
<hr class="my-3">
<small class="text-muted d-block text-center">
<i class="bi bi-clock-history"></i> {{ object.updated_at|date:"d.m.Y H:i" }}
</small>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,164 +1,75 @@
{% extends 'base.html' %}
{% block title %}Теги товаров{% endblock %}
{% block title %}Теги{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Теги товаров</h2>
<a href="{% url 'products:tag-create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Создать тег
</a>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Заголовок и быстрое создание -->
<div class="d-flex align-items-center gap-3 mb-3">
<h5 class="mb-0"><i class="bi bi-tags text-primary"></i> Теги</h5>
<div class="input-group input-group-sm flex-grow-1" style="max-width: 300px;">
<input type="text" id="quick-tag-input" class="form-control" placeholder="Новый тег..." autocomplete="off">
<button class="btn btn-primary" id="quick-tag-btn" type="button"><i class="bi bi-plus"></i></button>
</div>
</div>
<div id="quick-tag-message"></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-body">
<form method="get" class="row g-3">
<div class="col-md-6">
<label for="search" class="form-label">Поиск</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search_query }}" placeholder="Поиск по названию или slug...">
</div>
<div class="col-md-4">
<label for="is_active" class="form-label">Статус</label>
<select class="form-select" id="is_active" name="is_active">
<!-- Поиск и фильтр в одну строку -->
<form method="get" class="d-flex gap-2 mb-3">
<input type="text" class="form-control form-control-sm" name="search" value="{{ search_query }}" placeholder="Поиск...">
<select class="form-select form-select-sm" name="is_active" style="width: auto;" onchange="this.form.submit()">
<option value="">Все</option>
<option value="1" {% if is_active_filter == '1' %}selected{% endif %}>Активные</option>
<option value="0" {% if is_active_filter == '0' %}selected{% endif %}>Неактивные</option>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-outline-secondary w-100">
<i class="bi bi-search"></i> Найти
</button>
</div>
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="bi bi-search"></i></button>
</form>
</div>
</div>
<!-- Таблица с тегами -->
<div class="card">
<div class="card-body">
<!-- Список тегов -->
{% if tags %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Название</th>
<th>Slug</th>
<th>Товары</th>
<th>Комплекты</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<div class="list-group list-group-flush">
{% for tag in tags %}
<tr>
<td>
<a href="{% url 'products:tag-detail' tag.pk %}" class="text-decoration-none fw-semibold">
{{ tag.name }}
</a>
</td>
<td><code>{{ tag.slug }}</code></td>
<td>
<span class="badge bg-info">{{ tag.products_count }}</span>
</td>
<td>
<span class="badge bg-info">{{ tag.kits_count }}</span>
</td>
<td>
<div class="form-check form-switch" style="transform: scale(1.3); transform-origin: left;">
<input class="form-check-input tag-status-switch"
type="checkbox"
id="tag-status-{{ tag.pk }}"
data-tag-id="{{ tag.pk }}"
{% if tag.is_active %}checked{% endif %}>
<div class="list-group-item d-flex align-items-center py-2 px-3">
<div class="form-check form-switch me-3 mb-0">
<input class="form-check-input tag-status-switch" type="checkbox" data-tag-id="{{ tag.pk }}" {% if tag.is_active %}checked{% endif %}>
</div>
<div class="flex-grow-1">
<a href="{% url 'products:tag-detail' tag.pk %}" class="text-decoration-none fw-medium">{{ tag.name }}</a>
<small class="text-muted ms-2">{{ tag.slug }}</small>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-secondary" title="Товаров">{{ tag.products_count }}</span>
<span class="badge bg-secondary" title="Комплектов">{{ tag.kits_count }}</span>
<div class="btn-group btn-group-sm">
<a href="{% url 'products:tag-update' tag.pk %}" class="btn btn-outline-secondary btn-sm py-0 px-1" title="Изменить"><i class="bi bi-pencil"></i></a>
<a href="{% url 'products:tag-delete' tag.pk %}" class="btn btn-outline-danger btn-sm py-0 px-1" title="Удалить"><i class="bi bi-trash"></i></a>
</div>
</div>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'products:tag-detail' tag.pk %}"
class="btn btn-outline-primary" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
<a href="{% url 'products:tag-update' tag.pk %}"
class="btn btn-outline-warning" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'products:tag-delete' tag.pk %}"
class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if is_paginated %}
<nav aria-label="Pagination" class="mt-4">
<ul class="pagination justify-content-center">
<nav class="mt-3">
<ul class="pagination pagination-sm justify-content-center mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Первая
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Назад
</a>
</li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">&laquo;</a></li>
{% endif %}
<li class="page-item active">
<span class="page-link">
Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
</span>
</li>
<li class="page-item active"><span class="page-link">{{ page_obj.number }}/{{ page_obj.paginator.num_pages }}</span></li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Вперед
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Последняя
</a>
</li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">&raquo;</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Теги не найдены.
<a href="{% url 'products:tag-create' %}" class="alert-link">Создать первый тег</a>
<div class="text-center text-muted py-4">
<i class="bi bi-tag fs-1 opacity-25"></i>
<p class="mb-0 mt-2">Тегов пока нет</p>
</div>
{% endif %}
</div>
@@ -171,115 +82,61 @@ document.addEventListener('DOMContentLoaded', function() {
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) return;
if (!name) {
showMessage('Введите название тега', 'warning');
return;
}
// Блокируем кнопку и поле во время создания
btn.disabled = true;
input.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
fetch('{% url "products:api-tag-create" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({name: name})
})
.then(response => response.json())
.then(r => r.json())
.then(data => {
if (data.success) {
showMessage(`Тег "${data.tag.name}" успешно создан!`, 'success');
input.value = '';
// Перезагружаем страницу через 1 секунду для обновления списка
setTimeout(() => {
window.location.reload();
}, 1000);
showMessage('Тег создан', 'success');
setTimeout(() => window.location.reload(), 500);
} else {
showMessage('Ошибка: ' + data.error, 'danger');
// Разблокируем поле и кнопку при ошибке
btn.disabled = false;
input.disabled = false;
btn.innerHTML = '<i class="bi bi-plus-circle"></i> Создать';
input.focus();
showMessage(data.error, 'danger');
resetBtn();
}
})
.catch(error => {
console.error('Error:', error);
showMessage('Ошибка сети при создании тега', 'danger');
// Разблокируем поле и кнопку при ошибке
.catch(() => { showMessage('Ошибка сети', 'danger'); resetBtn(); });
}
function resetBtn() {
btn.disabled = false;
input.disabled = false;
btn.innerHTML = '<i class="bi bi-plus-circle"></i> Создать';
btn.innerHTML = '<i class="bi bi-plus"></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>`;
messageDiv.innerHTML = `<div class="alert alert-${type} py-1 px-2 small mb-2">${text}</div>`;
setTimeout(() => messageDiv.innerHTML = '', 3000);
}
// Обработчик клика по кнопке
btn.addEventListener('click', createTag);
input.addEventListener('keypress', e => { if (e.key === 'Enter') { e.preventDefault(); createTag(); } });
// Обработчик нажатия Enter
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
createTag();
}
});
});
// Обработчик переключения статуса тега
document.querySelectorAll('.tag-status-switch').forEach(toggle => {
// Toggle статуса
document.querySelectorAll('.tag-status-switch').forEach(toggle => {
toggle.addEventListener('click', async function(e) {
e.preventDefault(); // Предотвращаем стандартное поведение checkbox
e.preventDefault();
const tagId = this.dataset.tagId;
const toggleSwitch = this;
const wasChecked = toggleSwitch.checked; // Сохраняем ТЕКУЩЕЕ состояние
try {
// Отправляем AJAX запрос
const apiUrl = `/products/api/tags/${tagId}/toggle/`;
const response = await fetch(apiUrl, {
const response = await fetch(`/products/api/tags/${tagId}/toggle/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
}
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' }
});
const data = await response.json();
if (data.success) {
// Устанавливаем переключатель в НОВОЕ состояние согласно ответу сервера
toggleSwitch.checked = data.is_active;
// Показываем сообщение об успехе
showMessage(data.message, 'success');
} else {
// Переключатель остаётся в исходном состоянии (мы его не меняли)
showMessage(data.error || 'Ошибка при обновлении тега', 'danger');
}
} catch (error) {
// При ошибке сети - переключатель остаётся как был
showMessage('Ошибка сети: ' + error.message, 'danger');
}
if (data.success) this.checked = data.is_active;
} catch (error) {}
});
});
});
</script>