From b30589628a19ccc5185f7e0a60f38ab1d3ab8446 Mon Sep 17 00:00:00 2001 From: SongPro Date: Thu, 3 Jul 2025 22:05:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(components):=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=AA=A8=E6=9E=B6=E5=B1=8F=E7=BB=84=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8D=A1=E7=89=87=E5=8A=A0=E8=BD=BD=E4=BD=93?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现图片加载时的骨架屏效果,支持暗色模式 在AggregateCard、VideoCard和DemoCard中应用骨架屏 优化图片加载动画和状态管理 --- src/components/AggregateCard.tsx | 15 ++++++++++- src/components/DemoCard.tsx | 38 +++++++++++++++++++-------- src/components/ImagePlaceholder.tsx | 40 +++++++++++++++++++++++++++++ src/components/VideoCard.tsx | 19 ++++++++++---- 4 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/components/ImagePlaceholder.tsx diff --git a/src/components/AggregateCard.tsx b/src/components/AggregateCard.tsx index 8865e70..760b944 100644 --- a/src/components/AggregateCard.tsx +++ b/src/components/AggregateCard.tsx @@ -4,6 +4,8 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; import React, { useMemo, useState } from 'react'; +import { ImagePlaceholder } from '@/components/ImagePlaceholder'; + // 聚合卡需要的基本字段,与搜索接口保持一致 interface SearchResult { id: string; @@ -64,6 +66,7 @@ const AggregateCard: React.FC = ({ // 使用列表中的第一个结果做展示 & 播放 const first = items[0]; const [playHover, setPlayHover] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); const router = useRouter(); // 统计 items 中出现次数最多的(非 0) douban_id,用于跳转豆瓣页面 @@ -124,11 +127,21 @@ const AggregateCard: React.FC = ({
{/* 封面图片 2:3 */}
+ {/* 图片占位符 - 骨架屏效果 */} + + {first.title} setIsLoaded(true)} referrerPolicy='no-referrer' priority={false} /> diff --git a/src/components/DemoCard.tsx b/src/components/DemoCard.tsx index 2915d59..1995086 100644 --- a/src/components/DemoCard.tsx +++ b/src/components/DemoCard.tsx @@ -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(null); const handleClick = () => { router.push(`/aggregate?q=${encodeURIComponent(title)}&type=${type}`); @@ -59,18 +63,30 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => { > {/* 海报图片区域 */}
+ {/* 图片占位符 - 骨架屏效果 */} + + + {/* 图片组件 */} {title} setIsLoaded(true)} referrerPolicy='no-referrer' priority={false} /> - {/* 评分徽章 */} + {/* 评分徽章 - 暗色模式优化 */} {rate && ( -
+
{rate} @@ -78,7 +94,7 @@ const DemoCard = ({ id, title, poster, rate, type }: DemoCardProps) => { )} {/* 悬浮层 - 搜索按钮 */} -
+
setHover(true)} onMouseLeave={() => setHover(false)} @@ -90,7 +106,7 @@ 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)' > - - {/* 信息层 */} - - {title} + {/* 信息层 - 暗色模式优化 */} + + + {title} +
); diff --git a/src/components/ImagePlaceholder.tsx b/src/components/ImagePlaceholder.tsx new file mode 100644 index 0000000..fc4c402 --- /dev/null +++ b/src/components/ImagePlaceholder.tsx @@ -0,0 +1,40 @@ +// 图片占位符组件 - 实现骨架屏效果(支持暗色模式) +const ImagePlaceholder = ({ aspectRatio }: { aspectRatio: string }) => ( +
+ +
+); + +export { ImagePlaceholder }; diff --git a/src/components/VideoCard.tsx b/src/components/VideoCard.tsx index 0e75bdd..d8a0e33 100644 --- a/src/components/VideoCard.tsx +++ b/src/components/VideoCard.tsx @@ -6,6 +6,8 @@ import React, { useEffect, useState } from 'react'; import { deletePlayRecord, isFavorited, toggleFavorite } from '@/lib/db.client'; +import { ImagePlaceholder } from '@/components/ImagePlaceholder'; + interface VideoCardProps { id: string; source: string; @@ -89,6 +91,7 @@ export default function VideoCard({ }: VideoCardProps) { const [playHover, setPlayHover] = useState(false); const [favorited, setFavorited] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const router = useRouter(); @@ -161,15 +164,24 @@ export default function VideoCard({ > {/* 海报图片容器 */}
+ {/* 图片占位符 - 骨架屏效果 */} + + {title} setIsLoaded(true)} referrerPolicy='no-referrer' priority={false} /> - {/* Hover 效果层 */}
- {/* 继续观看 - 集数矩形展示框 */} {episodes && episodes > 1 && currentEpisode && (
@@ -243,7 +254,6 @@ export default function VideoCard({
)} - {/* 搜索非聚合 - 集数圆形展示框 */} {from === 'search' && (
@@ -252,7 +262,6 @@ export default function VideoCard({
)} - {/* 豆瓣链接按钮 */} {douban_id && from === 'search' && (