barneo-search-widget-lib
Version:
Библиотека для поиска по каталогу Barneo на Vue 3
1,121 lines (1,055 loc) • 43.4 kB
text/typescript
/**
* **Composable для работы с системой рекомендаций Ensi Cloud Adviser**
*
* Предоставляет реактивные методы для взаимодействия с API рекомендаций Ensi Cloud Adviser.
* Позволяет получать умные рекомендации товаров на основе поведения пользователей.
*
* ## Основные возможности:
* - Получение рекомендаций "People also search for this product" - что искали с этим товаром
* - Получение рекомендаций "Searchers also viewed" - что смотрели искавшие
* - Получение рекомендаций "Cross Sell" - с этим товаром покупают
* - Получение рекомендаций "Up Sell" - более дорогие альтернативы
* - Получение рекомендаций "Up Sell New" - новая версия up sell
* - Получение недавно просмотренных товаров
* - Получение популярных товаров
* - Получение похожих товаров
* - Система кэширования с настраиваемым TTL
* - Автоматическая интеграция с BarneoConfigManager
*
* ## Пример использования:
* ```typescript
* import { useAdviser } from 'barneo-search-widget-lib'
*
* const adviser = useAdviser({
* enableCache: true,
* cacheTTL: 300000, // 5 минут
* apiLimit: 8
* })
*
* // Получение рекомендаций
* await adviser.getCrossSell('product-123')
* await adviser.getPopularProducts()
* ```
*
* @module useAdviser
*/
import { ref, computed, readonly, onMounted, onUnmounted, inject } from "vue";
import {
AdviserService,
type AdviserServiceConfig,
} from "../services/AdviserService";
import type { BarneoConfigManager } from "../../../config/BarneoConfigManager";
import type {
RecommendationProductsRequest,
RecommendationQueryProductsRequest,
CrossSellProductsRequest,
UpSellProductsRequest,
RecentlyWatchedProductsRequest,
PopularProductsSearchRequest,
SimilarProductsSearchRequest,
} from "../types/requests";
import type {
RecommendationResponse,
PopularProductsResponse,
SimilarProductsResponse,
FullProduct,
} from "../types/responses";
import { AdviserBlockType, LoadingStatus } from "../types/enums";
/**
* Интерфейс состояния советника
*
* Содержит все данные о рекомендациях и состоянии загрузки.
* Все поля являются реактивными и автоматически обновляются при получении новых данных.
*/
interface AdviserState {
/** Статус загрузки: 'idle' | 'loading' | 'success' | 'error' */
loadingStatus: LoadingStatus;
/** Сообщение об ошибке или null если ошибок нет */
error: string | null;
/** Рекомендации "People also search for this product" - что искали с этим товаром */
peopleAlsoSearch: FullProduct[];
/** Рекомендации "Searchers also viewed" - что смотрели искавшие */
searchersAlsoViewed: FullProduct[];
/** Рекомендации "Cross Sell" - с этим товаром покупают */
crossSell: FullProduct[];
/** Рекомендации "Up Sell" - более дорогие альтернативы */
upSell: FullProduct[];
/** Рекомендации "Up Sell New" - новая версия up sell */
upSellNew: FullProduct[];
/** Недавно просмотренные товары */
recentlyWatched: FullProduct[];
/** Популярные товары */
popularProducts: FullProduct[];
/** Похожие товары */
similarProducts: FullProduct[];
/** Время последнего обновления данных */
lastUpdated: Date | null;
}
/**
* Интерфейс настроек советника
*
* Позволяет настроить поведение composable при инициализации.
* Все параметры являются опциональными и имеют разумные значения по умолчанию.
*/
interface AdviserOptions {
/**
* Конфигурация сервиса (опционально, если доступен BarneoConfigManager)
* Если не указана, будет автоматически получена из BarneoConfigManager
*/
serviceConfig?: AdviserServiceConfig;
/**
* Автоматическое обновление данных
* @default false
*/
autoRefresh?: boolean;
/**
* Интервал обновления в миллисекундах
* @default 30000 (30 секунд)
*/
refreshInterval?: number;
/**
* Максимальное количество рекомендаций для отображения
* @default 10
*/
maxRecommendations?: number;
/**
* Включить кэширование для оптимизации производительности
* @default true
*/
enableCache?: boolean;
/**
* Время жизни кэша в миллисекундах
* @default 300000 (5 минут)
*/
cacheTTL?: number;
/**
* Лимит записей для API запросов
* @default 5
*/
apiLimit?: number;
}
/**
* Composable для работы с системой рекомендаций Ensi Cloud Adviser
*
* Основная функция для получения умных рекомендаций товаров на основе поведения пользователей.
* Автоматически интегрируется с BarneoConfigManager для получения конфигурации API.
*
* ## Возвращаемые значения:
*
* ### Состояние:
* - `state` - реактивное состояние с данными рекомендаций
* - `isLoading` - вычисляемое свойство, true если идет загрузка
* - `hasError` - вычисляемое свойство, true если есть ошибка
* - `errorMessage` - вычисляемое свойство с текстом ошибки
* - `lastUpdateTime` - вычисляемое свойство с временем последнего обновления
*
* ### Методы получения рекомендаций:
* - `getPeopleAlsoSearch()` - что искали с этим товаром
* - `getSearchersAlsoViewed()` - что смотрели искавшие
* - `getCrossSell()` - с этим товаром покупают
* - `getUpSell()` - более дорогие альтернативы
* - `getUpSellNew()` - новая версия up sell
* - `getRecentlyWatched()` - недавно просмотренные товары
* - `getPopularProducts()` - популярные товары
* - `getSimilarProducts()` - похожие товары
*
* ### Утилиты:
* - `clearData()` - очистить все данные
* - `refreshData()` - обновить все данные
* - `clearCache()` - очистить кэш
* - `forceClear()` - принудительно очистить все данные и кэш
* - `startAutoRefresh()` - запустить автообновление
* - `stopAutoRefresh()` - остановить автообновление
*
* @param options - Опциональные настройки для настройки поведения
* @returns Объект с состоянием и методами для работы с рекомендациями
*
* @example
* ```typescript
* const adviser = useAdviser({
* enableCache: true,
* maxRecommendations: 8,
* apiLimit: 10
* })
*
* // Получение рекомендаций
* await adviser.getCrossSell('product-123')
* await adviser.getPopularProducts()
*
* // Проверка состояния
* console.log('Загрузка:', adviser.isLoading.value)
* console.log('Ошибка:', adviser.errorMessage.value)
* console.log('Данные:', adviser.state.value.crossSell)
* ```
*/
export function useAdviser(options?: Partial<AdviserOptions>) {
// Получаем менеджер конфигурации из Vue контекста или напрямую
const configManager =
inject<BarneoConfigManager>("barneoConfigManager") ||
(typeof window !== "undefined"
? (window as any).__barneoConfigManager
: null);
const {
autoRefresh = false,
refreshInterval = 30000, // 30 секунд
maxRecommendations = 10,
enableCache = true,
cacheTTL = 300000, // 5 минут
apiLimit = 5, // Лимит для API запросов
} = options || {};
// Автоматически создаем конфигурацию из менеджера, если не передана
let serviceConfig: AdviserServiceConfig;
if (options?.serviceConfig) {
serviceConfig = { ...options.serviceConfig, apiLimit };
} else if (configManager) {
try {
const apiConfig = configManager.getApiConfig();
serviceConfig = {
baseUrl: apiConfig.baseUrl,
token: apiConfig.token,
customerId: apiConfig.customerId,
locationId: apiConfig.locationId,
apiVersion: apiConfig.apiVersion, // Может быть undefined
apiLimit,
};
} catch (err) {
console.warn("Failed to initialize adviser with config manager:", err);
throw new Error(
"Adviser requires serviceConfig when BarneoConfigManager is not available"
);
}
} else {
throw new Error(
"Adviser requires serviceConfig when BarneoConfigManager is not available"
);
}
// Создаем экземпляр сервиса
const adviserService = new AdviserService(serviceConfig);
// Состояние
const state = ref<AdviserState>({
loadingStatus: LoadingStatus.IDLE,
error: null,
peopleAlsoSearch: [],
searchersAlsoViewed: [],
crossSell: [],
upSell: [],
upSellNew: [],
recentlyWatched: [],
popularProducts: [],
similarProducts: [],
lastUpdated: null,
});
// Кэш
const cache = ref<Map<string, { data: any; timestamp: number }>>(new Map());
// Принудительно очищаем состояние при инициализации, если кэш отключен
if (!enableCache) {
console.log("🧹 Очищаем кэш и состояние (enableCache: false)");
console.log("📊 Размер кэша до очистки:", cache.value.size);
// Очищаем кэш
cache.value.clear();
console.log("📊 Размер кэша после очистки:", cache.value.size);
// Сбрасываем состояние
state.value = {
loadingStatus: LoadingStatus.IDLE,
error: null,
peopleAlsoSearch: [],
searchersAlsoViewed: [],
crossSell: [],
upSell: [],
upSellNew: [],
recentlyWatched: [],
popularProducts: [],
similarProducts: [],
lastUpdated: null,
};
console.log("✅ Состояние очищено");
}
// Таймер для автообновления
let refreshTimer: NodeJS.Timeout | null = null;
// Вычисляемые свойства
const isLoading = computed(
() => state.value.loadingStatus === LoadingStatus.LOADING
);
const hasError = computed(() => !!state.value.error);
const errorMessage = computed(() => state.value.error);
const lastUpdateTime = computed(() => state.value.lastUpdated);
/**
* Проверить актуальность кэша по ключу
*
* Проверяет, не истек ли срок действия кэшированных данных для указанного ключа.
*
* @param key - Ключ кэша для проверки
* @returns true если кэш актуален, false если истек или отсутствует
*
* @internal
*/
const isCacheValid = (key: string): boolean => {
if (!enableCache) return false;
const cached = cache.value.get(key);
if (!cached) return false;
return Date.now() - cached.timestamp < cacheTTL;
};
/**
* Получить данные из кэша по ключу
*
* Извлекает данные из кэша, если они актуальны.
*
* @param key - Ключ кэша для получения данных
* @returns Кэшированные данные или null если кэш неактуален или отсутствует
*
* @internal
*/
const getFromCache = <T>(key: string): T | null => {
if (!enableCache || !isCacheValid(key)) return null;
const cached = cache.value.get(key)?.data as T;
if (cached) {
console.log("📋 Данные получены из кэша:", key);
}
return cached;
};
/**
* Сохранить данные в кэш
*
* Сохраняет данные в кэш с текущим временем для последующего использования.
*
* @param key - Ключ кэша для сохранения данных
* @param data - Данные для сохранения в кэше
*
* @internal
*/
const setCache = <T>(key: string, data: T): void => {
if (!enableCache) return;
cache.value.set(key, {
data,
timestamp: Date.now(),
});
};
/**
* Очистить кэш рекомендаций
*
* Очищает внутренний кэш, заставляя все последующие запросы обращаться к API.
* Полезно для получения свежих данных при изменении конфигурации.
*
* @example
* ```typescript
* // Очистить кэш
* adviser.clearCache()
*
* // Теперь запросы будут обращаться к API
* await adviser.getPopularProducts()
* ```
*/
const clearCache = (): void => {
cache.value.clear();
};
/**
* Принудительно очистить все данные и кэш
*
* Полностью очищает все данные рекомендаций и кэш, сбрасывая состояние
* к начальному. Полезно для полного сброса при тестировании или смене пользователя.
*
* @example
* ```typescript
* // Принудительно очистить все
* adviser.forceClear()
*
* // Проверка очистки
* console.log('Все очищено:', {
* hasData: adviser.state.value.crossSell.length === 0,
* hasError: adviser.state.value.error === null,
* status: adviser.state.value.loadingStatus === 'idle'
* })
* ```
*/
const forceClear = (): void => {
cache.value.clear();
state.value = {
loadingStatus: LoadingStatus.IDLE,
error: null,
peopleAlsoSearch: [],
searchersAlsoViewed: [],
crossSell: [],
upSell: [],
upSellNew: [],
recentlyWatched: [],
popularProducts: [],
similarProducts: [],
lastUpdated: null,
};
};
/**
* Установить состояние загрузки
*
* Обновляет статус загрузки и время последнего обновления.
*
* @param loading - true для установки статуса загрузки, false для успешного завершения
*
* @internal
*/
const setLoading = (loading: boolean): void => {
state.value.loadingStatus = loading
? LoadingStatus.LOADING
: LoadingStatus.SUCCESS;
if (!loading) {
state.value.lastUpdated = new Date();
}
};
/**
* Установить ошибку
*
* Обновляет сообщение об ошибке и статус загрузки.
*
* @param error - Сообщение об ошибке или null для очистки ошибки
*
* @internal
*/
const setError = (error: string | null): void => {
state.value.error = error;
state.value.loadingStatus = error
? LoadingStatus.ERROR
: LoadingStatus.SUCCESS;
};
/**
* Получить рекомендации "People also search for this product"
*
* Предлагает пользователям товары, которые были просмотрены другими покупателями
* с идентичным поисковым запросом. Это помогает расширить выбор и повысить
* вероятность покупки.
*
* **Рекомендации по использованию:** Используйте этот блок на странице поиска при
* нулевых результатах, что позволит удержать клиента, если нужный товар не найден.
*
* @param productId - ID товара для получения рекомендаций
* @param request - Опциональные параметры запроса (сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getPeopleAlsoSearch('product-123')
*
* // С дополнительными параметрами
* await adviser.getPeopleAlsoSearch('product-123', {
* pagination: { limit: 10 },
* sort: 'name',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Рекомендации:', adviser.state.value.peopleAlsoSearch)
* ```
*/
const getPeopleAlsoSearch = async (
productId: string,
request?: Partial<Omit<RecommendationProductsRequest, "filter">>
): Promise<void> => {
console.log("🔍 getPeopleAlsoSearch вызван для productId:", productId);
console.log("📊 Состояние ДО вызова:", {
peopleAlsoSearch: state.value.peopleAlsoSearch.length,
totalProducts: Object.values(state.value)
.filter(Array.isArray)
.reduce((sum, arr) => sum + arr.length, 0),
});
try {
setLoading(true);
setError(null);
const cacheKey = `people_also_search_${productId}_${JSON.stringify(
request
)}`;
const cached = getFromCache<RecommendationResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.peopleAlsoSearch = fullProducts.slice(
0,
maxRecommendations
);
console.log("📋 Данные получены из кэша для peopleAlsoSearch");
return;
}
const response = await adviserService.getRecommendationProducts(
productId,
request
);
const fullProducts = response.data?.full_products || [];
state.value.peopleAlsoSearch = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
console.log("🌐 Данные получены из API для peopleAlsoSearch");
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error(
"Ошибка при получении рекомендаций 'People also search':",
error
);
} finally {
setLoading(false);
}
console.log("📊 Состояние ПОСЛЕ вызова:", {
peopleAlsoSearch: state.value.peopleAlsoSearch.length,
totalProducts: Object.values(state.value)
.filter(Array.isArray)
.reduce((sum, arr) => sum + arr.length, 0),
});
};
/**
* Получить рекомендации "Searchers also viewed"
*
* Предлагает пользователям товары, которые были просмотрены другими покупателями
* с идентичным поисковым запросом. Это помогает расширить выбор и повысить
* вероятность покупки.
*
* **Рекомендации по использованию:** Используйте этот блок на странице поиска при
* нулевых результатах, что позволит удержать клиента, если нужный товар не найден.
*
* @param query - Поисковый запрос для получения рекомендаций
* @param request - Опциональные параметры запроса (сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getSearchersAlsoViewed('сковорода')
*
* // С дополнительными параметрами
* await adviser.getSearchersAlsoViewed('сковорода', {
* pagination: { limit: 8 },
* sort: '-price',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Рекомендации:', adviser.state.value.searchersAlsoViewed)
* ```
*/
const getSearchersAlsoViewed = async (
query: string,
request?: Partial<Omit<RecommendationQueryProductsRequest, "filter">>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `searchers_also_viewed_${query}_${JSON.stringify(
request
)}`;
const cached = getFromCache<RecommendationResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.searchersAlsoViewed = fullProducts.slice(
0,
maxRecommendations
);
return;
}
const response = await adviserService.getRecommendationQueryProducts(
query,
request
);
const fullProducts = response.data?.full_products || [];
state.value.searchersAlsoViewed = fullProducts.slice(
0,
maxRecommendations
);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error(
"Ошибка при получении рекомендаций 'Searchers also viewed':",
error
);
} finally {
setLoading(false);
}
};
/**
* Получить рекомендации "Cross Sell" (с этим товаром покупают)
*
* Рекомендует сопутствующие товары, которые часто покупаются вместе с основным
* товаром. Это увеличивает средний чек за счет дополнительных продаж.
*
* **Рекомендации по использованию:** Размещайте этот блок на страницах товара и в
* корзине.
*
* @param productId - ID товара для получения рекомендаций
* @param request - Опциональные параметры запроса (сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getCrossSell('product-123')
*
* // С дополнительными параметрами
* await adviser.getCrossSell('product-123', {
* pagination: { limit: 6 },
* sort: 'price',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Сопутствующие товары:', adviser.state.value.crossSell)
* ```
*/
const getCrossSell = async (
productId: string,
request?: Partial<Omit<CrossSellProductsRequest, "filter">>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `cross_sell_${productId}_${JSON.stringify(request)}`;
const cached = getFromCache<RecommendationResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.crossSell = fullProducts.slice(0, maxRecommendations);
return;
}
const response = await adviserService.getCrossSellProducts(
productId,
request
);
const fullProducts = response.data?.full_products || [];
state.value.crossSell = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error("Ошибка при получении рекомендаций 'Cross Sell':", error);
} finally {
setLoading(false);
}
};
/**
* Получить рекомендации "Up Sell" (более дорогие альтернативы)
*
* Предлагает более дорогие или качественные альтернативы выбранному товару. Это
* может помочь повысить прибыль за счет увеличения среднего чека.
*
* **Рекомендации по использованию:** Размещайте этот блок на страницах товара.
*
* @param productId - ID товара для получения рекомендаций
* @param request - Опциональные параметры запроса (сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getUpSell('product-123')
*
* // С дополнительными параметрами
* await adviser.getUpSell('product-123', {
* pagination: { limit: 5 },
* sort: 'price',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Дорогие альтернативы:', adviser.state.value.upSell)
* ```
*/
const getUpSell = async (
productId: string,
request?: Partial<Omit<UpSellProductsRequest, "filter">>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `up_sell_${productId}_${JSON.stringify(request)}`;
const cached = getFromCache<RecommendationResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.upSell = fullProducts.slice(0, maxRecommendations);
return;
}
const response = await adviserService.getUpSellProducts(
productId,
request
);
const fullProducts = response.data?.full_products || [];
state.value.upSell = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error("Ошибка при получении рекомендаций 'Up Sell':", error);
} finally {
setLoading(false);
}
};
/**
* Получить рекомендации "Up Sell New" (новая версия up sell)
*
* Новая версия алгоритма Up Sell, предлагающая более дорогие или качественные
* альтернативы выбранному товару с улучшенной логикой подбора.
*
* **Рекомендации по использованию:** Размещайте этот блок на страницах товара.
*
* @param productId - ID товара для получения рекомендаций
* @param request - Опциональные параметры запроса (сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getUpSellNew('product-123')
*
* // С дополнительными параметрами
* await adviser.getUpSellNew('product-123', {
* pagination: { limit: 5 },
* sort: 'price',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Новые дорогие альтернативы:', adviser.state.value.upSellNew)
* ```
*/
const getUpSellNew = async (
productId: string,
request?: Partial<Omit<UpSellProductsRequest, "filter">>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `up_sell_new_${productId}_${JSON.stringify(request)}`;
const cached = getFromCache<RecommendationResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.upSellNew = fullProducts.slice(0, maxRecommendations);
return;
}
const response = await adviserService.getUpSellProductsNew(
productId,
request
);
const fullProducts = response.data?.full_products || [];
state.value.upSellNew = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error("Ошибка при получении рекомендаций 'Up Sell New':", error);
} finally {
setLoading(false);
}
};
/**
* Получить недавно просмотренные товары
*
* Позволяет пользователям быстро вернуться к ранее просмотренным товарам, что
* повышает вероятность завершения покупки.
*
* **Рекомендации по использованию:** Размещайте этот блок в личных кабинетах
* пользователей или на страницах товаров для легкого доступа к интересующим
* позициям.
*
* @param customerId - ID клиента для получения истории просмотров
* @param request - Опциональные параметры запроса (пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getRecentlyWatched('customer-456')
*
* // С дополнительными параметрами
* await adviser.getRecentlyWatched('customer-456', {
* pagination: { limit: 10 },
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Недавно просмотренные:', adviser.state.value.recentlyWatched)
* ```
*/
const getRecentlyWatched = async (
customerId: string,
request?: Partial<Omit<RecentlyWatchedProductsRequest, "filter">>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `recently_watched_${customerId}_${JSON.stringify(
request
)}`;
const cached = getFromCache<RecommendationResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.recentlyWatched = fullProducts.slice(0, maxRecommendations);
return;
}
const response = await adviserService.getRecentlyWatchedProducts(
customerId,
request
);
const fullProducts = response.data?.full_products || [];
state.value.recentlyWatched = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error(
"Ошибка при получении недавно просмотренных товаров:",
error
);
} finally {
setLoading(false);
}
};
/**
* Получить популярные товары
*
* Демонстрирует товары, которые пользуются высоким спросом среди других
* покупателей. Это создает ощущение доверия и может побудить пользователей к
* покупке.
*
* **Рекомендации по использованию:** Размещайте этот блок на главной странице или
* странице корзины, чтобы привлечь внимание к наиболее востребованным товарам.
*
* @param request - Опциональные параметры запроса (сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getPopularProducts()
*
* // С дополнительными параметрами
* await adviser.getPopularProducts({
* pagination: { limit: 12 },
* sort: '-popularity',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Популярные товары:', adviser.state.value.popularProducts)
* ```
*/
const getPopularProducts = async (
request?: Partial<PopularProductsSearchRequest>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `popular_products_${JSON.stringify(request)}`;
const cached = getFromCache<PopularProductsResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.popularProducts = fullProducts.slice(0, maxRecommendations);
return;
}
const response = await adviserService.getPopularProducts(request);
const fullProducts = response.data?.full_products || [];
state.value.popularProducts = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error("Ошибка при получении популярных товаров:", error);
} finally {
setLoading(false);
}
};
/**
* Получить похожие товары
*
* Демонстрирует связанные товары, которые имеют сходство по одной из характеристик
* – категория, бренд или единая группа (например, один товар разных цветов).
*
* **Рекомендации по использованию:** Размещайте этот блок на страницах товара.
*
* @param request - Опциональные параметры запроса (фильтр по товару, сортировка, пагинация, загрузка полных данных)
*
* @example
* ```typescript
* // Базовое использование
* await adviser.getSimilarProducts()
*
* // С дополнительными параметрами
* await adviser.getSimilarProducts({
* filter: { product_id: 'product-123' },
* pagination: { limit: 8 },
* sort: 'name',
* load_full_products: {
* location_id: '1',
* include: ['properties']
* }
* })
*
* // Проверка результата
* console.log('Похожие товары:', adviser.state.value.similarProducts)
* ```
*/
const getSimilarProducts = async (
request?: Partial<SimilarProductsSearchRequest>
): Promise<void> => {
try {
setLoading(true);
setError(null);
const cacheKey = `similar_products_${JSON.stringify(request)}`;
const cached = getFromCache<SimilarProductsResponse>(cacheKey);
if (cached) {
const fullProducts = cached.data?.full_products || [];
state.value.similarProducts = fullProducts.slice(0, maxRecommendations);
return;
}
const response = await adviserService.getSimilarProducts(request);
const fullProducts = response.data?.full_products || [];
state.value.similarProducts = fullProducts.slice(0, maxRecommendations);
setCache(cacheKey, response);
} catch (error) {
setError(error instanceof Error ? error.message : "Неизвестная ошибка");
console.error("Ошибка при получении похожих товаров:", error);
} finally {
setLoading(false);
}
};
/**
* Очистить все данные рекомендаций
*
* Очищает все массивы рекомендаций в состоянии, но сохраняет кэш.
* Полезно для сброса отображаемых данных без потери кэшированных результатов.
*
* @example
* ```typescript
* // Очистить все данные
* adviser.clearData()
*
* // Проверка результата
* console.log('Данные очищены:', adviser.state.value.crossSell.length === 0)
* ```
*/
const clearData = (): void => {
state.value.peopleAlsoSearch = [];
state.value.searchersAlsoViewed = [];
state.value.crossSell = [];
state.value.upSell = [];
state.value.upSellNew = [];
state.value.recentlyWatched = [];
state.value.popularProducts = [];
state.value.similarProducts = [];
state.value.error = null;
clearCache();
};
/**
* Обновить все данные рекомендаций
*
* Очищает все данные и кэш, подготавливая к новым запросам.
* Полезно для принудительного обновления всех рекомендаций.
*
* @example
* ```typescript
* // Обновить все данные
* await adviser.refreshData()
*
* // Теперь можно загружать новые данные
* await adviser.getPopularProducts()
* ```
*/
const refreshData = async (): Promise<void> => {
clearData();
// Здесь можно добавить логику для обновления всех данных
};
/**
* Запустить автообновление данных
*
* Запускает автоматическое обновление данных с заданным интервалом.
* Работает только если `autoRefresh: true` в настройках.
*
* @example
* ```typescript
* // Запустить автообновление
* adviser.startAutoRefresh()
*
* // Проверка активности
* console.log('Автообновление запущено')
* ```
*/
const startAutoRefresh = (): void => {
if (autoRefresh && !refreshTimer) {
refreshTimer = setInterval(() => {
refreshData();
}, refreshInterval);
}
};
/**
* Остановить автообновление данных
*
* Останавливает автоматическое обновление данных.
* Полезно для экономии ресурсов при неактивном состоянии.
*
* @example
* ```typescript
* // Остановить автообновление
* adviser.stopAutoRefresh()
*
* // Проверка остановки
* console.log('Автообновление остановлено')
* ```
*/
const stopAutoRefresh = (): void => {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
};
// Жизненный цикл
onMounted(() => {
if (autoRefresh) {
startAutoRefresh();
}
});
onUnmounted(() => {
stopAutoRefresh();
});
return {
// Состояние
/** Реактивное состояние с данными рекомендаций (только для чтения) */
state: readonly(state),
/** Вычисляемое свойство: true если идет загрузка */
isLoading,
/** Вычисляемое свойство: true если есть ошибка */
hasError,
/** Вычисляемое свойство: текст ошибки или null */
errorMessage,
/** Вычисляемое свойство: время последнего обновления */
lastUpdateTime,
// Методы получения рекомендаций
/** Получить рекомендации "People also search for this product" - что искали с этим товаром */
getPeopleAlsoSearch,
/** Получить рекомендации "Searchers also viewed" - что смотрели искавшие */
getSearchersAlsoViewed,
/** Получить рекомендации "Cross Sell" - с этим товаром покупают */
getCrossSell,
/** Получить рекомендации "Up Sell" - более дорогие альтернативы */
getUpSell,
/** Получить рекомендации "Up Sell New" - новая версия up sell */
getUpSellNew,
/** Получить недавно просмотренные товары */
getRecentlyWatched,
/** Получить популярные товары */
getPopularProducts,
/** Получить похожие товары */
getSimilarProducts,
// Утилиты
/** Очистить все данные рекомендаций */
clearData,
/** Обновить все данные рекомендаций */
refreshData,
/** Запустить автообновление данных */
startAutoRefresh,
/** Остановить автообновление данных */
stopAutoRefresh,
/** Очистить кэш рекомендаций */
clearCache,
/** Принудительно очистить все данные и кэш */
forceClear,
};
}