import { ref, watch, onMounted, nextTick } from "vue"; import { useSharedState } from "../../sharedState/composables/useSharedState"; import { useSearchStateManager } from "../../sharedState/composables/useSearchStateManager"; import { useBarneoConfig } from "../../../config/BarneoConfigManager"; import type { UrlSearchParams, UrlManagerConfig } from "../types"; export function useUrlManager(config: UrlManagerConfig = {}) { const sharedState = useSharedState(); const { apiService, productsWidgetConfig } = useBarneoConfig(); const searchStateManager = useSearchStateManager(apiService, { productsWidget: productsWidgetConfig, }); const isInitialized = ref(false); const isUpdatingUrl = ref(false); const pendingFilters = ref(null); // Сохраняем фильтры из URL до загрузки данных // Конфигурация по умолчанию const defaultConfig: Required = { enabled: true, paramKeys: { query: "q", sort: "sort", page: "page", filters: "filters", }, debounceMs: 300, }; const mergedConfig = { ...defaultConfig, ...config }; // Проверка, является ли range фильтр дефолтным const isDefaultRangeFilter = ( filterCode: string, gte?: string, lte?: string ): boolean => { // Получаем доступные range фильтры из sharedState const availableFilters = sharedState.searchState.filters; const rangeFilter = availableFilters.find( (filter) => filter.code === filterCode && filter.type === "range" ); if (!rangeFilter) return false; const defaultGte = (rangeFilter as any).gte || "0"; const defaultLte = (rangeFilter as any).lte || "999999999"; // Проверяем, совпадают ли значения с дефолтными const isDefaultGte = !gte || gte === defaultGte; const isDefaultLte = !lte || lte === defaultLte; return isDefaultGte && isDefaultLte; }; // Получение параметров из URL const getUrlParams = (): UrlSearchParams => { if (!mergedConfig.enabled) return {}; const urlParams = new URLSearchParams(window.location.search); const params: UrlSearchParams = {}; const query = urlParams.get(mergedConfig.paramKeys.query); if (query) { params.query = query; } const sort = urlParams.get(mergedConfig.paramKeys.sort); if (sort) { params.sort = sort; } const page = urlParams.get(mergedConfig.paramKeys.page); if (page) { params.page = page; } const filters = urlParams.get(mergedConfig.paramKeys.filters); if (filters) { params.filters = filters; } return params; }; // Обновление URL с новыми параметрами const updateUrl = (params: Partial) => { if (!mergedConfig.enabled || isUpdatingUrl.value) return; isUpdatingUrl.value = true; const url = new URL(window.location.href); const searchParams = url.searchParams; // Обновляем параметры if (params.query !== undefined) { if (params.query) { searchParams.set(mergedConfig.paramKeys.query, params.query); } else { searchParams.delete(mergedConfig.paramKeys.query); } } if (params.sort !== undefined) { if (params.sort && params.sort !== "relevance") { searchParams.set(mergedConfig.paramKeys.sort, params.sort); } else { searchParams.delete(mergedConfig.paramKeys.sort); } } if (params.page !== undefined) { if (params.page && params.page !== "1") { searchParams.set(mergedConfig.paramKeys.page, params.page); } else { searchParams.delete(mergedConfig.paramKeys.page); } } if (params.filters !== undefined) { if (params.filters) { searchParams.set(mergedConfig.paramKeys.filters, params.filters); } else { searchParams.delete(mergedConfig.paramKeys.filters); } } // Обновляем URL без перезагрузки страницы window.history.replaceState({}, "", url.toString()); isUpdatingUrl.value = false; }; // Парсинг фильтров из JSON строки const parseFilters = (filtersString: string) => { try { return JSON.parse(filtersString); } catch (error) { console.error("Ошибка парсинга фильтров из URL:", error); return {}; } }; // Сериализация фильтров в JSON строку const serializeFilters = ( activeFilters: any[], activeRangeFilters?: any[] ) => { if ( (!activeFilters || activeFilters.length === 0) && (!activeRangeFilters || activeRangeFilters.length === 0) ) { return ""; } const filtersObject: Record = {}; const rangesObject: Record = {}; // Обрабатываем checkbox фильтры if (activeFilters) { activeFilters.forEach((filter) => { const selectedValues = filter.values .filter((value: any) => value.isSelected) .map((value: any) => value.id); if (selectedValues.length > 0) { switch (filter.code) { case "brand": filtersObject.brands = selectedValues; break; case "category": filtersObject.category_ids = selectedValues; break; case "country": filtersObject.countries = selectedValues; break; default: filtersObject[filter.code] = selectedValues; } } }); } // Обрабатываем range фильтры if (activeRangeFilters) { activeRangeFilters.forEach((filter) => { // Проверяем, что хотя бы одно значение не пустое и не undefined // activeRangeFilters содержит объекты с value_gte и value_lte const gte = filter.value_gte || filter.gte; const lte = filter.value_lte || filter.lte; // Проверяем, не являются ли значения дефолтными const isDefaultRange = isDefaultRangeFilter(filter.code, gte, lte); if ( ((gte && gte.trim() !== "") || (lte && lte.trim() !== "")) && !isDefaultRange ) { rangesObject[filter.code] = { gte: gte || undefined, lte: lte || undefined, }; } else { } }); } const result: any = {}; if (Object.keys(filtersObject).length > 0) { result.filters = filtersObject; } if (Object.keys(rangesObject).length > 0) { result.ranges = rangesObject; } const resultString = Object.keys(result).length > 0 ? JSON.stringify(result) : ""; return resultString; }; // Инициализация из URL параметров const initializeFromUrl = async () => { if (!mergedConfig.enabled || isInitialized.value) return; const urlParams = getUrlParams(); if (urlParams.query) { // Устанавливаем состояние загрузки для восстановления из URL sharedState.setLoading(true); // Сохраняем фильтры из URL для последующего восстановления if (urlParams.filters) { pendingFilters.value = urlParams.filters; } // Преобразуем фильтры из URL в формат для API let filtersForApi = {}; let productsForApi: Array<{ code: string; value_gte?: string; value_lte?: string; }> = []; if (urlParams.filters) { const parsedFilters = parseFilters(urlParams.filters); // Обрабатываем checkbox фильтры if (parsedFilters.filters) { filtersForApi = parsedFilters.filters; } // Обрабатываем range фильтры if (parsedFilters.ranges) { Object.entries(parsedFilters.ranges).forEach( ([code, rangeData]: [string, any]) => { if (rangeData.gte || rangeData.lte) { productsForApi.push({ code, value_gte: rangeData.gte, value_lte: rangeData.lte, }); } } ); } } // Вычисляем offset для пагинации, используя itemsPerPage из конфигурации const page = urlParams.page ? parseInt(urlParams.page) : 1; const itemsPerPage = productsWidgetConfig?.itemsPerPage || 20; const offset = (page - 1) * itemsPerPage; try { // Выполняем поиск с параметрами из URL, включая фильтры // performSearchWithFilters сам установит все состояние (query, sort, pagination) await searchStateManager.performSearchWithFilters(urlParams.query, { updateSharedState: true, filters: filtersForApi, products: productsForApi.length > 0 ? productsForApi : undefined, sort: urlParams.sort || "relevance", pagination: { offset_products: offset, limit_products: itemsPerPage, }, }); // Если фильтров нет в URL, снимаем состояние загрузки сразу if (!urlParams.filters) { sharedState.setLoading(false); } else { } // Если фильтры есть, состояние загрузки будет снято в restorePendingFilters } catch (error) { // В случае ошибки тоже снимаем состояние загрузки sharedState.setLoading(false); } } isInitialized.value = true; }; // Восстановление фильтров после загрузки данных const restorePendingFilters = () => { if (pendingFilters.value) { const parsedFilters = parseFilters(pendingFilters.value); } if (pendingFilters.value && sharedState.searchState.filters.length > 0) { const filtersData = parseFilters(pendingFilters.value); const { activeFilters, activeProducts } = convertFiltersObjectToActiveFilters(filtersData); if (activeFilters.length > 0) { sharedState.setActiveFilters(activeFilters); } if (activeProducts.length > 0) { sharedState.setActiveProducts(activeProducts); } pendingFilters.value = null; // Очищаем после восстановления // Снимаем состояние загрузки только после полного восстановления sharedState.setLoading(false); } else if (pendingFilters.value) { // Если есть pendingFilters, но фильтры еще не загружены, оставляем состояние загрузки } else { // Если нет pendingFilters, снимаем состояние загрузки sharedState.setLoading(false); } }; // Преобразование объекта фильтров в формат activeFilters и activeProducts const convertFiltersObjectToActiveFilters = (filtersData: { filters?: Record; ranges?: Record; }) => { const activeFilters: any[] = []; const activeProducts: any[] = []; // Получаем доступные фильтры из sharedState const availableFilters = sharedState.searchState.filters; // Если фильтры еще не загружены, возвращаем пустой массив if (!availableFilters || availableFilters.length === 0) { return { activeFilters, activeProducts }; } // Проходим по всем доступным фильтрам availableFilters.forEach((filter) => { if (filter.type === "range") { // Обрабатываем range фильтры if (filtersData.ranges && filtersData.ranges[filter.code]) { const rangeData = filtersData.ranges[filter.code]; const rangeProduct = { code: filter.code, value_gte: rangeData.gte, value_lte: rangeData.lte, }; activeProducts.push(rangeProduct); } else { } } else { // Обрабатываем checkbox фильтры let selectedValues: string[] = []; // Определяем какие значения должны быть выбраны для этого фильтра if (filtersData.filters) { switch (filter.code) { case "brand": if (filtersData.filters.brands) { selectedValues = filtersData.filters.brands; } break; case "category": if (filtersData.filters.category_ids) { selectedValues = filtersData.filters.category_ids; } break; case "country": if (filtersData.filters.countries) { selectedValues = filtersData.filters.countries; } break; default: if (filtersData.filters[filter.code]) { selectedValues = filtersData.filters[filter.code]; } } } // Если есть выбранные значения, создаем активный фильтр if (selectedValues.length > 0) { const activeFilter = { ...filter, values: filter.values.map((value: any) => ({ ...value, isSelected: selectedValues.includes(value.id), })), }; activeFilters.push(activeFilter); } } }); return { activeFilters, activeProducts }; }; // Слушаем изменения в sharedState и обновляем URL const setupUrlSync = () => { if (!mergedConfig.enabled) return; // Слушаем изменения запроса watch( () => sharedState.searchState.query, (newQuery) => { if (isInitialized.value) { updateUrl({ query: newQuery }); } } ); // Слушаем изменения сортировки watch( () => sharedState.searchState.sort, (newSort) => { if (isInitialized.value) { updateUrl({ sort: newSort }); } } ); // Слушаем изменения пагинации watch( () => sharedState.searchState.pagination.offset_products, (newOffset) => { if (isInitialized.value) { const itemsPerPage = productsWidgetConfig?.itemsPerPage || 20; const page = Math.floor(newOffset / itemsPerPage) + 1; updateUrl({ page: page.toString() }); } } ); // Слушаем изменения фильтров (объединяем checkbox и range фильтры) watch( [ () => sharedState.searchState.activeFilters, () => sharedState.searchState.activeProducts, ], ([newFilters, newRangeFilters]) => { if (isInitialized.value) { const filtersString = serializeFilters( [...newFilters], newRangeFilters as any ); updateUrl({ filters: filtersString }); } }, { deep: true } ); // Слушаем загрузку фильтров для восстановления из URL watch( () => sharedState.searchState.filters, (newFilters) => { if (newFilters && newFilters.length > 0 && pendingFilters.value) { // Небольшая задержка чтобы убедиться что фильтры полностью загружены restorePendingFilters(); } } ); }; // Инициализация при монтировании onMounted(() => { setupUrlSync(); initializeFromUrl(); }); return { isInitialized, isUpdatingUrl, getUrlParams, updateUrl, initializeFromUrl, parseFilters, serializeFilters, }; }