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

View File

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