barneo-search-widget-lib
Version:
Библиотека для поиска по каталогу Barneo на Vue 3
256 lines (224 loc) • 8.82 kB
text/typescript
import { ref, computed, watch } from "vue";
import type { ProductsWidgetConfig, ProductsWidgetEmits } from "../types";
import type { SearchApiService } from "../../searchWidget/types";
import { useProductsApi } from "./useProductsApi";
import { useSharedState } from "../../sharedState/composables/useSharedState";
import { useBarneoConfig } from "../../../config/BarneoConfigManager";
export function useProductsWidget(
config: ProductsWidgetConfig,
apiService: SearchApiService | undefined,
emit: ProductsWidgetEmits
) {
const api = useProductsApi(apiService);
const sharedState = useSharedState();
const configManager = useBarneoConfig();
// Простое состояние загрузки
const isLoading = ref(false);
const error = ref<string | null>(null);
// Синхронизируем состояние загрузки с sharedState
watch(
() => sharedState.searchState.isLoading,
(loading) => {
isLoading.value = loading;
}
);
// Пагинация
const currentPage = ref(1);
const itemsPerPage = config.itemsPerPage || 20;
// Следим за изменением запроса и сбрасываем страницу
watch(
() => sharedState.searchState.query,
() => {
currentPage.value = 1;
}
);
// Следим за изменением активных фильтров и сбрасываем страницу
watch(
() => sharedState.searchState.activeFilters,
(newFilters) => {
// Проверяем, не происходит ли восстановление из URL
// Если пагинация уже установлена правильно (не на первой странице), то не сбрасываем
const expectedPage =
Math.floor(
sharedState.searchState.pagination.offset_products / itemsPerPage
) + 1;
const isRestoringFromUrl =
expectedPage > 1 && currentPage.value === expectedPage;
if (!isRestoringFromUrl) {
currentPage.value = 1;
}
},
{ deep: true }
);
// Следим за изменением сортировки и сбрасываем страницу
watch(
() => sharedState.searchState.sort,
() => {
currentPage.value = 1;
}
);
// Следим за изменением offset_products в sharedState и синхронизируем currentPage
watch(
() => sharedState.searchState.pagination.offset_products,
(newOffset) => {
const newPage = Math.floor(newOffset / itemsPerPage) + 1;
if (newPage !== currentPage.value) {
currentPage.value = newPage;
}
}
);
// Вычисляем общее количество страниц
const totalPages = computed(() => {
const total = sharedState.searchState.totalProducts || 0;
return Math.ceil(total / itemsPerPage);
});
// Маппинг внутренних кодов фильтров на названия полей API
const filterCodeMapping: Record<string, string> = {
brand: "brands",
category: "category_ids",
country: "countries",
availability: "availability",
};
// Функция для получения активных фильтров из sharedState
const getActiveFilters = () => {
const activeFilters = sharedState.searchState.activeFilters;
return activeFilters.reduce((acc, filter) => {
const apiFieldName = filterCodeMapping[filter.code] || filter.code;
acc[apiFieldName] = filter.values
.filter((v) => v.isSelected)
.map((v) => v.id);
return acc;
}, {} as Record<string, string[]>);
};
// Функция для получения range фильтров из sharedState
const getRangeFilters = () => {
const activeProducts = sharedState.searchState.activeProducts;
return activeProducts
.filter((product) => product.value_gte || product.value_lte)
.map((product) => ({
code: product.code,
value_gte: product.value_gte,
value_lte: product.value_lte,
}));
};
// Функция для загрузки товаров с пагинацией
const loadProducts = async (page: number = 1) => {
if (!apiService || !sharedState.searchState.query?.trim()) {
return;
}
// Устанавливаем состояние загрузки через sharedState
sharedState.setLoading(true);
error.value = null;
emit("loading", true);
try {
const offset = (page - 1) * itemsPerPage;
const activeFilters = getActiveFilters();
const rangeFilters = getRangeFilters();
// Обновляем пагинацию в sharedState
sharedState.setPagination({
limit_products: itemsPerPage,
limit_categories: 10,
limit_brands: 10,
offset_products: offset,
});
const response = await api.searchProducts({
query: sharedState.searchState.query,
offset,
limit: itemsPerPage,
filters: activeFilters,
products: rangeFilters.length > 0 ? rangeFilters : undefined,
sort: sharedState.searchState.sort,
});
if (response) {
// Обновляем sharedState с новыми товарами
const fullProducts = response.products.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
price: p.price,
picture: p.image,
url: p.url,
properties: p.properties,
}));
sharedState.setSearchResults(
sharedState.searchState.query,
[...sharedState.searchState.results],
sharedState.searchState.isDeepSearch,
fullProducts,
response.totalProducts
);
emit("products-updated", response.products);
}
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Ошибка загрузки товаров";
error.value = errorMessage;
emit("error", errorMessage);
} finally {
// Снимаем состояние загрузки через sharedState
sharedState.setLoading(false);
emit("loading", false);
}
};
const handlePageChange = async (page: number) => {
if (page !== currentPage.value && page >= 1 && page <= totalPages.value) {
// Отслеживаем использование пагинации для аналитики
try {
await configManager.apiService.useSearchFunction("filter");
} catch (err) {
// Игнорируем ошибки отслеживания
}
// Сразу устанавливаем текущую страницу
currentPage.value = page;
emit("page-change", page);
// Плавно прокручиваем к началу товаров
const productsContainer = document.querySelector(
".barneo-products-container"
);
if (productsContainer) {
productsContainer.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
// Затем загружаем данные
await loadProducts(page);
}
};
const handleProductSelect = async (product: any, event?: MouseEvent) => {
// Отслеживаем конверсию товара для аналитики
try {
// Вычисляем позицию товара в текущем списке
const currentProducts = sharedState.searchState.full_products || [];
const productPosition =
currentProducts.findIndex((p) => p.id === product.id) + 1;
await configManager.apiService.trackProductConversion({
product_id: product.id,
conversion: "click",
source: "search",
position: productPosition > 0 ? productPosition : undefined,
query: sharedState.searchState.query || undefined,
});
} catch (err) {
// Игнорируем ошибки отслеживания
}
emit("product-select", product, event);
};
return {
// State
isLoading: computed(() => isLoading.value),
error: computed(() => error.value),
// Pagination
pagination: computed(() => ({
currentPage: currentPage.value,
totalPages: totalPages.value,
totalProducts: sharedState.searchState.totalProducts || 0,
itemsPerPage,
offset: (currentPage.value - 1) * itemsPerPage,
})),
// Methods
loadProducts,
handlePageChange,
handleProductSelect,
};
}