barneo-search-widget-lib
Version:
Библиотека для поиска по каталогу Barneo на Vue 3
569 lines (497 loc) • 19.7 kB
text/typescript
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,
};
}