/* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any */ 'use client'; import { ChevronUp, Search, X } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useEffect, useMemo, useRef, useState } from 'react'; import { addSearchHistory, clearSearchHistory, deleteSearchHistory, getSearchHistory, subscribeToDataUpdates, } from '@/lib/db.client'; import { SearchResult } from '@/lib/types'; import { yellowWords } from '@/lib/yellow'; import PageLayout from '@/components/PageLayout'; import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter'; import SearchSuggestions from '@/components/SearchSuggestions'; import VideoCard from '@/components/VideoCard'; function SearchPageClient() { // 搜索历史 const [searchHistory, setSearchHistory] = useState([]); // 返回顶部按钮显示状态 const [showBackToTop, setShowBackToTop] = useState(false); 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 [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'; }); // 简化的年份排序: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]); // 构建筛选选项 const filterOptions = useMemo(() => { const sourcesSet = new Map(); const titlesSet = new Set(); const yearsSet = new Set(); searchResults.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 (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 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); // 监听搜索历史更新事件 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); requestAnimationFrame(checkScrollPosition); }; // 启动持续检测 isRunning = true; checkScrollPosition(); // 监听 body 元素的滚动事件 const handleScroll = () => { const scrollTop = getScrollTop(); setShowBackToTop(scrollTop > 300); }; 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); fetchSearchResults(query); setShowSuggestions(false); // 保存到搜索历史 (事件监听会自动更新界面) addSearchHistory(query); } else { setShowResults(false); setShowSuggestions(false); } }, [searchParams]); const fetchSearchResults = async (query: string) => { // 在函数开始时缓存查询参数 const cachedQuery = query.trim(); try { setIsLoading(true); const response = await fetch( `/api/search?q=${encodeURIComponent(cachedQuery)}` ); const data = await response.json(); let results = data.results; if ( typeof window !== 'undefined' && !(window as any).RUNTIME_CONFIG?.DISABLE_YELLOW_FILTER ) { results = results.filter((result: SearchResult) => { const typeName = result.type_name || ''; return !yellowWords.some((word: string) => typeName.includes(word)); }); } // 在 setSearchResults 之前检查当前页面的 query 与缓存的查询是否一致 if (currentQueryRef.current !== cachedQuery) { // 查询已经改变,不需要设置结果,直接返回 return; } // 不在这里进行排序,让 filteredAllResults 和 filteredAggResults 处理所有排序逻辑 setSearchResults(results); setShowResults(true); } catch (error) { setSearchResults([]); } finally { setIsLoading(false); } }; // 输入框内容变化时触发,显示搜索建议 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; // 回显搜索框 setSearchQuery(trimmed); setIsLoading(true); setShowResults(true); setShowSuggestions(false); router.push(`/search?q=${encodeURIComponent(trimmed)}`); // 直接发请求 fetchSearchResults(trimmed); // 保存到搜索历史 (事件监听会自动更新界面) addSearchHistory(trimmed); }; const handleSuggestionSelect = (suggestion: string) => { setSearchQuery(suggestion); setShowSuggestions(false); // 自动执行搜索 setIsLoading(true); setShowResults(true); router.push(`/search?q=${encodeURIComponent(suggestion)}`); fetchSearchResults(suggestion); addSearchHistory(suggestion); }; // 返回顶部功能 const scrollToTop = () => { try { // 根据调试结果,真正的滚动容器是 document.body document.body.scrollTo({ top: 0, behavior: 'smooth', }); } catch (error) { // 如果平滑滚动完全失败,使用立即滚动 document.body.scrollTop = 0; } }; return (
{/* 搜索框 */}
{/* 搜索建议 */} setShowSuggestions(false)} />
{/* 搜索结果或搜索历史 */}
{isLoading ? (
) : showResults ? (
{/* 标题 */}

搜索结果

{/* 筛选器 + 聚合开关 同行 */}
{viewMode === 'agg' ? ( setFilterAgg(v as any)} /> ) : ( setFilterAll(v as any)} /> )}
{/* 聚合开关 */}
{viewMode === 'agg' ? filteredAggResults.map(([mapKey, group]) => { return (
); }) : filteredAllResults.map((item) => (
1 ? 'tv' : 'movie'} />
))} {searchResults.length === 0 && (
未找到相关结果
)}
) : searchHistory.length > 0 ? ( // 搜索历史

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

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