feat: show sources in aggregate card
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAvailableApiSites, getCacheTime } from '@/lib/config';
|
||||
import { getAvailableApiSites } from '@/lib/config';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { ChevronUp, Search, X } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
addSearchHistory,
|
||||
@@ -28,6 +28,7 @@ function SearchPageClient() {
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const currentQueryRef = useRef<string>('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
@@ -276,7 +277,9 @@ function SearchPageClient() {
|
||||
|
||||
useEffect(() => {
|
||||
// 当搜索参数变化时更新搜索状态
|
||||
const query = searchParams.get('q');
|
||||
const query = searchParams.get('q') || '';
|
||||
currentQueryRef.current = query.trim();
|
||||
|
||||
if (query) {
|
||||
setSearchQuery(query);
|
||||
fetchSearchResults(query);
|
||||
@@ -291,10 +294,13 @@ function SearchPageClient() {
|
||||
}, [searchParams]);
|
||||
|
||||
const fetchSearchResults = async (query: string) => {
|
||||
// 在函数开始时缓存查询参数
|
||||
const cachedQuery = query.trim();
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(
|
||||
`/api/search?q=${encodeURIComponent(query.trim())}`
|
||||
`/api/search?q=${encodeURIComponent(cachedQuery)}`
|
||||
);
|
||||
const data = await response.json();
|
||||
let results = data.results;
|
||||
@@ -307,11 +313,18 @@ function SearchPageClient() {
|
||||
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
// 在 setSearchResults 之前检查当前页面的 query 与缓存的查询是否一致
|
||||
if (currentQueryRef.current !== cachedQuery) {
|
||||
// 查询已经改变,不需要设置结果,直接返回
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchResults(
|
||||
results.sort((a: SearchResult, b: SearchResult) => {
|
||||
// 优先排序:标题与搜索词完全一致的排在前面
|
||||
const aExactMatch = a.title === query.trim();
|
||||
const bExactMatch = b.title === query.trim();
|
||||
const aExactMatch = a.title === cachedQuery;
|
||||
const bExactMatch = b.title === cachedQuery;
|
||||
|
||||
if (aExactMatch && !bExactMatch) return -1;
|
||||
if (!aExactMatch && bExactMatch) return 1;
|
||||
|
||||
@@ -230,6 +230,7 @@ export default function VideoCard({
|
||||
showCheckCircle: true,
|
||||
showDoubanLink: false,
|
||||
showRating: false,
|
||||
showYear: false,
|
||||
},
|
||||
favorite: {
|
||||
showSourceName: true,
|
||||
@@ -239,6 +240,7 @@ export default function VideoCard({
|
||||
showCheckCircle: false,
|
||||
showDoubanLink: false,
|
||||
showRating: false,
|
||||
showYear: false,
|
||||
},
|
||||
search: {
|
||||
showSourceName: true,
|
||||
@@ -246,8 +248,9 @@ export default function VideoCard({
|
||||
showPlayButton: true,
|
||||
showHeart: false,
|
||||
showCheckCircle: false,
|
||||
showDoubanLink: !!actualDoubanId,
|
||||
showDoubanLink: false,
|
||||
showRating: false,
|
||||
showYear: true,
|
||||
},
|
||||
douban: {
|
||||
showSourceName: false,
|
||||
@@ -257,6 +260,7 @@ export default function VideoCard({
|
||||
showCheckCircle: false,
|
||||
showDoubanLink: true,
|
||||
showRating: !!rate,
|
||||
showYear: false,
|
||||
},
|
||||
};
|
||||
return configs[from] || configs.search;
|
||||
@@ -329,6 +333,16 @@ export default function VideoCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 年份徽章 */}
|
||||
{config.showYear && actualYear && actualYear !== 'unknown' && actualYear.trim() !== '' && (
|
||||
<div className={`absolute top-2 bg-black/50 text-white text-xs font-medium px-2 py-1 rounded backdrop-blur-sm shadow-sm transition-all duration-300 ease-out group-hover:opacity-90 ${config.showDoubanLink && actualDoubanId && actualDoubanId !== 0
|
||||
? 'left-2 group-hover:left-11'
|
||||
: 'left-2'
|
||||
}`}>
|
||||
{actualYear}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 徽章 */}
|
||||
{config.showRating && rate && (
|
||||
<div className='absolute top-2 right-2 bg-pink-500 text-white text-xs font-bold w-7 h-7 rounded-full flex items-center justify-center shadow-md transition-all duration-300 ease-out group-hover:scale-110'>
|
||||
@@ -362,6 +376,73 @@ export default function VideoCard({
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{/* 聚合播放源指示器 */}
|
||||
{isAggregate && items && items.length > 0 && (() => {
|
||||
const uniqueSources = Array.from(new Set(items.map(item => item.source_name)));
|
||||
const sourceCount = uniqueSources.length;
|
||||
|
||||
return (
|
||||
<div className='absolute bottom-2 right-2 opacity-0 transition-all duration-300 ease-in-out delay-75 group-hover:opacity-100'>
|
||||
<div className='relative group/sources'>
|
||||
<div className='bg-gray-700 text-white text-xs font-bold w-7 h-7 rounded-full flex items-center justify-center shadow-md hover:bg-gray-600 hover:scale-[1.1] transition-all duration-300 ease-out cursor-pointer'>
|
||||
{sourceCount}
|
||||
</div>
|
||||
|
||||
{/* 播放源详情悬浮框 */}
|
||||
{(() => {
|
||||
// 优先显示的播放源(常见的主流平台)
|
||||
const prioritySources = ['爱奇艺', '腾讯视频', '优酷', '芒果TV', '哔哩哔哩', 'Netflix', 'Disney+'];
|
||||
|
||||
// 按优先级排序播放源
|
||||
const sortedSources = uniqueSources.sort((a, b) => {
|
||||
const aIndex = prioritySources.indexOf(a);
|
||||
const bIndex = prioritySources.indexOf(b);
|
||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||
if (aIndex !== -1) return -1;
|
||||
if (bIndex !== -1) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const maxDisplayCount = 6; // 最多显示6个
|
||||
const displaySources = sortedSources.slice(0, maxDisplayCount);
|
||||
const hasMore = sortedSources.length > maxDisplayCount;
|
||||
const remainingCount = sortedSources.length - maxDisplayCount;
|
||||
|
||||
return (
|
||||
<div className='absolute bottom-full right-0 mb-2 opacity-0 invisible group-hover/sources:opacity-100 group-hover/sources:visible transition-all duration-200 ease-out delay-100 pointer-events-none z-50'>
|
||||
<div className='bg-gray-800/90 backdrop-blur-sm text-white text-xs rounded-lg shadow-xl border border-white/10 p-2 min-w-[120px] max-w-[200px]'>
|
||||
{/* 单列布局 */}
|
||||
<div className='space-y-1'>
|
||||
{displaySources.map((sourceName, index) => (
|
||||
<div key={index} className='flex items-center gap-1.5'>
|
||||
<div className='w-1 h-1 bg-blue-400 rounded-full flex-shrink-0'></div>
|
||||
<span className='truncate text-xs' title={sourceName}>
|
||||
{sourceName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 显示更多提示 */}
|
||||
{hasMore && (
|
||||
<div className='mt-2 pt-1.5 border-t border-gray-700/50'>
|
||||
<div className='flex items-center justify-center text-gray-400'>
|
||||
<span className='text-xs font-medium'>+{remainingCount} 播放源</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 小箭头 */}
|
||||
<div className='absolute top-full right-3 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-transparent border-t-gray-800/90'></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
|
||||
Reference in New Issue
Block a user