|
1 | 1 | 'use client' |
2 | | -import { useState } from 'react' |
| 2 | +import { useState, useMemo } from 'react' |
3 | 3 | import { Media } from '@/components/Media' |
4 | | -import { RenderImageContext, RenderImageProps, RowsPhotoAlbum } from 'react-photo-album' |
5 | | -import 'react-photo-album/rows.css' |
6 | 4 |
|
7 | 5 | import ImageLightbox from './ImageLightbox' |
8 | 6 |
|
| 7 | +import type { Media as MediaType } from '@/payload-types' |
| 8 | + |
9 | 9 | interface ImageGridProps { |
10 | 10 | images: { |
11 | 11 | id: string |
12 | | - image: { |
13 | | - url: string |
14 | | - width: number |
15 | | - height: number |
16 | | - alt?: string |
17 | | - } |
| 12 | + image: MediaType |
18 | 13 | }[] |
19 | 14 | } |
20 | 15 |
|
21 | | -function NextJsImage({ alt = '' }: RenderImageProps, { photo, width, height }: RenderImageContext) { |
22 | | - return ( |
23 | | - <div |
24 | | - style={{ |
25 | | - width: '100%', |
26 | | - position: 'relative', |
27 | | - aspectRatio: `${width} / ${height}`, |
28 | | - }} |
29 | | - > |
30 | | - <Media fill src={photo} alt={alt} pictureClassName="mt-0 mb-0" /> |
31 | | - </div> |
32 | | - ) |
| 16 | +type SlideType = { |
| 17 | + key: string |
| 18 | + src: string |
| 19 | + width?: number |
| 20 | + height?: number |
| 21 | + alt?: string |
| 22 | + type?: 'image' | 'video' | undefined |
| 23 | + autoPlay?: boolean |
| 24 | + sources?: { |
| 25 | + src: string |
| 26 | + type: string |
| 27 | + }[] |
33 | 28 | } |
34 | 29 |
|
| 30 | +const IMAGE_BASE_HEIGHT = 200 |
| 31 | + |
35 | 32 | export default function ImageGrid({ images }: ImageGridProps) { |
36 | 33 | const [index, setIndex] = useState(-1) |
37 | 34 |
|
38 | | - const photos = images |
39 | | - .filter((i) => i.image) |
40 | | - .map((item) => ({ |
| 35 | + const { photos, sliders } = useMemo(() => { |
| 36 | + const validItems = images.filter((i) => i.image && i.image.url) |
| 37 | + |
| 38 | + const processedPhotos = validItems.map((item) => ({ |
41 | 39 | ...item.image, |
42 | 40 | key: item.id, |
43 | | - src: item.image.url, |
44 | | - width: item.image.width, |
45 | | - height: item.image.height, |
| 41 | + aspectRatio: (item.image.width ?? 100) / (item.image.height ?? 100), |
46 | 42 | })) |
47 | 43 |
|
| 44 | + const processedSliders: SlideType[] = processedPhotos.map((photo) => { |
| 45 | + const slide: SlideType = { |
| 46 | + key: photo.key, |
| 47 | + src: photo.url || '', |
| 48 | + width: photo.width ?? undefined, |
| 49 | + height: photo.height ?? undefined, |
| 50 | + alt: photo.alt || '', |
| 51 | + type: 'image', |
| 52 | + } |
| 53 | + if (photo.mimeType?.startsWith('video/')) { |
| 54 | + return { |
| 55 | + ...slide, |
| 56 | + type: 'video', |
| 57 | + autoPlay: true, |
| 58 | + sources: [ |
| 59 | + { |
| 60 | + src: photo.url || '', |
| 61 | + type: photo.mimeType, |
| 62 | + }, |
| 63 | + ], |
| 64 | + } |
| 65 | + } |
| 66 | + return slide |
| 67 | + }) |
| 68 | + |
| 69 | + return { photos: processedPhotos, sliders: processedSliders } |
| 70 | + }, [images]) |
| 71 | + |
48 | 72 | return ( |
49 | 73 | <> |
50 | | - <RowsPhotoAlbum |
51 | | - photos={photos} |
52 | | - render={{ |
53 | | - image: NextJsImage, |
54 | | - }} |
55 | | - targetRowHeight={200} |
56 | | - spacing={(width) => (width < 300 ? 5 : 10)} |
57 | | - onClick={({ index: current }) => setIndex(current)} |
58 | | - /> |
59 | | - <ImageLightbox index={index} open={index >= 0} close={() => setIndex(-1)} slides={photos} /> |
| 74 | + <div className="flex flex-wrap gap-2"> |
| 75 | + {photos.map((photo, photoIndex) => { |
| 76 | + const aspectRatio = photo.aspectRatio |
| 77 | + return ( |
| 78 | + <div |
| 79 | + key={photo.key} |
| 80 | + className="relative overflow-hidden rounded-lg group cursor-pointer h-[160px] md:h-[200px]" |
| 81 | + style={{ |
| 82 | + aspectRatio, |
| 83 | + flexGrow: aspectRatio, |
| 84 | + flexShrink: 0, |
| 85 | + flexBasis: `${IMAGE_BASE_HEIGHT * photo.aspectRatio}px`, |
| 86 | + // 限制最大宽度,防止单张图片过宽 |
| 87 | + maxWidth: `${IMAGE_BASE_HEIGHT * aspectRatio * 1.2}px`, |
| 88 | + }} |
| 89 | + onClick={() => { |
| 90 | + setIndex(photoIndex) |
| 91 | + }} |
| 92 | + > |
| 93 | + <Media |
| 94 | + resource={photo} |
| 95 | + alt={photo.alt || ''} |
| 96 | + pictureClassName="mt-0 mb-0" |
| 97 | + className="absolute inset-0 w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 flex justify-center items-center" |
| 98 | + /> |
| 99 | + </div> |
| 100 | + ) |
| 101 | + })} |
| 102 | + </div> |
| 103 | + <ImageLightbox index={index} open={index >= 0} close={() => setIndex(-1)} slides={sliders} /> |
60 | 104 | </> |
61 | 105 | ) |
62 | 106 | } |
0 commit comments