动漫磁力搜索增加蜜柑

This commit is contained in:
mtvpls
2026-01-22 11:24:11 +08:00
parent a41f2fd1d6
commit 06efd8121f
3 changed files with 365 additions and 167 deletions

View File

@@ -5,6 +5,7 @@ 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;
@@ -29,11 +30,14 @@ interface AcgSearchProps {
onError?: (error: string) => void;
}
type AcgSearchSource = 'acgrip' | 'mikan';
export default function AcgSearch({
keyword,
triggerSearch,
onError,
}: AcgSearchProps) {
const [source, setSource] = useState<AcgSearchSource>('acgrip');
const [loading, setLoading] = useState(false);
const [allItems, setAllItems] = useState<AcgSearchItem[]>([]); // 所有加载的项目
const [error, setError] = useState<string | null>(null);
@@ -46,17 +50,20 @@ export default function AcgSearch({
const [toast, setToast] = useState<ToastProps | null>(null);
const loadMoreRef = useRef<HTMLDivElement>(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;
isLoadingMoreRef.current = true;
setLoading(true);
setError(null);
try {
const response = await fetch('/api/acg/search', {
const apiUrl = source === 'mikan' ? '/api/acg/mikan' : '/api/acg/acgrip';
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -78,12 +85,12 @@ export default function AcgSearch({
// 追加新数据
setAllItems(prev => [...prev, ...data.items]);
// 如果当前页没有结果,说明没有更多了
setHasMore(data.items.length > 0);
setHasMore(source === 'acgrip' && data.items.length > 0);
} else {
// 新搜索,重置数据
setAllItems(data.items);
// 如果第一页有结果,假设可能还有更多
setHasMore(data.items.length > 0);
setHasMore(source === 'acgrip' && data.items.length > 0);
}
setCurrentPage(page);
@@ -115,12 +122,29 @@ export default function AcgSearch({
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 (!loading && hasMore && !isLoadingMoreRef.current) {
performSearch(currentPage + 1, true);
}
}, [loading, hasMore, currentPage]);
}, [loading, hasMore, currentPage, source]);
// 使用 Intersection Observer 监听滚动到底部
useEffect(() => {
@@ -200,161 +224,180 @@ export default function AcgSearch({
}
};
if (loading && allItems.length === 0) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-center'>
<Loader2 className='mx-auto h-8 w-8 animate-spin text-green-600 dark:text-green-400' />
<p className='mt-4 text-sm text-gray-600 dark:text-gray-400'>
...
</p>
</div>
</div>
);
}
if (error) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-center'>
<AlertCircle className='mx-auto h-12 w-12 text-red-500 dark:text-red-400' />
<p className='mt-4 text-sm text-red-600 dark:text-red-400'>{error}</p>
</div>
</div>
);
}
if (allItems.length === 0) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-center'>
<AlertCircle className='mx-auto h-12 w-12 text-gray-400 dark:text-gray-600' />
<p className='mt-4 text-sm text-gray-600 dark:text-gray-400'>
</p>
</div>
</div>
);
}
return (
<div className='space-y-6'>
{/* 结果列表 */}
<div className='space-y-3'>
{allItems.map((item) => (
<div
key={item.guid}
className='p-4 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 transition-colors'
>
{/* 标题 */}
<div className='mb-2 font-medium text-gray-900 dark:text-gray-100'>
{item.title}
</div>
{/* 发布时间 */}
<div className='mb-2 text-xs text-gray-500 dark:text-gray-400'>
{new Date(item.pubDate).toLocaleString('zh-CN')}
</div>
{/* 图片预览 */}
{item.images && item.images.length > 0 && (
<div className='mb-3 flex gap-2 overflow-x-auto'>
{item.images.slice(0, 3).map((img, imgIndex) => (
<img
key={imgIndex}
src={img}
alt=''
className='h-20 w-auto rounded object-cover'
loading='lazy'
/>
))}
</div>
)}
{/* 操作按钮 */}
<div className='flex items-center gap-2'>
<button
onClick={() => handleOpenDownloadDialog(item)}
disabled={downloadingId === item.guid}
className='flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-green-600 text-white text-sm hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
title='存到私人影库'
>
{downloadingId === item.guid ? (
<>
<Loader2 className='h-4 w-4 animate-spin' />
<span>...</span>
</>
) : (
<>
<Download className='h-4 w-4' />
<span></span>
</>
)}
</button>
<a
href={item.link}
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-gray-200 text-gray-700 text-sm hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors'
title='查看详情'
>
<ExternalLink className='h-4 w-4' />
<span></span>
</a>
</div>
</div>
))}
</div>
{/* 加载更多指示器 */}
{hasMore && (
<div ref={loadMoreRef} className='flex items-center justify-center py-8'>
const renderBody = () => {
if (loading && allItems.length === 0) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-center'>
<Loader2 className='mx-auto h-6 w-6 animate-spin text-green-600 dark:text-green-400' />
<p className='mt-2 text-sm text-gray-600 dark:text-gray-400'>
...
<Loader2 className='mx-auto h-8 w-8 animate-spin text-green-600 dark:text-green-400' />
<p className='mt-4 text-sm text-gray-600 dark:text-gray-400'>
...
</p>
</div>
</div>
)}
);
}
{/* 命名弹窗 */}
{showNameDialog && (
<div className='fixed inset-0 z-[1000] flex items-center justify-center bg-black/50'>
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4'>
</h3>
<input
type='text'
value={customName}
onChange={(e) => 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
/>
<div className='mt-4 flex gap-2 justify-end'>
<button
onClick={() => {
setShowNameDialog(false);
setSelectedItem(null);
setCustomName('');
}}
className='px-4 py-2 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors'
>
</button>
<button
onClick={handleConfirmDownload}
disabled={!customName.trim()}
className='px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
>
</button>
</div>
if (error) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-center'>
<AlertCircle className='mx-auto h-12 w-12 text-red-500 dark:text-red-400' />
<p className='mt-4 text-sm text-red-600 dark:text-red-400'>{error}</p>
</div>
</div>
)}
);
}
if (allItems.length === 0) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-center'>
<AlertCircle className='mx-auto h-12 w-12 text-gray-400 dark:text-gray-600' />
<p className='mt-4 text-sm text-gray-600 dark:text-gray-400'>
</p>
</div>
</div>
);
}
return (
<>
{/* 结果列表 */}
<div className='space-y-3'>
{allItems.map((item) => (
<div
key={item.guid}
className='p-4 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 transition-colors'
>
{/* 标题 */}
<div className='mb-2 font-medium text-gray-900 dark:text-gray-100'>
{item.title}
</div>
{/* 发布时间 */}
<div className='mb-2 text-xs text-gray-500 dark:text-gray-400'>
{new Date(item.pubDate).toLocaleString('zh-CN')}
</div>
{/* 图片预览 */}
{item.images && item.images.length > 0 && (
<div className='mb-3 flex gap-2 overflow-x-auto'>
{item.images.slice(0, 3).map((img, imgIndex) => (
<img
key={imgIndex}
src={img}
alt=''
className='h-20 w-auto rounded object-cover'
loading='lazy'
/>
))}
</div>
)}
{/* 操作按钮 */}
<div className='flex items-center gap-2'>
<button
onClick={() => handleOpenDownloadDialog(item)}
disabled={downloadingId === item.guid}
className='flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-green-600 text-white text-sm hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
title='存到私人影库'
>
{downloadingId === item.guid ? (
<>
<Loader2 className='h-4 w-4 animate-spin' />
<span>...</span>
</>
) : (
<>
<Download className='h-4 w-4' />
<span></span>
</>
)}
</button>
<a
href={item.link}
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-gray-200 text-gray-700 text-sm hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors'
title='查看详情'
>
<ExternalLink className='h-4 w-4' />
<span></span>
</a>
</div>
</div>
))}
</div>
{/* 加载更多指示器 */}
{source === 'acgrip' && hasMore && (
<div ref={loadMoreRef} className='flex items-center justify-center py-8'>
<div className='text-center'>
<Loader2 className='mx-auto h-6 w-6 animate-spin text-green-600 dark:text-green-400' />
<p className='mt-2 text-sm text-gray-600 dark:text-gray-400'>
...
</p>
</div>
</div>
)}
{/* 命名弹窗 */}
{showNameDialog && (
<div className='fixed inset-0 z-[1000] flex items-center justify-center bg-black/50'>
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4'>
</h3>
<input
type='text'
value={customName}
onChange={(e) => 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
/>
<div className='mt-4 flex gap-2 justify-end'>
<button
onClick={() => {
setShowNameDialog(false);
setSelectedItem(null);
setCustomName('');
}}
className='px-4 py-2 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors'
>
</button>
<button
onClick={handleConfirmDownload}
disabled={!customName.trim()}
className='px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
>
</button>
</div>
</div>
</div>
)}
</>
);
};
return (
<div className='space-y-6'>
{/* 搜索源切换 */}
<div className='flex justify-center'>
<CapsuleSwitch
options={[
{ label: 'ACG.RIP', value: 'acgrip' },
{ label: '蜜柑', value: 'mikan' },
]}
active={source}
onChange={(value) => setSource(value as AcgSearchSource)}
/>
</div>
{renderBody()}
{/* Toast 提示 */}
{toast && <Toast {...toast} />}