/* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,no-empty */ 'use client'; import { ChevronUp, Film, Grid2x2, HardDrive, List, Magnet, RefreshCw, Search, X, } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import React, { startTransition, Suspense, useEffect, useMemo, useRef, useState, } from 'react'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { addSearchHistory, clearSearchHistory, deleteSearchHistory, getSearchHistory, subscribeToDataUpdates, } from '@/lib/db.client'; import { SearchResult } from '@/lib/types'; import { processImageUrl } from '@/lib/utils'; import AcgSearch from '@/components/AcgSearch'; import CapsuleSwitch from '@/components/CapsuleSwitch'; import ImageViewer from '@/components/ImageViewer'; import PageLayout from '@/components/PageLayout'; import PansouSearch from '@/components/PansouSearch'; import ProxyImage from '@/components/ProxyImage'; import SearchResultFilter, { SearchFilterCategory, } from '@/components/SearchResultFilter'; import SearchSuggestions from '@/components/SearchSuggestions'; import VideoCard, { VideoCardHandle } from '@/components/VideoCard'; import VirtualScrollableGrid from '@/components/VirtualScrollableGrid'; function SearchPageClient() { // 搜索历史 const [searchHistory, setSearchHistory] = useState([]); // 返回顶部按钮显示状态 const [showBackToTop, setShowBackToTop] = useState(false); // 选项卡状态: 'video' 或 'pansou' 或 'acg' const [activeTab, setActiveTab] = useState<'video' | 'pansou' | 'acg'>( 'video' ); // Pansou 搜索触发标志 const [triggerPansouSearch, setTriggerPansouSearch] = useState(false); // ACG 搜索触发标志 const [triggerAcgSearch, setTriggerAcgSearch] = useState(false); // 用户权限 const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>( null ); // 繁体转简体转换器 const converterRef = useRef<((text: string) => string) | null>(null); // 转换器是否已初始化 const [converterReady, setConverterReady] = useState(false); const router = useRouter(); const searchParams = useSearchParams(); const submittedSearchQuery = searchParams.get('q')?.trim() || ''; 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< Map< string, { douban_id?: number; episodes?: number; source_names: string[] } > >(new Map()); // 强制刷新状态 const [forceRefresh, setForceRefresh] = useState(false); // 是否使用了缓存结果 const [isFromCache, setIsFromCache] = useState(false); // 精确搜索开关 const [exactSearch, setExactSearch] = useState(true); // 生成缓存键 const getCacheKey = (query: string) => { return `search_cache_${query.trim()}`; }; // 从 sessionStorage 获取缓存的搜索结果 const getCachedResults = (query: string): SearchResult[] | null => { if (typeof window === 'undefined') return null; try { const cacheKey = getCacheKey(query); const cached = sessionStorage.getItem(cacheKey); if (cached) { return JSON.parse(cached); } } catch (error) { console.error('Failed to get cached results:', error); } return null; }; // 保存搜索结果到 sessionStorage const setCachedResults = (query: string, results: SearchResult[]) => { if (typeof window === 'undefined') return; try { const cacheKey = getCacheKey(query); sessionStorage.setItem(cacheKey, JSON.stringify(results)); } catch (error) { console.error('Failed to cache results:', error); } }; // 清除指定查询的缓存 const clearCachedResults = (query: string) => { if (typeof window === 'undefined') return; try { const cacheKey = getCacheKey(query); sessionStorage.removeItem(cacheKey); } catch (error) { console.error('Failed to clear cached results:', error); } }; 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 [resultDisplayMode, setResultDisplayMode] = useState<'card' | 'list'>( () => { if (typeof window !== 'undefined') { const saved = localStorage.getItem('searchResultDisplayMode'); if (saved === 'card' || saved === 'list') { return saved; } } return 'card'; } ); const [expandedSourceTags, setExpandedSourceTags] = useState< Record >({}); const [previewImage, setPreviewImage] = useState<{ url: string; alt: string; } | null>(null); // 在“无排序”场景用于每个源批次的预排序:完全匹配标题优先,其次年份倒序,未知年份最后 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; }); }; // 简化的年份排序: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 normalizeTitle = (title: string) => { return title .replace(/[\s\u3000]/g, '') // 去除空格和全角空格 .replace(/[()()[\]【】{}「」『』<>《》]/g, '') // 去除各种括号 .replace(/[^\w\u4e00-\u9fa5]/g, ''); // 去除特殊符号,保留字母、数字、下划线和中文 }; // 辅助函数:获取视频类型 const getType = (item: SearchResult): 'movie' | 'tv' => { // 1. Emby 和 OpenList 源:使用 type_name(基于 TMDB,最可靠) if ( item.source === 'emby' || item.source?.startsWith('emby_') || item.source === 'openlist' ) { return item.type_name === '电影' ? 'movie' : 'tv'; } // 2. API 采集源:综合判断 const typeName = item.type_name?.toLowerCase() || ''; // 2.1 明确包含"电影"或"movie"或"片"的,判断为电影 if ( typeName.includes('电影') || typeName.includes('movie') || (typeName.endsWith('片') && !typeName.includes('动漫')) ) { return 'movie'; } // 2.2 包含"剧"、"动漫"、"综艺"等关键词的,判断为剧集 if ( typeName.includes('剧') || typeName.includes('动漫') || typeName.includes('综艺') || typeName.includes('anime') ) { return 'tv'; } // 2.3 检查 episodes_titles:如果包含"第X集",判断为剧集 if (item.episodes_titles && item.episodes_titles.length > 0) { const firstTitle = item.episodes_titles[0] || ''; if (/第\d+集|第\d+话|EP?\d+/i.test(firstTitle)) { return 'tv'; } } // 2.4 兜底:使用 episodes.length(最不可靠) return item.episodes.length === 1 ? 'movie' : 'tv'; }; // 辅助函数:检查标题是否包含搜索词(用于精确搜索) const titleContainsQuery = (title: string, query: string): boolean => { if (!exactSearch) return true; // 如果未开启精确搜索,不过滤 if (!query || !title) return true; // 如果没有搜索词或标题,不过滤 const normalizedTitle = title.toLowerCase(); const normalizedQuery = query.toLowerCase(); return normalizedTitle.includes(normalizedQuery); }; const allExactSearchResults = useMemo(() => { if (!exactSearch) return searchResults; return searchResults.filter((item) => titleContainsQuery(item.title, submittedSearchQuery) ); }, [searchResults, submittedSearchQuery, exactSearch]); // 聚合后的结果(按标题和年份分组) const aggregatedResults = useMemo(() => { //===== 阶段1:按 normalizedTitle-type 初步分组 ===== const preliminaryMap = new Map(); allExactSearchResults.forEach((item) => { const normalizedTitle = normalizeTitle(item.title); const type = getType(item); const preliminaryKey = `${normalizedTitle}-${type}`; const arr = preliminaryMap.get(preliminaryKey) || []; arr.push(item); preliminaryMap.set(preliminaryKey, arr); }); //===== 阶段2:智能年份推断和最终分组 ===== const finalMap = new Map(); const keyOrder: string[] = []; preliminaryMap.forEach((group, preliminaryKey) => { // 分离有年份和无年份的结果 const withYear = new Map(); const withoutYear: SearchResult[] = []; group.forEach((item) => { const year = item.year; // 判断是否为有效年份:必须是4位数字,且不能是空字符串或'unknown' if ( year && year.trim() !== '' && year !== 'unknown' && /^\d{4}$/.test(year) ) { // 有有效年份 const arr = withYear.get(year) || []; arr.push(item); withYear.set(year, arr); } else { // 无年份(包括空字符串、'unknown'、null、undefined等) withoutYear.push(item); } }); // 如果有有效年份组 if (withYear.size > 0) { // 将无年份的结果复制到每个有年份的组中 withYear.forEach((yearGroup, year) => { const finalKey = `${preliminaryKey}-${year}`; // 合并:有年份的 + 无年份的(复制) const mergedGroup = [...yearGroup, ...withoutYear]; finalMap.set(finalKey, mergedGroup); keyOrder.push(finalKey); }); } else if (withoutYear.length > 0) { // 如果完全没有年份信息,单独成组 const finalKey = `${preliminaryKey}-unknown`; finalMap.set(finalKey, withoutYear); keyOrder.push(finalKey); } }); // 按出现顺序返回聚合结果 return keyOrder.map( (key) => [key, finalMap.get(key)!] as [string, SearchResult[]] ); }, [allExactSearchResults]); // 当聚合结果变化时,如果某个聚合已存在,则调用其卡片 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 exactSearchFiltered = exactSearch ? searchResults.filter((item) => titleContainsQuery(item.title, currentQueryRef.current) ) : searchResults; const buildSourceOptions = ( sourceEntries: Array<{ source: string; source_name: string }> ) => [ { label: '全部来源', value: 'all' }, ...Array.from( new Map( sourceEntries .filter( (item) => item.source && item.source_name && item.source.trim() !== '' && item.source_name.trim() !== '' ) .map((item) => [item.source, item.source_name]) ).entries() ) .sort((a, b) => { const aIsOpenList = a[0] === 'openlist'; const bIsOpenList = b[0] === 'openlist'; const aIsEmby = a[0] === 'emby' || a[0].startsWith('emby_'); const bIsEmby = b[0] === 'emby' || b[0].startsWith('emby_'); const aPriority = aIsOpenList ? 100 : aIsEmby ? 90 : 0; const bPriority = bIsOpenList ? 100 : bIsEmby ? 90 : 0; if (aPriority !== bPriority) { return bPriority - aPriority; } return a[1].localeCompare(b[1]); }) .map(([value, label]) => ({ label, value })), ]; const buildTitleOptions = (titles: string[]) => [ { label: '全部标题', value: 'all' }, ...Array.from(new Set(titles)) .filter((title) => title && title.trim() !== '') .sort((a, b) => a.localeCompare(b)) .map((title) => ({ label: title, value: title })), ]; const buildYearOptions = (years: string[]) => { const yearSet = Array.from( new Set(years.filter((year) => year && year.trim() !== '')) ); const knownYears = yearSet .filter((year) => year !== 'unknown') .sort((a, b) => parseInt(b) - parseInt(a)); const hasUnknown = yearSet.includes('unknown'); return [ { label: '全部年份', value: 'all' }, ...knownYears.map((year) => ({ label: year, value: year })), ...(hasUnknown ? [{ label: '未知', value: 'unknown' }] : []), ]; }; const allForSourceOptions = exactSearchFiltered.filter((item) => { if (filterAll.title !== 'all' && item.title !== filterAll.title) return false; if (filterAll.year !== 'all' && item.year !== filterAll.year) return false; return true; }); const allForTitleOptions = exactSearchFiltered.filter((item) => { if (filterAll.source !== 'all' && item.source !== filterAll.source) return false; if (filterAll.year !== 'all' && item.year !== filterAll.year) return false; return true; }); const allForYearOptions = exactSearchFiltered.filter((item) => { if (filterAll.source !== 'all' && item.source !== filterAll.source) return false; if (filterAll.title !== 'all' && item.title !== filterAll.title) return false; return true; }); const aggForSourceOptions = aggregatedResults.filter(([_, group]) => { const gTitle = group[0]?.title ?? ''; const gYear = group[0]?.year ?? 'unknown'; if (filterAgg.title !== 'all' && gTitle !== filterAgg.title) return false; if (filterAgg.year !== 'all' && gYear !== filterAgg.year) return false; return true; }); const aggForTitleOptions = aggregatedResults.filter(([_, group]) => { const gYear = group[0]?.year ?? 'unknown'; const hasSource = filterAgg.source === 'all' ? true : group.some((item) => item.source === filterAgg.source); if (!hasSource) return false; if (filterAgg.year !== 'all' && gYear !== filterAgg.year) return false; return true; }); const aggForYearOptions = aggregatedResults.filter(([_, group]) => { const gTitle = group[0]?.title ?? ''; const hasSource = filterAgg.source === 'all' ? true : group.some((item) => item.source === filterAgg.source); if (!hasSource) return false; if (filterAgg.title !== 'all' && gTitle !== filterAgg.title) return false; return true; }); const categoriesAll: SearchFilterCategory[] = [ { key: 'source', label: '来源', options: buildSourceOptions( allForSourceOptions.map((item) => ({ source: item.source, source_name: item.source_name, })) ), }, { key: 'title', label: '标题', options: buildTitleOptions( allForTitleOptions.map((item) => item.title) ), }, { key: 'year', label: '年份', options: buildYearOptions(allForYearOptions.map((item) => item.year)), }, ]; const categoriesAgg: SearchFilterCategory[] = [ { key: 'source', label: '来源', options: buildSourceOptions( aggForSourceOptions.flatMap(([_, group]) => group.map((item) => ({ source: item.source, source_name: item.source_name, })) ) ), }, { key: 'title', label: '标题', options: buildTitleOptions( aggForTitleOptions.map(([_, group]) => group[0]?.title ?? '') ), }, { key: 'year', label: '年份', options: buildYearOptions( aggForYearOptions.map(([_, group]) => group[0]?.year ?? 'unknown') ), }, ]; return { categoriesAll, categoriesAgg }; }, [searchResults, aggregatedResults, exactSearch, filterAll, filterAgg]); // 非聚合:应用筛选与排序 const filteredAllResults = useMemo(() => { const { source, title, year, yearOrder } = filterAll; const filtered = allExactSearchResults.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); }); }, [allExactSearchResults, 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]); const useVirtualGrid = useMemo(() => { const cardCount = viewMode === 'agg' ? filteredAggResults.length : filteredAllResults.length; return resultDisplayMode === 'card' && cardCount >= 100; }, [ viewMode, resultDisplayMode, filteredAggResults.length, filteredAllResults.length, ]); const resultCountMeta = useMemo(() => { const isAggregateView = viewMode === 'agg'; const visibleCount = isAggregateView ? filteredAggResults.length : filteredAllResults.length; const totalCount = isAggregateView ? aggregatedResults.length : allExactSearchResults.length; return { visibleCount, totalCount, isFiltered: visibleCount !== totalCount, modeLabel: isAggregateView ? '聚合结果' : '搜索结果', unit: isAggregateView ? '组' : '条', }; }, [ viewMode, filteredAggResults.length, filteredAllResults.length, aggregatedResults.length, allExactSearchResults.length, ]); useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('searchResultDisplayMode', resultDisplayMode); } }, [resultDisplayMode]); const getSearchResultUrl = (params: { title: string; year?: string; type?: string; source?: string; id?: string; query?: string; isAggregate?: boolean; }) => { const yearParam = params.year && params.year !== 'unknown' ? `&year=${params.year}` : ''; const queryParam = params.query ? `&stitle=${encodeURIComponent(params.query.trim())}` : ''; const typeParam = params.type ? `&stype=${params.type}` : ''; const preferParam = params.isAggregate ? '&prefer=true' : ''; if (params.isAggregate || !params.source || !params.id) { return `/play?title=${encodeURIComponent( params.title.trim() )}${yearParam}${typeParam}${preferParam}${queryParam}`; } return `/play?source=${params.source}&id=${ params.id }&title=${encodeURIComponent( params.title.trim() )}${yearParam}${preferParam}${queryParam}${typeParam}`; }; const renderTag = (label: string, className: string) => ( {label} ); const renderListItem = (item: { key: string; title: string; poster: string; year?: string; type: 'movie' | 'tv'; episodes?: number; sourceName?: string; sourceNames?: string[]; doubanId?: number; desc?: string; vodRemarks?: string; isAggregate?: boolean; source?: string; id?: string; query?: string; }) => { const yearText = item.year && item.year !== 'unknown' ? item.year : ''; const sourceTags = item.isAggregate ? Array.from(new Set(item.sourceNames || [])) : item.sourceName ? [item.sourceName] : []; const isExpanded = !!expandedSourceTags[item.key]; const maxVisibleSourceTags = 3; const visibleSourceTags = isExpanded ? sourceTags : sourceTags.slice(0, maxVisibleSourceTags); const hiddenSourceCount = Math.max( 0, sourceTags.length - visibleSourceTags.length ); const description = (item.desc || '').trim(); const itemUrl = getSearchResultUrl({ title: item.title, year: item.year, type: item.type, source: item.source, id: item.id, query: item.query, isAggregate: item.isAggregate, }); return ( )} )} ); }; // 监听选项卡切换,自动执行搜索 useEffect(() => { // 如果切换到网盘搜索选项卡,且有搜索关键词,且已显示结果,则触发搜索 if (activeTab === 'pansou' && searchQuery.trim() && showResults) { setTriggerPansouSearch((prev) => !prev); } // 如果切换到 ACG 磁力搜索选项卡,且有搜索关键词,且已显示结果,则触发搜索 if (activeTab === 'acg' && searchQuery.trim() && showResults) { setTriggerAcgSearch((prev) => !prev); } }, [activeTab]); useEffect(() => { // 从 URL 读取搜索类型参数 const typeParam = searchParams.get('type'); const query = searchParams.get('q'); if (typeParam === 'pansou' || typeParam === 'acg') { setActiveTab(typeParam); // 如果有搜索关键词且显示结果,触发对应的搜索 if (query && query.trim()) { setSearchQuery(query); setShowResults(true); // 延迟触发搜索,确保组件已经切换到正确的标签页 setTimeout(() => { if (typeParam === 'pansou') { setTriggerPansouSearch((prev) => !prev); } else if (typeParam === 'acg') { setTriggerAcgSearch((prev) => !prev); } }, 100); } } else if (typeParam === 'video') { setActiveTab('video'); } else if (!typeParam && query) { // 如果没有 type 参数但有查询,默认为 video setActiveTab('video'); } // 无搜索参数时聚焦搜索框 !searchParams.get('q') && document.getElementById('searchInput')?.focus(); // 获取用户权限 const authInfo = getAuthInfoFromBrowserCookie(); setUserRole(authInfo?.role || null); // 初始化繁体转简体转换器 if (typeof window !== 'undefined') { import('opencc-js') .then((module) => { try { const OpenCC = module.default || module; const converter = OpenCC.Converter({ from: 'hk', to: 'cn' }); converterRef.current = converter; setConverterReady(true); } catch (error) { console.error('初始化繁体转简体转换器失败:', error); setConverterReady(true); // 即使失败也设置为 true,避免阻塞 } }) .catch((error) => { console.error('加载 opencc-js 失败:', error); setConverterReady(true); // 即使失败也设置为 true,避免阻塞 }); } else { setConverterReady(true); } // 初始加载搜索历史 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 savedExactSearch = localStorage.getItem('exactSearch'); if (savedExactSearch !== null) { setExactSearch(savedExactSearch === 'true'); } } // 监听搜索历史更新事件 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(() => { // 等待转换器初始化完成 if (!converterReady) { return; } // 当搜索参数变化时更新搜索状态 let query = searchParams.get('q') || ''; // 如果开启了繁体转简体,进行转换 if (query && typeof window !== 'undefined') { const searchTraditionalToSimplified = localStorage.getItem( 'searchTraditionalToSimplified' ); if (searchTraditionalToSimplified === 'true' && converterRef.current) { try { const originalQuery = query; query = converterRef.current(query); // 如果转换后的文本与原文本不同,更新 URL if (originalQuery !== query) { const trimmedConverted = query.trim(); // 使用 replace 而不是 push,避免在历史记录中留下繁体版本 router.replace( `/search?q=${encodeURIComponent(trimmedConverted)}${ searchParams.get('type') ? `&type=${searchParams.get('type')}` : '' }` ); return; // 等待 URL 更新后重新触发此 effect } } catch (error) { console.error('[URL参数监听] 繁体转简体转换失败:', error); } } } currentQueryRef.current = query.trim(); if (query) { setSearchQuery(query); const trimmed = query.trim(); // 检查是否有缓存且不是强制刷新 if (!forceRefresh) { const cachedResults = getCachedResults(trimmed); if (cachedResults && cachedResults.length > 0) { // 使用缓存的结果 setIsLoading(false); // 先设置加载状态为 false setSearchResults(cachedResults); setShowResults(true); setTotalSources(1); setCompletedSources(1); setShowSuggestions(false); setIsFromCache(true); // 标记为缓存结果 // 保存到搜索历史 addSearchHistory(query); return; } } // 如果是强制刷新,清除缓存 if (forceRefresh) { clearCachedResults(trimmed); setForceRefresh(false); } // 开始新搜索时,重置缓存标记 setIsFromCache(false); // 新搜索:关闭旧连接并清空结果 if (eventSourceRef.current) { try { eventSourceRef.current.close(); } catch {} eventSourceRef.current = null; } // 先设置加载状态,再清空结果,避免短暂显示"暂无搜索结果" setIsLoading(true); setShowResults(true); setSearchResults([]); setTotalSources(0); setCompletedSources(0); // 清理缓冲 pendingResultsRef.current = []; if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } // 每次搜索时重新读取设置,确保使用最新的配置 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) 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); // 缓存完整的搜索结果 setCachedResults(trimmed, newResults); return newResults; }); }); } else { // 即使没有待写入的缓冲,也缓存当前结果 setSearchResults((prev) => { setCachedResults(trimmed, prev); return prev; }); } 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) 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); // 缓存搜索结果 setCachedResults(trimmed, results); setTotalSources(1); setCompletedSources(1); } setIsLoading(false); }) .catch(() => { setIsLoading(false); }); } setShowSuggestions(false); // 保存到搜索历史 (事件监听会自动更新界面) addSearchHistory(query); } else { setShowResults(false); setShowSuggestions(false); } }, [searchParams, forceRefresh, converterReady]); // 组件卸载时,关闭可能存在的连接 useEffect(() => { return () => { if (eventSourceRef.current) { try { eventSourceRef.current.close(); } catch {} eventSourceRef.current = null; } if (flushTimerRef.current) { clearTimeout(flushTimerRef.current); flushTimerRef.current = null; } pendingResultsRef.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(); let trimmed = searchQuery.trim().replace(/\s+/g, ' '); if (!trimmed) return; // 如果开启了繁体转简体,进行转换 if (typeof window !== 'undefined') { const searchTraditionalToSimplified = localStorage.getItem( 'searchTraditionalToSimplified' ); if (searchTraditionalToSimplified === 'true' && converterRef.current) { try { trimmed = converterRef.current(trimmed); } catch (error) { console.error('繁体转简体转换失败:', error); } } } // 回显搜索框 setSearchQuery(trimmed); setShowResults(true); setShowSuggestions(false); // 立即设置加载状态,避免显示"未找到相关结果" setIsLoading(true); // 根据当前选项卡执行不同的搜索 if (activeTab === 'video') { // 影视搜索 router.push(`/search?q=${encodeURIComponent(trimmed)}&type=video`); // 其余由 searchParams 变化的 effect 处理 } else if (activeTab === 'pansou') { // 网盘搜索 - 触发搜索 router.push(`/search?q=${encodeURIComponent(trimmed)}&type=pansou`); setTriggerPansouSearch((prev) => !prev); // 切换状态来触发搜索 } else if (activeTab === 'acg') { // ACG 磁力搜索 - 触发搜索 router.push(`/search?q=${encodeURIComponent(trimmed)}&type=acg`); setTriggerAcgSearch((prev) => !prev); } }; const handleSuggestionSelect = (suggestion: string) => { let processedSuggestion = suggestion; // 如果开启了繁体转简体,进行转换 if (typeof window !== 'undefined') { const searchTraditionalToSimplified = localStorage.getItem( 'searchTraditionalToSimplified' ); if (searchTraditionalToSimplified === 'true' && converterRef.current) { try { processedSuggestion = converterRef.current(suggestion); } catch (error) { console.error('繁体转简体转换失败:', error); } } } setSearchQuery(processedSuggestion); setShowSuggestions(false); // 自动执行搜索 setShowResults(true); // 立即设置加载状态,避免显示"未找到相关结果" setIsLoading(true); // 根据当前选项卡执行不同的搜索 if (activeTab === 'video') { // 影视搜索 router.push( `/search?q=${encodeURIComponent(processedSuggestion)}&type=video` ); // 其余由 searchParams 变化的 effect 处理 } else if (activeTab === 'pansou') { // 网盘搜索 - 触发搜索 router.push( `/search?q=${encodeURIComponent(processedSuggestion)}&type=pansou` ); setTriggerPansouSearch((prev) => !prev); } else if (activeTab === 'acg') { // ACG 磁力搜索 - 触发搜索 router.push( `/search?q=${encodeURIComponent(processedSuggestion)}&type=acg` ); setTriggerAcgSearch((prev) => !prev); } }; // 返回顶部功能 const scrollToTop = () => { try { // 根据调试结果,真正的滚动容器是 document.body document.body.scrollTo({ top: 0, behavior: 'smooth', }); } catch (error) { // 如果平滑滚动完全失败,使用立即滚动 document.body.scrollTop = 0; } }; // 处理标签切换 const handleTabChange = (newTab: 'video' | 'pansou' | 'acg') => { setActiveTab(newTab); // 如果有搜索关键词,更新 URL const currentQuery = searchParams.get('q'); if (currentQuery) { router.push( `/search?q=${encodeURIComponent(currentQuery)}&type=${newTab}` ); } }; return (
{/* 搜索框 */}
{/* 清除按钮 */} {searchQuery && ( )} {/* 搜索建议 */} setShowSuggestions(false)} onEnterKey={() => { // 当用户按回车键时,使用搜索框的实际内容进行搜索 const trimmed = searchQuery.trim().replace(/\s+/g, ' '); if (!trimmed) return; // 回显搜索框 setSearchQuery(trimmed); setShowResults(true); setShowSuggestions(false); router.push(`/search?q=${encodeURIComponent(trimmed)}`); }} />
{/* 选项卡 */}
, }, { label: '网盘搜索', value: 'pansou', icon: , }, // 仅管理员和站长显示 ACG 磁力搜索 ...(userRole === 'admin' || userRole === 'owner' ? [ { label: '动漫磁力', value: 'acg' as const, icon: , }, ] : []), ]} active={activeTab} onChange={(value) => handleTabChange(value as 'video' | 'pansou' | 'acg') } />
{/* 搜索结果或搜索历史 */}
{showResults ? (
{activeTab === 'video' ? ( <> {/* 影视搜索结果 */} {/* 标题 */}

搜索结果 {isFromCache ? ( 缓存 ) : ( <> {totalSources > 0 && useFluidSearch && ( 源 {completedSources}/{totalSources} )} {isLoading && useFluidSearch && ( )} )}

{resultCountMeta.modeLabel}{' '} {resultCountMeta.visibleCount.toLocaleString()}{' '} {resultCountMeta.unit} {resultCountMeta.isFiltered && ( 筛选前{' '} {resultCountMeta.totalCount.toLocaleString()}{' '} {resultCountMeta.unit} )}
{searchQuery && ( )}
{viewMode === 'agg' ? ( setFilterAgg(v as any)} /> ) : ( setFilterAll(v as any)} /> )}
{searchResults.length === 0 ? ( isLoading ? (
) : (
未找到相关结果
) ) : ( (() => { const gridClassName = 'justify-start grid grid-cols-3 gap-x-2 gap-y-14 px-0 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-2'; const listClassName = 'space-y-4'; const resultChildren = viewMode === 'agg' ? filteredAggResults.map(([mapKey, group]) => { const title = group[0]?.title || ''; const poster = group[0]?.poster || ''; const year = group[0]?.year || 'unknown'; const desc = group.find((entry) => entry.desc?.trim()) ?.desc || ''; const vodRemarks = group.find((entry) => entry.vod_remarks?.trim()) ?.vod_remarks || ''; const { episodes, source_names, douban_id } = computeGroupStats(group); const lastDashIndex = mapKey.lastIndexOf('-'); const secondLastDashIndex = mapKey.lastIndexOf( '-', lastDashIndex - 1 ); const type = secondLastDashIndex > 0 ? (mapKey.substring( secondLastDashIndex + 1, lastDashIndex ) as 'movie' | 'tv') : episodes === 1 ? 'movie' : 'tv'; if (!groupStatsRef.current.has(mapKey)) { groupStatsRef.current.set(mapKey, { episodes, source_names, douban_id, }); } if (resultDisplayMode === 'list') { return renderListItem({ key: `agg-${mapKey}`, title, poster, year, type, episodes, sourceNames: source_names, doubanId: douban_id, desc, vodRemarks, isAggregate: true, query: searchQuery.trim() !== title ? searchQuery.trim() : '', }); } return (
); }) : filteredAllResults.map((item) => { const type = item.episodes.length > 1 ? 'tv' : 'movie'; if (resultDisplayMode === 'list') { return renderListItem({ key: `all-${item.source}-${item.id}`, id: item.id, title: item.title, poster: item.poster, episodes: item.episodes.length, source: item.source, sourceName: item.source_name, doubanId: item.douban_id, query: searchQuery.trim() !== item.title ? searchQuery.trim() : '', year: item.year, type, desc: item.desc, vodRemarks: item.vod_remarks, }); } return (
); }); if (useVirtualGrid) { return ( {resultChildren} ); } return (
{resultChildren}
); })() )} ) : activeTab === 'pansou' ? ( <> {/* 网盘搜索结果 */}

网盘搜索结果

) : ( <> {/* ACG 磁力搜索结果 */}

动漫磁力搜索结果

)}
) : searchHistory.length > 0 ? ( // 搜索历史

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

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