动漫磁力搜索增加蜜柑
This commit is contained in:
@@ -7,8 +7,8 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* POST /api/acg/search
|
||||
* 搜索 ACG 磁力资源(仅管理员和站长可用)
|
||||
* POST /api/acg/acgrip
|
||||
* 搜索 ACG.RIP 磁力资源(仅管理员和站长可用,支持分页)
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -47,10 +47,10 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// 请求 acg.rip API
|
||||
const acgUrl = `https://acg.rip/page/${pageNum}.xml?term=${encodeURIComponent(trimmedKeyword)}`;
|
||||
// 请求 acg.rip RSS
|
||||
const searchUrl = `https://acg.rip/page/${pageNum}.xml?term=${encodeURIComponent(trimmedKeyword)}`;
|
||||
|
||||
const response = await fetch(acgUrl, {
|
||||
const response = await fetch(searchUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
},
|
||||
@@ -78,10 +78,12 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// 转换为标准格式
|
||||
const results = items.map((item: any) => {
|
||||
const description = item.description?.[0] || '';
|
||||
|
||||
// 提取描述中的图片(如果有)
|
||||
let images: string[] = [];
|
||||
if (item.description?.[0]) {
|
||||
const imgMatches = item.description[0].match(/src="([^"]+)"/g);
|
||||
if (description) {
|
||||
const imgMatches = description.match(/src="([^"]+)"/g);
|
||||
if (imgMatches) {
|
||||
images = imgMatches.map((match: string) => {
|
||||
const urlMatch = match.match(/src="([^"]+)"/);
|
||||
@@ -90,13 +92,19 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const title = item.title?.[0] || '';
|
||||
const link = item.link?.[0] || '';
|
||||
const guid = item.guid?.[0] || link || `${title}-${item.pubDate?.[0] || ''}`;
|
||||
const pubDate = item.pubDate?.[0] || '';
|
||||
const torrentUrl = item.enclosure?.[0]?.$?.url || '';
|
||||
|
||||
return {
|
||||
title: item.title?.[0] || '',
|
||||
link: item.link?.[0] || '',
|
||||
guid: item.guid?.[0] || '',
|
||||
pubDate: item.pubDate?.[0] || '',
|
||||
torrentUrl: item.enclosure?.[0]?.$?.url || '',
|
||||
description: item.description?.[0] || '',
|
||||
title,
|
||||
link,
|
||||
guid,
|
||||
pubDate,
|
||||
torrentUrl,
|
||||
description,
|
||||
images,
|
||||
};
|
||||
});
|
||||
@@ -107,12 +115,12 @@ export async function POST(req: NextRequest) {
|
||||
total: results.length,
|
||||
items: results,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('ACG 搜索失败:', error);
|
||||
console.error('ACG.RIP 搜索失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || '搜索失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
147
src/app/api/acg/mikan/route.ts
Normal file
147
src/app/api/acg/mikan/route.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseStringPromise } from 'xml2js';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
const pickText = (value: any): string => {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (Array.isArray(value)) return String(value[0] ?? '');
|
||||
return String(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/acg/mikan
|
||||
* 搜索 Mikan RSS(仅管理员和站长可用,不支持分页)
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// 检查权限
|
||||
const authInfo = getAuthInfoFromCookie(req);
|
||||
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
|
||||
return NextResponse.json(
|
||||
{ error: '无权限访问' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { keyword, page = 1 } = await req.json();
|
||||
|
||||
if (!keyword || typeof keyword !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: '搜索关键词不能为空' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const trimmedKeyword = keyword.trim();
|
||||
if (!trimmedKeyword) {
|
||||
return NextResponse.json(
|
||||
{ error: '搜索关键词不能为空' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证页码(Mikan RSS 不支持分页,这里仍接收 page 以保持接口一致)
|
||||
const pageNum = parseInt(String(page), 10);
|
||||
if (isNaN(pageNum) || pageNum < 1) {
|
||||
return NextResponse.json(
|
||||
{ error: '页码必须是大于0的整数' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (pageNum > 1) {
|
||||
return NextResponse.json({
|
||||
keyword: trimmedKeyword,
|
||||
page: pageNum,
|
||||
total: 0,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
|
||||
const searchUrl = `https://mikanani.me/RSS/Search?searchstr=${encodeURIComponent(trimmedKeyword)}`;
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Mikan API 请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const xmlData = await response.text();
|
||||
const parsed = await parseStringPromise(xmlData);
|
||||
|
||||
if (!parsed?.rss?.channel?.[0]?.item) {
|
||||
return NextResponse.json({
|
||||
keyword: trimmedKeyword,
|
||||
page: pageNum,
|
||||
total: 0,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
|
||||
const items = parsed.rss.channel[0].item;
|
||||
|
||||
const results = items.map((item: any) => {
|
||||
const title = pickText(item.title);
|
||||
const link = pickText(item.link);
|
||||
const guid = pickText(item.guid) || link || `${title}-${pickText(item.torrent?.[0]?.pubDate)}`;
|
||||
const pubDate =
|
||||
pickText(item.pubDate) ||
|
||||
pickText(item.torrent?.[0]?.pubDate) ||
|
||||
pickText(item['dc:date']);
|
||||
|
||||
const description =
|
||||
pickText(item.description) ||
|
||||
pickText(item['content:encoded']) ||
|
||||
'';
|
||||
|
||||
const torrentUrl =
|
||||
pickText(item.enclosure?.[0]?.$?.url) ||
|
||||
pickText(item.enclosure?.[0]?.$?.href) ||
|
||||
'';
|
||||
|
||||
// 提取描述中的图片(如果有)
|
||||
let images: string[] = [];
|
||||
if (description) {
|
||||
const imgMatches = description.match(/src="([^"]+)"/g);
|
||||
if (imgMatches) {
|
||||
images = imgMatches.map((match: string) => {
|
||||
const urlMatch = match.match(/src="([^"]+)"/);
|
||||
return urlMatch ? urlMatch[1] : '';
|
||||
}).filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
link,
|
||||
guid,
|
||||
pubDate,
|
||||
torrentUrl,
|
||||
description,
|
||||
images,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
keyword: trimmedKeyword,
|
||||
page: pageNum,
|
||||
total: results.length,
|
||||
items: results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Mikan 搜索失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || '搜索失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
Reference in New Issue
Block a user