refactor(组件): 优化滚动行和视频卡的样式与动画效果

- 调整 ScrollableRow 的内边距样式
- 简化 ImagePlaceholder 的类名并修改圆角大小
- 重构 VideoCard 的悬停动画和交互效果,改进加载状态处理
- 统一过渡动画的缓动函数和持续时间
This commit is contained in:
SongPro
2025-07-15 11:27:58 +08:00
parent 656c1c256f
commit f187e56e2e
3 changed files with 43 additions and 58 deletions

View File

@@ -1,7 +1,7 @@
// 图片占位符组件 - 实现骨架屏效果(支持暗色模式)
const ImagePlaceholder = ({ aspectRatio }: { aspectRatio: string }) => (
<div
className={`w-full ${aspectRatio} rounded-md overflow-hidden transition-opacity duration-500`}
className={`w-full ${aspectRatio} rounded-lg`}
style={{
background:
'linear-gradient(90deg, var(--skeleton-color) 25%, var(--skeleton-highlight) 50%, var(--skeleton-color) 75%)',

View File

@@ -102,7 +102,7 @@ export default function ScrollableRow({
>
<div
ref={containerRef}
className='flex space-x-6 overflow-x-auto scrollbar-hide py-1 sm:py-2 pb-12 sm:pb-14'
className='flex space-x-6 overflow-x-auto scrollbar-hide py-1 sm:py-2 pb-12 sm:pb-14 px-4 sm:px-6'
onScroll={checkScroll}
>
{children}

View File

@@ -54,7 +54,7 @@ export default function VideoCard({
}: VideoCardProps) {
const router = useRouter();
const [favorited, setFavorited] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const isAggregate = from === 'search' && !!items?.length;
@@ -254,130 +254,115 @@ export default function VideoCard({
return (
<div
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'
className='group relative w-full rounded-lg bg-transparent cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.05]'
onClick={handleClick}
>
{/* 海报容器 */}
<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' />
)}
{/* 图片加载动画 - 改进淡入和锐化效果 */}
<div className='relative aspect-[2/3] overflow-hidden rounded-lg'>
{/* 骨架屏 */}
{!isLoading && <ImagePlaceholder aspectRatio='aspect-[2/3]' />}
{/* 图片 */}
<Image
src={processImageUrl(actualPoster)}
alt={actualTitle}
fill
className={`object-cover transition-all duration-700 ease-out ${
isLoaded ? 'opacity-100 blur-0' : 'opacity-0 blur-md'
}`}
onLoadingComplete={() => setIsLoaded(true)}
className='object-cover'
referrerPolicy='no-referrer'
priority={false}
onLoadingComplete={() => setIsLoading(true)}
/>
{/* 悬浮层 - 改进渐变和元素过渡 */}
<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>
{/* 悬浮遮罩 */}
<div className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 transition-opacity duration-300 ease-in-out group-hover:opacity-100' />
{/* 播放按钮 */}
{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'>
<div className='absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-300 ease-in-out delay-75 group-hover:opacity-100 group-hover:scale-100'>
<PlayCircleIcon
size={50}
strokeWidth={0.8}
className='rounded-full text-white fill-transparent transition-[fill,transform] duration-200 ease-out [-webkit-transform:translateZ(0)] [-webkit-backface-visibility:hidden] [backface-visibility:hidden] [will-change:transform,fill,opacity] hover:fill-green-500 hover:scale-110'
className='text-white fill-transparent transition-all duration-300 ease-out hover:fill-green-500 hover:scale-[1.1]'
/>
</div>
)}
{/* 已看 / 收藏按钮 - 改进延迟和缓动效果 */}
{/* 操作按钮 */}
{(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'>
<div className='absolute bottom-3 right-3 flex gap-3 opacity-0 translate-y-2 transition-all duration-300 ease-in-out group-hover:opacity-100 group-hover:translate-y-0'>
{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'
className='text-white transition-all duration-300 ease-out hover:stroke-green-500 hover:scale-[1.1]'
/>
)}
{config.showHeart && (
<Heart
onClick={handleToggleFavorite}
size={20}
className={`rounded-full transition-all duration-300 ease-out hover:scale-110 ${
className={`transition-all duration-300 ease-out ${
favorited
? 'fill-red-600 stroke-red-600 animate-pulse-subtle'
? 'fill-red-600 stroke-red-600'
: 'fill-transparent stroke-white hover:stroke-red-400'
}`}
} hover:scale-[1.1]`}
/>
)}
</div>
)}
{/* 评分徽章 - 添加弹出效果 */}
{/* 徽章 */}
{config.showRating && rate && (
<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'>
<div className='absolute top-2 right-2 bg-pink-500 text-white text-xs font-bold w-7 h-7 rounded-full flex items-center justify-center shadow-md transition-all duration-300 ease-out group-hover:scale-110'>
{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 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 transition-all duration-300 ease-out transform scale-90 group-hover:scale-105 group-hover:shadow-lg'>
{actualEpisodes}
{actualEpisodes && actualEpisodes > 1 && (
<div className='absolute top-2 right-2 bg-green-500 text-white text-xs font-semibold px-2 py-1 rounded-md shadow-md transition-all duration-300 ease-out group-hover:scale-110'>
{currentEpisode
? `${currentEpisode}/${actualEpisodes}`
: 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-4 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300 ease-out delay-100'
className='absolute top-2 left-2 opacity-0 -translate-x-2 transition-all duration-300 ease-in-out delay-100 group-hover:opacity-100 group-hover:translate-x-0'
>
<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'>
<div className='bg-green-500 text-white text-xs font-bold w-7 h-7 rounded-full flex items-center justify-center shadow-md hover:bg-green-600 hover:scale-[1.1] transition-all duration-300 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='mt-1 h-1 w-full bg-gray-200 rounded-full overflow-hidden'>
<div
className='h-full bg-green-500 rounded-full transition-all duration-1000 ease-out'
className='h-full bg-green-500 transition-all duration-500 ease-out'
style={{ width: `${progress}%` }}
/>
</div>
)}
{/* 标题与来源信息 - 改进颜色过渡和延迟 */}
<div className='relative'>
<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 peer'>
{/* 标题与来源 */}
<div className='mt-2 text-center'>
<span className='block text-sm font-semibold truncate text-gray-900 dark:text-gray-100 transition-colors duration-300 ease-in-out group-hover:text-green-600 dark:group-hover:text-green-400'>
{actualTitle}
</span>
{/* 自定义 tooltip */}
<div className='absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1 bg-gray-800 text-white text-xs rounded-md shadow-lg opacity-0 invisible peer-hover:opacity-100 peer-hover:visible transition-all duration-200 ease-out delay-100 whitespace-nowrap z-50 pointer-events-none'>
{actualTitle}
<div className='absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-800'></div>
</div>
</div>
{config.showSourceName && source_name && (
<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}
{config.showSourceName && source_name && (
<span className='block text-xs text-gray-500 dark:text-gray-400 mt-1'>
<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-in-out group-hover:border-green-500/60 group-hover:text-green-600 dark:group-hover:text-green-400'>
{source_name}
</span>
</span>
</span>
)}
)}
</div>
</div>
);
}