/* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,no-empty */ 'use client'; import { ChevronUp, Search, X } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import React, { startTransition, Suspense, useEffect, useMemo, useRef, useState } from 'react'; import { addSearchHistory, clearSearchHistory, deleteSearchHistory, getSearchHistory, subscribeToDataUpdates, } from '@/lib/db.client'; import { SearchResult } from '@/lib/types'; import PageLayout from '@/components/PageLayout'; import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter'; import SearchSuggestions from '@/components/SearchSuggestions'; import VideoCard, { VideoCardHandle } from '@/components/VideoCard'; function SearchPageClient() { // 搜索历史 const [searchHistory, setSearchHistory] = useState([]); // 返回顶部按钮显示状态 const [showBackToTop, setShowBackToTop] = useState(false); // 滚动进度状态 const [scrollProgress, setScrollProgress] = useState(0); const router = useRouter(); const searchParams = useSearchParams(); const currentQueryRef = useRef(''); const [searchQuery, setSearchQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); const [showResults, setShowResults] = useState(false); const [searchResults, setSearchResults] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const eventSourceRef = useRef(null); const [totalSources, setTotalSources] = useState(0); const [completedSources, setCompletedSources] = useState(0); const pendingResultsRef = useRef([]); const flushTimerRef = useRef(null); const [useFluidSearch, setUseFluidSearch] = useState(true); // 聚合卡片 refs 与聚合统计缓存 const groupRefs = useRef>>(new Map()); const groupStatsRef = useRef>(new Map()); // 执行搜索的通用函数 const performSearch = (query: string) => { const trimmed = query.trim(); if (!trimmed) return; // 更新搜索查询和状态 setSearchQuery(trimmed); currentQueryRef.current = trimmed; // 清理缓存标记,确保执行新搜索 sessionStorage.removeItem('fromPlayPage'); // 清空旧的搜索结果和状态 if (eventSourceRef.current) { try { eventSourceRef.current.close(); } catch { } eventSourceRef.current = null; } setSearchResults([]); setTotalSources(0); setCompletedSources(0); pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } // 清理聚合统计缓存和refs groupStatsRef.current.clear(); groupRefs.current.clear(); setIsLoading(true); setShowResults(true); // 读取流式搜索设置 let currentFluidSearch = useFluidSearch; if (typeof window !== 'undefined') { const savedFluidSearch = localStorage.getItem('fluidSearch'); if (savedFluidSearch !== null) { currentFluidSearch = JSON.parse(savedFluidSearch); } else { const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; currentFluidSearch = defaultFluidSearch; } } if (currentFluidSearch !== useFluidSearch) { setUseFluidSearch(currentFluidSearch); } if (currentFluidSearch) { // 流式搜索 const es = new EventSource(`/api/search/ws?q=${encodeURIComponent(trimmed)}`); eventSourceRef.current = es; es.onmessage = (event) => { if (!event.data) return; try { const payload = JSON.parse(event.data); if (currentQueryRef.current !== trimmed || eventSourceRef.current !== es) { console.warn('忽略过期的搜索响应:', payload.type, '当前查询:', currentQueryRef.current, '响应查询:', trimmed); return; } switch (payload.type) { case 'start': setTotalSources(payload.totalSources || 0); setCompletedSources(0); break; case 'source_result': { setCompletedSources((prev) => prev + 1); if (Array.isArray(payload.results) && payload.results.length > 0) { const activeYearOrder = (viewMode === 'agg' ? (filterAgg.yearOrder) : (filterAll.yearOrder)); const incoming: SearchResult[] = activeYearOrder === 'none' ? sortBatchForNoOrder(payload.results as SearchResult[]) : (payload.results as SearchResult[]); pendingResultsRef.current.push(...incoming); if (!flushTimerRef.current) { flushTimerRef.current = window.setTimeout(() => { const toAppend = pendingResultsRef.current; pendingResultsRef.current = []; startTransition(() => { setSearchResults((prev) => prev.concat(toAppend)); }); flushTimerRef.current = null; }, 80); } } break; } case 'source_error': setCompletedSources((prev) => prev + 1); break; case 'complete': setCompletedSources(payload.completedSources || totalSources); if (pendingResultsRef.current.length > 0) { const toAppend = pendingResultsRef.current; pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } startTransition(() => { setSearchResults((prev) => { const newResults = prev.concat(toAppend); try { sessionStorage.setItem('cachedSearchQuery', trimmed); sessionStorage.setItem('cachedSearchResults', JSON.stringify(newResults)); sessionStorage.setItem('cachedSearchState', JSON.stringify({ totalSources: payload.completedSources || totalSources, completedSources: payload.completedSources || totalSources, })); sessionStorage.setItem('cachedSearchFilters', JSON.stringify({ filterAll, filterAgg, })); sessionStorage.setItem('cachedViewMode', viewMode); } catch (error) { console.error('缓存搜索结果失败:', error); } return newResults; }); }); } else { setTimeout(() => { setSearchResults((prev) => { try { sessionStorage.setItem('cachedSearchQuery', trimmed); sessionStorage.setItem('cachedSearchResults', JSON.stringify(prev)); sessionStorage.setItem('cachedSearchState', JSON.stringify({ totalSources: payload.completedSources || totalSources, completedSources: payload.completedSources || totalSources, })); sessionStorage.setItem('cachedSearchFilters', JSON.stringify({ filterAll, filterAgg, })); sessionStorage.setItem('cachedViewMode', viewMode); } catch (error) { console.error('缓存搜索结果失败:', error); } return prev; }); }, 100); } setIsLoading(false); try { es.close(); } catch { } if (eventSourceRef.current === es) { eventSourceRef.current = null; } break; } } catch { } }; es.onerror = () => { setIsLoading(false); if (pendingResultsRef.current.length > 0) { const toAppend = pendingResultsRef.current; pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } startTransition(() => { setSearchResults((prev) => prev.concat(toAppend)); }); } try { es.close(); } catch { } if (eventSourceRef.current === es) { eventSourceRef.current = null; } }; } else { // 传统搜索 fetch(`/api/search?q=${encodeURIComponent(trimmed)}`) .then(response => response.json()) .then(data => { if (currentQueryRef.current !== trimmed) { console.warn('忽略过期的搜索响应 (传统):', '当前查询:', currentQueryRef.current, '响应查询:', trimmed); return; } if (data.results && Array.isArray(data.results)) { const activeYearOrder = (viewMode === 'agg' ? (filterAgg.yearOrder) : (filterAll.yearOrder)); const results: SearchResult[] = activeYearOrder === 'none' ? sortBatchForNoOrder(data.results as SearchResult[]) : (data.results as SearchResult[]); setSearchResults(results); setTotalSources(1); setCompletedSources(1); try { sessionStorage.setItem('cachedSearchQuery', trimmed); sessionStorage.setItem('cachedSearchResults', JSON.stringify(results)); sessionStorage.setItem('cachedSearchState', JSON.stringify({ totalSources: 1, completedSources: 1, })); sessionStorage.setItem('cachedSearchFilters', JSON.stringify({ filterAll, filterAgg, })); sessionStorage.setItem('cachedViewMode', viewMode); } catch (error) { console.error('缓存搜索结果失败:', error); } } setIsLoading(false); }) .catch(() => { setIsLoading(false); }); } // 保存到搜索历史 addSearchHistory(trimmed); // 更新URL但不触发重新渲染 const newUrl = `/search?q=${encodeURIComponent(trimmed)}`; window.history.replaceState(null, '', newUrl); }; const getGroupRef = (key: string) => { let ref = groupRefs.current.get(key); if (!ref) { ref = React.createRef(); groupRefs.current.set(key, ref); } return ref; }; const computeGroupStats = (group: SearchResult[]) => { const episodes = (() => { const countMap = new Map(); group.forEach((g) => { const len = g.episodes?.length || 0; if (len > 0) countMap.set(len, (countMap.get(len) || 0) + 1); }); let max = 0; let res = 0; countMap.forEach((v, k) => { if (v > max) { max = v; res = k; } }); return res; })(); const source_names = Array.from(new Set(group.map((g) => g.source_name).filter(Boolean))) as string[]; const douban_id = (() => { const countMap = new Map(); group.forEach((g) => { if (g.douban_id && g.douban_id > 0) { countMap.set(g.douban_id, (countMap.get(g.douban_id) || 0) + 1); } }); let max = 0; let res: number | undefined; countMap.forEach((v, k) => { if (v > max) { max = v; res = k; } }); return res; })(); return { episodes, source_names, douban_id }; }; // 过滤器:非聚合与聚合 const [filterAll, setFilterAll] = useState<{ source: string; title: string; year: string; yearOrder: 'none' | 'asc' | 'desc' }>({ source: 'all', title: 'all', year: 'all', yearOrder: 'none', }); const [filterAgg, setFilterAgg] = useState<{ source: string; title: string; year: string; yearOrder: 'none' | 'asc' | 'desc' }>({ source: 'all', title: 'all', year: 'all', yearOrder: 'none', }); // 获取默认聚合设置:只读取用户本地设置,默认为 true const getDefaultAggregate = () => { if (typeof window !== 'undefined') { const userSetting = localStorage.getItem('defaultAggregateSearch'); if (userSetting !== null) { return JSON.parse(userSetting); } } return true; // 默认启用聚合 }; const [viewMode, setViewMode] = useState<'agg' | 'all'>(() => { return getDefaultAggregate() ? 'agg' : 'all'; }); // 在“无排序”场景用于每个源批次的预排序:完全匹配标题优先,其次年份倒序,未知年份最后 const sortBatchForNoOrder = (items: SearchResult[]) => { const q = currentQueryRef.current.trim(); return items.slice().sort((a, b) => { const aExact = (a.title || '').trim() === q; const bExact = (b.title || '').trim() === q; if (aExact && !bExact) return -1; if (!aExact && bExact) return 1; const aNum = Number.parseInt(a.year as any, 10); const bNum = Number.parseInt(b.year as any, 10); const aValid = !Number.isNaN(aNum); const bValid = !Number.isNaN(bNum); if (aValid && !bValid) return -1; if (!aValid && bValid) return 1; if (aValid && bValid) return bNum - aNum; // 年份倒序 return 0; }); }; // 检查搜索结果与关键字的相关性 const isRelevantResult = (item: SearchResult, query: string) => { if (!query.trim()) return true; const searchTerms = query.trim().toLowerCase().split(/\s+/); const title = (item.title || '').toLowerCase(); const typeName = (item.type_name || '').toLowerCase(); // 至少匹配一个搜索关键字 return searchTerms.some(term => { // 标题包含关键字 if (title.includes(term)) return true; // 类型名包含关键字 if (typeName.includes(term)) return true; // 支持年份搜索 if (term.match(/^\d{4}$/) && item.year === term) return true; // 支持模糊匹配(去除空格和标点符号后的匹配) const cleanTitle = title.replace(/[\s\-_\.]/g, ''); const cleanTerm = term.replace(/[\s\-_\.]/g, ''); if (cleanTitle.includes(cleanTerm)) return true; return false; }); }; // 简化的年份排序:unknown/空值始终在最后 const compareYear = (aYear: string, bYear: string, order: 'none' | 'asc' | 'desc') => { // 如果是无排序状态,返回0(保持原顺序) if (order === 'none') return 0; // 处理空值和unknown const aIsEmpty = !aYear || aYear === 'unknown'; const bIsEmpty = !bYear || bYear === 'unknown'; if (aIsEmpty && bIsEmpty) return 0; if (aIsEmpty) return 1; // a 在后 if (bIsEmpty) return -1; // b 在后 // 都是有效年份,按数字比较 const aNum = parseInt(aYear, 10); const bNum = parseInt(bYear, 10); return order === 'asc' ? aNum - bNum : bNum - aNum; }; // 聚合后的结果(按标题和年份分组) const aggregatedResults = useMemo(() => { const map = new Map(); const keyOrder: string[] = []; // 记录键出现的顺序 searchResults.forEach((item) => { // 使用 title + year + type 作为键,year 必然存在,但依然兜底 'unknown' const key = `${item.title.replaceAll(' ', '')}-${item.year || 'unknown' }-${item.episodes.length === 1 ? 'movie' : 'tv'}`; const arr = map.get(key) || []; // 如果是新的键,记录其顺序 if (arr.length === 0) { keyOrder.push(key); } arr.push(item); map.set(key, arr); }); // 按出现顺序返回聚合结果 return keyOrder.map(key => [key, map.get(key)!] as [string, SearchResult[]]); }, [searchResults]); // 当聚合结果变化时,如果某个聚合已存在,则调用其卡片 ref 的 set 方法增量更新 useEffect(() => { aggregatedResults.forEach(([mapKey, group]) => { const stats = computeGroupStats(group); const prev = groupStatsRef.current.get(mapKey); if (!prev) { // 第一次出现,记录初始值,不调用 ref(由初始 props 渲染) groupStatsRef.current.set(mapKey, stats); return; } // 对比变化并调用对应的 set 方法 const ref = groupRefs.current.get(mapKey); if (ref && ref.current) { if (prev.episodes !== stats.episodes) { ref.current.setEpisodes(stats.episodes); } const prevNames = (prev.source_names || []).join('|'); const nextNames = (stats.source_names || []).join('|'); if (prevNames !== nextNames) { ref.current.setSourceNames(stats.source_names); } if (prev.douban_id !== stats.douban_id) { ref.current.setDoubanId(stats.douban_id); } groupStatsRef.current.set(mapKey, stats); } }); }, [aggregatedResults]); // 构建筛选选项 - 只基于相关的搜索结果 const filterOptions = useMemo(() => { const sourcesSet = new Map(); const titlesSet = new Set(); const yearsSet = new Set(); // 只考虑与搜索关键字相关的结果来构建过滤选项 const relevantResults = searchResults.filter(item => isRelevantResult(item, searchQuery)); relevantResults.forEach((item) => { if (item.source && item.source_name) { sourcesSet.set(item.source, item.source_name); } if (item.title) titlesSet.add(item.title); if (item.year) yearsSet.add(item.year); }); const sourceOptions: { label: string; value: string }[] = [ { label: '全部来源', value: 'all' }, ...Array.from(sourcesSet.entries()) .sort((a, b) => a[1].localeCompare(b[1])) .map(([value, label]) => ({ label, value })), ]; const titleOptions: { label: string; value: string }[] = [ { label: '全部标题', value: 'all' }, ...Array.from(titlesSet.values()) .sort((a, b) => a.localeCompare(b)) .map((t) => ({ label: t, value: t })), ]; // 年份: 将 unknown 放末尾 const years = Array.from(yearsSet.values()); const knownYears = years.filter((y) => y !== 'unknown').sort((a, b) => parseInt(b) - parseInt(a)); const hasUnknown = years.includes('unknown'); const yearOptions: { label: string; value: string }[] = [ { label: '全部年份', value: 'all' }, ...knownYears.map((y) => ({ label: y, value: y })), ...(hasUnknown ? [{ label: '未知', value: 'unknown' }] : []), ]; const categoriesAll: SearchFilterCategory[] = [ { key: 'source', label: '来源', options: sourceOptions }, { key: 'title', label: '标题', options: titleOptions }, { key: 'year', label: '年份', options: yearOptions }, ]; const categoriesAgg: SearchFilterCategory[] = [ { key: 'source', label: '来源', options: sourceOptions }, { key: 'title', label: '标题', options: titleOptions }, { key: 'year', label: '年份', options: yearOptions }, ]; return { categoriesAll, categoriesAgg }; }, [searchResults]); // 非聚合:应用筛选与排序 const filteredAllResults = useMemo(() => { const { source, title, year, yearOrder } = filterAll; const filtered = searchResults.filter((item) => { // 首先检查相关性 if (!isRelevantResult(item, searchQuery)) return false; // 然后应用其他过滤器 if (source !== 'all' && item.source !== source) return false; if (title !== 'all' && item.title !== title) return false; if (year !== 'all' && item.year !== year) return false; return true; }); // 如果是无排序状态,直接返回过滤后的原始顺序 if (yearOrder === 'none') { return filtered; } // 简化排序:1. 年份排序,2. 年份相同时精确匹配在前,3. 标题排序 return filtered.sort((a, b) => { // 首先按年份排序 const yearComp = compareYear(a.year, b.year, yearOrder); if (yearComp !== 0) return yearComp; // 年份相同时,精确匹配在前 const aExactMatch = a.title === searchQuery.trim(); const bExactMatch = b.title === searchQuery.trim(); if (aExactMatch && !bExactMatch) return -1; if (!aExactMatch && bExactMatch) return 1; // 最后按标题排序,正序时字母序,倒序时反字母序 return yearOrder === 'asc' ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title); }); }, [searchResults, filterAll, searchQuery]); // 聚合:应用筛选与排序 const filteredAggResults = useMemo(() => { const { source, title, year, yearOrder } = filterAgg as any; const filtered = aggregatedResults.filter(([_, group]) => { // 检查聚合组中是否至少有一个结果与搜索关键字相关 const hasRelevantResult = group.some(item => isRelevantResult(item, searchQuery)); if (!hasRelevantResult) return false; const gTitle = group[0]?.title ?? ''; const gYear = group[0]?.year ?? 'unknown'; const hasSource = source === 'all' ? true : group.some((item) => item.source === source); if (!hasSource) return false; if (title !== 'all' && gTitle !== title) return false; if (year !== 'all' && gYear !== year) return false; return true; }); // 如果是无排序状态,保持按关键字+年份+类型出现的原始顺序 if (yearOrder === 'none') { return filtered; } // 简化排序:1. 年份排序,2. 年份相同时精确匹配在前,3. 标题排序 return filtered.sort((a, b) => { // 首先按年份排序 const aYear = a[1][0].year; const bYear = b[1][0].year; const yearComp = compareYear(aYear, bYear, yearOrder); if (yearComp !== 0) return yearComp; // 年份相同时,精确匹配在前 const aExactMatch = a[1][0].title === searchQuery.trim(); const bExactMatch = b[1][0].title === searchQuery.trim(); if (aExactMatch && !bExactMatch) return -1; if (!aExactMatch && bExactMatch) return 1; // 最后按标题排序,正序时字母序,倒序时反字母序 const aTitle = a[1][0].title; const bTitle = b[1][0].title; return yearOrder === 'asc' ? aTitle.localeCompare(bTitle) : bTitle.localeCompare(aTitle); }); }, [aggregatedResults, filterAgg, searchQuery]); useEffect(() => { // 无搜索参数时聚焦搜索框 !searchParams.get('q') && document.getElementById('searchInput')?.focus(); // 初始加载搜索历史 getSearchHistory().then(setSearchHistory); // 读取流式搜索设置 if (typeof window !== 'undefined') { const savedFluidSearch = localStorage.getItem('fluidSearch'); const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; if (savedFluidSearch !== null) { setUseFluidSearch(JSON.parse(savedFluidSearch)); } else if (defaultFluidSearch !== undefined) { setUseFluidSearch(defaultFluidSearch); } } // 监听搜索历史更新事件 const unsubscribe = subscribeToDataUpdates( 'searchHistoryUpdated', (newHistory: string[]) => { setSearchHistory(newHistory); } ); // 获取滚动位置的函数 - 专门针对 body 滚动 const getScrollTop = () => { return document.body.scrollTop || 0; }; // 使用 requestAnimationFrame 持续检测滚动位置 let isRunning = false; const checkScrollPosition = () => { if (!isRunning) return; const scrollTop = getScrollTop(); const shouldShow = scrollTop > 300; setShowBackToTop(shouldShow); // 计算滚动进度 const documentHeight = document.body.scrollHeight - document.body.clientHeight; const progress = documentHeight > 0 ? Math.min((scrollTop / documentHeight) * 100, 100) : 0; setScrollProgress(progress); requestAnimationFrame(checkScrollPosition); }; // 启动持续检测 isRunning = true; checkScrollPosition(); // 监听 body 元素的滚动事件 const handleScroll = () => { const scrollTop = getScrollTop(); setShowBackToTop(scrollTop > 300); // 计算滚动进度 const documentHeight = document.body.scrollHeight - document.body.clientHeight; const progress = documentHeight > 0 ? Math.min((scrollTop / documentHeight) * 100, 100) : 0; setScrollProgress(progress); }; document.body.addEventListener('scroll', handleScroll, { passive: true }); return () => { unsubscribe(); isRunning = false; // 停止 requestAnimationFrame 循环 // 移除 body 滚动事件监听器 document.body.removeEventListener('scroll', handleScroll); }; }, []); useEffect(() => { // 当搜索参数变化时更新搜索状态 const query = searchParams.get('q') || ''; currentQueryRef.current = query.trim(); if (query) { setSearchQuery(query); // 检查是否从播放页返回,如果是则尝试使用缓存 const fromPlayPage = sessionStorage.getItem('fromPlayPage'); const cachedQuery = sessionStorage.getItem('cachedSearchQuery'); const cachedResults = sessionStorage.getItem('cachedSearchResults'); const cachedState = sessionStorage.getItem('cachedSearchState'); const cachedFilters = sessionStorage.getItem('cachedSearchFilters'); const cachedViewMode = sessionStorage.getItem('cachedViewMode'); if (fromPlayPage === 'true' && cachedQuery === query.trim() && cachedResults && cachedState) { // 从播放页返回且有缓存,使用缓存的搜索结果 console.log('使用缓存的搜索结果'); try { const results = JSON.parse(cachedResults); const state = JSON.parse(cachedState); // 恢复缓存的过滤器和视图状态 if (cachedFilters) { const filters = JSON.parse(cachedFilters); if (filters.filterAll) setFilterAll(filters.filterAll); if (filters.filterAgg) setFilterAgg(filters.filterAgg); } if (cachedViewMode && ['agg', 'all'].includes(cachedViewMode)) { setViewMode(cachedViewMode as 'agg' | 'all'); } // 恢复搜索结果和状态 setSearchResults(results); setTotalSources(state.totalSources || 0); setCompletedSources(state.completedSources || 0); setIsLoading(false); setShowResults(true); // 清理导航标记,避免影响后续搜索 sessionStorage.removeItem('fromPlayPage'); return; // 直接返回,不执行新搜索 } catch (error) { console.error('恢复缓存的搜索结果失败:', error); // 缓存损坏,清理缓存并继续正常搜索 sessionStorage.removeItem('cachedSearchQuery'); sessionStorage.removeItem('cachedSearchResults'); sessionStorage.removeItem('cachedSearchState'); sessionStorage.removeItem('cachedSearchFilters'); sessionStorage.removeItem('cachedViewMode'); sessionStorage.removeItem('fromPlayPage'); } } // 执行新搜索 - 使用performSearch函数(不更新URL,因为URL已经由路由处理了) const trimmed = query.trim(); // 清空旧的搜索结果和状态 if (eventSourceRef.current) { try { eventSourceRef.current.close(); } catch { } eventSourceRef.current = null; } setSearchResults([]); setTotalSources(0); setCompletedSources(0); pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } // 清理聚合统计缓存和refs groupStatsRef.current.clear(); groupRefs.current.clear(); setIsLoading(true); setShowResults(true); // 读取流式搜索设置 let currentFluidSearch = useFluidSearch; if (typeof window !== 'undefined') { const savedFluidSearch = localStorage.getItem('fluidSearch'); if (savedFluidSearch !== null) { currentFluidSearch = JSON.parse(savedFluidSearch); } else { const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; currentFluidSearch = defaultFluidSearch; } } if (currentFluidSearch !== useFluidSearch) { setUseFluidSearch(currentFluidSearch); } if (currentFluidSearch) { // 流式搜索 const es = new EventSource(`/api/search/ws?q=${encodeURIComponent(trimmed)}`); eventSourceRef.current = es; es.onmessage = (event) => { if (!event.data) return; try { const payload = JSON.parse(event.data); if (currentQueryRef.current !== trimmed || eventSourceRef.current !== es) { console.warn('忽略过期的搜索响应:', payload.type, '当前查询:', currentQueryRef.current, '响应查询:', trimmed); return; } switch (payload.type) { case 'start': setTotalSources(payload.totalSources || 0); setCompletedSources(0); break; case 'source_result': { setCompletedSources((prev) => prev + 1); if (Array.isArray(payload.results) && payload.results.length > 0) { const activeYearOrder = (viewMode === 'agg' ? (filterAgg.yearOrder) : (filterAll.yearOrder)); const incoming: SearchResult[] = activeYearOrder === 'none' ? sortBatchForNoOrder(payload.results as SearchResult[]) : (payload.results as SearchResult[]); pendingResultsRef.current.push(...incoming); if (!flushTimerRef.current) { flushTimerRef.current = window.setTimeout(() => { const toAppend = pendingResultsRef.current; pendingResultsRef.current = []; startTransition(() => { setSearchResults((prev) => prev.concat(toAppend)); }); flushTimerRef.current = null; }, 80); } } break; } case 'source_error': setCompletedSources((prev) => prev + 1); break; case 'complete': setCompletedSources(payload.completedSources || totalSources); if (pendingResultsRef.current.length > 0) { const toAppend = pendingResultsRef.current; pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } startTransition(() => { setSearchResults((prev) => { const newResults = prev.concat(toAppend); try { sessionStorage.setItem('cachedSearchQuery', trimmed); sessionStorage.setItem('cachedSearchResults', JSON.stringify(newResults)); sessionStorage.setItem('cachedSearchState', JSON.stringify({ totalSources: payload.completedSources || totalSources, completedSources: payload.completedSources || totalSources, })); sessionStorage.setItem('cachedSearchFilters', JSON.stringify({ filterAll, filterAgg, })); sessionStorage.setItem('cachedViewMode', viewMode); } catch (error) { console.error('缓存搜索结果失败:', error); } return newResults; }); }); } else { setTimeout(() => { setSearchResults((prev) => { try { sessionStorage.setItem('cachedSearchQuery', trimmed); sessionStorage.setItem('cachedSearchResults', JSON.stringify(prev)); sessionStorage.setItem('cachedSearchState', JSON.stringify({ totalSources: payload.completedSources || totalSources, completedSources: payload.completedSources || totalSources, })); sessionStorage.setItem('cachedSearchFilters', JSON.stringify({ filterAll, filterAgg, })); sessionStorage.setItem('cachedViewMode', viewMode); } catch (error) { console.error('缓存搜索结果失败:', error); } return prev; }); }, 100); } setIsLoading(false); try { es.close(); } catch { } if (eventSourceRef.current === es) { eventSourceRef.current = null; } break; } } catch { } }; es.onerror = () => { setIsLoading(false); if (pendingResultsRef.current.length > 0) { const toAppend = pendingResultsRef.current; pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } startTransition(() => { setSearchResults((prev) => prev.concat(toAppend)); }); } try { es.close(); } catch { } if (eventSourceRef.current === es) { eventSourceRef.current = null; } }; } else { // 传统搜索 fetch(`/api/search?q=${encodeURIComponent(trimmed)}`) .then(response => response.json()) .then(data => { if (currentQueryRef.current !== trimmed) { console.warn('忽略过期的搜索响应 (传统):', '当前查询:', currentQueryRef.current, '响应查询:', trimmed); return; } if (data.results && Array.isArray(data.results)) { const activeYearOrder = (viewMode === 'agg' ? (filterAgg.yearOrder) : (filterAll.yearOrder)); const results: SearchResult[] = activeYearOrder === 'none' ? sortBatchForNoOrder(data.results as SearchResult[]) : (data.results as SearchResult[]); setSearchResults(results); setTotalSources(1); setCompletedSources(1); try { sessionStorage.setItem('cachedSearchQuery', trimmed); sessionStorage.setItem('cachedSearchResults', JSON.stringify(results)); sessionStorage.setItem('cachedSearchState', JSON.stringify({ totalSources: 1, completedSources: 1, })); sessionStorage.setItem('cachedSearchFilters', JSON.stringify({ filterAll, filterAgg, })); sessionStorage.setItem('cachedViewMode', viewMode); } catch (error) { console.error('缓存搜索结果失败:', error); } } setIsLoading(false); }) .catch(() => { setIsLoading(false); }); } setShowSuggestions(false); // 保存到搜索历史 addSearchHistory(trimmed); } else { setShowResults(false); setShowSuggestions(false); } }, [searchParams]); // 组件卸载时,关闭可能存在的连接并清理所有状态 useEffect(() => { return () => { if (eventSourceRef.current) { try { eventSourceRef.current.close(); } catch { } eventSourceRef.current = null; } if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } pendingResultsRef.current = []; // 清理聚合统计缓存和refs,防止状态泄露 groupStatsRef.current.clear(); groupRefs.current.clear(); // 重置当前查询引用 currentQueryRef.current = ''; }; }, []); // 输入框内容变化时触发,显示搜索建议 const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; setSearchQuery(value); if (value.trim()) { setShowSuggestions(true); } else { setShowSuggestions(false); } }; // 搜索框聚焦时触发,显示搜索建议 const handleInputFocus = () => { if (searchQuery.trim()) { setShowSuggestions(true); } }; // 搜索表单提交时触发,处理搜索逻辑 const handleSearch = (e: React.FormEvent) => { e.preventDefault(); const trimmed = searchQuery.trim().replace(/\s+/g, ' '); if (!trimmed) return; setShowSuggestions(false); // 直接调用搜索函数 performSearch(trimmed); }; const handleSuggestionSelect = (suggestion: string) => { setShowSuggestions(false); // 直接调用搜索函数 performSearch(suggestion); }; // 返回顶部功能 const scrollToTop = () => { try { // 根据调试结果,真正的滚动容器是 document.body document.body.scrollTo({ top: 0, behavior: 'smooth', }); } catch (error) { // 如果平滑滚动完全失败,使用立即滚动 document.body.scrollTop = 0; } }; return (
{/* 搜索框 */}
{/* 清除按钮 */} {searchQuery && ( )} {/* 搜索建议 */} setShowSuggestions(false)} onEnterKey={() => { // 当用户按回车键时,使用搜索框的实际内容进行搜索 const trimmed = searchQuery.trim().replace(/\s+/g, ' '); if (!trimmed) return; setShowSuggestions(false); // 直接调用搜索函数 performSearch(trimmed); }} />
{/* 搜索结果或搜索历史 */}
{showResults ? (
{/* 标题 */}

搜索结果 {totalSources > 0 && useFluidSearch && ( {completedSources}/{totalSources} )} {isLoading && useFluidSearch && ( )}

{/* 筛选器 + 聚合开关 同行 */}
{viewMode === 'agg' ? ( setFilterAgg(v as any)} /> ) : ( setFilterAll(v as any)} /> )}
{/* 聚合开关 */}
{searchResults.length === 0 ? ( isLoading ? (
) : (
未找到相关结果
) ) : (
{viewMode === 'agg' ? filteredAggResults.map(([mapKey, group]) => { const title = group[0]?.title || ''; const poster = group[0]?.poster || ''; const year = group[0]?.year || 'unknown'; const { episodes, source_names, douban_id } = computeGroupStats(group); const type = episodes === 1 ? 'movie' : 'tv'; // 如果该聚合第一次出现,写入初始统计 if (!groupStatsRef.current.has(mapKey)) { groupStatsRef.current.set(mapKey, { episodes, source_names, douban_id }); } return (
); }) : filteredAllResults.map((item) => (
1 ? 'tv' : 'movie'} />
))}
)}
) : searchHistory.length > 0 ? ( // 搜索历史

搜索历史 {searchHistory.length > 0 && ( )}

{searchHistory.map((item) => (
{/* 删除按钮 */}
))}
) : null}
{/* 返回顶部悬浮按钮 - 科技风格 */}
); } export default function SearchPage() { return ( ); }