Разработка фильтрации по диапазону цен 1С-Битрикс
Стандартный умный фильтр 1С-Битрикс включает ценовой диапазон в виде двух текстовых полей «от» и «до». В большинстве случаев этого достаточно — пока не появляется требование: слайдер с двумя ползунками, мгновенное обновление каталога без перезагрузки страницы, корректная работа с несколькими типами цен и граничными значениями из реальных данных. Стандартный компонент фильтра не покрывает эти сценарии без кастомной разработки.
Как работает ценовой фильтр в Битрикс
Умный фильтр формирует параметр запроса вида PRICE_1[MIN]=100&PRICE_1[MAX]=5000, где 1 — ID типа цены. CIBlockElement::GetList принимает эти параметры через arFilter с ключами >=PRICE и <=PRICE. При нескольких типах цен фильтрация ведётся по базовой цене или по цене для группы текущего пользователя.
Получение минимальной и максимальной цены из каталога для инициализации слайдера:
// Получение граничных значений цены из раздела каталога
$priceRange = [];
$res = CPrice::GetList(
['PRICE' => 'ASC'],
[
'CATALOG_GROUP_ID' => 1,
'ELEMENT_IBLOCK_ID' => $iblockId,
],
false,
['nPageSize' => 1]
);
if ($item = $res->Fetch()) {
$priceRange['min'] = floatval($item['PRICE']);
}
$res = CPrice::GetList(
['PRICE' => 'DESC'],
[
'CATALOG_GROUP_ID' => 1,
'ELEMENT_IBLOCK_ID' => $iblockId,
],
false,
['nPageSize' => 1]
);
if ($item = $res->Fetch()) {
$priceRange['max'] = floatval($item['PRICE']);
}
Эти значения передаются в JavaScript через data-* атрибуты или JSON в теге <script>.
Реализация слайдера двойного диапазона
Нативный HTML не поддерживает слайдер с двумя ползунками. Реализация через два input[type=range] с CSS-позиционированием:
class PriceRangeSlider {
constructor(container, options) {
this.container = container;
this.min = options.min || 0;
this.max = options.max || 100000;
this.valueMin = options.valueMin || this.min;
this.valueMax = options.valueMax || this.max;
this.onChange = options.onChange || function() {};
this.render();
this.bindEvents();
}
render() {
this.container.innerHTML = `
<div class="price-slider">
<div class="price-slider__track">
<div class="price-slider__range" id="sliderRange"></div>
</div>
<input type="range" class="price-slider__thumb price-slider__thumb--min"
min="${this.min}" max="${this.max}" value="${this.valueMin}" step="100">
<input type="range" class="price-slider__thumb price-slider__thumb--max"
min="${this.min}" max="${this.max}" value="${this.valueMax}" step="100">
</div>
<div class="price-inputs">
<input type="number" class="price-input price-input--min" value="${this.valueMin}">
<span>—</span>
<input type="number" class="price-input price-input--max" value="${this.valueMax}">
</div>
`;
this.thumbMin = this.container.querySelector('.price-slider__thumb--min');
this.thumbMax = this.container.querySelector('.price-slider__thumb--max');
this.rangeEl = this.container.querySelector('#sliderRange');
this.inputMin = this.container.querySelector('.price-input--min');
this.inputMax = this.container.querySelector('.price-input--max');
this.updateTrack();
}
updateTrack() {
const percent1 = ((this.valueMin - this.min) / (this.max - this.min)) * 100;
const percent2 = ((this.valueMax - this.min) / (this.max - this.min)) * 100;
this.rangeEl.style.left = percent1 + '%';
this.rangeEl.style.width = (percent2 - percent1) + '%';
}
bindEvents() {
this.thumbMin.addEventListener('input', (e) => {
const val = Math.min(parseInt(e.target.value), this.valueMax - 100);
this.valueMin = val;
e.target.value = val;
this.inputMin.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
this.thumbMax.addEventListener('input', (e) => {
const val = Math.max(parseInt(e.target.value), this.valueMin + 100);
this.valueMax = val;
e.target.value = val;
this.inputMax.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
this.inputMin.addEventListener('change', (e) => {
const val = Math.max(this.min, Math.min(parseInt(e.target.value) || this.min, this.valueMax - 100));
this.valueMin = val;
e.target.value = val;
this.thumbMin.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
this.inputMax.addEventListener('change', (e) => {
const val = Math.min(this.max, Math.max(parseInt(e.target.value) || this.max, this.valueMin + 100));
this.valueMax = val;
e.target.value = val;
this.thumbMax.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
}
}
AJAX-применение ценового фильтра
Интеграция слайдера с AJAX-обновлением каталога:
// Инициализация
const priceData = window.__PRICE_RANGE__ || { min: 0, max: 100000 };
const urlParams = new URLSearchParams(window.location.search);
const currentMin = parseInt(urlParams.get('PRICE_1_MIN')) || priceData.min;
const currentMax = parseInt(urlParams.get('PRICE_1_MAX')) || priceData.max;
const slider = new PriceRangeSlider(
document.getElementById('price-range-container'),
{
min: priceData.min,
max: priceData.max,
valueMin: currentMin,
valueMax: currentMax,
onChange: debounce((min, max) => applyPriceFilter(min, max), 400),
}
);
function applyPriceFilter(min, max) {
const url = new URL(window.location.href);
if (min > priceData.min) {
url.searchParams.set('PRICE_1_MIN', min);
} else {
url.searchParams.delete('PRICE_1_MIN');
}
if (max < priceData.max) {
url.searchParams.set('PRICE_1_MAX', max);
} else {
url.searchParams.delete('PRICE_1_MAX');
}
// Сброс пагинации при изменении фильтра
url.searchParams.delete('PAGEN_1');
loadCatalog(url.toString());
}
function loadCatalog(url) {
const catalogEl = document.getElementById('catalog-container');
catalogEl.classList.add('loading');
fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newCatalog = doc.getElementById('catalog-container');
if (newCatalog) {
catalogEl.innerHTML = newCatalog.innerHTML;
}
catalogEl.classList.remove('loading');
history.pushState(null, '', url);
});
}
Серверная обработка параметров цены
В компоненте каталога или шаблоне фильтра — приём параметров и передача в CIBlockElement::GetList:
// template.php компонента каталога
$filterPrice = [];
if (!empty($_REQUEST['PRICE_1_MIN'])) {
$filterPrice['>=MIN_PRICE'] = floatval($_REQUEST['PRICE_1_MIN']);
}
if (!empty($_REQUEST['PRICE_1_MAX'])) {
$filterPrice['<=MAX_PRICE'] = floatval($_REQUEST['PRICE_1_MAX']);
}
// Объединение с основным фильтром
$arFilter = array_merge($arFilter, $filterPrice);
При работе через умный фильтр (компонент bitrix:catalog.smart.filter) параметры обрабатываются автоматически, если слайдер передаёт значения в стандартных полях формы фильтра.
Фильтрация по нескольким типам цен
В B2B-каталогах часто несколько типов цен для разных групп покупателей. Слайдер должен работать с ценой текущей группы:
// Определение ID типа цены для текущего пользователя
$userGroupIds = CUser::GetUserGroup($USER->GetID());
$priceTypeId = 1; // базовая по умолчанию
$res = CCatalogGroup::GetList(
['ID' => 'ASC'],
['BUY' => 'Y'],
);
while ($group = $res->Fetch()) {
if (array_intersect($userGroupIds, $group['BUY_GROUP_IDS'])) {
$priceTypeId = $group['ID'];
break;
}
}
// Передача ID типа цены в JS
echo '<script>window.__PRICE_TYPE_ID__ = ' . intval($priceTypeId) . ';</script>';
Кейс: каталог электроники с широким ценовым диапазоном
Интернет-магазин бытовой техники имел каталог с диапазоном цен от 300 до 450 000 рублей. Стандартные поля ввода «от/до» работали некорректно: пользователи вводили значения вручную, ошибались с нулями. Слайдер с логарифмической шкалой решил проблему — нижний диапазон (300–5000 руб.) занимал столько же экранного пространства, сколько верхний (100 000–450 000 руб.).
Логарифмическое преобразование значений слайдера:
function toSliderPosition(value, min, max) {
return (Math.log(value) - Math.log(min)) / (Math.log(max) - Math.log(min));
}
function fromSliderPosition(position, min, max) {
return Math.round(Math.exp(
Math.log(min) + position * (Math.log(max) - Math.log(min))
) / 100) * 100; // Округление до сотен
}
После внедрения доля пользователей, применивших ценовой фильтр, выросла с 12% до 29%, конверсия из фильтрованного каталога — выше на 18% по сравнению с нефильтрованным.
Отображение количества товаров
Динамическое обновление счётчика без перезагрузки каталога:
// Ajax-метод для получения количества
if ($_REQUEST['action'] === 'price_count') {
$min = floatval($_REQUEST['min']);
$max = floatval($_REQUEST['max']);
$res = CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
'>=PRICE' => $min,
'<=PRICE' => $max,
],
[]
);
$count = $res->SelectedRowsCount();
header('Content-Type: application/json');
echo json_encode(['count' => $count]);
die();
}
JavaScript запрашивает счётчик с дебаунсом 600 мс во время движения ползунка и обновляет кнопку «Показать N товаров».
Сроки выполнения
Слайдер с AJAX-обновлением и стандартными параметрами одного типа цены — 2–3 рабочих дня. Полная реализация с логарифмической шкалой, несколькими типами цен, счётчиком товаров и синхронизацией с умным фильтром — 4–6 рабочих дней.







