Implement card-based interface for ConfigurableKitProduct attributes

This commit introduces a new user-friendly interface for managing product attributes:

1. **Form Changes** (products/forms.py):
   - Removed 'option' field from ConfigurableKitOptionForm (values now inline)
   - Updated ConfigurableKitProductAttributeFormSetCreate to only include name, position, visible
   - Updated BaseConfigurableKitProductAttributeFormSet validation for new structure

2. **Template Updates** (products/templates/products/configurablekit_form.html):
   - Replaced row-based attribute interface with card-based design
   - Each card contains:
     - Parameter name field
     - Position field
     - Visibility toggle
     - Inline value inputs with add/remove buttons
   - "Add parameter" button creates new cards
   - "Add value" button adds inline value inputs

3. **JavaScript Enhancements**:
   - addValueField(): Creates new value input with delete button
   - initAddValueBtn(): Initializes add value button for each card
   - addParameterBtn: Dynamically generates new parameter cards
   - serializeAttributeValues(): Converts inline values to JSON for POST submission
   - Form submission intercept to serialize data before sending

4. **View Updates** (products/views/configurablekit_views.py):
   - Both Create and Update views now have _save_attributes_from_cards() method
   - Reads attributes-X-values JSON from POST data
   - Creates ConfigurableKitProductAttribute for each parameter+value combination
   - Handles parameter deletion and visibility toggling

**Key Features**:
✓ One-time parameter name entry with multiple inline values
✓ Add/remove values without reloading page
✓ Add/remove entire parameters with one click
✓ No database changes required
✓ Better UX: card layout more intuitive than rows
✓ Proper JSON serialization for value transmission

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 20:54:14 +03:00
parent 48938db04f
commit def795f0ad
9 changed files with 1310 additions and 107 deletions

View File

