import { Heart, Link as LinkIcon } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import React, { useEffect, useMemo, useState } from 'react'; import { deletePlayRecord, isFavorited, toggleFavorite } from '@/lib/db.client'; import { ImagePlaceholder } from '@/components/ImagePlaceholder'; // 聚合卡需要的基本字段,与搜索接口保持一致 interface SearchResult { id: string; title: string; poster: string; source: string; source_name: string; douban_id?: number; episodes: string[]; } interface VideoCardProps { id: string; source: string; title: string; poster: string; episodes?: number; source_name: string; progress?: number; year?: string; from?: string; currentEpisode?: number; douban_id?: number; onDelete?: () => void; // 可选属性,根据存在与否决定卡片行为 rate?: string; // 如果存在,按demo卡片处理 items?: SearchResult[]; // 如果存在,按aggregate卡片处理 } function CheckCircleCustom() { return ( ); } function PlayCircleSolid({ className = '', fillColor = 'none', }: { className?: string; fillColor?: string; }) { return ( ); } export default function VideoCard({ id, title, poster, episodes, source, source_name, progress, year, from, currentEpisode, douban_id, onDelete, rate, items, }: VideoCardProps) { const [playHover, setPlayHover] = useState(false); const [favorited, setFavorited] = useState(false); const [isLoaded, setIsLoaded] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const router = useRouter(); // 判断卡片类型 const isDemo = !!rate; const isAggregate = !!items && items.length > 0; const isStandard = !isDemo && !isAggregate; // 处理聚合卡片的逻辑 const aggregateData = useMemo(() => { if (!isAggregate) { return null; } const first = items[0]; // 统计出现次数最多的(非 0) douban_id const countMap = new Map(); items.forEach((item) => { if (item.douban_id && item.douban_id !== 0) { countMap.set(item.douban_id, (countMap.get(item.douban_id) || 0) + 1); } }); let mostFrequentDoubanId: number | undefined; let maxCount = 0; countMap.forEach((cnt, id) => { if (cnt > maxCount) { maxCount = cnt; mostFrequentDoubanId = id; } }); // 统计最频繁的集数 const episodeCountMap = new Map(); items.forEach((item) => { const len = item.episodes?.length || 0; if (len > 0) { episodeCountMap.set(len, (episodeCountMap.get(len) || 0) + 1); } }); let mostFrequentEpisodes = 0; let maxEpisodeCount = 0; episodeCountMap.forEach((cnt, len) => { if (cnt > maxEpisodeCount) { maxEpisodeCount = cnt; mostFrequentEpisodes = len; } }); return { first, mostFrequentDoubanId, mostFrequentEpisodes, }; }, [isAggregate, items]); // 根据卡片类型决定实际使用的数据 const actualTitle = isAggregate && aggregateData ? aggregateData.first.title : title; const actualPoster = isAggregate && aggregateData ? aggregateData.first.poster : poster; const actualSource = isAggregate && aggregateData ? aggregateData.first.source : source; const actualId = isAggregate && aggregateData ? aggregateData.first.id : id; const actualDoubanId = isAggregate && aggregateData ? aggregateData.mostFrequentDoubanId : douban_id; const actualEpisodes = isAggregate && aggregateData ? aggregateData.mostFrequentEpisodes : episodes; // 检查初始收藏状态(仅标准卡片) useEffect(() => { if (!isStandard) return; (async () => { try { const fav = await isFavorited(actualSource, actualId); setFavorited(fav); } catch (err) { throw new Error('检查收藏状态失败'); } })(); }, [isStandard, actualSource, actualId]); // 切换收藏状态(仅标准卡片) const handleToggleFavorite = async ( e: React.MouseEvent ) => { e.preventDefault(); e.stopPropagation(); if (!isStandard) return; try { const newState = await toggleFavorite(actualSource, actualId, { title: actualTitle, source_name, year: year || '', cover: actualPoster, total_episodes: actualEpisodes ?? 1, save_time: Date.now(), }); setFavorited(newState); } catch (err) { // 如果删除失败且是收藏夹,恢复显示 if (isDeleting) { setIsDeleting(false); } throw new Error('切换收藏状态失败'); } }; // 删除对应播放记录(仅标准卡片) const handleDeleteRecord = async ( e: React.MouseEvent ) => { e.preventDefault(); e.stopPropagation(); if (!isStandard) return; try { await deletePlayRecord(actualSource, actualId); onDelete?.(); } catch (err) { throw new Error('删除播放记录失败'); } }; // 点击处理逻辑 const handleClick = () => { if (isDemo) { router.push(`/play?title=${encodeURIComponent(actualTitle.trim())}`); } else { router.push( `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent( actualTitle.trim() )}${year ? `&year=${year}` : ''}` ); } }; // 播放按钮点击处理 const handlePlayClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); handleClick(); }; const hideCheckCircle = from === 'favorites' || from === 'search' || !isStandard; const alwaysShowHeart = from !== 'favorites' && isStandard; const showHoverLayer = isStandard ? alwaysShowHeart ? 'opacity-50 group-hover:opacity-100' : 'opacity-0 group-hover:opacity-100' : 'opacity-0 group-hover:opacity-100'; return ( {/* 海报图片容器 */} {/* 图片占位符 - 骨架屏效果 */} setIsLoaded(true)} referrerPolicy='no-referrer' priority={false} /> {/* Hover 效果层 */} {/* 播放按钮 */} setPlayHover(true)} onMouseLeave={() => setPlayHover(false)} > {/* 右侧操作按钮组(仅标准卡片) */} {isStandard && ( {!hideCheckCircle && ( )} )} {/* 评分徽章(如果有rate字段) */} {rate && ( {rate} )} {/* 继续观看 - 集数矩形展示框(标准卡片) */} {isStandard && actualEpisodes && actualEpisodes > 1 && currentEpisode && ( {currentEpisode} / {actualEpisodes} )} {/* 搜索非聚合/聚合 - 集数圆形展示框 */} {(isStandard || isAggregate) && actualEpisodes && actualEpisodes > 1 && !currentEpisode && ( {actualEpisodes} )} {/* 豆瓣链接按钮 */} {actualDoubanId && (isDemo || (isStandard && from === 'search') || isAggregate) && ( e.stopPropagation()} className='absolute top-2 left-2 scale-90 group-hover:scale-100 opacity-0 group-hover:opacity-100 transition-all duration-300 cubic-bezier(0.4,0,0.2,1)' > )} {/* 播放进度条(仅标准卡片) */} {isStandard && progress !== undefined && ( )} {/* 信息层 */} {actualTitle} {/* 来源信息(仅标准卡片) */} {isStandard && actualSource && ( {source_name} )} ); }