From f6db363dcb0fe868f9afc33d3155bea39028e200 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 6 Jun 2026 17:00:23 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=9B=98=E6=90=9C=E7=B4=A2=E5=89=8D?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9D=A5=E6=BA=90=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/pansou/search/route.ts | 17 ++- src/app/search/page.tsx | 203 ++++++++++++++++++++++++++++- src/components/PansouSearch.tsx | 12 +- 3 files changed, 217 insertions(+), 15 deletions(-) diff --git a/src/app/api/pansou/search/route.ts b/src/app/api/pansou/search/route.ts index 78d3956..95ae78d 100644 --- a/src/app/api/pansou/search/route.ts +++ b/src/app/api/pansou/search/route.ts @@ -19,12 +19,15 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { keyword } = body; + const cloudTypes = Array.isArray(body.cloud_types) + ? body.cloud_types.filter( + (item: unknown): item is string => + typeof item === 'string' && item.trim().length > 0 + ) + : undefined; if (!keyword) { - return NextResponse.json( - { error: '关键词不能为空' }, - { status: 400 } - ); + return NextResponse.json({ error: '关键词不能为空' }, { status: 400 }); } // 从系统配置中获取 Pansou 配置 @@ -37,6 +40,7 @@ export async function POST(request: NextRequest) { keyword, apiUrl: apiUrl ? '已配置' : '未配置', hasAuth: !!(username && password), + cloudTypes: cloudTypes?.length ? cloudTypes : 'all', }); if (!apiUrl) { @@ -50,6 +54,7 @@ export async function POST(request: NextRequest) { const results = await searchPansou(apiUrl, keyword, { username, password, + cloudTypes, }); const rawBlocklist = config.SiteConfig.PansouKeywordBlocklist || ''; @@ -66,7 +71,9 @@ export async function POST(request: NextRequest) { let total = 0; const shouldBlock = (link: PansouLink) => { - const content = `${link.note || ''} ${link.url || ''} ${link.source || ''}`.toLowerCase(); + const content = `${link.note || ''} ${link.url || ''} ${ + link.source || '' + }`.toLowerCase(); return blockedKeywords.some((item) => content.includes(item.toLowerCase()) ); diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 0d0254b..ae8cfde 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -21,6 +21,7 @@ import React, { useRef, useState, } from 'react'; +import { createPortal } from 'react-dom'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { @@ -37,7 +38,7 @@ 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 PansouSearch, { CLOUD_TYPE_NAMES } from '@/components/PansouSearch'; import ProxyImage from '@/components/ProxyImage'; import SearchResultFilter, { SearchFilterCategory, @@ -46,6 +47,10 @@ import SearchSuggestions from '@/components/SearchSuggestions'; import VideoCard, { VideoCardHandle } from '@/components/VideoCard'; import VirtualScrollableGrid from '@/components/VirtualScrollableGrid'; +const PANSOU_CLOUD_TYPE_OPTIONS = Object.entries(CLOUD_TYPE_NAMES).map( + ([value, label]) => ({ value, label }) +); + type SearchCachePayload = { status: 'complete' | 'partial'; results: SearchResult[]; @@ -66,6 +71,17 @@ function SearchPageClient() { const [triggerPansouSearch, setTriggerPansouSearch] = useState(false); // ACG 搜索触发标志 const [triggerAcgSearch, setTriggerAcgSearch] = useState(false); + const [selectedPansouCloudTypes, setSelectedPansouCloudTypes] = useState< + string[] + >([]); + const [pansouCloudFilterOpen, setPansouCloudFilterOpen] = useState(false); + const [pansouCloudFilterPosition, setPansouCloudFilterPosition] = useState({ + x: 0, + y: 0, + width: 0, + }); + const pansouCloudFilterButtonRef = useRef(null); + const pansouCloudFilterDropdownRef = useRef(null); // 用户权限 const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>( null @@ -1520,6 +1536,115 @@ function SearchPageClient() { } }; + const togglePansouCloudType = (cloudType: string) => { + setSelectedPansouCloudTypes((prev) => + prev.includes(cloudType) + ? prev.filter((type) => type !== cloudType) + : [...prev, cloudType] + ); + }; + + const calculatePansouCloudFilterPosition = () => { + const element = pansouCloudFilterButtonRef.current; + if (!element) return; + + const rect = element.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const padding = 16; + const width = Math.min(320, viewportWidth - padding * 2); + let x = rect.left; + + if (x + width > viewportWidth - padding) { + x = viewportWidth - width - padding; + } + if (x < padding) { + x = padding; + } + + setPansouCloudFilterPosition({ x, y: rect.bottom + 8, width }); + }; + + const selectedPansouCloudTypeLabels = selectedPansouCloudTypes + .map((type) => CLOUD_TYPE_NAMES[type] || type) + .filter(Boolean); + + const renderPansouCloudTypeFilter = () => { + const hasFilter = selectedPansouCloudTypes.length > 0; + const displayText = hasFilter + ? selectedPansouCloudTypes.length === 1 + ? selectedPansouCloudTypeLabels[0] + : `网盘类型 · ${selectedPansouCloudTypes.length}` + : '网盘类型'; + + return ( +
+ +
+ ); + }; + + useEffect(() => { + if (!pansouCloudFilterOpen) return; + + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if ( + pansouCloudFilterButtonRef.current?.contains(target) || + pansouCloudFilterDropdownRef.current?.contains(target) + ) { + return; + } + setPansouCloudFilterOpen(false); + }; + + const handleScroll = () => setPansouCloudFilterOpen(false); + const handleResize = () => calculatePansouCloudFilterPosition(); + + document.addEventListener('mousedown', handleClickOutside); + document.body.addEventListener('scroll', handleScroll, { passive: true }); + window.addEventListener('resize', handleResize); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.body.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleResize); + }; + }, [pansouCloudFilterOpen]); + // 返回顶部功能 const scrollToTop = () => { try { @@ -1551,7 +1676,7 @@ function SearchPageClient() {
{/* 搜索框 */} -
+
@@ -1600,6 +1725,11 @@ function SearchPageClient() { router.push( `/search?q=${encodeURIComponent(trimmed)}&type=${activeTab}` ); + if (activeTab === 'pansou') { + setTriggerPansouSearch((prev) => !prev); + } else if (activeTab === 'acg') { + setTriggerAcgSearch((prev) => !prev); + } }} />
@@ -1639,10 +1769,65 @@ function SearchPageClient() { } />
+ + {activeTab === 'pansou' && + netdiskSearchEnabled && + renderPansouCloudTypeFilter()}
+ {pansouCloudFilterOpen && + createPortal( +
+
+ + {PANSOU_CLOUD_TYPE_OPTIONS.map(({ value, label }) => { + const selected = selectedPansouCloudTypes.includes(value); + return ( + + ); + })} +
+
, + document.body + )} + {/* 搜索结果或搜索历史 */} -
+
{showResults ? (
{activeTab === 'video' ? ( @@ -1680,8 +1865,7 @@ function SearchPageClient() { {resultCountMeta.isFiltered && ( - 筛选前{' '} - {resultCountMeta.totalCount.toLocaleString()}{' '} + 筛选前 {resultCountMeta.totalCount.toLocaleString()}{' '} {resultCountMeta.unit} )} @@ -1851,7 +2035,9 @@ function SearchPageClient() { ) : ( diff --git a/src/components/PansouSearch.tsx b/src/components/PansouSearch.tsx index e86c5f6..0002baa 100644 --- a/src/components/PansouSearch.tsx +++ b/src/components/PansouSearch.tsx @@ -20,6 +20,7 @@ interface PansouSearchProps { keyword: string; triggerSearch?: boolean; // 触发搜索的标志 onError?: (error: string) => void; + cloudTypes?: string[]; } type DownloadTool = 'aria2' | 'Transmission' | 'qBittorrent'; @@ -31,7 +32,7 @@ const downloadToolOptions: Array<{ value: DownloadTool; label: string }> = [ ]; // 网盘类型映射 -const CLOUD_TYPE_NAMES: Record = { +export const CLOUD_TYPE_NAMES: Record = { baidu: '百度网盘', aliyun: '阿里云盘', quark: '夸克网盘', @@ -150,6 +151,7 @@ export default function PansouSearch({ keyword, triggerSearch, onError, + cloudTypes = [], }: PansouSearchProps) { const router = useRouter(); const [loading, setLoading] = useState(false); @@ -264,6 +266,7 @@ export default function PansouSearch({ setLoading(true); setError(null); setResults(null); + setSelectedType('all'); try { const response = await fetch('/api/pansou/search', { @@ -273,6 +276,7 @@ export default function PansouSearch({ }, body: JSON.stringify({ keyword: currentKeyword, + cloud_types: cloudTypes, }), }); @@ -290,7 +294,7 @@ export default function PansouSearch({ } finally { setLoading(false); } - }, [keyword, onError]); + }, [keyword, onError, cloudTypes]); useEffect(() => { // triggerSearch 变化时触发搜索(无论是 true 还是 false) @@ -881,7 +885,9 @@ export default function PansouSearch({ {downloadingUrl === link.url ? ( <> - 下载中... + + 下载中... + ) : ( <>