UNPKG

barneo-search-widget-lib

Version:

Библиотека для поиска по каталогу Barneo на Vue 3

1,788 lines (1,475 loc) 55.7 kB
# 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