style(VideoCard): 优化卡片悬停动画和交互效果

改进悬停时的缩放、阴影和过渡动画效果
调整按钮的延迟和缓动效果
优化图片加载动画和评分徽章样式
This commit is contained in:
SongPro
2025-07-12 22:38:03 +08:00
parent 323c1ba953
commit f4fd12a991

View File

@@ -49,13 +49,10 @@ export default function VideoCard({
const isAggregate = from === 'search' && !!items?.length;
// 聚合数据(仅在 search 模式下)
const aggregateData = useMemo(() => {
if (!isAggregate || !items) return null;
const countMap = new Map<string | number, number>();
const episodeCountMap = new Map<number, number>();
items.forEach((item) => {
if (item.douban_id && item.douban_id !== 0) {
countMap.set(item.douban_id, (countMap.get(item.douban_id) || 0) + 1);
@@ -121,9 +118,7 @@ export default function VideoCard({
async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (from === 'douban' || !actualSource || !actualId) return;
try {
const newState = await toggleFavorite(actualSource, actualId, {
title: actualTitle,
@@ -154,9 +149,7 @@ export default function VideoCard({
async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (from !== 'playrecord' || !actualSource || !actualId) return;
try {
await deletePlayRecord(actualSource, actualId);
onDelete?.();
@@ -237,119 +230,119 @@ export default function VideoCard({
return (
<div
className='group relative w-full rounded-lg bg-transparent'
className='group relative w-full rounded-lg bg-transparent transition-all duration-300 ease-out hover:-translate-y-1 hover:scale-[1.02] cursor-pointer'
onClick={handleClick}
>
{/* 海报容器 */}
<div className='relative aspect-[2/3] overflow-hidden rounded-lg'>
{/* 骨架屏 */}
{!isLoaded && <ImagePlaceholder aspectRatio='aspect-[2/3]' />}
<div className='relative aspect-[2/3] overflow-hidden rounded-lg shadow-md transition-shadow duration-300 ease-out group-hover:shadow-xl'>
{/* 骨架屏 - 添加渐入动画 */}
{!isLoaded && (
<ImagePlaceholder aspectRatio='aspect-[2/3] transition-opacity duration-500 ease-out' />
)}
{/* 图片加载动画 */}
{/* 图片加载动画 - 改进淡入和锐化效果 */}
<Image
src={actualPoster}
alt={actualTitle}
fill
className={`object-cover transition-all duration-700 ease-out ${
isLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-95 blur-sm'
} group-hover:scale-105`}
isLoaded ? 'opacity-100 blur-0' : 'opacity-0 blur-md'
}`}
onLoadingComplete={() => setIsLoaded(true)}
referrerPolicy='no-referrer'
priority={false}
/>
{/* 悬浮层 */}
<div className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center cursor-pointer'>
{config.showPlayButton && (
{/* 悬浮层 - 改进渐变和元素过渡 */}
<div className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out pointer-events-auto'></div>
{/* 播放按钮 */}
{config.showPlayButton && (
<div className='absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all duration-200 pointer-events-auto'>
<PlayCircleIcon
size={52}
strokeWidth={1}
className='rounded-full cursor-pointer play-icon-fixed'
size={50}
strokeWidth={0.8}
className='rounded-full text-white hover:fill-green-500 transition-all duration-200 ease-out hover:scale-110'
/>
)}
</div>
)}
{/* 已看 / 收藏按钮 */}
{(config.showHeart || config.showCheckCircle) && (
<div className='absolute bottom-3 right-3 flex items-center gap-3 transform translate-y-2 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300 ease-out'>
{config.showCheckCircle && (
<CheckCircle
onClick={handleDeleteRecord}
size={20}
className='rounded-full transition-all duration-300ms transform hover:scale-110 text-white hover:stroke-green-500'
aria-label='标记为已看'
/>
)}
{/* 已看 / 收藏按钮 - 改进延迟和缓动效果 */}
{(config.showHeart || config.showCheckCircle) && (
<div className='absolute bottom-3 right-3 flex items-center gap-3 translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300 ease-out delay-75 pointer-events-auto'>
{config.showCheckCircle && (
<CheckCircle
onClick={handleDeleteRecord}
size={20}
className='rounded-full text-white hover:stroke-green-500 transition-transform duration-200 ease-out hover:scale-110'
/>
)}
{config.showHeart && (
<Heart
onClick={handleToggleFavorite}
size={20}
className={`rounded-full transition-all duration-300 ease-out hover:scale-110 ${
favorited
? 'fill-red-600 stroke-red-600 animate-pulse-subtle'
: 'fill-transparent stroke-white hover:stroke-red-400'
}`}
/>
)}
</div>
)}
{config.showHeart && (
<Heart
onClick={handleToggleFavorite}
size={20}
className={`rounded-full transition-all duration-300ms transform hover:scale-110 ${
favorited
? 'fill-red-600 stroke-red-600'
: 'fill-transparent stroke-white hover:stroke-red-400'
}`}
aria-label={favorited ? '取消收藏' : '加入收藏'}
/>
)}
</div>
)}
</div>
{/* 评分徽章 */}
{/* 评分徽章 - 添加弹出效果 */}
{config.showRating && rate && (
<div className='absolute top-2 right-2 bg-pink-500 text-white text-xs font-bold w-6 h-6 sm:w-7 sm:h-7 rounded-full flex items-center justify-center shadow-md transform transition-transform duration-300 group-hover:scale-110'>
<div className='absolute top-2 right-2 bg-pink-500 text-white text-xs font-bold p-1 sm:w-7 sm:h-7 rounded-full flex items-center justify-center shadow-md transition-all duration-300 ease-out transform scale-90 group-hover:scale-110 group-hover:shadow-lg'>
{rate}
</div>
)}
{/* 集数徽章 */}
{/* 集数徽章 - 改进缩放和阴影 */}
{actualEpisodes && actualEpisodes > 1 && currentEpisode && (
<div className='absolute top-2 right-2 bg-green-500 text-white text-xs font-semibold rounded-md px-2 py-1 shadow-md transform transition-transform duration-300 group-hover:scale-105'>
<div className='absolute top-2 right-2 bg-green-500 text-white text-xs font-semibold rounded-md px-2 py-1 shadow-md transition-all duration-300 ease-out transform scale-90 group-hover:scale-105 group-hover:shadow-lg'>
{currentEpisode}/{actualEpisodes}
</div>
)}
{actualEpisodes && actualEpisodes > 1 && !currentEpisode && (
<div className='absolute top-2 right-2 bg-green-500 text-white text-xs font-semibold rounded-md px-2 py-1 shadow-md transform transition-transform duration-300 group-hover:scale-105'>
<div className='absolute top-2 right-2 bg-green-500 text-white text-xs font-semibold rounded-md px-2 py-1 shadow-md transition-all duration-300 ease-out transform scale-90 group-hover:scale-105 group-hover:shadow-lg'>
{actualEpisodes}
</div>
)}
{/* 豆瓣链接按钮 */}
{/* 豆瓣链接按钮 - 改进进入方向和延迟 */}
{config.showDoubanLink && actualDoubanId && (
<a
href={`https://movie.douban.com/subject/${actualDoubanId}`}
target='_blank'
rel='noopener noreferrer'
onClick={(e) => e.stopPropagation()}
className='absolute top-2 left-2 opacity-0 -translate-x-2 group-hover:translate-x-0 group-hover:opacity-100 group-hover:scale-110 transition-all duration-300'
className='absolute top-2 left-2 opacity-0 -translate-x-4 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300 ease-out delay-100'
>
<div className='bg-green-500 text-white text-xs font-bold p-1 rounded-full flex-center shadow-md hover:bg-green-600 hover:scale-110 transition-all duration-300'>
<div className='bg-green-500 text-white text-xs font-bold p-1 sm:w-7 sm:h-7 rounded-full flex items-center justify-center shadow-md hover:bg-green-600 hover:scale-110 transition-all duration-200 ease-out'>
<Link size={16} />
</div>
</a>
)}
</div>
{/* 进度条 */}
{/* 进度条 - 添加动画效果 */}
{config.showProgress && progress !== undefined && (
<div className='mt-1 h-1 w-full bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden'>
<div
className='h-full bg-green-500 rounded-full transition-all duration-300'
className='h-full bg-green-500 rounded-full transition-all duration-1000 ease-out'
style={{ width: `${progress}%` }}
/>
</div>
)}
{/* 标题与来源信息 */}
<span className='mt-2 block text-center text-sm font-semibold truncate text-gray-900 dark:text-gray-100 transition-colors duration-300 group-hover:text-green-600 dark:group-hover:text-green-400'>
{/* 标题与来源信息 - 改进颜色过渡和延迟 */}
<span className='mt-2 block text-center text-sm font-semibold truncate text-gray-900 dark:text-gray-100 transition-all duration-300 ease-out group-hover:text-green-600 dark:group-hover:text-green-400'>
{actualTitle}
</span>
{config.showSourceName && source_name && (
<span className='block text-center text-xs text-gray-500 dark:text-gray-400 mt-1 transition-all duration-300 group-hover:text-green-500 dark:group-hover:text-green-500 group-hover:scale-105'>
<span className='inline-block border rounded px-2 py-0.5 border-gray-500/60 dark:border-gray-400/60 transition-all duration-300 group-hover:border-green-500/60'>
<span className='block text-center text-xs text-gray-500 dark:text-gray-400 mt-1 transition-all duration-300 ease-out delay-75 group-hover:text-green-500 dark:group-hover:text-green-500 group-hover:scale-105'>
<span className='inline-block border rounded px-2 py-0.5 border-gray-500/60 dark:border-gray-400/60 transition-all duration-300 ease-out group-hover:border-green-500/60'>
{source_name}
</span>
</span>