From 390d547e97b95a937c5f311cd74f29b8ab3033be Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 2 Nov 2025 19:40:47 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=BA=D0=BE=D1=80=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализована логика чтобы только одно из четырёх полей корректировки цены можно было заполнить одновременно: JavaScript валидация: - При заполнении одного поля остальные 3 автоматически отключаются - При попытке заполнить два поля одновременно: - Оставляется только первое заполненное - Остальные очищаются и помечаются как ошибка - При очистке всех полей они снова активируются CSS стили: - Disabled поля: серый фон, пониженная прозрачность, запрещённый курсор - Invalid поля: красная граница и shadow (Bootstrap стиль) Валидация работает на обе стороны: - Frontend JavaScript (instant feedback) - Backend Python валидация (безопасность) Файлы: - products/templates/products/productkit_create.html - products/templates/products/productkit_edit.html - products/forms.py (документация) --- myproject/products/forms.py | 4 +- .../templates/products/productkit_create.html | 97 ++++++++++++++++++- .../templates/products/productkit_edit.html | 97 ++++++++++++++++++- 3 files changed, 193 insertions(+), 5 deletions(-) diff --git a/myproject/products/forms.py b/myproject/products/forms.py index e6d9b06..c28e70e 100644 --- a/myproject/products/forms.py +++ b/myproject/products/forms.py @@ -139,7 +139,9 @@ class ProductKitForm(forms.ModelForm): def clean(self): """ Валидация формы комплекта. - Проверяет что если выбран тип корректировки, указано значение. + Проверяет: + 1. Что если выбран тип корректировки, указано значение + 2. Что заполнено максимум одно поле корректировки (увеличение или уменьшение) """ cleaned_data = super().clean() diff --git a/myproject/products/templates/products/productkit_create.html b/myproject/products/templates/products/productkit_create.html index 76d9871..e761365 100644 --- a/myproject/products/templates/products/productkit_create.html +++ b/myproject/products/templates/products/productkit_create.html @@ -387,6 +387,25 @@ color: #0d6efd; } +/* Стили для полей корректировки цены */ +#id_increase_percent:disabled, +#id_increase_amount:disabled, +#id_decrease_percent:disabled, +#id_decrease_amount:disabled { + background-color: #e9ecef; + color: #6c757d; + cursor: not-allowed; + opacity: 0.6; +} + +#id_increase_percent.is-invalid, +#id_increase_amount.is-invalid, +#id_decrease_percent.is-invalid, +#id_decrease_amount.is-invalid { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + /* Адаптивность */ @media (max-width: 991px) { .col-lg-8, .col-lg-4 { @@ -584,11 +603,85 @@ document.addEventListener('DOMContentLoaded', function() { finalPriceDisplay.textContent = finalPrice.toFixed(2) + ' руб.'; } + // Функция для управления состоянием полей (только одно может быть заполнено) + function validateSingleAdjustment() { + const increasePercent = parseFloat(increasePercentInput.value) || 0; + const increaseAmount = parseFloat(increaseAmountInput.value) || 0; + const decreasePercent = parseFloat(decreasePercentInput.value) || 0; + const decreaseAmount = parseFloat(decreaseAmountInput.value) || 0; + + // Подсчитываем сколько полей заполнено + const filledCount = (increasePercent > 0 ? 1 : 0) + + (increaseAmount > 0 ? 1 : 0) + + (decreasePercent > 0 ? 1 : 0) + + (decreaseAmount > 0 ? 1 : 0); + + const allInputs = [increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput]; + + if (filledCount === 0) { + // Ничего не заполнено - все поля активны + allInputs.forEach(input => { + input.disabled = false; + input.classList.remove('is-invalid'); + }); + } else if (filledCount === 1) { + // Ровно одно поле заполнено - отключаем остальные + allInputs.forEach(input => { + const isCurrentField = ( + input === increasePercentInput && increasePercent > 0 || + input === increaseAmountInput && increaseAmount > 0 || + input === decreasePercentInput && decreasePercent > 0 || + input === decreaseAmountInput && decreaseAmount > 0 + ); + + input.disabled = !isCurrentField; + input.classList.remove('is-invalid'); + }); + } else { + // Несколько полей заполнено - помечаем как ошибку и очищаем лишние + allInputs.forEach(input => { + input.classList.add('is-invalid'); + }); + + // Оставляем только первое заполненное поле, остальные очищаем + let foundFirst = false; + if (increasePercent > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || increasePercent > 0) { + increasePercentInput.value = ''; + } + + if (increaseAmount > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || increaseAmount > 0) { + increaseAmountInput.value = ''; + } + + if (decreasePercent > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || decreasePercent > 0) { + decreasePercentInput.value = ''; + } + + if (decreaseAmount > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || decreaseAmount > 0) { + decreaseAmountInput.value = ''; + } + } + } + // Добавляем обработчики для всех полей цены [increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput].forEach(input => { if (input) { - input.addEventListener('input', calculateFinalPrice); - input.addEventListener('change', calculateFinalPrice); + input.addEventListener('input', () => { + validateSingleAdjustment(); + calculateFinalPrice(); + }); + input.addEventListener('change', () => { + validateSingleAdjustment(); + calculateFinalPrice(); + }); } }); diff --git a/myproject/products/templates/products/productkit_edit.html b/myproject/products/templates/products/productkit_edit.html index e690dbb..ade4117 100644 --- a/myproject/products/templates/products/productkit_edit.html +++ b/myproject/products/templates/products/productkit_edit.html @@ -388,6 +388,25 @@ color: #0d6efd; } +/* Стили для полей корректировки цены */ +#id_increase_percent:disabled, +#id_increase_amount:disabled, +#id_decrease_percent:disabled, +#id_decrease_amount:disabled { + background-color: #e9ecef; + color: #6c757d; + cursor: not-allowed; + opacity: 0.6; +} + +#id_increase_percent.is-invalid, +#id_increase_amount.is-invalid, +#id_decrease_percent.is-invalid, +#id_decrease_amount.is-invalid { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + /* Адаптивность */ @media (max-width: 991px) { .col-lg-8, .col-lg-4 { @@ -585,11 +604,85 @@ document.addEventListener('DOMContentLoaded', function() { finalPriceDisplay.textContent = finalPrice.toFixed(2) + ' руб.'; } + // Функция для управления состоянием полей (только одно может быть заполнено) + function validateSingleAdjustment() { + const increasePercent = parseFloat(increasePercentInput.value) || 0; + const increaseAmount = parseFloat(increaseAmountInput.value) || 0; + const decreasePercent = parseFloat(decreasePercentInput.value) || 0; + const decreaseAmount = parseFloat(decreaseAmountInput.value) || 0; + + // Подсчитываем сколько полей заполнено + const filledCount = (increasePercent > 0 ? 1 : 0) + + (increaseAmount > 0 ? 1 : 0) + + (decreasePercent > 0 ? 1 : 0) + + (decreaseAmount > 0 ? 1 : 0); + + const allInputs = [increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput]; + + if (filledCount === 0) { + // Ничего не заполнено - все поля активны + allInputs.forEach(input => { + input.disabled = false; + input.classList.remove('is-invalid'); + }); + } else if (filledCount === 1) { + // Ровно одно поле заполнено - отключаем остальные + allInputs.forEach(input => { + const isCurrentField = ( + input === increasePercentInput && increasePercent > 0 || + input === increaseAmountInput && increaseAmount > 0 || + input === decreasePercentInput && decreasePercent > 0 || + input === decreaseAmountInput && decreaseAmount > 0 + ); + + input.disabled = !isCurrentField; + input.classList.remove('is-invalid'); + }); + } else { + // Несколько полей заполнено - помечаем как ошибку и очищаем лишние + allInputs.forEach(input => { + input.classList.add('is-invalid'); + }); + + // Оставляем только первое заполненное поле, остальные очищаем + let foundFirst = false; + if (increasePercent > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || increasePercent > 0) { + increasePercentInput.value = ''; + } + + if (increaseAmount > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || increaseAmount > 0) { + increaseAmountInput.value = ''; + } + + if (decreasePercent > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || decreasePercent > 0) { + decreasePercentInput.value = ''; + } + + if (decreaseAmount > 0 && !foundFirst) { + foundFirst = true; + } else if (foundFirst || decreaseAmount > 0) { + decreaseAmountInput.value = ''; + } + } + } + // Добавляем обработчики для всех полей цены [increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput].forEach(input => { if (input) { - input.addEventListener('input', calculateFinalPrice); - input.addEventListener('change', calculateFinalPrice); + input.addEventListener('input', () => { + validateSingleAdjustment(); + calculateFinalPrice(); + }); + input.addEventListener('change', () => { + validateSingleAdjustment(); + calculateFinalPrice(); + }); } });