import { ref, computed, watch, onMounted } from "vue"; import type { SearchWidgetConfig, SearchApiService, SearchWidgetEmits, SearchProduct, SearchHint, } from "../types"; import { useSearchState } from "./useSearchState"; import { useSearchApi } from "./useSearchApi"; import { useSearchEvents } from "./useSearchEvents"; import { useSearchUtils } from "./useSearchUtils"; import { useSharedState } from "../../sharedState/composables/useSharedState"; /** * Основной composable для виджета поиска */ export function useSearchWidget( config: SearchWidgetConfig, apiService: SearchApiService | undefined, emit: SearchWidgetEmits ) { // Инициализируем composables const state = useSearchState(); const api = apiService ? useSearchApi(apiService) : null; const events = useSearchEvents(emit); const utils = useSearchUtils(); const sharedState = useSharedState(); // Debounce для поиска const debouncedSearch = api ? utils.createDebounce( (query: string) => performSearch(query, true), config.debounceMs ?? 300 ) : null; // Синхронизируем searchQuery с sharedState watch( () => sharedState.searchState.query, (newQuery) => { if (newQuery && newQuery !== state.searchQuery.value) { state.updateSearchQuery(newQuery); } } ); /** * Выполняет поиск */ const performSearch = async (query: string, isFastResult: boolean = true) => { if (!api || !query.trim()) return; if (isFastResult) { // Для быстрых запросов используем локальное состояние state.setFastLoading(true); } else { // Для глубоких запросов используем глобальное состояние sharedState.setLoading(true); } sharedState.setError(""); try { const searchResponse = await api.searchCatalog(query, isFastResult); if (searchResponse && searchResponse.data) { const results = searchResponse.data.full_products || []; const hints = searchResponse.data.product_hints || []; const fullHints = searchResponse.data.full_hints || []; // Обновляем локальное состояние state.searchResults.value = results; state.productHints.value = hints; state.fullHints.value = fullHints; state.hasSearchResults.value = true; // Эмитим события events.emitSearchResults(searchResponse); events.emitSearch(query.trim()); // Эмитим событие глубокого поиска только для глубоких запросов if (!isFastResult) { events.emitDeepSearch(query.trim()); } // Отслеживаем использование только для быстрых запросов if (isFastResult) { await api.trackQueryUse(query.trim()); } } else { events.emitNoResults(query); } } catch (err) { sharedState.setError("Ошибка поиска"); state.resetSearchState(); } finally { if (isFastResult) { // Для быстрых запросов снимаем локальное состояние state.setFastLoading(false); } else { // Для глубоких запросов снимаем глобальное состояние sharedState.setLoading(false); } } }; /** * Загружает предложения (история, популярные запросы, популярные товары) */ const loadSuggestions = async () => { if (!api) return; sharedState.setLoading(true); sharedState.setError(""); try { // Загружаем все предложения параллельно const [historyQueries, popularQueries, popularProducts] = await Promise.all([ api.loadHistoryQueries(), api.loadPopularQueries(), api.loadPopularProducts(), ]); state.historyQueries.value = historyQueries; state.popularQueries.value = popularQueries; state.popularProducts.value = popularProducts; } catch (err) { sharedState.setError("Не удалось загрузить предложения"); } finally { sharedState.setLoading(false); } }; /** * Обработчик ввода в поле поиска */ const handleInput = (event: Event) => { const target = event.target as HTMLInputElement; const value = target.value; // Обновляем поисковый запрос state.updateSearchQuery(value); events.emitUpdateModel(value); events.emitInput(value); // Показываем предложения state.toggleSuggestions(true); // Выполняем поиск с debounce если запрос достаточно длинный const minLength = config.minSearchLength ?? 2; if (value.trim().length >= minLength && debouncedSearch) { debouncedSearch(value.trim()); } else { // Сбрасываем результаты если запрос слишком короткий state.resetSearchState(); } }; /** * Обработчик поиска (Enter или кнопка) */ const handleSearch = async () => { if (state.searchQuery.value.trim()) { await performSearch(state.searchQuery.value.trim(), false); state.toggleSuggestions(false); } }; /** * Обработчик фокуса */ const handleFocus = async () => { state.toggleSuggestions(true); await events.emitFocus(); }; /** * Обработчик клика */ const handleClick = async () => { state.toggleSuggestions(true); }; /** * Обработчик потери фокуса */ const handleBlur = async () => { // Небольшая задержка чтобы успеть обработать клик по предложению setTimeout(() => { state.toggleSuggestions(false); }, 150); await events.emitBlur(); }; /** * Скрывает предложения */ const hideSuggestions = () => { state.toggleSuggestions(false); }; /** * Очищает поле поиска */ const handleClear = () => { state.updateSearchQuery(""); state.resetSearchState(); events.emitUpdateModel(""); events.emitInput(""); state.toggleSuggestions(false); }; /** * Выбирает запрос из истории */ const selectHistoryQuery = async (query: string) => { state.updateSearchQuery(query); events.emitUpdateModel(query); events.emitHistorySelect(query); if (api) { await api.trackSearchFunction("history"); } await performSearch(query, true); // НЕ закрываем предложения при быстром поиске }; /** * Выбирает популярный запрос */ const selectQuery = async (query: string) => { state.updateSearchQuery(query); events.emitUpdateModel(query); events.emitPopularQuerySelect(query); if (api) { await api.trackSearchFunction("popular_auto_queries"); } await performSearch(query, true); // НЕ закрываем предложения при быстром поиске }; /** * Выбирает подсказку */ const selectHint = async (hint: SearchHint) => { const newQuery = hint.hint; state.updateSearchQuery(newQuery); events.emitUpdateModel(newQuery); if (api) { await api.trackHintUse(state.searchQuery.value.trim(), hint.hint); } await performSearch(newQuery, true); // НЕ закрываем предложения при быстром поиске }; /** * Выбирает полную подсказку */ const selectFullHint = async (hint: string) => { state.updateSearchQuery(hint); events.emitUpdateModel(hint); if (api) { await api.trackHintUse(hint, hint); } await performSearch(hint, true); // НЕ закрываем предложения при быстром поиске }; /** * Выбирает подсказку по полному тексту */ const selectHintByFullText = async (hint: SearchHint) => { state.updateSearchQuery(hint.hint); events.emitUpdateModel(hint.hint); if (api) { await api.trackHintUse(hint.hint, hint.hint); } await performSearch(hint.hint, true); // НЕ закрываем предложения при быстром поиске }; /** * Выбирает товар */ const selectProduct = async (product: SearchProduct, event?: MouseEvent) => { events.emitSelect(product, event); if (api) { const { source, position, query } = utils.getProductSource( state.hasSearchResults.value, state.searchResults.value, product, state.searchQuery.value ); await api.trackProductConversion(product, source, position, query); } }; /** * Удаляет запрос из истории поиска */ const deleteHistoryQuery = async (query: string) => { if (api) { const success = await api.deleteHistoryQuery(query); if (success) { state.historyQueries.value = state.historyQueries.value.filter( (q) => q !== query ); } } else { // Если API сервис не передан, удаляем локально state.historyQueries.value = state.historyQueries.value.filter( (q) => q !== query ); } }; /** * Очищает всю историю поиска */ const clearHistory = async () => { if (api) { const success = await api.clearAllHistory(); if (success) { state.historyQueries.value = []; events.emitClearHistory(); } } else { // Если API сервис не передан, очищаем локально state.historyQueries.value = []; events.emitClearHistory(); } }; // Следим за изменениями поискового запроса watch(state.searchQuery, (newValue) => { events.emitUpdateModel(newValue); }); // Следим за изменениями API сервиса watch( () => apiService, (newService) => { // Не загружаем предложения при изменении API сервиса, // так как onMounted уже загружает их при инициализации } ); // Жизненный цикл onMounted(() => { if (api) { loadSuggestions(); } }); return { // Состояние ...state, // Обработчики событий handleInput, handleSearch, handleFocus, handleClick, handleBlur, hideSuggestions, handleClear, // Обработчики выбора selectHistoryQuery, selectQuery, selectHint, selectFullHint, selectHintByFullText, selectProduct, // Обработчики истории deleteHistoryQuery, clearHistory, // Утилиты formatPrice: utils.formatPrice, handleImageError: utils.handleImageError, }; }