/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import { AlertCircle, Download, ExternalLink, Loader2 } from 'lucide-react'; import { useEffect, useState, useRef, useCallback } from 'react'; import Toast, { ToastProps } from '@/components/Toast'; import CapsuleSwitch from '@/components/CapsuleSwitch'; interface AcgSearchItem { title: string; link: string; guid: string; pubDate: string; torrentUrl: string; description: string; images: string[]; } interface AcgSearchResult { keyword: string; page: number; total: number; items: AcgSearchItem[]; } interface AcgSearchProps { keyword: string; triggerSearch?: boolean; onError?: (error: string) => void; } type AcgSearchSource = 'acgrip' | 'mikan' | 'dmhy'; export default function AcgSearch({ keyword, triggerSearch, onError, }: AcgSearchProps) { const [source, setSource] = useState('acgrip'); const [loading, setLoading] = useState(false); const [allItems, setAllItems] = useState([]); // 所有加载的项目 const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [downloadingId, setDownloadingId] = useState(null); const [showNameDialog, setShowNameDialog] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [customName, setCustomName] = useState(''); const [toast, setToast] = useState(null); const loadMoreRef = useRef(null); const isLoadingMoreRef = useRef(false); const didInitSourceRef = useRef(false); // 执行搜索 const performSearch = async (page: number, isLoadMore = false) => { if (isLoadingMoreRef.current) return; if (source === 'mikan' && page > 1) return; if (source === 'dmhy' && page > 1) return; isLoadingMoreRef.current = true; setLoading(true); setError(null); try { const apiUrl = source === 'mikan' ? '/api/acg/mikan' : source === 'dmhy' ? '/api/acg/dmhy' : '/api/acg/acgrip'; const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ keyword: keyword.trim(), page, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '搜索失败'); } const data: AcgSearchResult = await response.json(); if (isLoadMore) { // 追加新数据 setAllItems(prev => [...prev, ...data.items]); // 如果当前页没有结果,说明没有更多了 setHasMore(source !== 'mikan' && source !== 'dmhy' && data.items.length > 0); } else { // 新搜索,重置数据 setAllItems(data.items); // 如果第一页有结果,假设可能还有更多 setHasMore(source !== 'mikan' && source !== 'dmhy' && data.items.length > 0); } setCurrentPage(page); } catch (err: any) { const errorMsg = err.message || '搜索失败,请稍后重试'; setError(errorMsg); onError?.(errorMsg); } finally { setLoading(false); isLoadingMoreRef.current = false; } }; useEffect(() => { // triggerSearch 变化时触发搜索(无论是 true 还是 false) if (triggerSearch === undefined) { return; } const currentKeyword = keyword.trim(); if (!currentKeyword) { return; } // 重置状态并开始新搜索 setAllItems([]); setCurrentPage(1); setHasMore(true); performSearch(1, false); }, [triggerSearch]); // 切换搜索源时,自动重新搜索(避免组件初次挂载时重复触发) useEffect(() => { if (!didInitSourceRef.current) { didInitSourceRef.current = true; return; } const currentKeyword = keyword.trim(); if (!currentKeyword) return; setAllItems([]); setCurrentPage(1); setHasMore(true); performSearch(1, false); }, [source]); // 加载更多数据 const loadMore = useCallback(() => { if (source === 'mikan') return; if (source === 'dmhy') return; if (!loading && hasMore && !isLoadingMoreRef.current) { performSearch(currentPage + 1, true); } }, [loading, hasMore, currentPage, source]); // 使用 Intersection Observer 监听滚动到底部 useEffect(() => { const element = loadMoreRef.current; if (!element) return; const observer = new IntersectionObserver( (entries) => { const target = entries[0]; if (target.isIntersecting) { loadMore(); } }, { root: null, rootMargin: '100px', threshold: 0.1, } ); observer.observe(element); return () => { observer.unobserve(element); }; }, [loadMore]); // 打开命名弹窗 const handleOpenDownloadDialog = (item: AcgSearchItem) => { setSelectedItem(item); setCustomName(keyword.trim()); setShowNameDialog(true); }; // 确认下载 const handleConfirmDownload = async () => { if (!selectedItem || !customName.trim()) { return; } setDownloadingId(selectedItem.guid); setShowNameDialog(false); try { const response = await fetch('/api/acg/download', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url: selectedItem.torrentUrl, name: customName.trim(), }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '添加下载任务失败'); } setToast({ message: data.message || '已添加到离线下载队列', type: 'success', onClose: () => setToast(null), }); } catch (err: any) { setToast({ message: err.message || '添加下载任务失败', type: 'error', onClose: () => setToast(null), }); } finally { setDownloadingId(null); setSelectedItem(null); setCustomName(''); } }; const renderBody = () => { if (loading && allItems.length === 0) { return (

正在搜索动漫资源...

); } if (error) { return (

{error}

); } if (allItems.length === 0) { return (

未找到相关资源

); } return ( <> {/* 结果列表 */}
{allItems.map((item) => (
{/* 标题 */}
{item.title}
{/* 发布时间 */}
{new Date(item.pubDate).toLocaleString('zh-CN')}
{/* 图片预览 */} {item.images && item.images.length > 0 && (
{item.images.slice(0, 3).map((img, imgIndex) => ( ))}
)} {/* 操作按钮 */}
详情
))}
{/* 加载更多指示器 */} {source !== 'mikan' && source !== 'dmhy' && hasMore && (

加载更多...

)} {/* 命名弹窗 */} {showNameDialog && (

设置资源名称

setCustomName(e.target.value)} placeholder='请输入资源名称' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500' autoFocus />
)} ); }; return (
{/* 搜索源切换 */}
setSource(value as AcgSearchSource)} />
{renderBody()} {/* Toast 提示 */} {toast && }
); }