'use client'; /** * VideoGroupCard - Displays grouped videos with same name as single card * Following Liquid Glass design system */ import { memo, useMemo } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Icons } from '@/components/ui/Icon'; import { LatencyBadge } from '@/components/ui/LatencyBadge'; import { FavoriteButton } from '@/components/favorites/FavoriteButton'; import { Video } from '@/lib/types'; import { parseVideoTitle } from '@/lib/utils/video'; export interface GroupedVideo { /** Representative video (lowest latency) */ representative: Video; /** All videos in this group */ videos: Video[]; /** Group name (vod_name) */ name: string; } interface VideoGroupCardProps { group: GroupedVideo; cardId: string; isActive: boolean; onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void; isPremium?: boolean; latencies?: Record; } export const VideoGroupCard = memo(({ group, cardId, isActive, onCardClick, isPremium = false, latencies = {} }) => { const { representative, videos, name } = group; // Best latency from the group, preferring real-time updates const bestLatency = useMemo(() => { const currentLatencies = videos.map(v => latencies[v.source] ?? v.latency).filter(l => l !== undefined) as number[]; return currentLatencies.length > 0 ? Math.min(...currentLatencies) : undefined; }, [videos, latencies]); // Generate URL with grouped sources data const videoUrl = useMemo(() => { const params = new URLSearchParams({ id: String(representative.vod_id), source: representative.source, title: representative.vod_name, }); // Add group data if multiple sources if (videos.length > 1) { const groupData = videos.map(v => ({ id: v.vod_id, source: v.source, sourceName: v.sourceName, latency: v.latency, pic: v.vod_pic, typeName: v.type_name, remarks: v.vod_remarks, })); params.set('groupedSources', JSON.stringify(groupData)); } return `/player?${params.toString()}`; }, [representative, videos]); return (
(e.currentTarget.style.zIndex = '100')} onMouseLeave={(e) => (e.currentTarget.style.zIndex = '1')} > onCardClick(e, cardId, videoUrl)} role="listitem" aria-label={`${name} - ${videos.length} 个源${representative.vod_remarks ? ` - ${representative.vod_remarks}` : ''}`} prefetch={false} data-focusable className="group cursor-pointer hover:translate-y-[-2px] transition-transform duration-200 ease-out block h-full" > {/* Poster */}
{representative.vod_pic ? ( {name} { const target = e.currentTarget as HTMLImageElement; target.style.opacity = '0'; }} /> ) : (
)} {/* Fallback Icon - visible when image fails */}
{name}
{/* Badge Container */}
{/* Source count badge */} {videos.length} 源 {bestLatency !== undefined && ( )}
{/* Favorite Button - Top Right */}
{/* Overlay */}
{isActive && (
再次点击播放 →
)} {representative.type_name && ( {representative.type_name} )} {representative.vod_year && (
{representative.vod_year}
)}
{/* Info */}
{(() => { const { cleanTitle, quality } = parseVideoTitle(name); const displayQuality = quality || representative.vod_remarks; return ( <>

{cleanTitle}

{displayQuality && (

{displayQuality}

)} {representative.vod_lang && (

{representative.vod_lang}

)} ); })()}
); }); VideoGroupCard.displayName = 'VideoGroupCard';