增加盘搜
This commit is contained in:
@@ -5105,6 +5105,9 @@ const SiteConfigComponent = ({
|
|||||||
DanmakuApiToken: '87654321',
|
DanmakuApiToken: '87654321',
|
||||||
TMDBApiKey: '',
|
TMDBApiKey: '',
|
||||||
TMDBProxy: '',
|
TMDBProxy: '',
|
||||||
|
PansouApiUrl: '',
|
||||||
|
PansouUsername: '',
|
||||||
|
PansouPassword: '',
|
||||||
EnableComments: false,
|
EnableComments: false,
|
||||||
EnableRegistration: false,
|
EnableRegistration: false,
|
||||||
RegistrationRequireTurnstile: false,
|
RegistrationRequireTurnstile: false,
|
||||||
@@ -5189,6 +5192,9 @@ const SiteConfigComponent = ({
|
|||||||
DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321',
|
DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321',
|
||||||
TMDBApiKey: config.SiteConfig.TMDBApiKey || '',
|
TMDBApiKey: config.SiteConfig.TMDBApiKey || '',
|
||||||
TMDBProxy: config.SiteConfig.TMDBProxy || '',
|
TMDBProxy: config.SiteConfig.TMDBProxy || '',
|
||||||
|
PansouApiUrl: config.SiteConfig.PansouApiUrl || '',
|
||||||
|
PansouUsername: config.SiteConfig.PansouUsername || '',
|
||||||
|
PansouPassword: config.SiteConfig.PansouPassword || '',
|
||||||
EnableComments: config.SiteConfig.EnableComments || false,
|
EnableComments: config.SiteConfig.EnableComments || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5779,6 +5785,87 @@ const SiteConfigComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pansou 配置 */}
|
||||||
|
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||||
|
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
Pansou 网盘搜索配置
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Pansou API 地址 */}
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
|
Pansou API 地址
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='请输入 Pansou API 地址,如:http://localhost:8888'
|
||||||
|
value={siteSettings.PansouApiUrl}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
PansouApiUrl: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||||
|
/>
|
||||||
|
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
配置 Pansou 服务器地址,用于网盘资源搜索。项目地址:{' '}
|
||||||
|
<a
|
||||||
|
href='https://github.com/fish2018/pansou'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||||
|
>
|
||||||
|
https://github.com/fish2018/pansou
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pansou 账号 */}
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
|
Pansou 账号(可选)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='如果 Pansou 启用了认证,请输入账号'
|
||||||
|
value={siteSettings.PansouUsername}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
PansouUsername: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||||
|
/>
|
||||||
|
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
如果 Pansou 服务启用了认证功能,需要提供账号密码
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pansou 密码 */}
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
|
Pansou 密码(可选)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='password'
|
||||||
|
placeholder='如果 Pansou 启用了认证,请输入密码'
|
||||||
|
value={siteSettings.PansouPassword}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
PansouPassword: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||||
|
/>
|
||||||
|
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
配置账号密码后,系统会自动登录并缓存 Token
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 评论功能配置 */}
|
{/* 评论功能配置 */}
|
||||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export async function POST(request: NextRequest) {
|
|||||||
DanmakuApiToken,
|
DanmakuApiToken,
|
||||||
TMDBApiKey,
|
TMDBApiKey,
|
||||||
TMDBProxy,
|
TMDBProxy,
|
||||||
|
PansouApiUrl,
|
||||||
|
PansouUsername,
|
||||||
|
PansouPassword,
|
||||||
EnableComments,
|
EnableComments,
|
||||||
CustomAdFilterCode,
|
CustomAdFilterCode,
|
||||||
CustomAdFilterVersion,
|
CustomAdFilterVersion,
|
||||||
@@ -76,6 +79,9 @@ export async function POST(request: NextRequest) {
|
|||||||
DanmakuApiToken: string;
|
DanmakuApiToken: string;
|
||||||
TMDBApiKey?: string;
|
TMDBApiKey?: string;
|
||||||
TMDBProxy?: string;
|
TMDBProxy?: string;
|
||||||
|
PansouApiUrl?: string;
|
||||||
|
PansouUsername?: string;
|
||||||
|
PansouPassword?: string;
|
||||||
EnableComments: boolean;
|
EnableComments: boolean;
|
||||||
CustomAdFilterCode?: string;
|
CustomAdFilterCode?: string;
|
||||||
CustomAdFilterVersion?: number;
|
CustomAdFilterVersion?: number;
|
||||||
@@ -163,6 +169,9 @@ export async function POST(request: NextRequest) {
|
|||||||
DanmakuApiToken,
|
DanmakuApiToken,
|
||||||
TMDBApiKey,
|
TMDBApiKey,
|
||||||
TMDBProxy,
|
TMDBProxy,
|
||||||
|
PansouApiUrl,
|
||||||
|
PansouUsername,
|
||||||
|
PansouPassword,
|
||||||
EnableComments,
|
EnableComments,
|
||||||
CustomAdFilterCode,
|
CustomAdFilterCode,
|
||||||
CustomAdFilterVersion,
|
CustomAdFilterVersion,
|
||||||
|
|||||||
61
src/app/api/pansou/search/route.ts
Normal file
61
src/app/api/pansou/search/route.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { searchPansou } from '@/lib/pansou.client';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { keyword } = body;
|
||||||
|
|
||||||
|
if (!keyword) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '关键词不能为空' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从系统配置中获取 Pansou 配置
|
||||||
|
const config = await getConfig();
|
||||||
|
const apiUrl = config.SiteConfig.PansouApiUrl;
|
||||||
|
const username = config.SiteConfig.PansouUsername;
|
||||||
|
const password = config.SiteConfig.PansouPassword;
|
||||||
|
|
||||||
|
console.log('Pansou 搜索请求:', {
|
||||||
|
keyword,
|
||||||
|
apiUrl: apiUrl ? '已配置' : '未配置',
|
||||||
|
hasAuth: !!(username && password),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiUrl) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '未配置 Pansou API 地址,请在管理面板配置' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 Pansou 搜索
|
||||||
|
const results = await searchPansou(apiUrl, keyword, {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Pansou 搜索结果:', {
|
||||||
|
total: results.total,
|
||||||
|
hasData: !!results.merged_by_type,
|
||||||
|
types: results.merged_by_type ? Object.keys(results.merged_by_type) : [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(results);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Pansou 搜索失败:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message || '搜索失败' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,10 @@ export async function GET(request: NextRequest) {
|
|||||||
EnableOIDCLogin: config.SiteConfig.EnableOIDCLogin || false,
|
EnableOIDCLogin: config.SiteConfig.EnableOIDCLogin || false,
|
||||||
EnableOIDCRegistration: config.SiteConfig.EnableOIDCRegistration || false,
|
EnableOIDCRegistration: config.SiteConfig.EnableOIDCRegistration || false,
|
||||||
OIDCButtonText: config.SiteConfig.OIDCButtonText || '',
|
OIDCButtonText: config.SiteConfig.OIDCButtonText || '',
|
||||||
|
SiteConfig: {
|
||||||
|
PansouApiUrl: config.SiteConfig.PansouApiUrl || '',
|
||||||
|
// 不暴露用户名和密码,认证在后端处理
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Heart } from 'lucide-react';
|
import { Heart, Search, X } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ import DoubanComments from '@/components/DoubanComments';
|
|||||||
import DanmakuFilterSettings from '@/components/DanmakuFilterSettings';
|
import DanmakuFilterSettings from '@/components/DanmakuFilterSettings';
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||||
|
import PansouSearch from '@/components/PansouSearch';
|
||||||
|
|
||||||
// 扩展 HTMLVideoElement 类型以支持 hls 属性
|
// 扩展 HTMLVideoElement 类型以支持 hls 属性
|
||||||
declare global {
|
declare global {
|
||||||
@@ -96,6 +97,9 @@ function PlayPageClient() {
|
|||||||
// 收藏状态
|
// 收藏状态
|
||||||
const [favorited, setFavorited] = useState(false);
|
const [favorited, setFavorited] = useState(false);
|
||||||
|
|
||||||
|
// 网盘搜索弹窗状态
|
||||||
|
const [showPansouDialog, setShowPansouDialog] = useState(false);
|
||||||
|
|
||||||
// 跳过片头片尾配置
|
// 跳过片头片尾配置
|
||||||
const [skipConfig, setSkipConfig] = useState<{
|
const [skipConfig, setSkipConfig] = useState<{
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
@@ -4890,6 +4894,17 @@ function PlayPageClient() {
|
|||||||
>
|
>
|
||||||
<FavoriteIcon filled={favorited} />
|
<FavoriteIcon filled={favorited} />
|
||||||
</button>
|
</button>
|
||||||
|
{/* 网盘搜索按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowPansouDialog(true);
|
||||||
|
}}
|
||||||
|
className='flex-shrink-0 hover:opacity-80 transition-opacity'
|
||||||
|
title='搜索网盘资源'
|
||||||
|
>
|
||||||
|
<Search className='h-6 w-6 text-gray-700 dark:text-gray-300' />
|
||||||
|
</button>
|
||||||
{/* 豆瓣评分显示 */}
|
{/* 豆瓣评分显示 */}
|
||||||
{doubanRating && doubanRating.value > 0 && (
|
{doubanRating && doubanRating.value > 0 && (
|
||||||
<div className='flex items-center gap-2 text-base font-normal'>
|
<div className='flex items-center gap-2 text-base font-normal'>
|
||||||
@@ -5107,6 +5122,40 @@ function PlayPageClient() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 网盘搜索弹窗 */}
|
||||||
|
{showPansouDialog && (
|
||||||
|
<div
|
||||||
|
className='fixed inset-0 z-[10000] flex items-center justify-center bg-black/50'
|
||||||
|
onClick={() => setShowPansouDialog(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='relative w-full max-w-4xl max-h-[80vh] overflow-y-auto bg-white dark:bg-gray-900 rounded-lg shadow-xl m-4'
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* 弹窗头部 */}
|
||||||
|
<div className='sticky top-0 z-10 flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900'>
|
||||||
|
<h2 className='text-xl font-bold text-gray-900 dark:text-gray-100'>
|
||||||
|
搜索网盘资源: {detail?.title || ''}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPansouDialog(false)}
|
||||||
|
className='p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
<X className='h-5 w-5 text-gray-600 dark:text-gray-400' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 弹窗内容 */}
|
||||||
|
<div className='p-4'>
|
||||||
|
<PansouSearch
|
||||||
|
keyword={detail?.title || ''}
|
||||||
|
triggerSearch={showPansouDialog}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,17 @@ import PageLayout from '@/components/PageLayout';
|
|||||||
import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
|
import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
|
||||||
import SearchSuggestions from '@/components/SearchSuggestions';
|
import SearchSuggestions from '@/components/SearchSuggestions';
|
||||||
import VideoCard, { VideoCardHandle } from '@/components/VideoCard';
|
import VideoCard, { VideoCardHandle } from '@/components/VideoCard';
|
||||||
|
import PansouSearch from '@/components/PansouSearch';
|
||||||
|
|
||||||
function SearchPageClient() {
|
function SearchPageClient() {
|
||||||
// 搜索历史
|
// 搜索历史
|
||||||
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
||||||
// 返回顶部按钮显示状态
|
// 返回顶部按钮显示状态
|
||||||
const [showBackToTop, setShowBackToTop] = useState(false);
|
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||||
|
// 选项卡状态: 'video' 或 'pansou'
|
||||||
|
const [activeTab, setActiveTab] = useState<'video' | 'pansou'>('video');
|
||||||
|
// Pansou 搜索触发标志
|
||||||
|
const [triggerPansouSearch, setTriggerPansouSearch] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -381,6 +386,14 @@ function SearchPageClient() {
|
|||||||
});
|
});
|
||||||
}, [aggregatedResults, filterAgg, searchQuery]);
|
}, [aggregatedResults, filterAgg, searchQuery]);
|
||||||
|
|
||||||
|
// 监听选项卡切换,自动执行搜索
|
||||||
|
useEffect(() => {
|
||||||
|
// 如果切换到网盘搜索选项卡,且有搜索关键词,且已显示结果,则触发搜索
|
||||||
|
if (activeTab === 'pansou' && searchQuery.trim() && showResults) {
|
||||||
|
setTriggerPansouSearch(prev => !prev);
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 无搜索参数时聚焦搜索框
|
// 无搜索参数时聚焦搜索框
|
||||||
!searchParams.get('q') && document.getElementById('searchInput')?.focus();
|
!searchParams.get('q') && document.getElementById('searchInput')?.focus();
|
||||||
@@ -693,8 +706,15 @@ function SearchPageClient() {
|
|||||||
setShowResults(true);
|
setShowResults(true);
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
|
|
||||||
router.push(`/search?q=${encodeURIComponent(trimmed)}`);
|
// 根据当前选项卡执行不同的搜索
|
||||||
// 其余由 searchParams 变化的 effect 处理
|
if (activeTab === 'video') {
|
||||||
|
// 影视搜索
|
||||||
|
router.push(`/search?q=${encodeURIComponent(trimmed)}`);
|
||||||
|
// 其余由 searchParams 变化的 effect 处理
|
||||||
|
} else if (activeTab === 'pansou') {
|
||||||
|
// 网盘搜索 - 触发搜索
|
||||||
|
setTriggerPansouSearch(prev => !prev); // 切换状态来触发搜索
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuggestionSelect = (suggestion: string) => {
|
const handleSuggestionSelect = (suggestion: string) => {
|
||||||
@@ -778,14 +798,41 @@ function SearchPageClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{/* 选项卡 */}
|
||||||
|
<div className='flex justify-center gap-2 mt-6'>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('video')}
|
||||||
|
className={`px-6 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
activeTab === 'video'
|
||||||
|
? 'bg-green-600 text-white dark:bg-green-600'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
影视搜索
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('pansou')}
|
||||||
|
className={`px-6 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
activeTab === 'pansou'
|
||||||
|
? 'bg-green-600 text-white dark:bg-green-600'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
网盘搜索
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索结果或搜索历史 */}
|
{/* 搜索结果或搜索历史 */}
|
||||||
<div className='max-w-[95%] mx-auto mt-12 overflow-visible'>
|
<div className='max-w-[95%] mx-auto mt-12 overflow-visible'>
|
||||||
{showResults ? (
|
{showResults ? (
|
||||||
<section className='mb-12'>
|
<section className='mb-12'>
|
||||||
{/* 标题 */}
|
{activeTab === 'video' ? (
|
||||||
<div className='mb-4 flex items-center justify-between'>
|
<>
|
||||||
|
{/* 影视搜索结果 */}
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className='mb-4 flex items-center justify-between'>
|
||||||
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||||
搜索结果
|
搜索结果
|
||||||
{isFromCache ? (
|
{isFromCache ? (
|
||||||
@@ -930,6 +977,21 @@ function SearchPageClient() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 网盘搜索结果 */}
|
||||||
|
<div className='mb-4'>
|
||||||
|
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||||
|
网盘搜索结果
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<PansouSearch
|
||||||
|
keyword={searchQuery}
|
||||||
|
triggerSearch={triggerPansouSearch}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
) : searchHistory.length > 0 ? (
|
) : searchHistory.length > 0 ? (
|
||||||
// 搜索历史
|
// 搜索历史
|
||||||
|
|||||||
314
src/components/PansouSearch.tsx
Normal file
314
src/components/PansouSearch.tsx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { AlertCircle, Copy, ExternalLink, Loader2 } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { PansouLink, PansouSearchResult } from '@/lib/pansou.client';
|
||||||
|
|
||||||
|
interface PansouSearchProps {
|
||||||
|
keyword: string;
|
||||||
|
triggerSearch?: boolean; // 触发搜索的标志
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网盘类型映射
|
||||||
|
const CLOUD_TYPE_NAMES: Record<string, string> = {
|
||||||
|
baidu: '百度网盘',
|
||||||
|
aliyun: '阿里云盘',
|
||||||
|
quark: '夸克网盘',
|
||||||
|
tianyi: '天翼云盘',
|
||||||
|
uc: 'UC网盘',
|
||||||
|
mobile: '移动云盘',
|
||||||
|
'115': '115网盘',
|
||||||
|
pikpak: 'PikPak',
|
||||||
|
xunlei: '迅雷网盘',
|
||||||
|
'123': '123网盘',
|
||||||
|
magnet: '磁力链接',
|
||||||
|
ed2k: '电驴链接',
|
||||||
|
others: '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 网盘类型颜色
|
||||||
|
const CLOUD_TYPE_COLORS: Record<string, string> = {
|
||||||
|
baidu: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-200',
|
||||||
|
aliyun: 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-200',
|
||||||
|
quark: 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200',
|
||||||
|
tianyi: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-200',
|
||||||
|
uc: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-200',
|
||||||
|
mobile: 'bg-pink-100 text-pink-800 dark:bg-pink-900/40 dark:text-pink-200',
|
||||||
|
'115': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-200',
|
||||||
|
pikpak: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-200',
|
||||||
|
xunlei: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/40 dark:text-cyan-200',
|
||||||
|
'123': 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-200',
|
||||||
|
magnet: 'bg-gray-100 text-gray-800 dark:bg-gray-700/40 dark:text-gray-200',
|
||||||
|
ed2k: 'bg-gray-100 text-gray-800 dark:bg-gray-700/40 dark:text-gray-200',
|
||||||
|
others: 'bg-gray-100 text-gray-800 dark:bg-gray-700/40 dark:text-gray-200',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PansouSearch({
|
||||||
|
keyword,
|
||||||
|
triggerSearch,
|
||||||
|
onError,
|
||||||
|
}: PansouSearchProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [results, setResults] = useState<PansouSearchResult | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [copiedUrl, setCopiedUrl] = useState<string | null>(null);
|
||||||
|
const [selectedType, setSelectedType] = useState<string>('all'); // 'all' 表示显示全部
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 只在 triggerSearch 变化时执行搜索,不响应 keyword 变化
|
||||||
|
if (!triggerSearch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentKeyword = keyword.trim();
|
||||||
|
if (!currentKeyword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchPansou = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResults(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/pansou/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
keyword: currentKeyword,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '搜索失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: PansouSearchResult = await response.json();
|
||||||
|
setResults(data);
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMsg = err.message || '搜索失败,请检查配置';
|
||||||
|
setError(errorMsg);
|
||||||
|
onError?.(errorMsg);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
searchPansou();
|
||||||
|
}, [triggerSearch, onError]); // 移除 keyword 依赖,只依赖 triggerSearch
|
||||||
|
|
||||||
|
const handleCopy = async (text: string, url: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopiedUrl(url);
|
||||||
|
setTimeout(() => setCopiedUrl(null), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenLink = (url: string) => {
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
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 (!results || results.total === 0 || !results.merged_by_type) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudTypes = Object.keys(results.merged_by_type);
|
||||||
|
|
||||||
|
// 过滤显示的网盘类型
|
||||||
|
const filteredCloudTypes = selectedType === 'all'
|
||||||
|
? cloudTypes
|
||||||
|
: cloudTypes.filter(type => type === selectedType);
|
||||||
|
|
||||||
|
// 计算每种网盘类型的数量
|
||||||
|
const typeStats = cloudTypes.map(type => ({
|
||||||
|
type,
|
||||||
|
count: results.merged_by_type[type]?.length || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
{/* 搜索结果统计 */}
|
||||||
|
<div className='text-sm text-gray-600 dark:text-gray-400'>
|
||||||
|
找到 <span className='font-semibold text-green-600 dark:text-green-400'>{results.total}</span> 个资源
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 网盘类型过滤器 */}
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedType('all')}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
selectedType === 'all'
|
||||||
|
? 'bg-green-600 text-white dark:bg-green-600'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
全部 ({results.total})
|
||||||
|
</button>
|
||||||
|
{typeStats.map(({ type, count }) => {
|
||||||
|
const typeName = CLOUD_TYPE_NAMES[type] || type;
|
||||||
|
const typeColor = CLOUD_TYPE_COLORS[type] || CLOUD_TYPE_COLORS.others;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => setSelectedType(type)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
selectedType === type
|
||||||
|
? 'bg-green-600 text-white dark:bg-green-600'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{typeName} ({count})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 按网盘类型分类显示 */}
|
||||||
|
{filteredCloudTypes.map((cloudType) => {
|
||||||
|
const links = results.merged_by_type[cloudType];
|
||||||
|
if (!links || links.length === 0) return null;
|
||||||
|
|
||||||
|
const typeName = CLOUD_TYPE_NAMES[cloudType] || cloudType;
|
||||||
|
const typeColor = CLOUD_TYPE_COLORS[cloudType] || CLOUD_TYPE_COLORS.others;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={cloudType} className='space-y-3'>
|
||||||
|
{/* 网盘类型标题 */}
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${typeColor}`}>
|
||||||
|
{typeName}
|
||||||
|
</span>
|
||||||
|
<span className='text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
{links.length} 个链接
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 链接列表 */}
|
||||||
|
<div className='space-y-2'>
|
||||||
|
{links.map((link: PansouLink, index: number) => (
|
||||||
|
<div
|
||||||
|
key={`${cloudType}-${index}`}
|
||||||
|
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'
|
||||||
|
>
|
||||||
|
{/* 资源标题 */}
|
||||||
|
{link.note && (
|
||||||
|
<div className='mb-2 text-sm font-medium text-gray-900 dark:text-gray-100'>
|
||||||
|
{link.note}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 链接和密码 */}
|
||||||
|
<div className='flex items-center gap-2 mb-2'>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<div className='text-xs text-gray-600 dark:text-gray-400 truncate'>
|
||||||
|
{link.url}
|
||||||
|
</div>
|
||||||
|
{link.password && (
|
||||||
|
<div className='text-xs text-gray-600 dark:text-gray-400 mt-1'>
|
||||||
|
提取码: <span className='font-mono font-semibold'>{link.password}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className='flex items-center gap-1 flex-shrink-0'>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(
|
||||||
|
link.password ? `${link.url}\n提取码: ${link.password}` : link.url,
|
||||||
|
link.url
|
||||||
|
)}
|
||||||
|
className='p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors'
|
||||||
|
title='复制链接'
|
||||||
|
>
|
||||||
|
{copiedUrl === link.url ? (
|
||||||
|
<span className='text-xs text-green-600 dark:text-green-400'>已复制</span>
|
||||||
|
) : (
|
||||||
|
<Copy className='h-4 w-4 text-gray-600 dark:text-gray-400' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenLink(link.url)}
|
||||||
|
className='p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors'
|
||||||
|
title='打开链接'
|
||||||
|
>
|
||||||
|
<ExternalLink className='h-4 w-4 text-gray-600 dark:text-gray-400' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 来源和时间 */}
|
||||||
|
<div className='flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
{link.source && (
|
||||||
|
<span>来源: {link.source}</span>
|
||||||
|
)}
|
||||||
|
{link.datetime && (
|
||||||
|
<span>{new Date(link.datetime).toLocaleDateString()}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图片预览 */}
|
||||||
|
{link.images && link.images.length > 0 && (
|
||||||
|
<div className='mt-3 flex gap-2 overflow-x-auto'>
|
||||||
|
{link.images.map((img, imgIndex) => (
|
||||||
|
<img
|
||||||
|
key={imgIndex}
|
||||||
|
src={img}
|
||||||
|
alt=''
|
||||||
|
className='h-20 w-auto rounded object-cover'
|
||||||
|
loading='lazy'
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ export interface AdminConfig {
|
|||||||
// TMDB配置
|
// TMDB配置
|
||||||
TMDBApiKey?: string;
|
TMDBApiKey?: string;
|
||||||
TMDBProxy?: string;
|
TMDBProxy?: string;
|
||||||
|
// Pansou配置
|
||||||
|
PansouApiUrl?: string;
|
||||||
|
PansouUsername?: string;
|
||||||
|
PansouPassword?: string;
|
||||||
// 评论功能开关
|
// 评论功能开关
|
||||||
EnableComments: boolean;
|
EnableComments: boolean;
|
||||||
// 自定义去广告代码
|
// 自定义去广告代码
|
||||||
|
|||||||
218
src/lib/pansou.client.ts
Normal file
218
src/lib/pansou.client.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pansou 网盘搜索 API 客户端
|
||||||
|
* 文档: https://github.com/fish2018/pansou
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Token 缓存
|
||||||
|
let cachedToken: string | null = null;
|
||||||
|
let tokenExpiry: number | null = null;
|
||||||
|
|
||||||
|
export interface PansouLink {
|
||||||
|
url: string;
|
||||||
|
password: string;
|
||||||
|
note: string;
|
||||||
|
datetime: string;
|
||||||
|
source: string;
|
||||||
|
images?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PansouSearchResult {
|
||||||
|
total: number;
|
||||||
|
merged_by_type?: {
|
||||||
|
[key: string]: PansouLink[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PansouLoginResponse {
|
||||||
|
token: string;
|
||||||
|
expires_at: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录 Pansou 获取 Token
|
||||||
|
*/
|
||||||
|
export async function loginPansou(
|
||||||
|
apiUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/api/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || '登录失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: PansouLoginResponse = await response.json();
|
||||||
|
|
||||||
|
// 缓存 Token
|
||||||
|
cachedToken = data.token;
|
||||||
|
tokenExpiry = data.expires_at;
|
||||||
|
|
||||||
|
return data.token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Pansou 登录失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取有效的 Token(自动处理登录和缓存)
|
||||||
|
*/
|
||||||
|
async function getValidToken(
|
||||||
|
apiUrl: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
// 如果没有配置账号密码,返回 null(不需要认证)
|
||||||
|
if (!username || !password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存的 Token 是否有效
|
||||||
|
if (cachedToken && tokenExpiry) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
// 提前 5 分钟刷新 Token
|
||||||
|
if (tokenExpiry - now > 300) {
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token 过期或不存在,重新登录
|
||||||
|
try {
|
||||||
|
return await loginPansou(apiUrl, username, password);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 Pansou Token 失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索网盘资源
|
||||||
|
*/
|
||||||
|
export async function searchPansou(
|
||||||
|
apiUrl: string,
|
||||||
|
keyword: string,
|
||||||
|
options?: {
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
refresh?: boolean;
|
||||||
|
cloudTypes?: string[];
|
||||||
|
}
|
||||||
|
): Promise<PansouSearchResult> {
|
||||||
|
try {
|
||||||
|
// 获取 Token(如果需要认证)
|
||||||
|
const token = await getValidToken(
|
||||||
|
apiUrl,
|
||||||
|
options?.username,
|
||||||
|
options?.password
|
||||||
|
);
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
const body: any = {
|
||||||
|
kw: keyword,
|
||||||
|
res: 'merge', // 只返回按网盘类型分类的结果
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.refresh) {
|
||||||
|
body.refresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.cloudTypes && options.cloudTypes.length > 0) {
|
||||||
|
body.cloud_types = options.cloudTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${apiUrl}/api/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || error.message || '搜索失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
// Pansou API 返回的数据结构是 { code, message, data }
|
||||||
|
// 实际数据在 data 字段中
|
||||||
|
let data: PansouSearchResult;
|
||||||
|
|
||||||
|
if (responseData.data) {
|
||||||
|
// 如果有 data 字段,使用 data 中的内容
|
||||||
|
data = responseData.data;
|
||||||
|
} else {
|
||||||
|
// 否则直接使用返回的数据
|
||||||
|
data = responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证返回的数据结构
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
throw new Error('返回数据格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 merged_by_type 存在
|
||||||
|
if (!data.merged_by_type) {
|
||||||
|
data.merged_by_type = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 total 存在
|
||||||
|
if (typeof data.total !== 'number') {
|
||||||
|
data.total = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Pansou 搜索失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存的 Token
|
||||||
|
*/
|
||||||
|
export function clearPansouToken(): void {
|
||||||
|
cachedToken = null;
|
||||||
|
tokenExpiry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 Pansou 服务是否可用
|
||||||
|
*/
|
||||||
|
export async function checkPansouHealth(apiUrl: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/api/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.status === 'ok';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Pansou 健康检查失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user