Создан переиспользуемый компонент поиска товаров
- product_search_picker.html - универсальный шаблон компонента поиска и выбора товаров - product-search-picker.js - JavaScript модуль с поддержкой фильтрации по категориям, тегам, наличию - product-search-picker.css - стили для компонента - Поддержка одиночного и множественного выбора товаров - Фильтрация по категориям, тегам и наличию на складе - Отображение фото товара в результатах поиска - Адаптивный интерфейс с прокруткой для больших списков - API для программного управления (init, search, clearSelection и др.) - Возможность кастомизации через параметры (заголовок, высота, текст кнопок)
This commit is contained in:
170
myproject/products/static/products/css/product-search-picker.css
Normal file
170
myproject/products/static/products/css/product-search-picker.css
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* ProductSearchPicker - стили компонента поиска и выбора товаров
|
||||||
|
*/
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-item {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-item:hover {
|
||||||
|
border-color: #dee2e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-item.selected {
|
||||||
|
border-color: #198754 !important;
|
||||||
|
background-color: #d1e7dd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-item.selected .card {
|
||||||
|
border-color: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid view - карточки */
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item .card {
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item:hover .card {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] .product-picker-item.selected .card {
|
||||||
|
box-shadow: 0 0 0 2px #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List view - список */
|
||||||
|
.product-search-picker .product-picker-grid[data-view="list"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-grid[data-view="list"] .col-6,
|
||||||
|
.product-search-picker .product-picker-grid[data-view="list"] .col-lg-4,
|
||||||
|
.product-search-picker .product-picker-grid[data-view="list"] .col-xl-3 {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.product-search-picker .product-picker-loading {
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контент - scrollable */
|
||||||
|
.product-search-picker .product-picker-content {
|
||||||
|
position: relative;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Фильтры */
|
||||||
|
.product-search-picker .product-picker-filters {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Счетчик выбранных */
|
||||||
|
.product-search-picker .product-picker-selected-count {
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка выбора в list view */
|
||||||
|
.product-search-picker .product-picker-grid[data-view="list"] .bi-circle,
|
||||||
|
.product-search-picker .product-picker-grid[data-view="list"] .bi-check-circle-fill {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка выбора в grid view */
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] .check-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пустой результат */
|
||||||
|
.product-search-picker .product-picker-empty {
|
||||||
|
min-height: 150px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопка очистки поиска */
|
||||||
|
.product-search-picker .product-picker-search-clear {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover эффект для кнопок действий */
|
||||||
|
.product-search-picker .card-footer .btn {
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] .col-6 {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .card-header .d-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch !important;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .card-header .input-group {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .card-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .card-footer > div {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .card-footer .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.product-search-picker .product-picker-grid[data-view="grid"] .col-6 {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-filters .row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-filters .col-auto {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-search-picker .product-picker-filters .col-auto.ms-auto {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
747
myproject/products/static/products/js/product-search-picker.js
Normal file
747
myproject/products/static/products/js/product-search-picker.js
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
/**
|
||||||
|
* ProductSearchPicker - переиспользуемый модуль для поиска и выбора товаров
|
||||||
|
*
|
||||||
|
* API:
|
||||||
|
* - ProductSearchPicker.init(containerSelector, options) - инициализация
|
||||||
|
* - instance.getSelected() - получить выбранные товары (массив объектов)
|
||||||
|
* - instance.getSelectedIds() - получить ID выбранных товаров (массив строк)
|
||||||
|
* - instance.clearSelection() - сбросить выбор
|
||||||
|
* - instance.setSelection(productIds) - установить выбор программно
|
||||||
|
* - instance.refresh() - обновить список товаров
|
||||||
|
* - instance.destroy() - уничтожить экземпляр
|
||||||
|
*
|
||||||
|
* Пример использования:
|
||||||
|
* const picker = ProductSearchPicker.init('#my-picker', {
|
||||||
|
* onAddSelected: function(products, instance) {
|
||||||
|
* console.log('Добавлены товары:', products);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
(function(window) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Приватное хранилище экземпляров
|
||||||
|
var instances = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конструктор компонента
|
||||||
|
*/
|
||||||
|
function ProductSearchPicker(container, options) {
|
||||||
|
this.container = typeof container === 'string'
|
||||||
|
? document.querySelector(container)
|
||||||
|
: container;
|
||||||
|
|
||||||
|
if (!this.container) {
|
||||||
|
console.error('ProductSearchPicker: container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройки по умолчанию
|
||||||
|
this.options = Object.assign({
|
||||||
|
apiUrl: this.container.dataset.apiUrl || '/products/api/search-products-variants/',
|
||||||
|
multiSelect: this.container.dataset.multiSelect !== 'false',
|
||||||
|
maxSelection: parseInt(this.container.dataset.maxSelection) || null,
|
||||||
|
excludeKits: this.container.dataset.excludeKits !== 'false',
|
||||||
|
debounceDelay: 300,
|
||||||
|
pageSize: 30,
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
onSelect: null, // (products, instance) => {}
|
||||||
|
onDeselect: null, // (products, instance) => {}
|
||||||
|
onSelectionChange: null, // (selectedProducts, instance) => {}
|
||||||
|
onAddSelected: null // (selectedProducts, instance) => {}
|
||||||
|
}, options || {});
|
||||||
|
|
||||||
|
// Состояние
|
||||||
|
this.state = {
|
||||||
|
products: [],
|
||||||
|
selected: {}, // {productId: productData}
|
||||||
|
currentPage: 1,
|
||||||
|
hasMore: false,
|
||||||
|
isLoading: false,
|
||||||
|
currentView: 'list',
|
||||||
|
filters: {
|
||||||
|
search: '',
|
||||||
|
category: '',
|
||||||
|
tag: '',
|
||||||
|
inStock: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Кэшируем DOM элементы
|
||||||
|
this._cacheElements();
|
||||||
|
|
||||||
|
// Привязываем обработчики
|
||||||
|
this._bindEvents();
|
||||||
|
|
||||||
|
// Начальная загрузка
|
||||||
|
this._loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кэширование DOM элементов
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._cacheElements = function() {
|
||||||
|
var c = this.container;
|
||||||
|
this.elements = {
|
||||||
|
searchInput: c.querySelector('.product-picker-search'),
|
||||||
|
searchClear: c.querySelector('.product-picker-search-clear'),
|
||||||
|
categorySelect: c.querySelector('.product-picker-category'),
|
||||||
|
tagSelect: c.querySelector('.product-picker-tag'),
|
||||||
|
inStockCheckbox: c.querySelector('.product-picker-in-stock'),
|
||||||
|
grid: c.querySelector('.product-picker-grid'),
|
||||||
|
loading: c.querySelector('.product-picker-loading'),
|
||||||
|
empty: c.querySelector('.product-picker-empty'),
|
||||||
|
content: c.querySelector('.product-picker-content'),
|
||||||
|
selectedCount: c.querySelector('.product-picker-selected-count'),
|
||||||
|
selectAllBtn: c.querySelector('.product-picker-select-all'),
|
||||||
|
clearSelectionBtn: c.querySelector('.product-picker-clear-selection'),
|
||||||
|
addSelectedBtn: c.querySelector('.product-picker-add-selected'),
|
||||||
|
viewButtons: c.querySelectorAll('.product-picker-view')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Начальный вид из data-атрибута
|
||||||
|
if (this.elements.grid) {
|
||||||
|
var initialView = this.elements.grid.dataset.view || 'list';
|
||||||
|
this.state.currentView = initialView;
|
||||||
|
this._updateViewButtons();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Привязка обработчиков событий
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._bindEvents = function() {
|
||||||
|
var self = this;
|
||||||
|
var debounceTimer;
|
||||||
|
|
||||||
|
// Поиск с debounce
|
||||||
|
if (this.elements.searchInput) {
|
||||||
|
this.elements.searchInput.addEventListener('input', function() {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
var value = this.value.trim();
|
||||||
|
|
||||||
|
// Показать/скрыть кнопку очистки
|
||||||
|
if (self.elements.searchClear) {
|
||||||
|
self.elements.searchClear.style.display = value ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimer = setTimeout(function() {
|
||||||
|
self.state.filters.search = value;
|
||||||
|
self.state.currentPage = 1;
|
||||||
|
self._loadProducts();
|
||||||
|
}, self.options.debounceDelay);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Поиск по Enter
|
||||||
|
this.elements.searchInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
self.state.filters.search = this.value.trim();
|
||||||
|
self.state.currentPage = 1;
|
||||||
|
self._loadProducts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка очистки поиска
|
||||||
|
if (this.elements.searchClear) {
|
||||||
|
this.elements.searchClear.addEventListener('click', function() {
|
||||||
|
self.elements.searchInput.value = '';
|
||||||
|
this.style.display = 'none';
|
||||||
|
self.state.filters.search = '';
|
||||||
|
self.state.currentPage = 1;
|
||||||
|
self._loadProducts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтр по категории
|
||||||
|
if (this.elements.categorySelect) {
|
||||||
|
this.elements.categorySelect.addEventListener('change', function() {
|
||||||
|
self.state.filters.category = this.value;
|
||||||
|
self.state.currentPage = 1;
|
||||||
|
self._loadProducts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтр по тегу
|
||||||
|
if (this.elements.tagSelect) {
|
||||||
|
this.elements.tagSelect.addEventListener('change', function() {
|
||||||
|
self.state.filters.tag = this.value;
|
||||||
|
self.state.currentPage = 1;
|
||||||
|
self._loadProducts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтр по наличию
|
||||||
|
if (this.elements.inStockCheckbox) {
|
||||||
|
this.elements.inStockCheckbox.addEventListener('change', function() {
|
||||||
|
self.state.filters.inStock = this.checked;
|
||||||
|
self.state.currentPage = 1;
|
||||||
|
self._loadProducts();
|
||||||
|
});
|
||||||
|
// Инициализация из checkbox
|
||||||
|
this.state.filters.inStock = this.elements.inStockCheckbox.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переключатель вида
|
||||||
|
this.elements.viewButtons.forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
var view = this.dataset.view;
|
||||||
|
self._setView(view);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Клик по товару (делегирование)
|
||||||
|
if (this.elements.grid) {
|
||||||
|
this.elements.grid.addEventListener('click', function(e) {
|
||||||
|
var productCard = e.target.closest('.product-picker-item');
|
||||||
|
if (productCard) {
|
||||||
|
var productId = productCard.dataset.productId;
|
||||||
|
self._toggleProduct(productId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выбрать все
|
||||||
|
if (this.elements.selectAllBtn) {
|
||||||
|
this.elements.selectAllBtn.addEventListener('click', function() {
|
||||||
|
self._selectAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбросить выбор
|
||||||
|
if (this.elements.clearSelectionBtn) {
|
||||||
|
this.elements.clearSelectionBtn.addEventListener('click', function() {
|
||||||
|
self.clearSelection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавить выбранные
|
||||||
|
if (this.elements.addSelectedBtn) {
|
||||||
|
this.elements.addSelectedBtn.addEventListener('click', function() {
|
||||||
|
if (self.options.onAddSelected) {
|
||||||
|
self.options.onAddSelected(self.getSelected(), self);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinite scroll
|
||||||
|
if (this.elements.content) {
|
||||||
|
this.elements.content.addEventListener('scroll', function() {
|
||||||
|
if (self.state.hasMore && !self.state.isLoading) {
|
||||||
|
var scrollTop = this.scrollTop;
|
||||||
|
var scrollHeight = this.scrollHeight;
|
||||||
|
var clientHeight = this.clientHeight;
|
||||||
|
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||||
|
self.state.currentPage++;
|
||||||
|
self._loadProducts(true); // append mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузка товаров через AJAX
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._loadProducts = function(append) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (this.state.isLoading) return;
|
||||||
|
this.state.isLoading = true;
|
||||||
|
|
||||||
|
// Показать индикатор загрузки
|
||||||
|
if (!append) {
|
||||||
|
if (this.elements.loading) this.elements.loading.style.display = 'flex';
|
||||||
|
if (this.elements.grid) this.elements.grid.innerHTML = '';
|
||||||
|
if (this.elements.empty) this.elements.empty.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем параметры запроса
|
||||||
|
var params = new URLSearchParams({
|
||||||
|
type: 'product', // Только штучные товары
|
||||||
|
page: this.state.currentPage
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.filters.search) {
|
||||||
|
params.append('q', this.state.filters.search);
|
||||||
|
}
|
||||||
|
if (this.state.filters.category) {
|
||||||
|
params.append('category', this.state.filters.category);
|
||||||
|
}
|
||||||
|
if (this.state.filters.tag) {
|
||||||
|
params.append('tag', this.state.filters.tag);
|
||||||
|
}
|
||||||
|
if (this.state.filters.inStock) {
|
||||||
|
params.append('in_stock', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(this.options.apiUrl + '?' + params.toString())
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
self.state.isLoading = false;
|
||||||
|
if (self.elements.loading) self.elements.loading.style.display = 'none';
|
||||||
|
|
||||||
|
// Извлекаем товары (учитываем группировку в API)
|
||||||
|
var products = [];
|
||||||
|
if (Array.isArray(data.results)) {
|
||||||
|
data.results.forEach(function(item) {
|
||||||
|
if (item.children) {
|
||||||
|
// Группированные результаты
|
||||||
|
item.children.forEach(function(p) {
|
||||||
|
if (p.type === 'product') {
|
||||||
|
products.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (item.type === 'product') {
|
||||||
|
products.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
self.state.products = self.state.products.concat(products);
|
||||||
|
} else {
|
||||||
|
self.state.products = products;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state.hasMore = data.pagination && data.pagination.more;
|
||||||
|
|
||||||
|
// Рендеринг
|
||||||
|
self._renderProducts(products, append);
|
||||||
|
|
||||||
|
// Показать "пусто" если нет результатов
|
||||||
|
if (self.state.products.length === 0) {
|
||||||
|
if (self.elements.empty) self.elements.empty.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
if (self.elements.empty) self.elements.empty.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('ProductSearchPicker: fetch error', error);
|
||||||
|
self.state.isLoading = false;
|
||||||
|
if (self.elements.loading) self.elements.loading.style.display = 'none';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рендеринг товаров
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._renderProducts = function(products, append) {
|
||||||
|
var self = this;
|
||||||
|
var fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
products.forEach(function(product) {
|
||||||
|
// Извлекаем ID (убираем префикс "product_")
|
||||||
|
var productId = String(product.id).replace('product_', '');
|
||||||
|
var isSelected = self.state.selected.hasOwnProperty(productId);
|
||||||
|
|
||||||
|
var html = self.state.currentView === 'grid'
|
||||||
|
? self._renderGridCard(product, productId, isSelected)
|
||||||
|
: self._renderListItem(product, productId, isSelected);
|
||||||
|
|
||||||
|
var temp = document.createElement('div');
|
||||||
|
temp.innerHTML = html;
|
||||||
|
fragment.appendChild(temp.firstElementChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
this.elements.grid.appendChild(fragment);
|
||||||
|
} else {
|
||||||
|
this.elements.grid.innerHTML = '';
|
||||||
|
this.elements.grid.appendChild(fragment);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рендеринг карточки товара (grid view)
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._renderGridCard = function(product, productId, isSelected) {
|
||||||
|
var inStockBadge = product.in_stock
|
||||||
|
? '<span class="badge bg-success" style="font-size: 0.65rem;">В наличии</span>'
|
||||||
|
: '<span class="badge bg-secondary" style="font-size: 0.65rem;">Нет</span>';
|
||||||
|
|
||||||
|
var selectedClass = isSelected ? 'selected' : '';
|
||||||
|
var checkIcon = isSelected
|
||||||
|
? '<i class="bi bi-check-circle-fill text-success check-icon"></i>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
var photoHtml = product.photo_url
|
||||||
|
? '<img src="' + product.photo_url + '" class="card-img-top" style="height: 100px; object-fit: cover;" alt="' + (product.text || '') + '">'
|
||||||
|
: '<div class="bg-light d-flex align-items-center justify-content-center" style="height: 100px;"><i class="bi bi-image text-muted fs-2"></i></div>';
|
||||||
|
|
||||||
|
var name = product.text || product.name || '';
|
||||||
|
var price = product.actual_price || product.price || '0';
|
||||||
|
|
||||||
|
return '<div class="col-6 col-lg-4 col-xl-3">' +
|
||||||
|
'<div class="card product-picker-item h-100 shadow-sm ' + selectedClass + '" ' +
|
||||||
|
'data-product-id="' + productId + '" style="cursor: pointer;">' +
|
||||||
|
'<div class="position-relative">' +
|
||||||
|
photoHtml +
|
||||||
|
checkIcon +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="card-body p-2">' +
|
||||||
|
'<div class="small text-truncate fw-medium">' + name + '</div>' +
|
||||||
|
'<div class="d-flex justify-content-between align-items-center mt-1">' +
|
||||||
|
'<span class="fw-bold text-primary small">' + price + ' руб.</span>' +
|
||||||
|
inStockBadge +
|
||||||
|
'</div>' +
|
||||||
|
'<small class="text-muted">' + (product.sku || '') + '</small>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рендеринг строки товара (list view)
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._renderListItem = function(product, productId, isSelected) {
|
||||||
|
var inStockBadge = product.in_stock
|
||||||
|
? '<span class="badge bg-success">В наличии</span>'
|
||||||
|
: '<span class="badge bg-secondary">Нет</span>';
|
||||||
|
|
||||||
|
var selectedClass = isSelected ? 'selected' : '';
|
||||||
|
var checkIcon = isSelected
|
||||||
|
? '<i class="bi bi-check-circle-fill text-success me-2"></i>'
|
||||||
|
: '<i class="bi bi-circle text-muted me-2"></i>';
|
||||||
|
|
||||||
|
var photoHtml = product.photo_url
|
||||||
|
? '<img src="' + product.photo_url + '" class="rounded" style="width: 40px; height: 40px; object-fit: cover;">'
|
||||||
|
: '<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-image text-muted"></i></div>';
|
||||||
|
|
||||||
|
var name = product.text || product.name || '';
|
||||||
|
var price = product.actual_price || product.price || '0';
|
||||||
|
|
||||||
|
return '<div class="col-12">' +
|
||||||
|
'<div class="product-picker-item d-flex align-items-center p-2 border rounded ' + selectedClass + '" ' +
|
||||||
|
'data-product-id="' + productId + '" style="cursor: pointer;">' +
|
||||||
|
checkIcon +
|
||||||
|
'<div class="flex-shrink-0 me-2">' +
|
||||||
|
photoHtml +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="flex-grow-1" style="min-width: 0;">' +
|
||||||
|
'<div class="text-truncate fw-medium">' + name + '</div>' +
|
||||||
|
'<small class="text-muted">' + (product.sku || '') + '</small>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="text-end ms-2 text-nowrap">' +
|
||||||
|
'<div class="fw-bold text-primary">' + price + ' руб.</div>' +
|
||||||
|
inStockBadge +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение выбора товара
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._toggleProduct = function(productId) {
|
||||||
|
var self = this;
|
||||||
|
var product = null;
|
||||||
|
|
||||||
|
// Находим товар в списке
|
||||||
|
for (var i = 0; i < this.state.products.length; i++) {
|
||||||
|
var p = this.state.products[i];
|
||||||
|
if (String(p.id).replace('product_', '') === productId) {
|
||||||
|
product = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
var isSelected = this.state.selected.hasOwnProperty(productId);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
// Снять выбор
|
||||||
|
delete this.state.selected[productId];
|
||||||
|
if (this.options.onDeselect) {
|
||||||
|
this.options.onDeselect([product], this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Проверка множественного выбора
|
||||||
|
if (!this.options.multiSelect) {
|
||||||
|
this.state.selected = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка лимита
|
||||||
|
if (this.options.maxSelection &&
|
||||||
|
Object.keys(this.state.selected).length >= this.options.maxSelection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавить в выбор
|
||||||
|
this.state.selected[productId] = product;
|
||||||
|
if (this.options.onSelect) {
|
||||||
|
this.options.onSelect([product], this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновить UI
|
||||||
|
this._updateProductUI(productId);
|
||||||
|
this._updateSelectionUI();
|
||||||
|
|
||||||
|
// Общий callback
|
||||||
|
if (this.options.onSelectionChange) {
|
||||||
|
this.options.onSelectionChange(this.getSelected(), this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновление UI товара
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._updateProductUI = function(productId) {
|
||||||
|
var card = this.elements.grid.querySelector('[data-product-id="' + productId + '"]');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
var isSelected = this.state.selected.hasOwnProperty(productId);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
card.classList.add('selected');
|
||||||
|
// Обновить иконку
|
||||||
|
var icon = card.querySelector('.bi-circle');
|
||||||
|
if (icon) {
|
||||||
|
icon.classList.remove('bi-circle', 'text-muted');
|
||||||
|
icon.classList.add('bi-check-circle-fill', 'text-success');
|
||||||
|
}
|
||||||
|
// Добавить иконку в grid view если её нет
|
||||||
|
if (this.state.currentView === 'grid') {
|
||||||
|
var posRel = card.querySelector('.position-relative');
|
||||||
|
if (posRel && !posRel.querySelector('.check-icon')) {
|
||||||
|
var checkEl = document.createElement('i');
|
||||||
|
checkEl.className = 'bi bi-check-circle-fill text-success check-icon';
|
||||||
|
posRel.appendChild(checkEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
card.classList.remove('selected');
|
||||||
|
var iconCheck = card.querySelector('.bi-check-circle-fill');
|
||||||
|
if (iconCheck) {
|
||||||
|
// В list view меняем иконку
|
||||||
|
if (this.state.currentView === 'list') {
|
||||||
|
iconCheck.classList.remove('bi-check-circle-fill', 'text-success');
|
||||||
|
iconCheck.classList.add('bi-circle', 'text-muted');
|
||||||
|
} else {
|
||||||
|
// В grid view удаляем иконку
|
||||||
|
if (iconCheck.classList.contains('check-icon')) {
|
||||||
|
iconCheck.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновление UI выбора (счетчик, кнопки)
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._updateSelectionUI = function() {
|
||||||
|
var count = Object.keys(this.state.selected).length;
|
||||||
|
|
||||||
|
// Счетчик
|
||||||
|
if (this.elements.selectedCount) {
|
||||||
|
this.elements.selectedCount.textContent = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка "Добавить"
|
||||||
|
if (this.elements.addSelectedBtn) {
|
||||||
|
this.elements.addSelectedBtn.disabled = count === 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выбрать все видимые товары
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._selectAll = function() {
|
||||||
|
var self = this;
|
||||||
|
var currentCount = Object.keys(this.state.selected).length;
|
||||||
|
var maxToSelect = this.options.maxSelection
|
||||||
|
? this.options.maxSelection - currentCount
|
||||||
|
: Infinity;
|
||||||
|
|
||||||
|
var added = 0;
|
||||||
|
this.state.products.forEach(function(product) {
|
||||||
|
if (added >= maxToSelect) return;
|
||||||
|
|
||||||
|
var productId = String(product.id).replace('product_', '');
|
||||||
|
if (!self.state.selected.hasOwnProperty(productId)) {
|
||||||
|
self.state.selected[productId] = product;
|
||||||
|
self._updateProductUI(productId);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._updateSelectionUI();
|
||||||
|
|
||||||
|
if (this.options.onSelectionChange) {
|
||||||
|
this.options.onSelectionChange(this.getSelected(), this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновление кнопок переключателя вида
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._updateViewButtons = function() {
|
||||||
|
var self = this;
|
||||||
|
this.elements.viewButtons.forEach(function(btn) {
|
||||||
|
if (btn.dataset.view === self.state.currentView) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение вида (grid/list)
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype._setView = function(view) {
|
||||||
|
this.state.currentView = view;
|
||||||
|
|
||||||
|
// Обновить data-атрибут
|
||||||
|
if (this.elements.grid) {
|
||||||
|
this.elements.grid.dataset.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновить кнопки
|
||||||
|
this._updateViewButtons();
|
||||||
|
|
||||||
|
// Перерисовать
|
||||||
|
this._renderProducts(this.state.products, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== ПУБЛИЧНОЕ API ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить массив выбранных товаров
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype.getSelected = function() {
|
||||||
|
var result = [];
|
||||||
|
for (var key in this.state.selected) {
|
||||||
|
if (this.state.selected.hasOwnProperty(key)) {
|
||||||
|
result.push(this.state.selected[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить массив ID выбранных товаров
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype.getSelectedIds = function() {
|
||||||
|
return Object.keys(this.state.selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сбросить выбор
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype.clearSelection = function() {
|
||||||
|
var self = this;
|
||||||
|
var ids = Object.keys(this.state.selected);
|
||||||
|
this.state.selected = {};
|
||||||
|
|
||||||
|
ids.forEach(function(productId) {
|
||||||
|
self._updateProductUI(productId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._updateSelectionUI();
|
||||||
|
|
||||||
|
if (this.options.onSelectionChange) {
|
||||||
|
this.options.onSelectionChange([], this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить выбор программно
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype.setSelection = function(productIds) {
|
||||||
|
var self = this;
|
||||||
|
this.clearSelection();
|
||||||
|
|
||||||
|
productIds.forEach(function(id) {
|
||||||
|
var productId = String(id).replace('product_', '');
|
||||||
|
// Ищем товар в загруженном списке
|
||||||
|
for (var i = 0; i < self.state.products.length; i++) {
|
||||||
|
var p = self.state.products[i];
|
||||||
|
if (String(p.id).replace('product_', '') === productId) {
|
||||||
|
self.state.selected[productId] = p;
|
||||||
|
self._updateProductUI(productId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._updateSelectionUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновить список товаров
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype.refresh = function() {
|
||||||
|
this.state.currentPage = 1;
|
||||||
|
this._loadProducts();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Уничтожить экземпляр
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.prototype.destroy = function() {
|
||||||
|
// Очистить состояние
|
||||||
|
this.state.products = [];
|
||||||
|
this.state.selected = {};
|
||||||
|
|
||||||
|
// Удалить из хранилища
|
||||||
|
if (this.container.id && instances[this.container.id]) {
|
||||||
|
delete instances[this.container.id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== СТАТИЧЕСКИЕ МЕТОДЫ ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация компонента
|
||||||
|
* @param {string|Element} container - селектор или DOM элемент
|
||||||
|
* @param {Object} options - настройки
|
||||||
|
* @returns {ProductSearchPicker}
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.init = function(container, options) {
|
||||||
|
var element = typeof container === 'string'
|
||||||
|
? document.querySelector(container)
|
||||||
|
: container;
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
console.error('ProductSearchPicker.init: container not found');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, не инициализирован ли уже
|
||||||
|
if (element.id && instances[element.id]) {
|
||||||
|
return instances[element.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = new ProductSearchPicker(element, options);
|
||||||
|
|
||||||
|
if (element.id) {
|
||||||
|
instances[element.id] = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить экземпляр по контейнеру
|
||||||
|
*/
|
||||||
|
ProductSearchPicker.getInstance = function(container) {
|
||||||
|
var element = typeof container === 'string'
|
||||||
|
? document.querySelector(container)
|
||||||
|
: container;
|
||||||
|
return element && element.id ? instances[element.id] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Экспорт в глобальную область
|
||||||
|
window.ProductSearchPicker = ProductSearchPicker;
|
||||||
|
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
{% comment %}
|
||||||
|
Переиспользуемый компонент поиска и выбора штучных товаров (Product).
|
||||||
|
|
||||||
|
Параметры (Django template variables):
|
||||||
|
- container_id: уникальный ID контейнера (default: 'product-search-picker')
|
||||||
|
- title: заголовок компонента (default: 'Выбор товаров')
|
||||||
|
- api_url: URL для AJAX поиска (default: '/products/api/search-products-variants/')
|
||||||
|
- show_filters: показывать фильтры (default: True)
|
||||||
|
- show_view_toggle: показывать переключатель вида (default: True)
|
||||||
|
- show_select_all: показывать кнопку "Выбрать все" (default: True)
|
||||||
|
- show_add_button: показывать кнопку "Добавить выбранные" (default: True)
|
||||||
|
- add_button_text: текст кнопки добавления (default: 'Добавить выбранные')
|
||||||
|
- initial_view: начальный вид 'grid' или 'list' (default: 'list')
|
||||||
|
- multi_select: множественный выбор (default: True)
|
||||||
|
- max_selection: максимальное количество выбранных товаров (default: null)
|
||||||
|
- filter_in_stock_only: показывать только товары в наличии (default: False)
|
||||||
|
- categories: список категорий для фильтра (queryset или list)
|
||||||
|
- tags: список тегов для фильтра (queryset или list)
|
||||||
|
- content_height: высота контейнера с товарами (default: '400px')
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
{% include 'products/components/product_search_picker.html' with
|
||||||
|
container_id='writeoff-products'
|
||||||
|
title='Выберите товары для списания'
|
||||||
|
show_filters=True
|
||||||
|
filter_in_stock_only=True
|
||||||
|
categories=categories
|
||||||
|
%}
|
||||||
|
|
||||||
|
После включения инициализируйте JS:
|
||||||
|
<script>
|
||||||
|
ProductSearchPicker.init('#writeoff-products', {
|
||||||
|
onAddSelected: function(products, instance) {
|
||||||
|
// products = [{id, text, sku, price, in_stock, photo_url}, ...]
|
||||||
|
products.forEach(addToForm);
|
||||||
|
instance.clearSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<div class="product-search-picker"
|
||||||
|
id="{{ container_id|default:'product-search-picker' }}"
|
||||||
|
data-api-url="{{ api_url|default:'/products/api/search-products-variants/' }}"
|
||||||
|
data-multi-select="{{ multi_select|default:'true' }}"
|
||||||
|
data-max-selection="{{ max_selection|default:'' }}"
|
||||||
|
data-exclude-kits="true">
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<!-- Заголовок -->
|
||||||
|
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<strong>
|
||||||
|
<i class="bi bi-box-seam text-primary"></i>
|
||||||
|
{{ title|default:'Выбор товаров' }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 align-items-center flex-wrap">
|
||||||
|
<!-- Поиск -->
|
||||||
|
<div class="input-group" style="width: 250px;">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm product-picker-search"
|
||||||
|
placeholder="Поиск по названию, артикулу...">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm product-picker-search-clear"
|
||||||
|
type="button" style="display: none;">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if show_view_toggle|default:True %}
|
||||||
|
<!-- Переключатель вида -->
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-secondary product-picker-view"
|
||||||
|
data-view="grid" title="Карточки">
|
||||||
|
<i class="bi bi-grid-3x3-gap"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary product-picker-view"
|
||||||
|
data-view="list" title="Список">
|
||||||
|
<i class="bi bi-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if show_filters|default:True %}
|
||||||
|
<!-- Фильтры -->
|
||||||
|
<div class="card-body border-bottom py-2 product-picker-filters">
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
{% if categories %}
|
||||||
|
<!-- Фильтр по категории -->
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm product-picker-category">
|
||||||
|
<option value="">Все категории</option>
|
||||||
|
{% for cat in categories %}
|
||||||
|
<option value="{{ cat.id }}">{{ cat.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tags %}
|
||||||
|
<!-- Фильтр по тегам -->
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select form-select-sm product-picker-tag">
|
||||||
|
<option value="">Все теги</option>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<option value="{{ tag.id }}">{{ tag.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Фильтр по наличию -->
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input product-picker-in-stock"
|
||||||
|
type="checkbox"
|
||||||
|
id="{{ container_id|default:'product-search-picker' }}-in-stock"
|
||||||
|
{% if filter_in_stock_only|default:False %}checked{% endif %}>
|
||||||
|
<label class="form-check-label small"
|
||||||
|
for="{{ container_id|default:'product-search-picker' }}-in-stock">
|
||||||
|
Только в наличии
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Счетчик выбранных -->
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<span class="badge bg-primary product-picker-selected-count">0</span>
|
||||||
|
<span class="text-muted small">выбрано</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Контент: сетка/список товаров -->
|
||||||
|
<div class="card-body product-picker-content" style="max-height: {{ content_height|default:'400px' }}; overflow-y: auto;">
|
||||||
|
<!-- Индикатор загрузки -->
|
||||||
|
<div class="product-picker-loading text-center py-4" style="display: none;">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сетка товаров -->
|
||||||
|
<div class="row g-2 product-picker-grid" data-view="{{ initial_view|default:'list' }}">
|
||||||
|
<!-- Товары загружаются через AJAX -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Пустой результат -->
|
||||||
|
<div class="product-picker-empty text-center py-4 text-muted" style="display: none;">
|
||||||
|
<i class="bi bi-search fs-1 opacity-25"></i>
|
||||||
|
<p class="mb-0 mt-2">Товары не найдены</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Футер с кнопками действий -->
|
||||||
|
<div class="card-footer bg-white py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
{% if show_select_all|default:True %}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm product-picker-select-all">
|
||||||
|
<i class="bi bi-check2-all"></i> Выбрать все
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm product-picker-clear-selection">
|
||||||
|
<i class="bi bi-x-lg"></i> Сбросить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_add_button|default:True %}
|
||||||
|
<button class="btn btn-primary btn-sm product-picker-add-selected" disabled>
|
||||||
|
<i class="bi bi-plus-circle"></i>
|
||||||
|
{{ add_button_text|default:'Добавить выбранные' }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user