网盘搜索前增加来源筛选

This commit is contained in:
mtvpls
2026-06-06 17:00:23 +08:00
parent c8898bcaea
commit f6db363dcb
3 changed files with 217 additions and 15 deletions
+12 -5
View File
@@ -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())
);
+196 -7
View File
@@ -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<HTMLButtonElement | null>(null);
const pansouCloudFilterDropdownRef = useRef<HTMLDivElement | null>(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 (
<div className='mx-auto mt-4 flex max-w-2xl justify-end overflow-visible'>
<button
ref={pansouCloudFilterButtonRef}
type='button'
onClick={() => {
if (!pansouCloudFilterOpen) {
calculatePansouCloudFilterPosition();
}
setPansouCloudFilterOpen((prev) => !prev);
}}
className={`relative z-10 rounded-full px-3 py-1 text-xs font-medium transition-all duration-200 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${
hasFilter
? 'cursor-pointer text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300'
: 'cursor-pointer text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'
}`}
aria-expanded={pansouCloudFilterOpen}
aria-haspopup='listbox'
>
<span>{displayText}</span>
<svg
className={`ml-1 inline-block h-3 w-3 transition-transform duration-200 ${
pansouCloudFilterOpen ? 'rotate-180' : ''
}`}
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
aria-hidden='true'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M19 9l-7 7-7-7'
/>
</svg>
</button>
</div>
);
};
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() {
<PageLayout activePath='/search'>
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible mb-10'>
{/* 搜索框 */}
<div className='mb-8'>
<div className='mb-0'>
<form onSubmit={handleSearch} className='max-w-2xl mx-auto'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-400 dark:text-gray-500' />
@@ -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);
}
}}
/>
</div>
@@ -1639,10 +1769,65 @@ function SearchPageClient() {
}
/>
</div>
{activeTab === 'pansou' &&
netdiskSearchEnabled &&
renderPansouCloudTypeFilter()}
</div>
{pansouCloudFilterOpen &&
createPortal(
<div
ref={pansouCloudFilterDropdownRef}
className='fixed z-[9999] max-h-[50vh] overflow-y-auto rounded-xl border border-gray-200/50 bg-white/95 p-2 backdrop-blur-sm dark:border-gray-700/50 dark:bg-gray-800/95'
style={{
left: `${pansouCloudFilterPosition.x}px`,
top: `${pansouCloudFilterPosition.y}px`,
width: `${pansouCloudFilterPosition.width}px`,
}}
>
<div className='grid grid-cols-3 gap-1.5 sm:grid-cols-4'>
<button
type='button'
onClick={() => setSelectedPansouCloudTypes([])}
className={`rounded-lg px-2 py-1.5 text-left text-xs transition-all duration-200 ${
selectedPansouCloudTypes.length === 0
? 'border border-green-200 bg-green-100 text-green-700 dark:border-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'text-gray-700 hover:bg-gray-100/80 dark:text-gray-300 dark:hover:bg-gray-700/80'
}`}
aria-pressed={selectedPansouCloudTypes.length === 0}
>
</button>
{PANSOU_CLOUD_TYPE_OPTIONS.map(({ value, label }) => {
const selected = selectedPansouCloudTypes.includes(value);
return (
<button
key={value}
type='button'
onClick={() => togglePansouCloudType(value)}
className={`rounded-lg px-2 py-1.5 text-left text-xs transition-all duration-200 ${
selected
? 'border border-green-200 bg-green-100 text-green-700 dark:border-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'text-gray-700 hover:bg-gray-100/80 dark:text-gray-300 dark:hover:bg-gray-700/80'
}`}
aria-pressed={selected}
>
{label}
</button>
);
})}
</div>
</div>,
document.body
)}
{/* 搜索结果或搜索历史 */}
<div className='max-w-[95%] mx-auto mt-12 overflow-visible'>
<div
className={`max-w-[95%] mx-auto overflow-visible ${
activeTab === 'pansou' ? 'mt-4' : 'mt-12'
}`}
>
{showResults ? (
<section className='mb-12'>
{activeTab === 'video' ? (
@@ -1680,8 +1865,7 @@ function SearchPageClient() {
</span>
{resultCountMeta.isFiltered && (
<span className='inline-flex items-center rounded-full bg-white/80 px-2.5 py-1 font-medium text-gray-500 ring-1 ring-gray-200 dark:bg-gray-900/70 dark:text-gray-400 dark:ring-gray-700'>
{' '}
{resultCountMeta.totalCount.toLocaleString()}{' '}
{resultCountMeta.totalCount.toLocaleString()}{' '}
{resultCountMeta.unit}
</span>
)}
@@ -1851,7 +2035,9 @@ function SearchPageClient() {
<VideoCard
ref={getGroupRef(mapKey)}
from='search'
onBeforeNavigate={savePartialCacheForPlayback}
onBeforeNavigate={
savePartialCacheForPlayback
}
isAggregate={true}
title={title}
poster={poster}
@@ -1901,7 +2087,9 @@ function SearchPageClient() {
>
<VideoCard
id={item.id}
onBeforeNavigate={savePartialCacheForPlayback}
onBeforeNavigate={
savePartialCacheForPlayback
}
title={item.title}
poster={item.poster}
episodes={item.episodes.length}
@@ -1958,6 +2146,7 @@ function SearchPageClient() {
<PansouSearch
keyword={searchQuery}
triggerSearch={triggerPansouSearch}
cloudTypes={selectedPansouCloudTypes}
/>
</>
) : (
+9 -3
View File
@@ -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<string, string> = {
export const CLOUD_TYPE_NAMES: Record<string, string> = {
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 ? (
<>
<Loader2 className='h-3.5 w-3.5 animate-spin' />
<span className='hidden sm:inline'>...</span>
<span className='hidden sm:inline'>
...
</span>
</>
) : (
<>