增加短剧查看更多功能
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { API_CONFIG, getCacheTime, getConfig } from '@/lib/config';
|
||||||
|
import { getDuanjuSources, isDuanjuTypeName } from '@/lib/duanju';
|
||||||
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
interface CmsClassResponse {
|
||||||
|
class?: Array<{
|
||||||
|
type_id: string | number;
|
||||||
|
type_name: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const sourceKey = searchParams.get('source');
|
||||||
|
|
||||||
|
if (!sourceKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 400, message: '缺少参数: source', data: [] },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sources = await getDuanjuSources();
|
||||||
|
const targetSource = sources.find((source) => source.key === sourceKey);
|
||||||
|
|
||||||
|
if (!targetSource) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 404, message: `未找到短剧采集源: ${sourceKey}`, data: [] },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${targetSource.api}?ac=list`, {
|
||||||
|
headers: API_CONFIG.search.headers,
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取分类列表失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: CmsClassResponse = await response.json();
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const categories = (data.class || [])
|
||||||
|
.filter((item) => {
|
||||||
|
const typeName = item.type_name || '';
|
||||||
|
if (!isDuanjuTypeName(typeName)) return false;
|
||||||
|
if (!config.SiteConfig.DisableYellowFilter) {
|
||||||
|
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((item) => ({
|
||||||
|
id: item.type_id.toString(),
|
||||||
|
name: item.type_name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultCategory = categories[0] || null;
|
||||||
|
|
||||||
|
const cacheTime = await getCacheTime();
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 200, message: '获取成功', data: categories, defaultCategory },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取短剧分类失败:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
code: 500,
|
||||||
|
message: '获取短剧分类失败',
|
||||||
|
data: [],
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { API_CONFIG, getCacheTime } from '@/lib/config';
|
||||||
|
import { getDuanjuSources } from '@/lib/duanju';
|
||||||
|
import { SearchResult } from '@/lib/types';
|
||||||
|
import { cleanHtmlTags } from '@/lib/utils';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
interface CmsVideoItem {
|
||||||
|
vod_id: string | number;
|
||||||
|
vod_name: string;
|
||||||
|
vod_pic?: string;
|
||||||
|
vod_remarks?: string;
|
||||||
|
vod_year?: string;
|
||||||
|
vod_play_from?: string;
|
||||||
|
vod_play_url?: string;
|
||||||
|
vod_class?: string;
|
||||||
|
vod_content?: string;
|
||||||
|
vod_douban_id?: number;
|
||||||
|
type_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmsVideoResponse {
|
||||||
|
list?: CmsVideoItem[];
|
||||||
|
total?: number;
|
||||||
|
page?: number;
|
||||||
|
pagecount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEpisodes(item: CmsVideoItem) {
|
||||||
|
const episodes: string[] = [];
|
||||||
|
const episodesTitles: string[] = [];
|
||||||
|
|
||||||
|
if (!item.vod_play_url) {
|
||||||
|
return { episodes, episodesTitles };
|
||||||
|
}
|
||||||
|
|
||||||
|
const playSources = item.vod_play_url.split('$$$');
|
||||||
|
playSources.forEach((sourceUrl) => {
|
||||||
|
sourceUrl.split('#').forEach((episodeStr) => {
|
||||||
|
const [name, url] = episodeStr.split('$');
|
||||||
|
if (name && url) {
|
||||||
|
episodes.push(url.trim());
|
||||||
|
episodesTitles.push(name.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { episodes, episodesTitles };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const sourceKey = searchParams.get('source');
|
||||||
|
const categoryId = searchParams.get('categoryId');
|
||||||
|
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10) || 1);
|
||||||
|
|
||||||
|
if (!sourceKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 400, message: '缺少参数: source', data: [] },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!categoryId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 400, message: '缺少参数: categoryId', data: [] },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sources = await getDuanjuSources();
|
||||||
|
const targetSource = sources.find((source) => source.key === sourceKey);
|
||||||
|
|
||||||
|
if (!targetSource) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 404, message: `未找到短剧采集源: ${sourceKey}`, data: [] },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${targetSource.api}?ac=videolist&t=${encodeURIComponent(categoryId)}&pg=${page}`,
|
||||||
|
{
|
||||||
|
headers: API_CONFIG.search.headers,
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取短剧列表失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoData: CmsVideoResponse = await response.json();
|
||||||
|
|
||||||
|
const results: SearchResult[] = (videoData.list || []).map((item) => {
|
||||||
|
const { episodes, episodesTitles } = parseEpisodes(item);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.vod_id.toString(),
|
||||||
|
title: (item.vod_name || '').trim().replace(/\s+/g, ' '),
|
||||||
|
poster: item.vod_pic || '',
|
||||||
|
year: item.vod_year ? item.vod_year.match(/\d{4}/)?.[0] || item.vod_year : 'unknown',
|
||||||
|
episodes,
|
||||||
|
episodes_titles: episodesTitles,
|
||||||
|
source: targetSource.key,
|
||||||
|
source_name: targetSource.name,
|
||||||
|
class: item.vod_class,
|
||||||
|
desc: cleanHtmlTags(item.vod_content || ''),
|
||||||
|
type_name: item.type_name,
|
||||||
|
douban_id: item.vod_douban_id,
|
||||||
|
vod_remarks: item.vod_remarks,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheTime = await getCacheTime();
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: results,
|
||||||
|
total: videoData.total || 0,
|
||||||
|
page: videoData.page || page,
|
||||||
|
pageCount: videoData.pagecount || (results.length > 0 ? page + 1 : page),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取短剧列表失败:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
code: 500,
|
||||||
|
message: '获取短剧列表失败',
|
||||||
|
data: [],
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { SearchResult } from '@/lib/types';
|
||||||
|
|
||||||
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
|
interface DuanjuSource {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
api: string;
|
||||||
|
typeId?: string;
|
||||||
|
typeName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DuanjuPageClient() {
|
||||||
|
const [sources, setSources] = useState<DuanjuSource[]>([]);
|
||||||
|
const [selectedSource, setSelectedSource] = useState('');
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState('');
|
||||||
|
const [videos, setVideos] = useState<SearchResult[]>([]);
|
||||||
|
const [isLoadingSources, setIsLoadingSources] = useState(true);
|
||||||
|
const [isLoadingVideos, setIsLoadingVideos] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sourceScrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isDraggingRef = useRef(false);
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const scrollLeftRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSources = async () => {
|
||||||
|
setIsLoadingSources(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/duanju/sources');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.code === 200 && Array.isArray(data.data)) {
|
||||||
|
setSources(data.data);
|
||||||
|
if (data.data.length > 0) {
|
||||||
|
setSelectedSource(data.data[0].key);
|
||||||
|
setSelectedCategory(data.data[0].typeId || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load duanju sources:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSources(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSources();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSourceChange = (sourceKey: string) => {
|
||||||
|
const source = sources.find((item) => item.key === sourceKey);
|
||||||
|
setSelectedSource(sourceKey);
|
||||||
|
setSelectedCategory(source?.typeId || '');
|
||||||
|
setCurrentPage(1);
|
||||||
|
setVideos([]);
|
||||||
|
setHasMore(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedSource || !selectedCategory) return;
|
||||||
|
|
||||||
|
const fetchVideos = async () => {
|
||||||
|
setIsLoadingVideos(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/duanju/videos?source=${encodeURIComponent(selectedSource)}&categoryId=${encodeURIComponent(selectedCategory)}&page=${currentPage}`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.code === 200 && Array.isArray(data.data)) {
|
||||||
|
if (currentPage === 1) {
|
||||||
|
setVideos(data.data);
|
||||||
|
} else {
|
||||||
|
setVideos((prev) => [...prev, ...data.data]);
|
||||||
|
}
|
||||||
|
setHasMore(data.page < data.pageCount);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load duanju videos:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingVideos(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchVideos();
|
||||||
|
}, [selectedSource, selectedCategory, currentPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadMoreRef.current) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const target = entries[0];
|
||||||
|
if (target.isIntersecting && hasMore && !isLoadingVideos) {
|
||||||
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '240px 0px', threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(loadMoreRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [hasMore, isLoadingVideos]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout activePath='/duanju'>
|
||||||
|
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible mb-10'>
|
||||||
|
<div className='mb-6 flex items-start justify-between gap-4'>
|
||||||
|
<div>
|
||||||
|
<h1 className='text-2xl font-bold text-gray-800 dark:text-gray-200'>
|
||||||
|
短剧
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-500 dark:text-gray-400 mt-1'>
|
||||||
|
浏览所有采集源中的短剧内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href='/'
|
||||||
|
className='inline-flex items-center gap-1 rounded-lg px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100'
|
||||||
|
>
|
||||||
|
<ArrowLeft className='h-4 w-4' />
|
||||||
|
返回首页
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='max-w-4xl mx-auto mb-8'>
|
||||||
|
<div className='relative'>
|
||||||
|
<div className='text-xs text-gray-500 dark:text-gray-400 mb-2 px-4'>
|
||||||
|
服务
|
||||||
|
</div>
|
||||||
|
{isLoadingSources ? (
|
||||||
|
<div className='flex items-center justify-center h-12 bg-gray-50/80 rounded-lg border border-gray-200/50 dark:bg-gray-800 dark:border-gray-700'>
|
||||||
|
<Loader2 className='h-5 w-5 animate-spin text-gray-400' />
|
||||||
|
<span className='ml-2 text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
加载采集源中...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : sources.length === 0 ? (
|
||||||
|
<div className='flex items-center justify-center h-12 bg-gray-50/80 rounded-lg border border-gray-200/50 dark:bg-gray-800 dark:border-gray-700'>
|
||||||
|
<span className='text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
暂无包含短剧分类的采集源
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='relative'>
|
||||||
|
<div
|
||||||
|
ref={sourceScrollContainerRef}
|
||||||
|
className='overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing'
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (!sourceScrollContainerRef.current) return;
|
||||||
|
isDraggingRef.current = true;
|
||||||
|
startXRef.current = e.pageX - sourceScrollContainerRef.current.offsetLeft;
|
||||||
|
scrollLeftRef.current = sourceScrollContainerRef.current.scrollLeft;
|
||||||
|
sourceScrollContainerRef.current.style.cursor = 'grabbing';
|
||||||
|
sourceScrollContainerRef.current.style.userSelect = 'none';
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (!sourceScrollContainerRef.current) return;
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
sourceScrollContainerRef.current.style.cursor = 'grab';
|
||||||
|
sourceScrollContainerRef.current.style.userSelect = 'auto';
|
||||||
|
}}
|
||||||
|
onMouseUp={() => {
|
||||||
|
if (!sourceScrollContainerRef.current) return;
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
sourceScrollContainerRef.current.style.cursor = 'grab';
|
||||||
|
sourceScrollContainerRef.current.style.userSelect = 'auto';
|
||||||
|
}}
|
||||||
|
onMouseMove={(e) => {
|
||||||
|
if (!isDraggingRef.current || !sourceScrollContainerRef.current) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const x = e.pageX - sourceScrollContainerRef.current.offsetLeft;
|
||||||
|
const walk = (x - startXRef.current) * 2;
|
||||||
|
sourceScrollContainerRef.current.scrollLeft = scrollLeftRef.current - walk;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 px-4 min-w-min'>
|
||||||
|
{sources.map((source) => (
|
||||||
|
<button
|
||||||
|
key={source.key}
|
||||||
|
onClick={() => handleSourceChange(source.key)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap flex-shrink-0 ${
|
||||||
|
selectedSource === source.key
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{source.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedSource && !selectedCategory && (
|
||||||
|
<div className='text-center text-gray-500 py-8 dark:text-gray-400'>
|
||||||
|
当前采集源暂无短剧分类
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedSource && selectedCategory && (
|
||||||
|
<div className='max-w-[95%] mx-auto mt-8'>
|
||||||
|
<div className='mb-4'>
|
||||||
|
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||||
|
短剧列表
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingVideos && currentPage === 1 ? (
|
||||||
|
<div className='flex justify-center items-center h-40'>
|
||||||
|
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500'></div>
|
||||||
|
</div>
|
||||||
|
) : videos.length === 0 ? (
|
||||||
|
<div className='text-center text-gray-500 py-8 dark:text-gray-400'>
|
||||||
|
暂无短剧
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className='grid grid-cols-3 gap-x-2 gap-y-14 sm:gap-y-20 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8'>
|
||||||
|
{videos.map((item) => (
|
||||||
|
<div key={`${item.source}-${item.id}`} className='w-full'>
|
||||||
|
<VideoCard
|
||||||
|
id={item.id}
|
||||||
|
title={item.title}
|
||||||
|
poster={item.poster}
|
||||||
|
episodes={item.episodes.length}
|
||||||
|
source={item.source}
|
||||||
|
source_name={item.source_name}
|
||||||
|
douban_id={item.douban_id}
|
||||||
|
year={item.year}
|
||||||
|
from='source-search'
|
||||||
|
type='tv'
|
||||||
|
cmsData={{
|
||||||
|
desc: item.desc,
|
||||||
|
episodes: item.episodes,
|
||||||
|
episodes_titles: item.episodes_titles,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={loadMoreRef} className='flex justify-center items-center py-8'>
|
||||||
|
{isLoadingVideos && (
|
||||||
|
<div className='animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500'></div>
|
||||||
|
)}
|
||||||
|
{!hasMore && videos.length > 0 && (
|
||||||
|
<span className='text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
没有更多了
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DuanjuPage() {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<DuanjuPageClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -501,6 +501,13 @@ function HomeClient() {
|
|||||||
<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'>
|
||||||
热播短剧
|
热播短剧
|
||||||
</h2>
|
</h2>
|
||||||
|
<Link
|
||||||
|
href='/duanju'
|
||||||
|
className='flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
|
>
|
||||||
|
查看更多
|
||||||
|
<ChevronRight className='w-4 h-4 ml-1' />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ScrollableRow>
|
<ScrollableRow>
|
||||||
{loading
|
{loading
|
||||||
|
|||||||
+27
-10
@@ -14,6 +14,17 @@ export interface DuanjuSource {
|
|||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
api: string;
|
api: string;
|
||||||
|
typeId?: string;
|
||||||
|
typeName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDuanjuTypeName(typeName: string): boolean {
|
||||||
|
const normalizedTypeName = typeName.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalizedTypeName.includes('短剧') ||
|
||||||
|
normalizedTypeName.includes('短视频') ||
|
||||||
|
normalizedTypeName.includes('微短剧')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +37,16 @@ export async function getDuanjuSources(): Promise<DuanjuSource[]> {
|
|||||||
|
|
||||||
if (cachedData !== null) {
|
if (cachedData !== null) {
|
||||||
// 有缓存,直接返回(getGlobalValue 已经处理了序列化问题)
|
// 有缓存,直接返回(getGlobalValue 已经处理了序列化问题)
|
||||||
return cachedData ? JSON.parse(cachedData) : [];
|
const cachedSources: DuanjuSource[] = cachedData ? JSON.parse(cachedData) : [];
|
||||||
|
// 旧版本缓存只保存采集源,不包含短剧分类 ID。缺少 typeId 时自动重建缓存。
|
||||||
|
if (
|
||||||
|
cachedSources.length === 0 ||
|
||||||
|
cachedSources.every((source) => source.typeId)
|
||||||
|
) {
|
||||||
|
return cachedSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('短剧视频源缓存缺少分类信息,重新筛选...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有缓存,开始筛选
|
// 没有缓存,开始筛选
|
||||||
@@ -56,20 +76,17 @@ export async function getDuanjuSources(): Promise<DuanjuSource[]> {
|
|||||||
|
|
||||||
// 检查是否有短剧分类
|
// 检查是否有短剧分类
|
||||||
if (data.class && Array.isArray(data.class)) {
|
if (data.class && Array.isArray(data.class)) {
|
||||||
const hasDuanju = data.class.some((item) => {
|
const duanjuType = data.class.find((item) =>
|
||||||
const typeName = item.type_name?.toLowerCase() || '';
|
isDuanjuTypeName(item.type_name || '')
|
||||||
return (
|
);
|
||||||
typeName.includes('短剧') ||
|
|
||||||
typeName.includes('短视频') ||
|
|
||||||
typeName.includes('微短剧')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasDuanju) {
|
if (duanjuType) {
|
||||||
return {
|
return {
|
||||||
key: source.key,
|
key: source.key,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
api: source.api,
|
api: source.api,
|
||||||
|
typeId: duanjuType.type_id.toString(),
|
||||||
|
typeName: duanjuType.type_name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user