UNPKG

barneo-search-widget-lib

Version:

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

569 lines (497 loc) 19.7 kB
import { watch } from "vue"; import type { FiltersWidgetConfig, Filter, FiltersWidgetEmits } from "../types"; import type { SearchApiService } from "../../searchWidget/types"; import { useFiltersState } from "./useFiltersState"; import { useFiltersApi } from "./useFiltersApi"; import { useSharedState } from "../../sharedState/composables/useSharedState"; import { useBarneoConfig } from "../../../config/BarneoConfigManager"; /** * Основной composable для виджета фильтров (без конфликтующих watch) */ export function useFiltersWidgetNoWatch( config: FiltersWidgetConfig, apiService: SearchApiService | undefined, emit: FiltersWidgetEmits ) { // Инициализируем composables const state = useFiltersState(); const api = useFiltersApi(apiService); const sharedState = useSharedState(); const configManager = useBarneoConfig(); // Debounce для применения фильтров let applyTimeout: NodeJS.Timeout | null = null; // Проверка, является ли range фильтр дефолтным const isDefaultRangeFilter = ( filterCode: string, gte?: string, lte?: string, availableRangeFilter?: any ): boolean => { if (!availableRangeFilter) return false; const defaultGte = availableRangeFilter.gte || "0"; const defaultLte = availableRangeFilter.lte || "999999999"; // Проверяем, совпадают ли значения с дефолтными const isDefaultGte = !gte || gte === defaultGte; const isDefaultLte = !lte || lte === defaultLte; return isDefaultGte && isDefaultLte; }; /** * Применяет фильтр с debounce */ const applyFilterWithDebounce = ( filterCode: string, valueId: string, isSelected: boolean ) => { if (applyTimeout) { clearTimeout(applyTimeout); } // Применяем фильтр в состоянии state.applyFilterValue(filterCode, valueId, isSelected); // Синхронизируем с sharedState const activeFilters = state.activeFilters.value; const filterStates = Object.entries(activeFilters).map( ([code, values]) => ({ id: code, name: code, code, values: (Array.isArray(values) ? values : [values]).map( (valueId: string) => ({ id: valueId, name: valueId, isSelected: true, }) ), }) ); sharedState.setActiveFilters(filterStates); // Эмитим событие изменения активных фильтров emit("active-filters-changed", activeFilters as Record<string, string[]>); // Всегда применяем фильтры автоматически applyTimeout = setTimeout(() => { applyFilters(); }, config.debounceMs || 300); }; /** * Применяет все активные фильтры */ const applyFilters = async () => { // Проверяем все необходимые условия if (!apiService) { return; } if ( !sharedState.searchState.query || !sharedState.searchState.query.trim() ) { return; } sharedState.setLoading(true); emit("loading", true); try { // Подготавливаем активные фильтры для передачи const activeFilters = state.activeFilters.value; const filterStates = Object.entries(activeFilters).map( ([code, values]) => ({ id: code, name: code, code, values: (Array.isArray(values) ? values : [values]).map( (valueId: string) => ({ id: valueId, name: valueId, isSelected: true, }) ), }) ); // Подготавливаем range фильтры const rangeFilters: Array<{ code: string; gte?: string; lte?: string }> = []; // Получаем range фильтры из состояния if (state.activeRangeFilters.value.length > 0) { rangeFilters.push(...state.activeRangeFilters.value); } // Сбрасываем пагинацию при применении фильтров sharedState.resetPagination(); // Применяем фильтры через API с текущими параметрами сортировки и пагинации const response = await api.applyFilters( sharedState.searchState.query, state.activeFilters.value as Record<string, string[]>, filterStates, { sort: sharedState.searchState.sort, pagination: sharedState.searchState.pagination, rangeFilters: rangeFilters.length > 0 ? rangeFilters : undefined, availableRangeFilters: state.rangeFilters.value, } ); if (response && response.data) { // Обновляем фильтры в sharedState (это автоматически обновит локальное состояние через watch) if (response.data.filters) { sharedState.setFilters(response.data.filters); } // Обновляем результаты в sharedState if (response.data.full_products) { sharedState.setSearchResults( sharedState.searchState.query, response.data.full_products, true, response.data.full_products, response.data.total_products || 0 ); } // Эмитим событие применения фильтров emit( "filters-applied", state.activeFilters.value as Record<string, string[]> ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Ошибка применения фильтров"; sharedState.setError(errorMessage); emit("error", errorMessage); } finally { sharedState.setLoading(false); emit("loading", false); } }; /** * Сбрасывает все фильтры */ const resetFilters = async () => { state.clearActiveFilters(); state.clearRangeFilters(); // Синхронизируем с sharedState sharedState.clearFilters(); sharedState.setActiveFilters([]); // Очищаем активные фильтры sharedState.setActiveProducts([]); // Очищаем активные range фильтры emit("filters-reset"); emit("active-filters-changed", {}); // Если есть активный запрос, перезагружаем результаты if (sharedState.searchState.query && apiService) { try { const response = await api.resetFilters(sharedState.searchState.query); if (response && response.data) { // Обновляем фильтры в sharedState (это автоматически обновит локальное состояние через watch) if (response.data.filters) { sharedState.setFilters(response.data.filters); } // Обновляем результаты в sharedState if (response.data.full_products) { sharedState.setSearchResults( sharedState.searchState.query, response.data.full_products, true, response.data.full_products, response.data.total_products || 0 ); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Ошибка сброса фильтров"; sharedState.setError(errorMessage); emit("error", errorMessage); } } }; /** * Удаляет значение фильтра */ const removeFilterValue = (filterCode: string, valueId: string) => { state.removeFilterValue(filterCode, valueId); emit("filter-removed", filterCode, valueId); emit( "active-filters-changed", state.activeFilters.value as Record<string, string[]> ); // Всегда применяем фильтры с задержкой if (applyTimeout) { clearTimeout(applyTimeout); } applyTimeout = setTimeout(() => { applyFilters(); }, config.debounceMs || 300); }; /** * Загружает фильтры для текущего запроса */ const loadFilters = async (query: string) => { if (!query.trim()) { state.clearFilters(); return; } sharedState.setLoading(true); try { const filters = await api.getFiltersForQuery(query); state.setFilters(filters); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Ошибка загрузки фильтров"; sharedState.setError(errorMessage); emit("error", errorMessage); } finally { sharedState.setLoading(false); } }; /** * Обработчик клика по значению фильтра */ const handleFilterValueClick = async ( filterCode: string, valueId: string ) => { const isCurrentlyActive = state.isFilterValueActive(filterCode, valueId); const newState = !isCurrentlyActive; // Эмитим событие применения фильтра emit("filter-applied", filterCode, valueId, newState); // Отслеживаем использование фильтров для аналитики try { await configManager.apiService.useSearchFunction("filter"); } catch (err) { // Игнорируем ошибки отслеживания } // Отслеживаем конверсию категории для аналитики (если это категория и фильтр применяется) if (filterCode === "category" && newState) { try { await configManager.apiService.trackCategoryConversion({ category_id: valueId, source: "search", conversion: "click", query: sharedState.searchState.query || undefined, }); } catch (err) { // Игнорируем ошибки отслеживания } } // Применяем фильтр applyFilterWithDebounce(filterCode, valueId, newState); }; /** * Обработчик применения range фильтра */ const handleRangeFilterApply = async ( filterCode: string, gte?: string, lte?: string ) => { // Отслеживаем использование фильтров для аналитики try { await configManager.apiService.useSearchFunction("filter"); } catch (err) { // Игнорируем ошибки отслеживания } // Получаем доступный range фильтр для автоматического заполнения значений const availableRangeFilter = state.rangeFilters.value.find( (filter) => filter.code === filterCode ); // Если заполнено только одно поле, автоматически заполняем второе let finalGte = gte; let finalLte = lte; if (availableRangeFilter) { if (gte && !lte) { // Если заполнено только gte, используем максимальное значение для lte finalLte = availableRangeFilter.lte || "999999999"; } else if (lte && !gte) { // Если заполнено только lte, используем минимальное значение для gte finalGte = availableRangeFilter.gte || "0"; } } // Проверяем, не являются ли значения дефолтными const isDefaultRange = isDefaultRangeFilter( filterCode, finalGte, finalLte, availableRangeFilter ); if (isDefaultRange) { // Если значения дефолтные, удаляем фильтр state.removeRangeFilter(filterCode); // Синхронизируем с sharedState.activeProducts const currentActiveProducts = [...sharedState.searchState.activeProducts]; const existingIndex = currentActiveProducts.findIndex( (product) => product.code === filterCode ); if (existingIndex >= 0) { currentActiveProducts.splice(existingIndex, 1); sharedState.setActiveProducts(currentActiveProducts); } // Эмитим событие удаления range фильтра emit("range-filter-removed", filterCode); // Применяем фильтры с задержкой if (applyTimeout) { clearTimeout(applyTimeout); } applyTimeout = setTimeout(() => { applyFilters(); }, config.debounceMs || 300); return; } // Применяем range фильтр в состоянии с заполненными значениями state.applyRangeFilter(filterCode, finalGte, finalLte); // Синхронизируем с sharedState.activeProducts const currentActiveProducts = [...sharedState.searchState.activeProducts]; const existingIndex = currentActiveProducts.findIndex( (product) => product.code === filterCode ); if (finalGte || finalLte) { const rangeProduct = { code: filterCode, value_gte: finalGte, value_lte: finalLte, }; if (existingIndex >= 0) { currentActiveProducts[existingIndex] = rangeProduct; } else { currentActiveProducts.push(rangeProduct); } } else { // Если значения пустые, удаляем фильтр if (existingIndex >= 0) { currentActiveProducts.splice(existingIndex, 1); } } sharedState.setActiveProducts(currentActiveProducts); // Эмитим событие применения range фильтра emit("range-filter-applied", filterCode, finalGte, finalLte); // Применяем фильтры с задержкой if (applyTimeout) { clearTimeout(applyTimeout); } applyTimeout = setTimeout(() => { applyFilters(); }, config.debounceMs || 300); }; /** * Слушаем изменения в sharedState для сброса фильтров при глубоком поиске */ watch( () => sharedState.searchState.isDeepSearch, (isDeepSearch) => { // Сбрасываем фильтры только при глубоком поиске if (isDeepSearch) { // Очищаем активные фильтры state.clearActiveFilters(); state.clearRangeFilters(); // Очищаем сами фильтры state.clearFilters(); state.clearAvailableRangeFilters(); // Уведомляем о сбросе emit("active-filters-changed", {}); emit("filters-reset"); } } ); /** * Слушаем изменения фильтров в sharedState */ watch( () => sharedState.searchState.filters, (newFilters) => { if (newFilters && newFilters.length > 0) { // Разделяем checkbox и range фильтры const checkboxFilters = newFilters.filter( (filter) => filter.type === "checkbox" ); const rangeFilters = newFilters.filter( (filter) => filter.type === "range" ); // Устанавливаем checkbox фильтры state.setFilters(checkboxFilters as Filter[]); // Устанавливаем range фильтры const availableRangeFilters = rangeFilters.map((filter) => ({ code: filter.code, name: filter.name || filter.code, gte: (filter as any).gte || "", lte: (filter as any).lte || "", })); state.setAvailableRangeFilters(availableRangeFilters); // НЕ очищаем активные фильтры, если они есть в sharedState // Это позволит сохранить выбранные фильтры при применении if (sharedState.searchState.activeFilters.length === 0) { state.clearActiveFilters(); } } else { state.clearFilters(); state.clearActiveFilters(); state.clearAvailableRangeFilters(); } }, { deep: true } ); /** * Синхронизируем активные фильтры из sharedState в локальное состояние */ watch( () => sharedState.searchState.activeFilters, (newActiveFilters) => { if (newActiveFilters && newActiveFilters.length > 0) { // Преобразуем активные фильтры в локальный формат const localActiveFilters: Record<string, string[]> = {}; newActiveFilters.forEach((filter) => { const selectedValues = filter.values .filter((v) => v.isSelected) .map((v) => v.id); if (selectedValues.length > 0) { localActiveFilters[filter.code] = selectedValues; } }); // Обновляем локальное состояние state.setActiveFilters(localActiveFilters); } else { state.clearActiveFilters(); } }, { deep: true } ); /** * Синхронизируем активные range фильтры из sharedState в локальное состояние */ watch( () => sharedState.searchState.activeProducts, (newActiveProducts) => { if (newActiveProducts && newActiveProducts.length > 0) { // Преобразуем activeProducts в локальный формат newActiveProducts.forEach((product) => { if (product.value_gte || product.value_lte) { state.applyRangeFilter( product.code, product.value_gte, product.value_lte ); } }); } else { state.clearRangeFilters(); } }, { deep: true } ); return { // Состояние filters: state.filters, isLoading: state.isLoading, error: state.error, activeFilters: state.activeFilters, rangeFilters: state.rangeFilters, activeRangeFilters: state.activeRangeFilters, // Вычисляемые свойства hasFilters: state.hasFilters, hasActiveFilters: state.hasActiveFilters, activeFiltersCount: state.activeFiltersCount, // Методы applyFilters, resetFilters, removeFilterValue, loadFilters, handleFilterValueClick, handleRangeFilterApply, // Методы состояния isFilterValueActive: state.isFilterValueActive, getActiveValuesForFilter: state.getActiveValuesForFilter, getActiveRangeFilter: state.getActiveRangeFilter, getRangeFilter: state.getRangeFilter, }; }