feat: add aggregate search result

This commit is contained in:
shinya
2025-06-24 13:22:47 +08:00
parent 85cae6dd98
commit 1f2675d80a
4 changed files with 186 additions and 21 deletions

View File

@@ -222,9 +222,11 @@ function AggregatePageClient() {
return (
<a
key={src.source}
href={`/detail?source=${src.source}&id=${
href={`/play?source=${src.source}&id=${
src.id
}&title=${encodeURIComponent(src.title)}`}
}&title=${encodeURIComponent(
src.title
)}&from=aggregate`}
className='relative flex items-center justify-center w-full h-14 bg-gray-500/80 hover:bg-green-500 rounded-lg transition-colors'
>
{/* 名称 */}
@@ -233,7 +235,7 @@ function AggregatePageClient() {
</span>
{/* 集数徽标 */}
{epCount && epCount > 1 ? (
<span className='absolute top-1 right-1 text-[10px] font-semibold text-green-900 bg-green-300/90 rounded-full px-1 pointer-events-none'>
<span className='absolute top-[2px] right-1 text-[10px] font-semibold text-green-900 bg-green-300/90 rounded-full px-1 pointer-events-none'>
{epCount}
</span>
) : null}

View File

@@ -773,9 +773,16 @@ function PlayPageClient() {
// 处理返回按钮点击
const handleBack = () => {
window.location.href = `/detail?source=${currentSource}&id=${currentId}&title=${encodeURIComponent(
videoTitle
)}`;
const urlParams = new URLSearchParams(window.location.search);
const fromAggregate = urlParams.get('from') === 'aggregate';
if (fromAggregate) {
window.location.href = `/aggregate?q=${encodeURIComponent(videoTitle)}`;
} else {
window.location.href = `/detail?source=${currentSource}&id=${currentId}&title=${encodeURIComponent(
videoTitle
)}`;
}
};
// 处理上一集

View File

@@ -3,8 +3,7 @@
import { Search } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
import {
addSearchHistory,
@@ -12,6 +11,7 @@ import {
getSearchHistory,
} from '@/lib/db.client';
import AggregateCard from '@/components/AggregateCard';
import PageLayout from '@/components/PageLayout';
import VideoCard from '@/components/VideoCard';
@@ -37,6 +37,20 @@ function SearchPageClient() {
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const searchInputRef = useRef<HTMLInputElement>(null);
// 视图模式:聚合(agg) 或 全部(all)
const [viewMode, setViewMode] = useState<'agg' | 'all'>('agg');
// 聚合后的结果(按标题分组)
const aggregatedResults = useMemo(() => {
const map = new Map<string, SearchResult[]>();
searchResults.forEach((item) => {
const arr = map.get(item.title) || [];
arr.push(item);
map.set(item.title, arr);
});
return Array.from(map.values());
}, [searchResults]);
useEffect(() => {
// 自动聚焦搜索框:仅当 URL 中没有搜索参数时
if (!searchParams.get('q')) {
@@ -127,19 +141,49 @@ function SearchPageClient() {
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-green-500'></div>
</div>
) : showResults ? (
// 搜索结果
<div className='justify-start 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 sm:px-4'>
{searchResults.map((item) => (
<div key={item.id} className='w-full'>
<VideoCard {...item} from='search' />
</div>
))}
{searchResults.length === 0 && (
<div className='col-span-full text-center text-gray-500 py-8'>
</div>
)}
</div>
<section className='mb-12'>
{/* 标题 + 聚合开关 */}
<div className='mb-8 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800'></h2>
{/* 聚合开关 */}
<label className='flex items-center gap-2 cursor-pointer select-none'>
<span className='text-sm text-gray-700'></span>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={viewMode === 'agg'}
onChange={() =>
setViewMode(viewMode === 'agg' ? 'all' : 'agg')
}
/>
<div className='w-9 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors'></div>
<div className='absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4'></div>
</div>
</label>
</div>
<div className='justify-start 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 sm:px-4'>
{viewMode === 'agg'
? aggregatedResults.map((group) => {
const key = group[0].title;
return (
<div key={key} className='w-full'>
<AggregateCard items={group} />
</div>
);
})
: searchResults.map((item) => (
<div key={item.id} className='w-full'>
<VideoCard {...item} from='search' />
</div>
))}
{searchResults.length === 0 && (
<div className='col-span-full text-center text-gray-500 py-8'>
</div>
)}
</div>
</section>
) : searchHistory.length > 0 ? (
// 搜索历史
<section className='mb-12'>