import { memo, useEffect, useRef, useState, type ReactNode } from 'react' import { Link } from 'react-router-dom' import { Film, Play, Layers, Star } from 'lucide-react' import { ARTWORK, imageURL } from '../api/client' import type { Media } from '../types' const ACTION_OVERLAY_CLASS = 'absolute right-2 top-2 z-20 flex flex-wrap justify-end gap-1 opacity-100 transition-opacity sm:pointer-events-none sm:opacity-0 sm:group-hover:pointer-events-auto sm:group-hover:opacity-100 sm:focus-within:pointer-events-auto sm:focus-within:opacity-100' // memo:父级状态变化(如轮播切图、其它卡片操作)不再级联重渲染所有卡片。 // 注意 actions/renderActions 必须引用稳定(用 renderActions 传函数)memo 才生效。 export const MediaCard = memo(function MediaCard({ media, progress, count, rating, linkTo, linkState, onClick, actions, renderActions, compact, }: { media: Media progress?: number count?: number rating?: number linkTo?: string linkState?: { from?: string } onClick?: () => void actions?: ReactNode renderActions?: (media: Media) => ReactNode compact?: boolean }) { const ref = useRef(null) const href = linkTo ?? `/media/${media.id}` const [posterFit, setPosterFit] = useState<'cover' | 'contain'>('cover') // 固定档位而不是在调用点拼尺寸:紧凑布局与普通卡片共用同一份缩略图缓存。 const posterSrc = imageURL(media.poster_url, media.updated_at, ARTWORK.posterCard) const blurredPosterSrc = imageURL(media.poster_url, media.updated_at, ARTWORK.posterTiny) const displayRating = rating ?? media.rating const versionCount = media.versions?.length ?? 0 // renderActions 延迟到卡片自身渲染时才调用,保证 memo 生效 const actionContent = actions ?? renderActions?.(media) useEffect(() => { setPosterFit('cover') }, [media.poster_url, media.updated_at]) const card = (
{/* Poster Wrapper */}
{media.poster_url ? ( <> {posterFit === 'contain' && ( )} {media.title} { const img = event.currentTarget setPosterFit(img.naturalWidth > img.naturalHeight ? 'contain' : 'cover') }} className={ 'relative block h-full w-full object-center transition-transform duration-700 ease-out group-hover:scale-105 ' + (posterFit === 'contain' ? 'object-contain p-1.5' : 'object-cover') } referrerPolicy="no-referrer" /> ) : (
No Poster
)} {/* Rating Badge */} {displayRating > 0 && ( {displayRating.toFixed(1)} )} {/* Premium Hover Overlay(CSS 过渡替代 framer-motion 逐卡动画实例) */}
立即观影 {!compact && (

{media.overview || "暂无简介内容"}

)}
{/* Progress Bar overlay */} {progress !== undefined && progress > 0 && progress < 1 && (
)}
{/* Media Metadata Info */}

{media.title}

{media.year > 0 ? media.year : "未知年份"} {/* 集数/版本数徽标:放在海报下方的信息行而不是压在画面上。海报底部通常 自带片名,角标会盖住它(Game of Thrones / Breaking Bad 都很明显)。 */} {(count !== undefined && count > 1) || (count === undefined && versionCount > 1) ? ( {count !== undefined && count > 1 ? `${count} 集` : `${versionCount} 版本`} ) : null} {media.video_codec && ( {media.video_codec} )}
) if (onClick) { return (
{actionContent && (
{actionContent}
)}
) } if (actionContent) { return (
{card}
{actionContent}
) } return ( {card} ) })