mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-22 10:34:42 +08:00
feat: remove aggregate page, use douban_id to assist search
This commit is contained in:
@@ -1,332 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps, no-console */
|
||||
|
||||
'use client';
|
||||
|
||||
import { Heart, LinkIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import { isFavorited, toggleFavorite } from '@/lib/db.client';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
|
||||
function AggregatePageClient() {
|
||||
const searchParams = useSearchParams();
|
||||
const query = searchParams.get('q') || '';
|
||||
const title = searchParams.get('title') || '';
|
||||
const year = searchParams.get('year') || '';
|
||||
const type = searchParams.get('type') || '';
|
||||
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!query) {
|
||||
setError('缺少搜索关键词');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/search?q=${encodeURIComponent(query.trim())}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error('搜索失败');
|
||||
}
|
||||
const data = await res.json();
|
||||
const all: SearchResult[] = data.results || [];
|
||||
const map = new Map<string, SearchResult[]>();
|
||||
all.forEach((r) => {
|
||||
// 根据传入参数进行精确匹配:
|
||||
// 1. 如果提供了 title,则按 title 精确匹配,否则按 query 精确匹配;
|
||||
// 2. 如果还提供了 year,则额外按 year 精确匹配。
|
||||
const titleMatch = title ? r.title === title : r.title === query;
|
||||
const yearMatch = year ? r.year === year : true;
|
||||
if (!titleMatch || !yearMatch) {
|
||||
return;
|
||||
}
|
||||
// 如果还传入了 type,则按 type 精确匹配
|
||||
if (type === 'tv' && r.episodes.length === 1) {
|
||||
return;
|
||||
}
|
||||
if (type === 'movie' && r.episodes.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const key = `${r.title}-${r.year}`;
|
||||
const arr = map.get(key) || [];
|
||||
arr.push(r);
|
||||
map.set(key, arr);
|
||||
});
|
||||
if (map.size === 0 && type) {
|
||||
// 无匹配,忽略 type 做重新匹配
|
||||
all.forEach((r) => {
|
||||
const titleMatch = title ? r.title === title : r.title === query;
|
||||
const yearMatch = year ? r.year === year : true;
|
||||
if (!titleMatch || !yearMatch) {
|
||||
return;
|
||||
}
|
||||
const key = `${r.title}-${r.year}`;
|
||||
const arr = map.get(key) || [];
|
||||
arr.push(r);
|
||||
map.set(key, arr);
|
||||
});
|
||||
}
|
||||
if (map.size == 1) {
|
||||
setResults(Array.from(map.values()).flat());
|
||||
} else if (map.size > 1) {
|
||||
// 存在多个匹配,跳转到搜索页
|
||||
router.push(`/search?q=${encodeURIComponent(query.trim())}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '搜索失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [query, router]);
|
||||
|
||||
// 选出信息最完整的字段
|
||||
const chooseString = (vals: (string | undefined)[]): string | undefined => {
|
||||
return vals.reduce<string | undefined>((best, v) => {
|
||||
if (!v) return best;
|
||||
if (!best) return v;
|
||||
return v.length > best.length ? v : best;
|
||||
}, undefined);
|
||||
};
|
||||
// 出现次数最多的非 0 数字
|
||||
const chooseNumber = (vals: (number | undefined)[]): number | undefined => {
|
||||
const countMap = new Map<number, number>();
|
||||
vals.forEach((v) => {
|
||||
if (v !== undefined && v !== 0) {
|
||||
countMap.set(v, (countMap.get(v) || 0) + 1);
|
||||
}
|
||||
});
|
||||
let selected: number | undefined = undefined;
|
||||
let maxCount = 0;
|
||||
countMap.forEach((cnt, num) => {
|
||||
if (cnt > maxCount) {
|
||||
maxCount = cnt;
|
||||
selected = num;
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
};
|
||||
|
||||
const aggregatedInfo = {
|
||||
title: title || query,
|
||||
cover: chooseString(results.map((d) => d.poster)),
|
||||
desc: chooseString(results.map((d) => d.desc)),
|
||||
type: chooseString(results.map((d) => d.type_name)),
|
||||
year: chooseString(results.map((d) => d.year)),
|
||||
remarks: chooseString(results.map((d) => d.class)),
|
||||
douban_id: chooseNumber(results.map((d) => d.douban_id)),
|
||||
};
|
||||
|
||||
const infoReady = Boolean(
|
||||
aggregatedInfo.cover ||
|
||||
aggregatedInfo.desc ||
|
||||
aggregatedInfo.type ||
|
||||
aggregatedInfo.year ||
|
||||
aggregatedInfo.remarks
|
||||
);
|
||||
|
||||
const uniqueSources = Array.from(
|
||||
new Map(results.map((r) => [r.source, r])).values()
|
||||
);
|
||||
|
||||
// 详情映射,便于快速获取每个源的集数
|
||||
const sourceDetailMap = new Map(results.map((d) => [d.source, d]));
|
||||
|
||||
// 新增:播放源卡片组件,包含收藏逻辑
|
||||
const SourceCard = ({ src }: { src: SearchResult }) => {
|
||||
const d = sourceDetailMap.get(src.source);
|
||||
const epCount = d ? d.episodes.length : src.episodes.length;
|
||||
|
||||
const [favorited, setFavorited] = useState(false);
|
||||
|
||||
// 初次加载检查收藏状态
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const fav = await isFavorited(src.source, src.id);
|
||||
setFavorited(fav);
|
||||
} catch {
|
||||
/* 忽略错误 */
|
||||
}
|
||||
})();
|
||||
}, [src.source, src.id]);
|
||||
|
||||
// 切换收藏状态
|
||||
const handleToggleFavorite = async (
|
||||
e: React.MouseEvent<HTMLSpanElement | SVGElement, MouseEvent>
|
||||
) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
try {
|
||||
const newState = await toggleFavorite(src.source, src.id, {
|
||||
title: src.title,
|
||||
source_name: src.source_name,
|
||||
year: src.year,
|
||||
cover: src.poster,
|
||||
total_episodes: src.episodes.length,
|
||||
save_time: Date.now(),
|
||||
});
|
||||
setFavorited(newState);
|
||||
} catch {
|
||||
/* 忽略错误 */
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
key={src.source}
|
||||
href={`/play?source=${src.source}&id=${
|
||||
src.id
|
||||
}&title=${encodeURIComponent(src.title.trim())}${
|
||||
src.year ? `&year=${src.year}` : ''
|
||||
}&from=aggregate`}
|
||||
className='group relative flex items-center justify-center w-full h-14 bg-gray-500/80 hover:bg-green-500 dark:bg-gray-700/80 dark:hover:bg-green-600 rounded-lg transition-colors'
|
||||
>
|
||||
{/* 收藏爱心 */}
|
||||
<span
|
||||
onClick={handleToggleFavorite}
|
||||
title={favorited ? '移除收藏' : '加入收藏'}
|
||||
className={`absolute top-[2px] left-1 inline-flex items-center justify-center cursor-pointer transition-opacity duration-200 ${
|
||||
favorited ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<Heart
|
||||
className={`w-5 h-5 ${
|
||||
favorited ? 'text-red-500' : 'text-white/90'
|
||||
}`}
|
||||
strokeWidth={2}
|
||||
fill={favorited ? 'currentColor' : 'none'}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* 名称 */}
|
||||
<span className='px-1 text-white text-sm font-medium truncate whitespace-nowrap'>
|
||||
{src.source_name}
|
||||
</span>
|
||||
{/* 集数徽标 */}
|
||||
{epCount && epCount > 1 ? (
|
||||
<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}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout activePath='/aggregate'>
|
||||
<div className='flex flex-col min-h-full px-2 sm:px-10 pt-4 sm:pt-8 pb-[calc(3.5rem+env(safe-area-inset-bottom))] overflow-visible'>
|
||||
{loading ? (
|
||||
<div className='flex items-center justify-center min-h-[60vh]'>
|
||||
<div className='animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500'></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className='flex items-center justify-center min-h-[60vh]'>
|
||||
<div className='text-red-500 text-center'>
|
||||
<div className='text-lg font-semibold mb-2'>加载失败</div>
|
||||
<div className='text-sm'>{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : !infoReady ? (
|
||||
<div className='flex items-center justify-center min-h-[60vh]'>
|
||||
<div className='text-gray-500 text-center'>
|
||||
<div className='text-lg font-semibold mb-2'>未找到匹配结果</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='max-w-[95%] mx-auto'>
|
||||
{/* 主信息区:左图右文 */}
|
||||
<div className='relative flex flex-col md:flex-row gap-8 mb-0 sm:mb-8 bg-transparent rounded-xl p-2 sm:p-6 md:items-start'>
|
||||
{/* 封面 */}
|
||||
<div className='flex-shrink-0 w-full max-w-[200px] sm:max-w-none md:w-72 mx-auto'>
|
||||
<Image
|
||||
src={aggregatedInfo.cover || '/images/placeholder.png'}
|
||||
alt={aggregatedInfo.title}
|
||||
width={288}
|
||||
height={432}
|
||||
className='w-full rounded-xl object-cover'
|
||||
style={{ aspectRatio: '2/3' }}
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
{/* 右侧信息 */}
|
||||
<div
|
||||
className='flex-1 flex flex-col min-h-0'
|
||||
style={{ height: '430px' }}
|
||||
>
|
||||
<h1 className='text-3xl font-bold mb-2 tracking-wide flex items-center flex-shrink-0 text-center md:text-left w-full'>
|
||||
{aggregatedInfo.title}
|
||||
{aggregatedInfo.douban_id && (
|
||||
<a
|
||||
href={`https://movie.douban.com/subject/${aggregatedInfo.douban_id}/`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className='ml-2'
|
||||
>
|
||||
<LinkIcon className='w-4 h-4' strokeWidth={2} />
|
||||
</a>
|
||||
)}
|
||||
</h1>
|
||||
<div className='flex flex-wrap items-center gap-3 text-base mb-4 opacity-80 flex-shrink-0'>
|
||||
{aggregatedInfo.remarks && (
|
||||
<span className='text-green-600 font-semibold'>
|
||||
{aggregatedInfo.remarks}
|
||||
</span>
|
||||
)}
|
||||
{aggregatedInfo.year && <span>{aggregatedInfo.year}</span>}
|
||||
{aggregatedInfo.type && <span>{aggregatedInfo.type}</span>}
|
||||
</div>
|
||||
<div
|
||||
className='mt-0 text-base leading-relaxed opacity-90 overflow-y-auto pr-2 flex-1 min-h-0 scrollbar-hide'
|
||||
style={{ whiteSpace: 'pre-line' }}
|
||||
>
|
||||
{aggregatedInfo.desc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 选播放源 */}
|
||||
{uniqueSources.length > 0 && (
|
||||
<div className='mt-0 sm:mt-8 bg-transparent rounded-xl p-2 sm:p-6'>
|
||||
<div className='flex items-center gap-2 mb-4'>
|
||||
<div className='text-xl font-semibold'>选择播放源</div>
|
||||
<div className='text-gray-400 ml-2'>
|
||||
共 {uniqueSources.length} 个
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2 sm:grid-cols-[repeat(auto-fill,_minmax(6rem,_1fr))] sm:gap-4 justify-start'>
|
||||
{uniqueSources.map((src) => (
|
||||
<SourceCard key={src.source} src={src} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AggregatePage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<AggregatePageClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -55,8 +55,12 @@ function PlayPageClient() {
|
||||
});
|
||||
|
||||
// 视频基本信息
|
||||
const [videoType, setVideoType] = useState(searchParams.get('type') || '');
|
||||
const [videoDoubanId, setVideoDoubanId] = useState(
|
||||
searchParams.get('douban_id') || ''
|
||||
);
|
||||
const [videoTitle, setVideoTitle] = useState(searchParams.get('title') || '');
|
||||
const videoYear = searchParams.get('year') || '';
|
||||
const [videoYear, setVideoYear] = useState(searchParams.get('year') || '');
|
||||
const [videoCover, setVideoCover] = useState('');
|
||||
// 当前源和ID
|
||||
const [currentSource, setCurrentSource] = useState(
|
||||
@@ -228,48 +232,77 @@ function PlayPageClient() {
|
||||
|
||||
// 获取视频详情
|
||||
useEffect(() => {
|
||||
if (!currentSource || !currentId) {
|
||||
setError('缺少必要参数');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
try {
|
||||
const detailData = await fetchVideoDetail({
|
||||
source: currentSource,
|
||||
id: currentId,
|
||||
fallbackTitle: videoTitle.trim(),
|
||||
fallbackYear: videoYear,
|
||||
});
|
||||
|
||||
// 更新状态保存详情
|
||||
setVideoTitle(detailData.title || videoTitle);
|
||||
setVideoCover(detailData.poster);
|
||||
setDetail(detailData);
|
||||
|
||||
// 确保集数索引在有效范围内
|
||||
if (currentEpisodeIndex >= detailData.episodes.length) {
|
||||
console.log('currentEpisodeIndex', currentEpisodeIndex);
|
||||
setCurrentEpisodeIndex(0);
|
||||
}
|
||||
|
||||
// 清理URL参数(移除index参数)
|
||||
if (searchParams.has('index')) {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.delete('index');
|
||||
newUrl.searchParams.delete('position');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取视频详情失败');
|
||||
} finally {
|
||||
const fetchDetailAsync = async () => {
|
||||
if (!currentSource && !currentId && !videoTitle) {
|
||||
setError('缺少必要参数');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSource && !currentId) {
|
||||
// 只包含视频标题,搜索视频
|
||||
setLoading(true);
|
||||
const searchResults = await handleSearchSources(videoTitle);
|
||||
console.log('searchResults', searchResults);
|
||||
if (searchResults.length == 0) {
|
||||
setError('未找到匹配结果');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setCurrentSource(searchResults[0].source);
|
||||
setCurrentId(searchResults[0].id);
|
||||
setVideoYear(searchResults[0].year);
|
||||
setVideoType('');
|
||||
setVideoDoubanId(''); // 清空豆瓣ID
|
||||
// 替换URL参数
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set('source', searchResults[0].source);
|
||||
newUrl.searchParams.set('id', searchResults[0].id);
|
||||
newUrl.searchParams.set('year', searchResults[0].year);
|
||||
newUrl.searchParams.delete('douban_id');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
try {
|
||||
const detailData = await fetchVideoDetail({
|
||||
source: currentSource,
|
||||
id: currentId,
|
||||
fallbackTitle: videoTitle.trim(),
|
||||
fallbackYear: videoYear,
|
||||
});
|
||||
|
||||
// 更新状态保存详情
|
||||
setVideoTitle(detailData.title || videoTitle);
|
||||
setVideoCover(detailData.poster);
|
||||
setDetail(detailData);
|
||||
|
||||
// 确保集数索引在有效范围内
|
||||
if (currentEpisodeIndex >= detailData.episodes.length) {
|
||||
console.log('currentEpisodeIndex', currentEpisodeIndex);
|
||||
setCurrentEpisodeIndex(0);
|
||||
}
|
||||
|
||||
// 清理URL参数(移除index参数)
|
||||
if (searchParams.has('index')) {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.delete('index');
|
||||
newUrl.searchParams.delete('position');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取视频详情失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDetail();
|
||||
};
|
||||
|
||||
fetchDetail();
|
||||
}, [currentSource]);
|
||||
fetchDetailAsync();
|
||||
}, [currentSource, currentId]);
|
||||
|
||||
// 播放记录处理
|
||||
useEffect(() => {
|
||||
@@ -332,10 +365,12 @@ function PlayPageClient() {
|
||||
// 换源搜索与切换
|
||||
// ---------------------------------------------------------------------------
|
||||
// 处理换源搜索
|
||||
const handleSearchSources = async (query: string) => {
|
||||
const handleSearchSources = async (
|
||||
query: string
|
||||
): Promise<SearchResult[]> => {
|
||||
if (!query.trim()) {
|
||||
setAvailableSources([]);
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
setSourceSearchLoading(true);
|
||||
@@ -370,27 +405,37 @@ function PlayPageClient() {
|
||||
if (results.length === 0) return;
|
||||
|
||||
// 只选择和当前视频标题完全匹配的结果,如果有年份,还需要年份完全匹配
|
||||
const exactMatch = results.find(
|
||||
const exactMatchs = results.filter(
|
||||
(result) =>
|
||||
result.title.toLowerCase() === videoTitle.toLowerCase() &&
|
||||
(videoYear
|
||||
? result.year.toLowerCase() === videoYear.toLowerCase()
|
||||
: true) &&
|
||||
detail?.episodes.length &&
|
||||
((detail?.episodes.length === 1 && result.episodes.length === 1) ||
|
||||
(detail?.episodes.length > 1 && result.episodes.length > 1))
|
||||
(detail
|
||||
? (detail.episodes.length === 1 &&
|
||||
result.episodes.length === 1) ||
|
||||
(detail.episodes.length > 1 && result.episodes.length > 1)
|
||||
: true) &&
|
||||
(videoDoubanId && result.douban_id
|
||||
? result.douban_id.toString() === videoDoubanId
|
||||
: true) &&
|
||||
(videoType
|
||||
? (videoType === 'movie' && result.episodes.length === 1) ||
|
||||
(videoType === 'tv' && result.episodes.length > 1)
|
||||
: true)
|
||||
);
|
||||
|
||||
if (exactMatch) {
|
||||
processedResults.push(exactMatch);
|
||||
return;
|
||||
if (exactMatchs.length > 0) {
|
||||
processedResults.push(...exactMatchs);
|
||||
}
|
||||
});
|
||||
console.log('processedResults', processedResults);
|
||||
|
||||
setAvailableSources(processedResults);
|
||||
return processedResults;
|
||||
} catch (err) {
|
||||
setSourceSearchError(err instanceof Error ? err.message : '搜索失败');
|
||||
setAvailableSources([]);
|
||||
return [];
|
||||
} finally {
|
||||
setSourceSearchLoading(false);
|
||||
}
|
||||
@@ -1142,10 +1187,16 @@ function PlayPageClient() {
|
||||
{error}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
onClick={() =>
|
||||
videoTitle
|
||||
? (window.location.href = `/search?q=${encodeURIComponent(
|
||||
videoTitle
|
||||
)}`)
|
||||
: window.history.back()
|
||||
}
|
||||
className='px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors'
|
||||
>
|
||||
重试
|
||||
{videoTitle ? '返回搜索' : '返回'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -186,11 +186,7 @@ function SearchPageClient() {
|
||||
? aggregatedResults.map(([mapKey, group]) => {
|
||||
return (
|
||||
<div key={`agg-${mapKey}`} className='w-full'>
|
||||
<AggregateCard
|
||||
items={group}
|
||||
query={searchQuery}
|
||||
year={group[0].year}
|
||||
/>
|
||||
<AggregateCard items={group} year={group[0].year} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -19,7 +19,6 @@ interface SearchResult {
|
||||
|
||||
interface AggregateCardProps {
|
||||
/** 同一标题下的多个搜索结果 */
|
||||
query?: string;
|
||||
year?: string;
|
||||
items: SearchResult[];
|
||||
}
|
||||
@@ -58,11 +57,7 @@ function PlayCircleSolid({
|
||||
* 点击播放按钮 -> 跳到第一个源播放
|
||||
* 点击卡片其他区域 -> 跳到聚合详情页 (/aggregate)
|
||||
*/
|
||||
const AggregateCard: React.FC<AggregateCardProps> = ({
|
||||
query = '',
|
||||
year = 0,
|
||||
items,
|
||||
}) => {
|
||||
const AggregateCard: React.FC<AggregateCardProps> = ({ year = 0, items }) => {
|
||||
// 使用列表中的第一个结果做展示 & 播放
|
||||
const first = items[0];
|
||||
const [playHover, setPlayHover] = useState(false);
|
||||
@@ -118,11 +113,9 @@ const AggregateCard: React.FC<AggregateCardProps> = ({
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/aggregate?q=${encodeURIComponent(
|
||||
query.trim()
|
||||
)}&title=${encodeURIComponent(first.title)}${
|
||||
year ? `&year=${encodeURIComponent(year)}` : ''
|
||||
}&type=${mostFrequentEpisodes > 1 ? 'tv' : 'movie'}`}
|
||||
href={`/play?source=${first.source}&id=${
|
||||
first.id
|
||||
}&title=${encodeURIComponent(first.title)}${year ? `&year=${year}` : ''}`}
|
||||
>
|
||||
<div className='group relative w-full rounded-lg bg-transparent flex flex-col cursor-pointer transition-all duration-300 ease-in-out'>
|
||||
{/* 封面图片 2:3 */}
|
||||
@@ -162,7 +155,7 @@ const AggregateCard: React.FC<AggregateCardProps> = ({
|
||||
first.id
|
||||
}&title=${encodeURIComponent(first.title)}${
|
||||
year ? `&year=${year}` : ''
|
||||
}&from=aggregate`
|
||||
}`
|
||||
);
|
||||
}}
|
||||
onMouseEnter={() => setPlayHover(true)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link as LinkIcon, Search } from 'lucide-react';
|
||||
import { Link as LinkIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useRef, useState } from 'react';
|
||||
@@ -13,7 +13,7 @@ interface DemoCardProps {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
function SearchCircle({
|
||||
function PlayCircleSolid({
|
||||
className = '',
|
||||
fillColor = 'none',
|
||||
}: {
|
||||
@@ -37,11 +37,7 @@ function SearchCircle({
|
||||
strokeWidth='1.5'
|
||||
fill={fillColor}
|
||||
/>
|
||||
<foreignObject x='0' y='0' width='44' height='44'>
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
<Search className='h-5 w-5 text-white' strokeWidth={2} />
|
||||
</div>
|
||||
</foreignObject>
|
||||
<polygon points='19,15 19,29 29,22' fill='white' />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -54,7 +50,9 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(
|
||||
`/aggregate?q=${encodeURIComponent(title.trim())}&type=${type}`
|
||||
`/play?title=${encodeURIComponent(
|
||||
title.trim()
|
||||
)}&douban_id=${id}&type=${type}`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,7 +101,7 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
|
||||
hover ? 'scale-110 rotate-12' : 'scale-90'
|
||||
}`}
|
||||
>
|
||||
<SearchCircle fillColor={hover ? '#22c55e' : 'none'} />
|
||||
<PlayCircleSolid fillColor={hover ? '#22c55e' : 'none'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||
return (
|
||||
<div className='w-full min-h-screen'>
|
||||
{/* 移动端头部 */}
|
||||
<MobileHeader
|
||||
showBackButton={['/play', '/aggregate'].includes(activePath)}
|
||||
/>
|
||||
<MobileHeader showBackButton={['/play'].includes(activePath)} />
|
||||
|
||||
{/* 主要布局容器 */}
|
||||
<div className='flex md:grid md:grid-cols-[auto_1fr] w-full min-h-screen md:min-h-auto'>
|
||||
@@ -28,7 +26,7 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
|
||||
{/* 主内容区域 */}
|
||||
<div className='relative min-w-0 flex-1 transition-all duration-300'>
|
||||
{/* 桌面端左上角返回按钮 */}
|
||||
{['/play', '/aggregate'].includes(activePath) && (
|
||||
{['/play'].includes(activePath) && (
|
||||
<div className='absolute top-3 left-1 z-20 hidden md:flex'>
|
||||
<BackButton />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user