barneo-search-widget-lib
Version:
Библиотека для поиска по каталогу Barneo на Vue 3
1,788 lines (1,475 loc) • 55.7 kB
Markdown
# Barneo Search Widget
Библиотека компонентов для поиска по каталогу Barneo с помощью Ensi
## 📋 Содержание
- [Установка](#-установка)
- [Подключение и конфигурация](#️-подключение-и-конфигурация)
- [Использование библиотеки](#-использование-библиотеки)
- [Компоненты](#-компоненты)
- [Кастомные слоты](#-кастомные-слоты---примеры)
- [Аналитика](#-аналитика)
- [Дополнительные возможности](#-дополнительные-возможности)
- [TypeScript поддержка](#-typescript-поддержка)
- [Поддержка](#-поддержка)
- [Лицензия](#-лицензия)
## 📦 Установка
```bash
npm install barneo-search-widget-lib
# или
yarn add barneo-search-widget-lib
```
## ⚙️ Подключение и конфигурация
### Vue 3
#### Глобальная регистрация (рекомендуется)
```typescript
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import BarneoSearchPlugin from "barneo-search-widget-lib";
const app = createApp(App);
const config = {
api: {
baseUrl: "https://api.search.ensi.cloud",
token: "your-api-token",
customerId: "4",
locationId: "1",
apiVersion: "v1",
},
searchWidget: {
placeholder: "Найти товары",
debounceMs: 300,
minSearchLength: 2,
darkMode: false,
focusMode: false,
},
filtersWidget: {
maxValuesPerFilter: 5,
debounceMs: 300,
darkMode: false,
},
productsWidget: {
itemsPerPage: 20,
mobileItemWidth: 160,
desktopItemWidth: 200,
darkMode: false,
showPagination: true,
showTotalCount: true,
},
sortWidget: {
darkMode: false,
defaultSort: "relevance",
},
};
app.use(BarneoSearchPlugin, config);
app.mount("#app");
```
**Примечание**: URL менеджер инициализируется в компонентах через `useUrlManagerInit()` composable, а не автоматически в плагине.
#### Локальная регистрация
```vue
<template>
<div>
<barneo-search-widget v-model="query" />
<barneo-filters-widget />
<barneo-sort-widget />
<barneo-products-widget />
</div>
</template>
<script setup>
import { ref } from "vue";
import {
BarneoSearchWidget,
BarneoFiltersWidget,
BarneoSortWidget,
BarneoProductsWidget,
} from "barneo-search-widget-lib";
const query = ref("");
</script>
```
### Nuxt 3
#### Создание плагина
```typescript
// plugins/barneo-search.client.ts
import { defineNuxtPlugin } from "nuxt/app";
import BarneoSearchPlugin from "barneo-search-widget-lib";
export default defineNuxtPlugin((nuxtApp: any) => {
const config = {
api: {
baseUrl:
process.env.NUXT_BARNEO_ENSI_BASE_URL ||
"https://api.search.ensi.cloud",
token:
process.env.NUXT_BARNEO_ENSI_TOKEN ||
"95246bb090387d5c92186f1b6d25cc72",
customerId: process.env.NUXT_BARNEO_ENSI_CUSTOMER_ID || "4",
locationId: process.env.NUXT_BARNEO_ENSI_LOCATION_ID || "1",
apiVersion: process.env.NUXT_BARNEO_ENSI_API_VERSION || "v1",
},
searchWidget: {
placeholder: "Найти товары",
debounceMs: 300,
minSearchLength: 2,
darkMode: false,
focusMode: false,
},
filtersWidget: {
maxValuesPerFilter: 5,
debounceMs: 300,
darkMode: false,
},
productsWidget: {
itemsPerPage: 20,
mobileItemWidth: 160,
desktopItemWidth: 200,
darkMode: false,
showPagination: true,
showTotalCount: true,
},
sortWidget: {
darkMode: false,
defaultSort: "relevance",
},
};
// Используем плагин библиотеки для глобальной регистрации
nuxtApp.vueApp.use(BarneoSearchPlugin, config);
// URL менеджер инициализируется в компонентах через useUrlManagerInit()
});
```
#### Настройка в nuxt.config.ts
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
css: ["barneo-search-widget-lib/dist/style.css"],
runtimeConfig: {
public: {
barneoEnsiBaseUrl: process.env.NUXT_BARNEO_ENSI_BASE_URL,
barneoEnsiToken: process.env.NUXT_BARNEO_ENSI_TOKEN,
barneoEnsiCustomerId: process.env.NUXT_BARNEO_ENSI_CUSTOMER_ID,
barneoEnsiLocationId: process.env.NUXT_BARNEO_ENSI_LOCATION_ID,
barneoEnsiApiVersion: process.env.NUXT_BARNEO_ENSI_API_VERSION,
},
},
});
```
### Описание конфигурации
#### API конфигурация
```typescript
api: {
baseUrl: string, // Базовый URL API (обязательно)
token: string, // API токен для авторизации (обязательно)
customerId: string, // ID клиента (обязательно)
locationId: string, // ID локации (обязательно)
apiVersion?: string // Версия API (опционально, если не указана используется только baseUrl)
}
```
#### Конфигурация поиска
```typescript
searchWidget: {
placeholder: string, // Placeholder для поля поиска (по умолчанию: 'Поиск...')
debounceMs: number, // Задержка перед отправкой запроса в мс (по умолчанию: 300)
minSearchLength: number, // Минимальная длина для поиска (по умолчанию: 2)
darkMode: boolean, // Принудительная темная тема (по умолчанию: false)
focusMode: boolean // Автофокус при монтировании (по умолчанию: false)
}
```
#### Конфигурация фильтров
```typescript
filtersWidget: {
maxValuesPerFilter: number, // Максимум значений в фильтре (по умолчанию: 5)
debounceMs: number, // Задержка применения фильтров в мс (по умолчанию: 300)
darkMode: boolean // Принудительная темная тема (по умолчанию: false)
}
```
#### Конфигурация товаров
```typescript
productsWidget: {
itemsPerPage: number, // Количество товаров на странице (по умолчанию: 20)
mobileItemWidth: number, // Ширина элемента на мобильных (по умолчанию: 160)
desktopItemWidth: number, // Ширина элемента на десктопе (по умолчанию: 200)
darkMode: boolean, // Принудительная темная тема (по умолчанию: false)
showPagination: boolean, // Показывать пагинацию (по умолчанию: true)
showTotalCount: boolean // Показывать общее количество (по умолчанию: true)
}
```
#### Конфигурация сортировки
```typescript
sortWidget: {
darkMode: boolean, // Принудительная темная тема (по умолчанию: false)
defaultSort: string // Сортировка по умолчанию (по умолчанию: 'relevance')
}
```
## 🚀 Использование библиотеки
### Базовое использование
```vue
<template>
<div>
<barneo-search-widget v-model="searchQuery" @search="handleSearch" />
<barneo-filters-widget @filter-applied="handleFilter" />
<barneo-sort-widget @sort-changed="handleSort" />
<barneo-products-widget @product-selected="handleProductSelect" />
<barneo-range-selector
ref="rangeSelector"
:min="0"
:max="10000"
@change="handleRangeChange"
/>
</div>
</template>
<script setup>
import { ref } from "vue";
const searchQuery = ref("");
const rangeSelector = ref();
const handleSearch = (query) => {
console.log("Поиск выполнен:", query);
};
const handleFilter = (filterCode, valueId, isSelected) => {
console.log("Фильтр применен:", filterCode, valueId, isSelected);
};
const handleSort = (sortValue) => {
console.log("Сортировка изменена:", sortValue);
};
const handleProductSelect = (product) => {
console.log("Товар выбран:", product);
};
const handleRangeChange = ([min, max]) => {
console.log("Диапазон изменен:", { min, max });
};
</script>
```
### Использование composables
```vue
<template>
<div>
<input v-model="searchQuery" @input="handleSearch" />
<div v-if="isLoading">Загрузка...</div>
<div v-else>
<div v-for="result in searchResults" :key="result.id">
{{ result.name }}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useSearchWidget, SearchApiService } from "barneo-search-widget-lib";
const searchQuery = ref("");
const isLoading = ref(false);
const searchResults = ref([]);
const { performSearch } = useSearchWidget();
const apiService = new SearchApiService({
baseUrl: "https://api.search.ensi.cloud",
token: "your-token",
});
const handleSearch = async () => {
if (searchQuery.value.length < 2) return;
isLoading.value = true;
try {
const results = await performSearch(searchQuery.value, apiService);
searchResults.value = results.data?.full_products || [];
} catch (error) {
console.error("Ошибка поиска:", error);
} finally {
isLoading.value = false;
}
};
</script>
```
## 🧩 Компоненты
### BarneoSearchWidget
Компонент поиска с автодополнением, быстрыми результатами и полным поиском.
#### Props
```typescript
interface SearchWidgetProps {
modelValue?: string; // Значение поля поиска (v-model)
placeholder?: string; // Placeholder для поля ввода
debounceMs?: number; // Задержка перед отправкой запроса
minSearchLength?: number; // Минимальная длина для поиска
darkMode?: boolean; // Принудительная темная тема
focusMode?: boolean; // Автофокус при монтировании
apiService?: SearchApiService; // Кастомный API сервис
}
```
#### События
- `@search` - выполнен поиск (автоматический или ручной)
```typescript
(query: string) => void
```
- `@select` - выбран товар из результатов
```typescript
(product: Product, event?: MouseEvent) => void
```
**Примечание**: Событие поддерживает `MouseEvent` для обработки средней кнопки мыши. При `event.button === 1` рекомендуется открывать товар в новой вкладке.
- `@searchResults` - получены результаты поиска
```typescript
(results: SearchResults) => void
```
- `@loading` - изменение состояния загрузки
```typescript
(isLoading: boolean) => void
```
- `@error` - произошла ошибка
```typescript
(error: string) => void
```
- `@focus` - поле получило фокус
```typescript
() => void
```
- `@blur` - поле потеряло фокус
```typescript
() => void
```
- `@input` - изменение значения поля
```typescript
(value: string) => void
```
#### Слоты
- `search-header` - заголовок компонента поиска
```vue
<template #search-header="{ config }">
<h1>🔍 Поиск товаров</h1>
</template>
```
- `search-input` - кастомное поле ввода
```vue
<template #search-input="{ value, onInput, onFocus, onBlur }">
<input :value="value" @input="onInput" @focus="onFocus" @blur="onBlur" />
</template>
```
- `search-results` - кастомные результаты поиска
```vue
<template
#search-results="{ results, isLoading, selectProduct, handleAuxClick }"
>
<div v-if="isLoading">Загрузка...</div>
<div v-else>
<div
v-for="result in results"
:key="result.id"
@click="(event) => selectProduct(result, event)"
@auxclick="(event) => handleAuxClick(result, event)"
>
{{ result.name }}
</div>
</div>
</template>
```
- `search-empty` - пустое состояние
```vue
<template #search-empty="{ query }">
<p>По запросу "{{ query }}" ничего не найдено</p>
</template>
```
- `loading` - состояние загрузки
```vue
<template #loading="{ isLoading }">
<div v-if="isLoading">⏳ Загрузка...</div>
</template>
```
- `error` - состояние ошибки
```vue
<template #error="{ error }">
<div v-if="error">❌ {{ error }}</div>
</template>
```
### BarneoFiltersWidget
Компонент фильтров с динамической загрузкой, автоматическим применением и кастомными слотами.
#### Props
```typescript
interface FiltersWidgetProps {
loading?: boolean; // Состояние загрузки
error?: string | null; // Текст ошибки
activeFilters?: Record<string, string[]>; // Активные фильтры
config?: FiltersWidgetConfig; // Локальная конфигурация
apiService?: SearchApiService; // Кастомный API сервис
}
```
#### События
- `@filter-applied` - применен фильтр
```typescript
(filterCode: string, valueId: string, isSelected: boolean) => void
```
- `@filter-removed` - удален фильтр
```typescript
(filterCode: string, valueId: string) => void
```
- `@filters-reset` - сброшены все фильтры
```typescript
() => void
```
- `@filters-applied` - применены фильтры
```typescript
(activeFilters: Record<string, string[]>) => void
```
- `@active-filters-changed` - изменились активные фильтры
```typescript
(activeFilters: Record<string, string[]>) => void
```
- `@loading` - изменение состояния загрузки
```typescript
(isLoading: boolean) => void
```
- `@error` - произошла ошибка
```typescript
(error: string) => void
```
#### Слоты
- `filters-title` - заголовок фильтров
```vue
<template #filters-title="{ config }">
<h2>🎯 Фильтры ({{ config.maxValuesPerFilter }} значений)</h2>
</template>
```
- `filters-skeleton` - скелетон загрузки
```vue
<template #filters-skeleton="{ isLoading, skeletonData }">
<div v-if="isLoading">
<div v-for="group in skeletonData" :key="group.id">
<!-- Скелетон группы -->
</div>
</div>
</template>
```
````
- `filter-group` - группа фильтров
```vue
<template
#filter-group="{ filter, visibleValues, isExpanded, toggleFilterExpansion }"
>
<div class="filter-group">
<h3>{{ filter.name }}</h3>
<div v-if="isExpanded">
<!-- Значения фильтров -->
</div>
<button @click="toggleFilterExpansion(filter.code)">
{{ isExpanded ? "Свернуть" : "Развернуть" }}
</button>
</div>
</template>
````
- `filter-value` - значение фильтра
```vue
<template #filter-value="{ filterCode, value, isActive, handleClick }">
<div @click="handleClick(filterCode, value.id)">
<input type="checkbox" :checked="isActive" />
<label>{{ value.name }}</label>
</div>
</template>
```
- `filter-checkbox` - кастомный чекбокс
```vue
<template #filter-checkbox="{ filterCode, value, isActive, handleClick }">
<div class="custom-checkbox">
<input
type="checkbox"
:checked="isActive"
@change="handleClick(filterCode, value.id)"
/>
<label>{{ value.name }}</label>
</div>
</template>
```
````
- `filter-toggle` - кнопка "Показать все"
```vue
<template #filter-toggle="{ filterCode, isExpanded, handleToggle }">
<button @click="handleToggle(filterCode)">
{{ isExpanded ? "Скрыть" : "Показать все" }}
</button>
</template>
````
- `filter-range` - кастомный range фильтр с под-слотами
```vue
<template
#filter-range="{
filter,
handleRangeSelectorChange,
rangeSelectorValue,
rangeStep,
rangeFormatValue,
isRangeActive,
rangeMin,
rangeMax,
}"
>
<div class="custom-range-filter">
<!-- Заголовок range фильтра -->
<template #filter-range-header="{ filter }">
<h4>{{ filter.description }}</h4>
</template>
<!-- Индикатор активного диапазона -->
<template #filter-range-indicator="{ isRangeActive, rangeFormatValue }">
<div v-if="isRangeActive" class="range-indicator">
Выбран диапазон: {{ rangeFormatValue }}
</div>
</template>
<!-- Кнопка сброса -->
<template
#filter-range-reset="{ handleRangeSelectorChange, rangeMin, rangeMax }"
>
<button
@click="handleRangeSelectorChange(filter.code, [rangeMin, rangeMax])"
>
Сбросить
</button>
</template>
</div>
</template>
```
- `filters-empty` - пустое состояние
```vue
<template #filters-empty="{ hasFilters }">
<div v-if="!hasFilters">
<p>🔍 Выполните поиск для получения фильтров</p>
</div>
</template>
```
- `loading` - состояние загрузки
```vue
<template #loading="{ isLoading }">
<div v-if="isLoading">⏳ Загрузка фильтров...</div>
</template>
```
- `error` - состояние ошибки
```vue
<template #error="{ error }">
<div v-if="error">❌ {{ error }}</div>
</template>
```
````
### BarneoSortWidget
Компонент сортировки с предустановленными опциями и кастомными слотами.
#### Props
```typescript
interface SortWidgetProps {
modelValue?: string; // Текущая сортировка (v-model)
options?: SortOption[]; // Кастомные опции сортировки
showLabels?: boolean; // Показывать метки опций
defaultSort?: string; // Сортировка по умолчанию
darkMode?: boolean; // Принудительная темная тема
disabled?: boolean; // Отключить компонент
}
````
#### События
- `@update:modelValue` - изменена сортировка
```typescript
(sortValue: string) => void
```
- `@sort-changed` - изменена сортировка
```typescript
(sortValue: string) => void
```
- `@loading` - изменение состояния загрузки
```typescript
(isLoading: boolean) => void
```
- `@error` - произошла ошибка
```typescript
(error: string) => void
```
#### Слоты
- `sort-options` - кастомный контейнер с опциями сортировки
```vue
<template
#sort-options="{ sortOptions, currentSort, isLoading, handleSortChange }"
>
<div class="custom-sort-container">
<div
v-for="option in sortOptions"
:key="option.value"
class="custom-sort-option"
:class="{ active: option.value === currentSort }"
@click="handleSortChange(option.value)"
>
{{ option.label }}
</div>
</div>
</template>
```
- `sort-option` - кастомная опция сортировки
```vue
<template #sort-option="{ option, isActive, isDisabled }">
<div
class="custom-sort-option"
:class="{ active: isActive, disabled: isDisabled }"
>
<span>{{ option.label }}</span>
<span v-if="isActive" class="loading-indicator">⏳</span>
</div>
</template>
```
- `loading` - состояние загрузки
```vue
<template #loading="{ isLoading }">
<div v-if="isLoading">⏳ Обновление...</div>
</template>
```
- `error` - состояние ошибки
```vue
<template #error="{ error }">
<div v-if="error">❌ {{ error }}</div>
</template>
```
### BarneoRangeSelector
Компонент для выбора диапазонов с прогрессивной шкалой, двухползунковым слайдером и капсуловидными полями ввода.
#### Особенности
- **Прогрессивная шкала**: Более точный выбор в нижних диапазонах (первые 50% движения покрывают 25% значений)
- **Двухползунковый слайдер**: Интуитивный выбор минимального и максимального значения
- **Капсуловидные поля ввода**: Прямой ввод значений с метками "ОТ" и "ДО"
- **Конвертация цен**: Автоматическая конвертация копеек в рубли для ценовых фильтров
- **Адаптивная разделительная линия**: Линия между полями ввода растягивается на доступное пространство
- **События при отпускании**: События эмитятся только при отпускании ползунка для оптимизации производительности
#### Props
```typescript
interface RangeSelectorProps {
min: number; // Минимальное значение
max: number; // Максимальное значение
value?: [number, number]; // Текущие значения [min, max]
step?: number; // Шаг изменения (по умолчанию: 1)
formatValue?: (value: number) => string; // Функция форматирования значений
}
```
#### События
- `@update:value` - изменены значения
```typescript
(value: [number, number]) => void
```
- `@change` - изменены значения (эмитится при отпускании ползунка или потере фокуса)
```typescript
(value: [number, number]) => void
```
#### Методы (через ref)
```typescript
interface RangeSelectorExpose {
setValue: (min: number, max: number) => void; // Установить значения
getValue: () => [number, number]; // Получить текущие значения
}
```
#### Пример использования
```vue
<template>
<barneo-range-selector
ref="rangeSelector"
:min="0"
:max="10000"
:value="[1000, 5000]"
:step="100"
:format-value="formatPrice"
@change="handleRangeChange"
/>
</template>
<script setup>
import { ref } from "vue";
const rangeSelector = ref();
const formatPrice = (value: number) => {
return `${Math.floor(value / 100)} ₽`; // Конвертация копеек в рубли
};
const handleRangeChange = ([min, max]: [number, number]) => {
console.log("Диапазон изменен:", { min, max });
};
// Программное изменение значений
const resetRange = () => {
rangeSelector.value?.setValue(0, 10000);
};
</script>
```
### BarneoProductsWidget
Компонент отображения товаров с пагинацией, адаптивной сеткой и кастомными слотами.
#### Props
```typescript
interface ProductsWidgetProps {
products?: Product[]; // Список товаров для отображения
loading?: boolean; // Состояние загрузки
error?: string | null; // Текст ошибки
pagination?: PaginationState; // Состояние пагинации
config?: ProductsWidgetConfig; // Локальная конфигурация
apiService?: SearchApiService; // Кастомный API сервис
}
```
#### События
- `@product-selected` - выбран товар
```typescript
(product: Product, event?: MouseEvent) => void
```
**Примечание**: Событие поддерживает `MouseEvent` для обработки средней кнопки мыши. При `event.button === 1` рекомендуется открывать товар в новой вкладке.
- `@page-changed` - изменена страница
```typescript
(page: number) => void
```
- `@loading` - изменение состояния загрузки
```typescript
(isLoading: boolean) => void
```
- `@error` - произошла ошибка
```typescript
(error: string) => void
```
#### Слоты
- `products-grid` - кастомная сетка товаров
```vue
<template #products-grid="{ products, isLoading, itemWidth }">
<div class="custom-grid" :style="{ '--item-width': itemWidth + 'px' }">
<div v-for="product in products" :key="product.id" class="grid-item">
<!-- Кастомная карточка товара -->
</div>
</div>
</template>
```
- `product-card` - кастомная карточка товара
```vue
<template #product-card="{ product, index, onSelect, handleAuxClick }">
<div
class="custom-product-card"
@click="(event) => onSelect(product, event)"
@auxclick="(event) => handleAuxClick(product, event)"
>
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p class="price">{{ formatPrice(product.price) }}</p>
</div>
</template>
```
- `products-empty` - пустое состояние
```vue
<template #products-empty="{ hasProducts, query }">
<div v-if="!hasProducts && query">
<p>По запросу "{{ query }}" товары не найдены</p>
</div>
</template>
```
- `products-pagination` - кастомная пагинация
```vue
<template #products-pagination="{ pagination, onPageChange }">
<div class="custom-pagination">
<button
v-for="page in pagination.totalPages"
:key="page"
:class="{ active: page === pagination.currentPage }"
@click="onPageChange(page)"
>
{{ page }}
</button>
</div>
</template>
```
- `loading` - состояние загрузки
```vue
<template #loading="{ isLoading }">
<div v-if="isLoading">⏳ Загрузка товаров...</div>
</template>
```
- `error` - состояние ошибки
```vue
<template #error="{ error }">
<div v-if="error">❌ {{ error }}</div>
</template>
```
## 🎨 Кастомные слоты - примеры
### Поиск с кастомными слотами
```vue
<barneo-search-widget v-model="query">
<template #search-header="{ config }">
<div class="custom-header">
<h1>🔍 Поиск товаров</h1>
<p>Конфигурация: {{ JSON.stringify(config) }}</p>
</div>
</template>
<template #search-results="{ results, isLoading }">
<div v-if="isLoading" class="loading">Загрузка...</div>
<div v-else class="results">
<div v-for="result in results" :key="result.id" class="result-item">
<h3>{{ result.name }}</h3>
<p>{{ result.description }}</p>
</div>
</div>
</template>
</barneo-search-widget>
```
### Фильтры с кастомными слотами
```vue
<barneo-filters-widget>
<template #filters-title="{ config }">
<div class="custom-filters-title">
🎯 Фильтры ({{ config.maxValuesPerFilter }} значений)
</div>
</template>
<template #filter-value="{ filterCode, value, isActive, handleClick }">
<div class="custom-filter-value" @click="handleClick(filterCode, value.id)">
<input
type="checkbox"
:checked="isActive"
:id="`${filterCode}-${value.id}`"
/>
<label :for="`${filterCode}-${value.id}`">
{{ value.name }}
</label>
</div>
</template>
<template #filter-range="{
filter,
handleRangeSelectorChange,
rangeSelectorValue,
rangeStep,
rangeFormatValue,
isRangeActive,
rangeMin,
rangeMax
}">
<div class="custom-range-filter">
<!-- Кастомный заголовок range фильтра -->
<template #filter-range-header="{ filter }">
<h4 class="range-title">{{ filter.description }}</h4>
</template>
<!-- Кастомный индикатор активного диапазона -->
<template #filter-range-indicator="{ isRangeActive, rangeFormatValue }">
<div v-if="isRangeActive" class="custom-range-indicator">
<span class="indicator-icon">🎯</span>
<span class="indicator-text">{{ rangeFormatValue }}</span>
</div>
</template>
<!-- Кастомная кнопка сброса -->
<template #filter-range-reset="{ handleRangeSelectorChange, rangeMin, rangeMax }">
<button
class="custom-reset-button"
@click="handleRangeSelectorChange(filter.code, [rangeMin, rangeMax])"
>
<span class="reset-icon">🔄</span>
Сбросить
</button>
</template>
</div>
</template>
<template #filters-empty="{ hasFilters }">
<div v-if="!hasFilters" class="custom-empty">
<p>🔍 Выполните поиск для получения фильтров</p>
</div>
</template>
</barneo-filters-widget>
```
### Сортировка с кастомными слотами
```vue
<barneo-sort-widget v-model="sortValue">
<template #sort-options="{ sortOptions, currentSort, isLoading, handleSortChange }">
<div class="custom-sort-container">
<div
v-for="option in sortOptions"
:key="option.value"
class="custom-sort-option"
:class="{ active: option.value === currentSort }"
@click="handleSortChange(option.value)"
>
{{ option.label }}
</div>
</div>
</template>
<template #sort-option="{ option, isActive, isDisabled }">
<div
class="custom-sort-option"
:class="{ active: isActive, disabled: isDisabled }"
>
<span class="sort-label">{{ option.label }}</span>
<span v-if="isActive" class="loading-spinner">⏳</span>
</div>
</template>
<template #loading="{ isLoading }">
<div v-if="isLoading" class="sort-loading">
Обновление результатов...
</div>
</template>
</barneo-sort-widget>
```
### Товары с кастомными слотами
```vue
<barneo-products-widget>
<template #product-card="{ product, index, onSelect, handleAuxClick }">
<div
class="custom-product-card"
@click="(event) => onSelect(product, event)"
@auxclick="(event) => handleAuxClick(product, event)"
>
<div class="product-image">
<img :src="product.image" :alt="product.name" />
</div>
<div class="product-info">
<h3 class="product-title">{{ product.name }}</h3>
<p class="product-price">{{ formatPrice(product.price) }}</p>
<p class="product-description">{{ product.description }}</p>
</div>
</div>
</template>
<template #products-pagination="{ pagination, onPageChange }">
<div class="custom-pagination">
<button
:disabled="pagination.currentPage === 1"
@click="onPageChange(pagination.currentPage - 1)"
>
← Назад
</button>
<span class="page-info">
Страница {{ pagination.currentPage }} из {{ pagination.totalPages }}
</span>
<button
:disabled="pagination.currentPage === pagination.totalPages"
@click="onPageChange(pagination.currentPage + 1)"
>
Вперед →
</button>
</div>
</template>
<template #products-empty="{ hasProducts, query }">
<div v-if="!hasProducts && query" class="custom-empty">
<p>😔 По запросу "{{ query }}" товары не найдены</p>
<p>Попробуйте изменить параметры поиска</p>
</div>
</template>
</barneo-products-widget>
```
### Обработка средней кнопки мыши
Библиотека поддерживает открытие товаров в новой вкладке при нажатии средней кнопки мыши (колесико).
#### Пример обработки в BarneoSearchWidget
```vue
<template>
<barneo-search-widget v-model="query" @select="handleProductSelect" />
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
const handleProductSelect = (product, event) => {
console.log("Выбран товар:", product);
// Если это средняя кнопка мыши (колесико), открываем в новой вкладке
if (event && event.button === 1) {
window.open(`/product/${product.id}`, "_blank");
} else {
// Обычный клик - переходим в текущей вкладке
router.push(`/product/${product.id}`);
}
};
</script>
```
#### Пример обработки в BarneoProductsWidget
```vue
<template>
<barneo-products-widget @product-select="handleProductSelect" />
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
const handleProductSelect = (product, event) => {
console.log("Выбран товар из списка:", product);
// Если это средняя кнопка мыши (колесико), открываем в новой вкладке
if (event && event.button === 1) {
window.open(`/product/${product.id}`, "_blank");
} else {
// Обычный клик - переходим в текущей вкладке
router.push(`/product/${product.id}`);
}
};
</script>
```
#### Использование в кастомных слотах
```vue
<template>
<barneo-search-widget>
<template
#search-results="{ searchResults, selectProduct, handleAuxClick }"
>
<div v-for="product in searchResults" :key="product.id">
<div
class="product-item"
@click="(event) => selectProduct(product, event)"
@auxclick="(event) => handleAuxClick(product, event)"
>
<h3>{{ product.name }}</h3>
<p>{{ product.price }}</p>
</div>
</div>
</template>
</barneo-search-widget>
</template>
```
## 📊 Аналитика
Библиотека включает в себя модуль аналитики для отслеживания пользовательского поведения и взаимодействий с поисковым интерфейсом.
### Использование аналитики
#### Базовое использование
```typescript
import { useAnalytics } from "barneo-search-widget-lib";
// Инициализация аналитики (автоматически использует конфигурацию из BarneoConfigManager)
const analytics = useAnalytics();
// Проверка инициализации
console.log("Аналитика инициализирована:", analytics.isInitialized.value);
console.log("Конфигурация:", analytics.getConfigInfo.value);
```
#### Отслеживание событий
```typescript
// Объединение неавторизованного и авторизованного пользователя
await analytics.mergeCustomers("guest-id-123", "authorized-id-456");
// Отслеживание поискового запроса
await analytics.trackQueryUse("сковорода");
// Отслеживание использования функций поиска
await analytics.trackFunctionUse("filter"); // фильтры
await analytics.trackFunctionUse("hint"); // подсказки
await analytics.trackFunctionUse("history"); // история
// Отслеживание применения подсказки
await analytics.trackHintUse("сков", "сковорода");
// Отслеживание конверсии товара
await analytics.trackProductConversion("product-123", "click", {
position: 1,
query: "сковорода",
source: "search",
});
// Отслеживание конверсии категории
await analytics.trackCategoryConversion("category-456", {
query: "посуда",
source: "search",
});
```
#### Быстрые методы (quickTrack)
```typescript
// Быстрое отслеживание клика по товару
await analytics.quickTrack.productClick("product-123", {
position: 1,
query: "сковорода",
source: "search",
});
// Быстрое отслеживание добавления в корзину
await analytics.quickTrack.productBasket("product-123", {
position: 2,
query: "сковорода",
source: "search",
});
// Быстрое отслеживание заказа
await analytics.quickTrack.productOrder("product-123", {
position: 3,
query: "сковорода",
source: "search",
});
// Быстрое отслеживание клика по категории
await analytics.quickTrack.categoryClick("category-456", {
query: "посуда",
source: "search",
});
// Быстрое отслеживание использования функций
await analytics.quickTrack.filterUse(); // фильтры
await analytics.quickTrack.hintUse(); // подсказки
await analytics.quickTrack.historyUse(); // история
```
## 🎯 Рекомендации (Adviser)
Библиотека включает в себя модуль рекомендаций для получения умных рекомендаций товаров на основе поведения пользователей.
### Использование рекомендаций
#### Базовое использование
```typescript
import { useAdviser } from "barneo-search-widget-lib";
// Инициализация рекомендаций (автоматически использует конфигурацию из BarneoConfigManager)
const adviser = useAdviser({
enableCache: true,
cacheTTL: 300000, // 5 минут
apiLimit: 8,
});
// Проверка инициализации
console.log("Рекомендации инициализированы:", adviser.state.value);
```
#### Получение рекомендаций
```typescript
// "People also search for this product" - что искали с этим товаром
const peopleAlsoSearch = await adviser.getPeopleAlsoSearch("product-123");
// "Searchers also viewed" - что смотрели искавшие
const searchersAlsoViewed = await adviser.getSearchersAlsoViewed("сковорода");
// "Cross Sell" - с этим товаром покупают
const crossSell = await adviser.getCrossSell("product-123");
// "Up Sell" - более дорогие альтернативы
const upSell = await adviser.getUpSell("product-123");
// "Up Sell New" - новая версия up sell
const upSellNew = await adviser.getUpSellNew("product-123");
// "Recently watched" - недавно просмотренные
const recentlyWatched = await adviser.getRecentlyWatched("customer-456");
// "Popular products" - популярные товары
const popularProducts = await adviser.getPopularProducts();
// "Similar products" - похожие товары
const similarProducts = await adviser.getSimilarProducts("product-123");
```
#### Состояние и кэширование
```typescript
// Проверка состояния загрузки
if (adviser.state.value.loadingStatus === "loading") {
console.log("Загрузка рекомендаций...");
}
// Обработка ошибок
if (adviser.state.value.error) {
console.error("Ошибка рекомендаций:", adviser.state.value.error);
}
// Принудительная очистка кэша
adviser.forceClear();
// Получение всех рекомендаций сразу
const allRecommendations = adviser.state.value;
console.log("Все рекомендации:", allRecommendations);
```
#### Тестирование всех рекомендаций
```typescript
// Тестирование всех типов рекомендаций
const testAdviser = async () => {
console.log("🎯 Тестирование модуля adviser...");
// Очищаем состояние для чистого теста
adviser.forceClear();
// Тестируем все типы рекомендаций
await adviser.getPeopleAlsoSearch("test-product");
await adviser.getSearchersAlsoViewed("тестовый запрос");
await adviser.getCrossSell("test-product");
await adviser.getUpSell("test-product");
await adviser.getUpSellNew("test-product");
await adviser.getRecentlyWatched("test-customer");
await adviser.getPopularProducts();
await adviser.getSimilarProducts("test-product");
console.log("✅ Все тесты рекомендаций завершены");
};
```
### Использование аналитики
#### Базовое использование
```typescript
import { useAnalytics } from "barneo-search-widget-lib";
// Инициализация аналитики (автоматически использует конфигурацию из BarneoConfigManager)
const analytics = useAnalytics();
// Проверка инициализации
console.log("Аналитика инициализирована:", analytics.isInitialized.value);
console.log("Конфигурация:", analytics.getConfigInfo.value);
```
#### Отслеживание событий
```typescript
// Объединение неавторизованного и авторизованного пользователя
await analytics.mergeCustomers("guest-id-123", "authorized-id-456");
// Отслеживание поискового запроса
await analytics.trackQueryUse("сковорода");
// Отслеживание использования функций поиска
await analytics.trackFunctionUse("filter"); // фильтры
await analytics.trackFunctionUse("hint"); // подсказки
await analytics.trackFunctionUse("history"); // история
// Отслеживание применения подсказки
await analytics.trackHintUse("сков", "сковорода");
// Отслеживание конверсии товара
await analytics.trackProductConversion("product-123", "click", {
position: 1,
query: "сковорода",
source: "search",
});
// Отслеживание конверсии категории
await analytics.trackCategoryConversion("category-456", {
query: "посуда",
source: "search",
});
```
#### Быстрые методы (quickTrack)
```typescript
// Быстрое отслеживание клика по товару
await analytics.quickTrack.productClick("product-123", {
position: 1,
query: "сковорода",
source: "search",
});
// Быстрое отслеживание добавления в корзину
await analytics.quickTrack.productBasket("product-123", {
position: 2,
query: "сковорода",
source: "search",
});
// Быстрое отслеживание заказа
await analytics.quickTrack.productOrder("product-123", {
position: 3,
query: "сковорода",
source: "search",
});
// Быстрое отслеживание клика по категории
await analytics.quickTrack.categoryClick("category-456", {
query: "посуда",
source: "search",
});
// Быстрое отслеживание использования функций
await analytics.quickTrack.filterUse(); // фильтры
await analytics.quickTrack.hintUse(); // подсказки
await analytics.quickTrack.historyUse(); // история
```
#### Состояние и обработка ошибок
```typescript
// Проверка состояния загрузки
if (analytics.isLoading.value) {
console.log("Отправка аналитических данных...");
}
// Обработка ошибок
if (analytics.error.value) {
console.error("Ошибка аналитики:", analytics.error.value);
}
// Очистка ошибки
analytics.clearError();
```
#### Ручная инициализация
```typescript
// Если нужно инициализировать аналитику с кастомной конфигурацией
analytics.initializeAnalytics({
baseUrl: "https://api.search.ensi.cloud",
token: "your-token",
customerId: "4",
locationId: "1",
apiVersion: "v1",
});
```
### Доступные типы событий
#### SearchFunction (функции поиска)
- `filter` - использование фильтров
- `hint` - использование подсказок
- `history` - использование истории поиска
- `popular_auto_queries` - популярные автозапросы
- `popular_manual_queries` - популярные ручные запросы
- `recommended_queries` - рекомендуемые запросы
#### ProductConversionType (типы конверсии товаров)
- `click` - клик по товару
- `basket` - добавление в корзину
- `order` - заказ товара
#### ProductConversionSource (источники конверсии товаров)
- `search` - из поиска
- `category` - из категории
- `recommendation` - из рекомендаций
#### CategoryConversionSource (источники конверсии категорий)
- `search` - из поиска
- `navigation` - из навигации
### Интеграция с компонентами
Аналитика автоматически интегрируется с компонентами библиотеки через `BarneoConfigManager`:
```typescript
import { useBarneoConfig } from "barneo-search-widget-lib";
const { analyticsService } = useBarneoConfig();
// Прямой доступ к сервису аналитики
await analyticsService.trackQueryUse("тестовый запрос");
```
### Пример полного использования
```vue
<template>
<div>
<barneo-search-widget v-model="query" @search="handleSearch" />
<barneo-products-widget @product-select="handleProductSelect" />
<!-- Кнопка для тестирования аналитики -->
<button @click="testAnalytics">🧪 Тестировать аналитику</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import {
useAnalytics,
ProductConversionSource,
} from "barneo-search-widget-lib";
const query = ref("");
const analytics = useAnalytics();
const handleSearch = async (searchQuery) => {
console.log("Поиск:", searchQuery);
// Отслеживаем поисковый запрос
await analytics.trackQueryUse(searchQuery);
};
const handleProductSelect = async (product, event) => {
console.log("Выбран товар:", product);
// Отслеживаем клик по товару
await analytics.quickTrack.productClick(product.id, {
position: 1,
query: query.value,
source: ProductConversionSource.SEARCH,
});
// Обработка средней кнопки мыши
if (event && event.button === 1) {
window.open(`/product/${product.id}`, "_blank");
} else {
router.push(`/product/${product.id}`);
}
};
const testAnalytics = async () => {
try {
// Тестируем все функции аналитики
await analytics.mergeCustomers("guest-123", "auth-456");
await analytics.trackQueryUse("тестовый запрос");
await analytics.quickTrack.filterUse();
await analytics.quickTrack.productClick("test-product", {
position: 1,
source: ProductConversionSource.SEARCH,
});
console.log("✅ Все тесты аналитики завершены");
} catch (error) {
console.error("❌ Ошибка аналитики:", error);
}
};
</script>
```
## 🔧 Дополнительные возможности
### URL менеджер
Библиотека поддерживает управление URL параметрами для сохранения состояния поиска. URL менеджер инициализируется в компонентах через composable.
#### Инициализация URL менеджера
```vue
<script setup>
import { useUrlManagerInit } from "barneo-search-widget-lib";
// Инициализируем URL менеджер для работы с query параметрами
const { urlManager } = useUrlManagerInit();
</script>
```
#### Использование в компонентах
```vue
<template>
<div>
<barneo-search-widget v-model="query" />
<barneo-filters-widget />
<barneo-sort-widget />
<barneo-products-widget />
</div>
</template>
<script setup>
import { useUrlManagerInit } from "barneo-search-widget-lib";
// Инициализируем URL менеджер в компоненте
const { urlManager } = useUrlManagerInit();
// URL менеджер автоматически синхронизирует состояние с URL параметрами
</script>
```
#### Управляемые URL параметры
- `q` - поисковый запрос
- `sort` - текущая сортировка
- `page` - текущая страница
- `filters` - активные фильтры (JSON)
### Динамическое обновление конфигурации
```typescript
import { BarneoConfigManager } from "barneo-search-widget-lib";
const configManager = BarneoConfigManager.getInstance();
// Обновление конфигурации поиска
configManager.updateSearchWidgetConfig({
placeholder: "Новый placeholder",
darkMode: true,
});
// Обновление конфигурации фильтров
configManager.updateFiltersWidgetConfig({
maxValuesPerFilter: 10,
darkMode: true,
});
// Обновление конфигурации товаров
configManager.updateProductsWidgetConfig({
itemsPerPage: 30,
showPagination: false,
});
// Обновление конфигурации сортировки
configManager.updateSortWidgetConfig({
defaultSort: "price",
darkMode: true,
});
```
### Управление URL параметрами
Библиотека автоматически управляет URL параметрами для сохранения состояния поиска:
- `q` - поисковый запрос
- `sort` - текущая сортировка
- `page` - текущая страница
- `filters` - активные фильтры (JSON)
```typescript
// Пример URL с параметрами
// https://example.com/search?q=сковорода&sort=price&page=2&filters=%7B%22brands%22%3A%5B%22Tefal%22%5D%7D
```
### Использование composables
```typescript
import {
useSearchWidget,
useFiltersWidget,
useSortWidget,
useProductsWidget,
useSharedState,
} from "barneo-search-widget-lib";
// Общее состояние
const { searchState } = useSharedState();
// Поиск
const { performSearch, searchQuery } = useSearchWidget();
// Фильтры
const { applyFilter, clearFilters, activeFilters } = useFiltersWidget();
// Сортировка
const { setSort, currentSort } = useSortWidget();
// Товары
const { products, pagination, changePage } = useProductsWidget();
```
## 📘 TypeScript поддержка
### Автоматическая типизация глобальных компонентов
При использовании плагина библиотеки глобальные компоненты автоматически получают типизацию:
```vue
<template>
<!-- TypeScript автоматически распознает типы этих компонентов -->
<barneo-search-widget v-model="query" @search="handleSearch" />
<barneo-filters-widget @filter-applied="handleFilter" />
<barneo-sort-widget @sort-changed="handleSort" />
<barneo-products-widget @product-selected="handleProductSelect" />
<barneo-range-selector
ref="rangeSelector"
:min="0"
:max="10000"
@change="handleRangeChange"
/>
</template>
```
<script setup lang="ts">
// Типизация работает автоматически без дополнительных импортов
const query = ref("")
const handleSearch = (query: string) => {
console.log("Поиск:", query)
}
</script>
````
### Ручная типизация (если нужно)
Если вы используете локальную регистрацию компонентов, добавьте типизацию:
```typescript
// types/barneo.d.ts
import type { BarneoSearchWidget } from 'barneo-search-widget-lib'
import type { BarneoFiltersWidget } from 'barneo-search-widget-lib'
import type { BarneoSortWidget } from 'barneo-search-widget-lib'
import type { BarneoProductsWidget } from 'barneo-search-widget-lib'
import type { BarneoRangeSelector } from 'barneo-search-widget-lib'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'barneo-search-widget': typeof BarneoSearchWidget
'barneo-filters-widget': typeof BarneoFiltersWidget
'barneo-sort-widget': typeof BarneoSortWidget
'barneo-products-widget': typeof BarneoProductsWidget
'barneo-range-selector': typeof BarneoRangeSelector
BarneoSearchWidget: typeof BarneoSearchWidget
BarneoFiltersWidget: typeof BarneoFiltersWidget
BarneoSortWidget: typeof BarneoSortWidget
BarneoProductsWidget: typeof BarneoProductsWidget
BarneoRangeSelector: typeof BarneoRangeSelector
}
}
export {}
````
### Импорт типов
```typescript
import type {
BarneoSearchWidget,
BarneoFiltersWidget,
BarneoSortWidget,
BarneoProductsWidget,
BarneoRangeSelector,
SearchConfig,
SearchResult,
SearchHint,
SearchApiService,
SearchRequest,
SearchResponse,
// Типы аналитики
SearchFunction,
ProductConversionType,
ProductConversionSource,
CategoryConversionSource,
CategoryConversionType,
AuthorizeCustomerRequest,
QueryUseRequest,
FunctionalUseRequest,
HintsUseRequest,
ProductsUseRequest,
CategoriesUseRequest,
// Типы рекомендаций
AdviserBlockType,
AdviserFilterType,
AdviserSortType,
LoadingStatus,
IncludeType,
FullProduct,
ProductProperty,
} from "barneo-search-widget-lib";
```
## 🤝 Поддержка
- Vue 3.x
- TypeScript
- Nuxt 3
- Vite
- Webpack
## 📄 Лицензия
MIT License