feat(components): 添加图片骨架屏组件并优化卡片加载体验

实现图片加载时的骨架屏效果,支持暗色模式
在AggregateCard、VideoCard和DemoCard中应用骨架屏
优化图片加载动画和状态管理
This commit is contained in:
SongPro
2025-07-03 22:05:54 +08:00
parent 92323fcb69
commit b30589628a
4 changed files with 96 additions and 16 deletions

View File

@@ -1,7 +1,9 @@
import { Link as LinkIcon, Search } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { ImagePlaceholder } from '@/components/ImagePlaceholder';
interface DemoCardProps {
id: string;
@@ -46,7 +48,9 @@ function SearchCircle({
const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
const [hover, setHover] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const router = useRouter();
const imgRef = useRef<HTMLImageElement>(null);
const handleClick = () => {
router.push(`/aggregate?q=${encodeURIComponent(title)}&type=${type}`);
@@ -59,18 +63,30 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
>
{/* 海报图片区域 */}
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-md group-hover:scale-[1.02] transition-all duration-400 cubic-bezier(0.4, 0, 0.2, 1) safari-fix'>
{/* 图片占位符 - 骨架屏效果 */}
<ImagePlaceholder aspectRatio='aspect-[2/3]' />
{/* 图片组件 */}
<Image
src={poster}
alt={title}
fill
className='object-cover transition-transform duration-500 cubic-bezier(0.4, 0, 0.2, 1) group-hover:scale-110'
ref={imgRef}
loading='lazy'
className={`object-cover transition-transform duration-500 cubic-bezier(0.4,0,0.2,1) group-hover:scale-110
${
isLoaded
? 'opacity-100 scale-100'
: 'opacity-0 scale-95'
}`}
onLoadingComplete={() => setIsLoaded(true)}
referrerPolicy='no-referrer'
priority={false}
/>
{/* 评分徽章 */}
{/* 评分徽章 - 暗色模式优化 */}
{rate && (
<div className='absolute top-2 right-2 min-w-[1.25rem] h-4 w-4 sm:h-7 sm:w-7 sm:min-w-[1.5rem] bg-pink-500 rounded-full flex items-center justify-center px-1 shadow-md transform transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) group-hover:scale-110 group-hover:rotate-3'>
<div className='absolute top-2 right-2 min-w-[1.25rem] h-4 w-4 sm:h-7 sm:w-7 sm:min-w-[1.5rem] bg-pink-500 dark:bg-pink-400 rounded-full flex items-center justify-center px-1 shadow-md transform transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) group-hover:scale-110 group-hover:rotate-3'>
<span className='text-white text-[0.5rem] sm:text-xs font-bold leading-none'>
{rate}
</span>
@@ -78,7 +94,7 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
)}
{/* 悬浮层 - 搜索按钮 */}
<div className='absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 cubic-bezier(0.4, 0, 0.2, 1) flex items-center justify-center'>
<div className='absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500 cubic-bezier(0.4, 0, 0.2, 1) flex items-center justify-center'>
<div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
@@ -90,7 +106,7 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
</div>
</div>
{/* 外部链接按钮 */}
{/* 外部链接按钮 - 暗色模式优化 */}
<a
href={`https://movie.douban.com/subject/${id}`}
target='_blank'
@@ -98,15 +114,17 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => {
onClick={(e) => 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)'
>
<div className='w-4 h-4 sm:w-7 sm:h-7 rounded-full bg-[#22c55e] flex items-center justify-center shadow-md opacity-70 hover:opacity-100 transition-all duration-200 ease-in-out hover:scale-110 hover:bg-[#16a34a]'>
<div className='w-4 h-4 sm:w-7 sm:h-7 rounded-full bg-[#22c55e] dark:bg-[#16a34a] flex items-center justify-center shadow-md opacity-70 hover:opacity-100 transition-all duration-200 ease-in-out hover:scale-110 hover:bg-[#16a34a] dark:hover:bg-[#15803d]'>
<LinkIcon className='w-4 h-4 text-white' strokeWidth={2} />
</div>
</a>
</div>
{/* 信息层 */}
<span className='mt-2 px-1 block text-gray-900 font-semibold truncate w-full text-center text-xs sm:text-sm dark:text-gray-200 transition-all duration-400 cubic-bezier(0.4, 0, 0.2, 1) group-hover:translate-y-[-2px] translate-y-1 opacity-80 group-hover:opacity-100 group-hover:text-green-600 dark:group-hover:text-green-400'>
{title}
{/* 信息层 - 暗色模式优化 */}
<span className='mt-2 px-1 block font-semibold truncate w-full text-center text-xs sm:text-sm transition-all duration-400 cubic-bezier(0.4, 0, 0.2, 1) group-hover:translate-y-[-2px] translate-y-1 opacity-80 group-hover:opacity-100'>
<span className='text-gray-900 dark:text-gray-200 group-hover:text-green-600 dark:group-hover:text-green-400'>
{title}
</span>
</span>
</div>
);