diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 37b9453..4d611a1 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -1,9 +1,26 @@ /* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,no-empty */ 'use client'; -import { ChevronUp, Film, HardDrive, Magnet,RefreshCw, Search, X } from 'lucide-react'; +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 React, { + startTransition, + Suspense, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { @@ -14,12 +31,16 @@ import { 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 SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter'; +import SearchResultFilter, { + SearchFilterCategory, +} from '@/components/SearchResultFilter'; import SearchSuggestions from '@/components/SearchSuggestions'; import VideoCard, { VideoCardHandle } from '@/components/VideoCard'; import VirtualScrollableGrid from '@/components/VirtualScrollableGrid'; @@ -30,13 +51,17 @@ function SearchPageClient() { // 返回顶部按钮显示状态 const [showBackToTop, setShowBackToTop] = useState(false); // 选项卡状态: 'video' 或 'pansou' 或 'acg' - const [activeTab, setActiveTab] = useState<'video' | 'pansou' | 'acg'>('video'); + 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 [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>( + null + ); // 繁体转简体转换器 const converterRef = useRef<((text: string) => string) | null>(null); // 转换器是否已初始化 @@ -57,8 +82,15 @@ function SearchPageClient() { const flushTimerRef = useRef(null); const [useFluidSearch, setUseFluidSearch] = useState(true); // 聚合卡片 refs 与聚合统计缓存 - const groupRefs = useRef>>(new Map()); - const groupStatsRef = useRef>(new Map()); + 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); // 是否使用了缓存结果 @@ -127,11 +159,16 @@ function SearchPageClient() { let max = 0; let res = 0; countMap.forEach((v, k) => { - if (v > max) { max = v; res = 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 source_names = Array.from( + new Set(group.map((g) => g.source_name).filter(Boolean)) + ) as string[]; const douban_id = (() => { const countMap = new Map(); @@ -143,7 +180,10 @@ function SearchPageClient() { let max = 0; let res: number | undefined; countMap.forEach((v, k) => { - if (v > max) { max = v; res = k; } + if (v > max) { + max = v; + res = k; + } }); return res; })(); @@ -151,13 +191,23 @@ function SearchPageClient() { return { episodes, source_names, douban_id }; }; // 过滤器:非聚合与聚合 - const [filterAll, setFilterAll] = useState<{ source: string; title: string; year: string; yearOrder: 'none' | 'asc' | 'desc' }>({ + 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' }>({ + const [filterAgg, setFilterAgg] = useState<{ + source: string; + title: string; + year: string; + yearOrder: 'none' | 'asc' | 'desc'; + }>({ source: 'all', title: 'all', year: 'all', @@ -178,6 +228,24 @@ function SearchPageClient() { 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[]) => { @@ -200,7 +268,11 @@ function SearchPageClient() { }; // 简化的年份排序:unknown/空值始终在最后 - const compareYear = (aYear: string, bYear: string, order: 'none' | 'asc' | 'desc') => { + const compareYear = ( + aYear: string, + bYear: string, + order: 'none' | 'asc' | 'desc' + ) => { // 如果是无排序状态,返回0(保持原顺序) if (order === 'none') return 0; @@ -230,7 +302,11 @@ function SearchPageClient() { // 辅助函数:获取视频类型 const getType = (item: SearchResult): 'movie' | 'tv' => { // 1. Emby 和 OpenList 源:使用 type_name(基于 TMDB,最可靠) - if (item.source === 'emby' || item.source?.startsWith('emby_') || item.source === 'openlist') { + if ( + item.source === 'emby' || + item.source?.startsWith('emby_') || + item.source === 'openlist' + ) { return item.type_name === '电影' ? 'movie' : 'tv'; } @@ -238,14 +314,21 @@ function SearchPageClient() { const typeName = item.type_name?.toLowerCase() || ''; // 2.1 明确包含"电影"或"movie"或"片"的,判断为电影 - if (typeName.includes('电影') || typeName.includes('movie') || - typeName.endsWith('片') && !typeName.includes('动漫')) { + if ( + typeName.includes('电影') || + typeName.includes('movie') || + (typeName.endsWith('片') && !typeName.includes('动漫')) + ) { return 'movie'; } // 2.2 包含"剧"、"动漫"、"综艺"等关键词的,判断为剧集 - if (typeName.includes('剧') || typeName.includes('动漫') || - typeName.includes('综艺') || typeName.includes('anime')) { + if ( + typeName.includes('剧') || + typeName.includes('动漫') || + typeName.includes('综艺') || + typeName.includes('anime') + ) { return 'tv'; } @@ -275,7 +358,9 @@ function SearchPageClient() { const aggregatedResults = useMemo(() => { // 首先应用精确搜索过滤 const filteredResults = exactSearch - ? searchResults.filter(item => titleContainsQuery(item.title, currentQueryRef.current)) + ? searchResults.filter((item) => + titleContainsQuery(item.title, currentQueryRef.current) + ) : searchResults; //===== 阶段1:按 normalizedTitle-type 初步分组 ===== @@ -297,16 +382,21 @@ function SearchPageClient() { preliminaryMap.forEach((group, preliminaryKey) => { // 分离有年份和无年份的结果 - const withYear = new Map(); + 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)) { + if ( + year && + year.trim() !== '' && + year !== 'unknown' && + /^\d{4}$/.test(year) + ) { // 有有效年份 - const arr = withYear.get(year) || []; + const arr = withYear.get(year) || []; arr.push(item); withYear.set(year, arr); } else { @@ -334,7 +424,9 @@ function SearchPageClient() { }); // 按出现顺序返回聚合结果 - return keyOrder.map(key => [key, finalMap.get(key)!] as [string, SearchResult[]]); + return keyOrder.map( + (key) => [key, finalMap.get(key)!] as [string, SearchResult[]] + ); }, [searchResults, exactSearch]); // 当聚合结果变化时,如果某个聚合已存在,则调用其卡片 ref 的 set 方法增量更新 @@ -373,7 +465,12 @@ function SearchPageClient() { const yearsSet = new Set(); searchResults.forEach((item) => { - if (item.source && item.source_name && item.source.trim() !== '' && item.source_name.trim() !== '') { + if ( + item.source && + item.source_name && + item.source.trim() !== '' && + item.source_name.trim() !== '' + ) { sourcesSet.set(item.source, item.source_name); } if (item.title && item.title.trim() !== '') titlesSet.add(item.title); @@ -415,7 +512,9 @@ function SearchPageClient() { // 年份: 将 unknown 放末尾 const years = Array.from(yearsSet.values()); - const knownYears = years.filter((y) => y !== 'unknown').sort((a, b) => parseInt(b) - parseInt(a)); + 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' }, @@ -444,7 +543,9 @@ function SearchPageClient() { // 首先应用精确搜索过滤 const exactSearchFiltered = exactSearch - ? searchResults.filter(item => titleContainsQuery(item.title, currentQueryRef.current)) + ? searchResults.filter((item) => + titleContainsQuery(item.title, currentQueryRef.current) + ) : searchResults; const filtered = exactSearchFiltered.filter((item) => { @@ -472,9 +573,9 @@ function SearchPageClient() { if (!aExactMatch && bExactMatch) return 1; // 最后按标题排序,正序时字母序,倒序时反字母序 - return yearOrder === 'asc' ? - a.title.localeCompare(b.title) : - b.title.localeCompare(a.title); + return yearOrder === 'asc' + ? a.title.localeCompare(b.title) + : b.title.localeCompare(a.title); }); }, [searchResults, filterAll, searchQuery, exactSearch]); @@ -484,7 +585,8 @@ function SearchPageClient() { 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); + 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; @@ -513,26 +615,228 @@ function SearchPageClient() { // 最后按标题排序,正序时字母序,倒序时反字母序 const aTitle = a[1][0].title; const bTitle = b[1][0].title; - return yearOrder === 'asc' ? - aTitle.localeCompare(bTitle) : - bTitle.localeCompare(aTitle); + return yearOrder === 'asc' + ? aTitle.localeCompare(bTitle) + : bTitle.localeCompare(aTitle); }); }, [aggregatedResults, filterAgg, searchQuery]); const useVirtualGrid = useMemo(() => { - const cardCount = viewMode === 'agg' ? filteredAggResults.length : filteredAllResults.length; - return cardCount >= 100; - }, [viewMode, filteredAggResults.length, filteredAllResults.length]); + const cardCount = + viewMode === 'agg' + ? filteredAggResults.length + : filteredAllResults.length; + return resultDisplayMode === 'card' && cardCount >= 100; + }, [ + viewMode, + resultDisplayMode, + filteredAggResults.length, + filteredAllResults.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); + setTriggerPansouSearch((prev) => !prev); } // 如果切换到 ACG 磁力搜索选项卡,且有搜索关键词,且已显示结果,则触发搜索 if (activeTab === 'acg' && searchQuery.trim() && showResults) { - setTriggerAcgSearch(prev => !prev); + setTriggerAcgSearch((prev) => !prev); } }, [activeTab]); @@ -552,9 +856,9 @@ function SearchPageClient() { // 延迟触发搜索,确保组件已经切换到正确的标签页 setTimeout(() => { if (typeParam === 'pansou') { - setTriggerPansouSearch(prev => !prev); + setTriggerPansouSearch((prev) => !prev); } else if (typeParam === 'acg') { - setTriggerAcgSearch(prev => !prev); + setTriggerAcgSearch((prev) => !prev); } }, 100); } @@ -574,20 +878,22 @@ function SearchPageClient() { // 初始化繁体转简体转换器 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); + 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,避免阻塞 - } - }).catch((error) => { - console.error('加载 opencc-js 失败:', error); - setConverterReady(true); // 即使失败也设置为 true,避免阻塞 - }); + }); } else { setConverterReady(true); } @@ -670,7 +976,9 @@ function SearchPageClient() { // 如果开启了繁体转简体,进行转换 if (query && typeof window !== 'undefined') { - const searchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + const searchTraditionalToSimplified = localStorage.getItem( + 'searchTraditionalToSimplified' + ); if (searchTraditionalToSimplified === 'true' && converterRef.current) { try { @@ -681,7 +989,13 @@ function SearchPageClient() { if (originalQuery !== query) { const trimmedConverted = query.trim(); // 使用 replace 而不是 push,避免在历史记录中留下繁体版本 - router.replace(`/search?q=${encodeURIComponent(trimmedConverted)}${searchParams.get('type') ? `&type=${searchParams.get('type')}` : ''}`); + router.replace( + `/search?q=${encodeURIComponent(trimmedConverted)}${ + searchParams.get('type') + ? `&type=${searchParams.get('type')}` + : '' + }` + ); return; // 等待 URL 更新后重新触发此 effect } } catch (error) { @@ -696,7 +1010,7 @@ function SearchPageClient() { setSearchQuery(query); const trimmed = query.trim(); - + // 检查是否有缓存且不是强制刷新 if (!forceRefresh) { const cachedResults = getCachedResults(trimmed); @@ -714,19 +1028,21 @@ function SearchPageClient() { return; } } - + // 如果是强制刷新,清除缓存 if (forceRefresh) { clearCachedResults(trimmed); setForceRefresh(false); } - + // 开始新搜索时,重置缓存标记 setIsFromCache(false); - + // 新搜索:关闭旧连接并清空结果 if (eventSourceRef.current) { - try { eventSourceRef.current.close(); } catch { } + try { + eventSourceRef.current.close(); + } catch {} eventSourceRef.current = null; } // 先设置加载状态,再清空结果,避免短暂显示"暂无搜索结果" @@ -749,7 +1065,8 @@ function SearchPageClient() { if (savedFluidSearch !== null) { currentFluidSearch = JSON.parse(savedFluidSearch); } else { - const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; + const defaultFluidSearch = + (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; currentFluidSearch = defaultFluidSearch; } } @@ -761,7 +1078,9 @@ function SearchPageClient() { if (currentFluidSearch) { // 流式搜索:打开新的流式连接 - const es = new EventSource(`/api/search/ws?q=${encodeURIComponent(trimmed)}`); + const es = new EventSource( + `/api/search/ws?q=${encodeURIComponent(trimmed)}` + ); eventSourceRef.current = es; es.onmessage = (event) => { @@ -776,9 +1095,15 @@ function SearchPageClient() { break; case 'source_result': { setCompletedSources((prev) => prev + 1); - if (Array.isArray(payload.results) && payload.results.length > 0) { + if ( + Array.isArray(payload.results) && + payload.results.length > 0 + ) { // 缓冲新增结果,节流刷入,避免频繁重渲染导致闪烁 - const activeYearOrder = (viewMode === 'agg' ? (filterAgg.yearOrder) : (filterAll.yearOrder)); + const activeYearOrder = + viewMode === 'agg' + ? filterAgg.yearOrder + : filterAll.yearOrder; const incoming: SearchResult[] = activeYearOrder === 'none' ? sortBatchForNoOrder(payload.results as SearchResult[]) @@ -826,13 +1151,15 @@ function SearchPageClient() { }); } setIsLoading(false); - try { es.close(); } catch { } + try { + es.close(); + } catch {} if (eventSourceRef.current === es) { eventSourceRef.current = null; } break; } - } catch { } + } catch {} }; es.onerror = () => { @@ -849,7 +1176,9 @@ function SearchPageClient() { setSearchResults((prev) => prev.concat(toAppend)); }); } - try { es.close(); } catch { } + try { + es.close(); + } catch {} if (eventSourceRef.current === es) { eventSourceRef.current = null; } @@ -857,12 +1186,13 @@ function SearchPageClient() { } else { // 传统搜索:使用普通接口 fetch(`/api/search?q=${encodeURIComponent(trimmed)}`) - .then(response => response.json()) - .then(data => { + .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 activeYearOrder = + viewMode === 'agg' ? filterAgg.yearOrder : filterAll.yearOrder; const results: SearchResult[] = activeYearOrder === 'none' ? sortBatchForNoOrder(data.results as SearchResult[]) @@ -894,7 +1224,9 @@ function SearchPageClient() { useEffect(() => { return () => { if (eventSourceRef.current) { - try { eventSourceRef.current.close(); } catch { } + try { + eventSourceRef.current.close(); + } catch {} eventSourceRef.current = null; } if (flushTimerRef.current) { @@ -932,7 +1264,9 @@ function SearchPageClient() { // 如果开启了繁体转简体,进行转换 if (typeof window !== 'undefined') { - const searchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + const searchTraditionalToSimplified = localStorage.getItem( + 'searchTraditionalToSimplified' + ); if (searchTraditionalToSimplified === 'true' && converterRef.current) { try { trimmed = converterRef.current(trimmed); @@ -957,11 +1291,11 @@ function SearchPageClient() { } else if (activeTab === 'pansou') { // 网盘搜索 - 触发搜索 router.push(`/search?q=${encodeURIComponent(trimmed)}&type=pansou`); - setTriggerPansouSearch(prev => !prev); // 切换状态来触发搜索 + setTriggerPansouSearch((prev) => !prev); // 切换状态来触发搜索 } else if (activeTab === 'acg') { // ACG 磁力搜索 - 触发搜索 router.push(`/search?q=${encodeURIComponent(trimmed)}&type=acg`); - setTriggerAcgSearch(prev => !prev); + setTriggerAcgSearch((prev) => !prev); } }; @@ -970,7 +1304,9 @@ function SearchPageClient() { // 如果开启了繁体转简体,进行转换 if (typeof window !== 'undefined') { - const searchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + const searchTraditionalToSimplified = localStorage.getItem( + 'searchTraditionalToSimplified' + ); if (searchTraditionalToSimplified === 'true' && converterRef.current) { try { processedSuggestion = converterRef.current(suggestion); @@ -991,16 +1327,22 @@ function SearchPageClient() { // 根据当前选项卡执行不同的搜索 if (activeTab === 'video') { // 影视搜索 - router.push(`/search?q=${encodeURIComponent(processedSuggestion)}&type=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); + 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); + router.push( + `/search?q=${encodeURIComponent(processedSuggestion)}&type=acg` + ); + setTriggerAcgSearch((prev) => !prev); } }; @@ -1025,7 +1367,9 @@ function SearchPageClient() { // 如果有搜索关键词,更新 URL const currentQuery = searchParams.get('q'); if (currentQuery) { - router.push(`/search?q=${encodeURIComponent(currentQuery)}&type=${newTab}`); + router.push( + `/search?q=${encodeURIComponent(currentQuery)}&type=${newTab}` + ); } }; @@ -1044,7 +1388,7 @@ function SearchPageClient() { onChange={handleInputChange} onFocus={handleInputFocus} placeholder='搜索电影、电视剧...' - autoComplete="off" + autoComplete='off' className='w-full h-12 rounded-lg bg-gray-50/80 py-3 pl-10 pr-12 text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-400 focus:bg-white border border-gray-200/50 shadow-sm dark:bg-gray-800 dark:text-gray-300 dark:placeholder-gray-500 dark:focus:bg-gray-700 dark:border-gray-700' /> @@ -1112,7 +1456,9 @@ function SearchPageClient() { : []), ]} active={activeTab} - onChange={(value) => handleTabChange(value as 'video' | 'pansou' | 'acg')} + onChange={(value) => + handleTabChange(value as 'video' | 'pansou' | 'acg') + } /> @@ -1126,178 +1472,284 @@ function SearchPageClient() { {/* 影视搜索结果 */} {/* 标题 */}
-

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

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

- {/* 强制刷新按钮 */} - {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 sm:gap-y-20 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8'; - - const gridChildren = - 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); - - // 从 mapKey 中提取类型(mapKey 格式:normalizedTitle-type-year) - // 找到最后一个 '-' 之前的部分,再找倒数第二个 '-' - 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 }); - } - - return ( -
- -
- ); - }) - : filteredAllResults.map((item) => ( -
- 1 ? 'tv' : 'movie'} - /> -
- )); - - if (useVirtualGrid) { - return ( - + {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' ? ( <> @@ -1357,20 +1809,26 @@ function SearchPageClient() { if (activeTab === 'video') { // 影视搜索 router.push( - `/search?q=${encodeURIComponent(item.trim())}&type=video` + `/search?q=${encodeURIComponent( + item.trim() + )}&type=video` ); } else if (activeTab === 'pansou') { // 网盘搜索 router.push( - `/search?q=${encodeURIComponent(item.trim())}&type=pansou` + `/search?q=${encodeURIComponent( + item.trim() + )}&type=pansou` ); - setTriggerPansouSearch(prev => !prev); + setTriggerPansouSearch((prev) => !prev); } else if (activeTab === 'acg') { // ACG 磁力搜索 router.push( - `/search?q=${encodeURIComponent(item.trim())}&type=acg` + `/search?q=${encodeURIComponent( + item.trim() + )}&type=acg` ); - setTriggerAcgSearch(prev => !prev); + setTriggerAcgSearch((prev) => !prev); } }} className='px-4 py-2 bg-gray-500/10 hover:bg-gray-300 rounded-full text-sm text-gray-700 transition-colors duration-200 dark:bg-gray-700/50 dark:hover:bg-gray-600 dark:text-gray-300' @@ -1397,13 +1855,23 @@ function SearchPageClient() { + {previewImage && ( + setPreviewImage(null)} + imageUrl={previewImage.url} + alt={previewImage.alt} + /> + )} + {/* 返回顶部悬浮按钮 */}