| 1 | /** |
| 2 | * 懒加载图片组件 |
| 3 | * 封装 Next.js Image 组件,提供加载状态骨架屏和淡入动画效果 |
| 4 | */ |
| 5 | |
| 6 | 'use client' |
| 7 | |
| 8 | import type { ImageProps } from 'next/image' |
| 9 | import Image from 'next/image' |
| 10 | import { memo, useState } from 'react' |
| 11 | import { OssImage } from '@/components/common/OssImage' |
| 12 | import { Skeleton } from '@/components/ui/skeleton' |
| 13 | import { cn } from '@/utils/className' |
| 14 | |
| 15 | interface LazyImageProps extends Omit<ImageProps, 'onLoad'> { |
| 16 | /** 骨架屏的额外类名 */ |
| 17 | skeletonClassName?: string |
| 18 | /** 容器的额外类名 */ |
| 19 | containerClassName?: string |
| 20 | /** 占位高度,用于图片加载前显示骨架屏 */ |
| 21 | placeholderHeight?: number | string |
| 22 | /** OSS/R2 图片是否使用云端缩略图 */ |
| 23 | useOssThumbnail?: boolean |
| 24 | } |
| 25 | |
| 26 | export const LazyImage = memo(({ |
| 27 | src, |
| 28 | alt, |
| 29 | className, |
| 30 | skeletonClassName, |
| 31 | containerClassName, |
| 32 | placeholderHeight, |
| 33 | useOssThumbnail, |
| 34 | ...props |
| 35 | }: LazyImageProps) => { |
| 36 | const [loaded, setLoaded] = useState(false) |
| 37 | const [error, setError] = useState(false) |
| 38 | |
| 39 | const handleLoad = () => { |
| 40 | setLoaded(true) |
| 41 | } |
| 42 | |
| 43 | const handleError = () => { |
| 44 | setError(true) |
| 45 | setLoaded(true) |
| 46 | } |
| 47 | |
| 48 | return ( |
| 49 | <div |
| 50 | className={cn('relative', containerClassName)} |
| 51 | style={!loaded && placeholderHeight ? { minHeight: placeholderHeight } : undefined} |
| 52 | > |
| 53 | {/* 骨架屏 */} |
| 54 | {!loaded && ( |
| 55 | <Skeleton |
| 56 | className={cn( |
| 57 | 'absolute inset-0 w-full h-full', |
| 58 | skeletonClassName, |
| 59 | )} |
| 60 | /> |
| 61 | )} |
| 62 | |
| 63 | {/* 图片 */} |
| 64 | {useOssThumbnail |
| 65 | ? ( |
| 66 | <OssImage |
| 67 | src={error ? '/images/placeholder.png' : src} |
| 68 | alt={alt} |
| 69 | className={cn( |
| 70 | 'transition-opacity duration-300', |
| 71 | loaded ? 'opacity-100' : 'opacity-0', |
| 72 | className, |
| 73 | )} |
| 74 | onLoad={handleLoad} |
| 75 | onError={handleError} |
| 76 | {...props} |
| 77 | /> |
| 78 | ) |
| 79 | : ( |
| 80 | <Image |
| 81 | src={error ? '/images/placeholder.png' : src} |
| 82 | alt={alt} |
| 83 | className={cn( |
| 84 | 'transition-opacity duration-300', |
| 85 | loaded ? 'opacity-100' : 'opacity-0', |
| 86 | className, |
| 87 | )} |
| 88 | onLoad={handleLoad} |
| 89 | onError={handleError} |
| 90 | {...props} |
| 91 | /> |
| 92 | )} |
| 93 | </div> |
| 94 | ) |
| 95 | }) |
| 96 | |
| 97 | LazyImage.displayName = 'LazyImage' |
| 98 |