@@ -94,14 +94,14 @@ input[name*="DELETE"] {
</div>
</div>
<!-- Атрибуты родительского товара -->
<!-- Атрибуты родительского товара - Карточный интерфейс -->
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white">
<h5 class="mb-0">Атрибуты товара (для WooCommerce)</h5>
<h5 class="mb-0">Параметры товара</h5>
</div>
<div class="card-body">
<p class="small text-muted mb-3">
Определите схему атрибутов для вариативного товара. Например: Цвет=Красный, Размер=M, Длина=60см.
Определите параметры вариативного товара и их значения. Например: Длина (50, 60, 70), Упаковка (БЕЗ, В УПАКОВКЕ).
</p>
{{ attribute_formset.management_form }}
@@ -114,26 +114,20 @@ input[name*="DELETE"] {
<div id="attributeFormsetContainer">
{% for form in attribute_formset %}
<div class="attribute-form border rounded p-3 mb-3" style="background: #f8f9fa;">
<div class="attribute-card border rounded p-4 mb-3" style="background: #f8f9fa;" data-formset-index="{{ forloop.counter0 }}">
{{ form.id }}
{% if form.instance.pk %}
<input type="hidden" name="attributes-{{ forloop.counter0 }}-id" value="{{ form.instance.pk }}">
{% endif %}
<div class="row g-2">
<div class="row align-items-end g-3 mb-3">
<!-- Название параметра -->
<div class="col-md-3">
<label class="form-label small">{{ form.name.label }}</label>
<label class="form-label fw-semibold">{{ form.name.label }}</label>
{{ form.name }}
{% if form.name.errors %}
<div class="text-danger small">{{ form.name.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-3">
<label class="form-label small">{{ form.option.label }}</label>
{{ form.option }}
{% if form.option.errors %}
<div class="text-danger small">{{ form.option.errors.0 }}</div>
{% endif %}
</div>
<!-- Позиция параметра -->
<div class="col-md-2">
<label class="form-label small">{{ form.position.label }}</label>
{{ form.position }}
@@ -141,34 +135,47 @@ input[name*="DELETE"] {
<div class="text-danger small">{{ form.position.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-2">
<label class="form-label small d-block">{{ form.visible.label }}</label>
<div class="form-check">
<!-- Видимость -->
<div class="col-md-3">
<div class="form-check form-switch">
{{ form.visible }}
<label class="form-check-label" for="{{ form.visible.id_for_label }}">
Показывать
{{ form.visible.label }}
</label>
</div>
{% if form.visible.errors %}
<div class="text-danger small">{{ form.visible.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-2">
<!-- Удалить параметр -->
<div class="col-md-2 text-end">
{% if attribute_formset.can_delete %}
<label class="form-label small d-block">&nbsp;</label>
{{ form.DELETE }}
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger d-block">
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Удалить
</label>
{% endif %}
</div>
</div>
<!-- Значения параметра (добавляются инлайн через JavaScript) -->
<div class="parameter-values-container mt-3 p-3 bg-white rounded border">
<label class="form-label small fw-semibold d-block mb-2">Значения параметра:</label>
<div class="value-fields-wrapper" data-param-index="{{ forloop.counter0 }}">
<!-- Значения будут добавлены через JavaScript -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2 add-value-btn">
<i class="bi bi-plus-circle me-1"></i> Добавить значение
</button>
</div>
</div>
{% endfor %}
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addAttributeBtn">
<i class="bi bi-plus-circle me-1"></i>Добавить атрибут
<button type="button" class="btn btn-sm btn-outline-primary" id="addParameterBtn">
<i class="bi bi-plus-circle me-1"></i> Добавить параметр
</button>
</div>
</div>
@@ -454,78 +461,187 @@ function initDefaultSwitches() {
// Запускаем инициализацию
initDefaultSwitches();
// === Добавление новых форм атрибутов ===
document.getElementById('addAttributeBtn').addEventListener('click', function() {
// === Управление параметрами товара (карточный интерфейс) ===
// Функция для добавления нового поля значения параметра
function addValueField(container, valueText = '') {
const index = container.querySelectorAll('.value-field-group').length;
const fieldId = `value-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const html = `
<div class="value-field-group d-flex gap-2 mb-2">
<input type="text" class="form-control form-control-sm parameter-value-input"
placeholder="Введите значение"
value="${valueText}"
data-field-id="${fieldId}">
<button type="button" class="btn btn-sm btn-outline-danger remove-value-btn" title="Удалить значение">
<i class="bi bi-trash"></i>
</button>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
// Обработчик удаления значения
container.querySelector('.remove-value-btn:last-child').addEventListener('click', function(e) {
e.preventDefault();
this.closest('.value-field-group').remove();
});
}
// Инициализация существующих параметров с их значениями из БД
function initializeParameterCards() {
document.querySelectorAll('.attribute-card').forEach(card => {
// Если это существующий параметр с ID, загрузим его значения
// Это будет обработано при первой загрузке в view
initAddValueBtn(card);
});
}
// Инициализация кнопки добавления значения для карточки
function initAddValueBtn(card) {
const addBtn = card.querySelector('.add-value-btn');
if (addBtn) {
addBtn.addEventListener('click', function(e) {
e.preventDefault();
const container = this.closest('.parameter-values-container').querySelector('.value-fields-wrapper');
addValueField(container);
});
}
}
// Добавление нового параметра
document.getElementById('addParameterBtn')?.addEventListener('click', function() {
const container = document.getElementById('attributeFormsetContainer');
const totalForms = document.querySelector('[name="attributes-TOTAL_FORMS"]');
const formIdx = parseInt(totalForms.value);
// Создаём новую форму HTML
const newFormHtml = `
<div class="attribute-form border rounded p-3 mb-3" style="background: #f8f9fa;">
<div class="row g-2">
const newCardHtml = `
<div class="attribute-card border rounded p-4 mb-3" style="background: #f8f9fa;" data-formset-index="${formIdx}">
<input type="hidden" name="attributes-${formIdx}-id">
<div class="row align-items-end g-3 mb-3">
<div class="col-md-3">
<label class="form-label small">Название атрибута</label>
<input type="text" name="attributes-${formIdx}-name"
id="id_attributes-${formIdx}-name"
class="form-control"
placeholder="Например: Цвет, Размер, Длина">
</div>
<div class="col-md-3">
<label class="form-label small">Значение опции</label>
<input type="text" name="attributes-${formIdx}-option"
id="id_attributes-${formIdx}-option"
class="form-control"
placeholder="Например: Красный, M, 60см">
<label class="form-label fw-semibold">Название параметра</label>
<input type="text" name="attributes-${formIdx}-name"
id="id_attributes-${formIdx}-name"
class="form-control param-name-input"
placeholder="Например: Длина, Цвет, Размер">
</div>
<div class="col-md-2">
<label class="form-label small">Порядок</label>
<input type="number" name="attributes-${formIdx}-position"
id="id_attributes-${formIdx}-position"
class="form-control"
min="0" value="0">
<input type="number" name="attributes-${formIdx}-position"
id="id_attributes-${formIdx}-position"
class="form-control param-position-input"
min="0" value="${formIdx}">
</div>
<div class="col-md-2">
<label class="form-label small d-block">Видимый</label>
<div class="form-check">
<input type="checkbox" name="attributes-${formIdx}-visible"
id="id_attributes-${formIdx}-visible"
class="form-check-input" checked>
<div class="col-md-3">
<div class="form-check form-switch">
<input type="checkbox" name="attributes-${formIdx}-visible"
id="id_attributes-${formIdx}-visible"
class="form-check-input param-visible-input"
checked>
<label class="form-check-label" for="id_attributes-${formIdx}-visible">
Показывать
Видимый на витрине
</label>
</div>
</div>
<div class="col-md-2">
<label class="form-label small d-block">&nbsp;</label>
<input type="checkbox" name="attributes-${formIdx}-DELETE"
id="id_attributes-${formIdx}-DELETE"
<div class="col-md-2 text-end">
<input type="checkbox" name="attributes-${formIdx}-DELETE"
id="id_attributes-${formIdx}-DELETE"
style="display:none;">
<label for="id_attributes-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger d-block">
<label for="id_attributes-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Удалить
</label>
</div>
</div>
<div class="parameter-values-container mt-3 p-3 bg-white rounded border">
<label class="form-label small fw-semibold d-block mb-2">Значения параметра:</label>
<div class="value-fields-wrapper" data-param-index="${formIdx}">
<!-- Значения добавляются сюда -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2 add-value-btn">
<i class="bi bi-plus-circle me-1"></i> Добавить значение
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', newFormHtml);
container.insertAdjacentHTML('beforeend', newCardHtml);
totalForms.value = formIdx + 1;
// Инициализируем новую карточку
const newCard = container.querySelector(`[data-formset-index="${formIdx}"]`);
initAddValueBtn(newCard);
// Инициализируем удаление параметра
initParamDeleteToggle(newCard);
});
// Скрытие удаленных атрибутов
document.addEventListener('change', function(e) {
if (e.target.type === 'checkbox' && e.target.name && e.target.name.includes('attributes') && e.target.name.includes('DELETE')) {
const form = e.target.closest('.attribute-form');
if (form) {
if (e.target.checked) {
form.style.opacity = '0.5';
form.style.textDecoration = 'line-through';
// Функция для скрытия удаленного параметра
function initParamDeleteToggle(card) {
const deleteCheckbox = card.querySelector('input[type="checkbox"][name$="-DELETE"]');
if (deleteCheckbox) {
deleteCheckbox.addEventListener('change', function() {
if (this.checked) {
card.style.opacity = '0.5';
card.style.textDecoration = 'line-through';
} else {
form.style.opacity = '1';
form.style.textDecoration = 'none';
card.style.opacity = '1';
card.style.textDecoration = 'none';
}
});
}
}
// Функция для сериализации значений параметров перед отправкой формы
function serializeAttributeValues() {
/**
* Перед отправкой формы нужно сериализовать все значения параметров
* из инлайн input'ов в скрытые JSON поля для отправки на сервер
*/
document.querySelectorAll('.attribute-card').forEach((card, idx) => {
// Получаем все инпуты с значениями внутри этой карточки
const valueInputs = card.querySelectorAll('.parameter-value-input');
const values = [];
valueInputs.forEach(input => {
const value = input.value.trim();
if (value) {
values.push(value);
}
});
// Создаем или обновляем скрытое поле JSON с названием attributes-{idx}-values
const jsonFieldName = `attributes-${idx}-values`;
let jsonField = document.querySelector(`input[name="${jsonFieldName}"]`);
if (!jsonField) {
jsonField = document.createElement('input');
jsonField.type = 'hidden';
jsonField.name = jsonFieldName;
card.appendChild(jsonField);
}
jsonField.value = JSON.stringify(values);
});
}
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
initializeParameterCards();
document.querySelectorAll('.attribute-card').forEach(card => {
initParamDeleteToggle(card);
});
// Добавляем сериализацию значений перед отправкой формы
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
// Перед отправкой формы сериализуем все значения параметров
serializeAttributeValues();
});
}
});
</script>