返回 AiToEarn
MediaGallery.tsx
根目录 / project / aitoearn-web / src / components / Chat / ChatMessage / MediaGallery.tsx
1 'use client'
2
3 import type { IUploadedMedia } from '../MediaUpload'
4 import { FileText, Music, Play } from 'lucide-react'
5 import React from 'react'
6 import { cn } from '@/utils/className'
7 import { getOssUrl } from '@/utils/oss'
8
9 /** 视频文件扩展名列表 */
10 const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v', '.wmv', '.flv']
11
12 /** 通过 URL 扩展名判断是否为视频 */
13 function isVideoUrl(url: string): boolean {
14 if (!url)
15 return false
16 const lowerUrl = url.toLowerCase().split('?')[0] // 移除查询参数
17 return VIDEO_EXTENSIONS.some(ext => lowerUrl.endsWith(ext))
18 }
19
20 interface MediaGalleryProps {
21 medias: IUploadedMedia[]
22 onPreviewByIndex?: (index: number) => void
23 onPreviewByUrl?: (url: string) => void
24 /** 媒体项尺寸:default (112x80) 用于 AI 消息,large (160x112) 用于用户消息 */
25 size?: 'default' | 'large'
26 }
27
28 /** 尺寸样式映射 */
29 const SIZE_CLASSES = {
30 default: 'w-28 h-20', // 112px x 80px
31 large: 'w-56 h-40', // 224px x 160px
32 } as const
33
34 export function MediaGallery({
35 medias,
36 onPreviewByIndex,
37 onPreviewByUrl,
38 size = 'default',
39 }: MediaGalleryProps) {
40 if (!medias || medias.length === 0)
41 return null
42
43 return (
44 <div className="flex flex-wrap gap-2">
45 {medias.map((media, idx) => {
46 if (media.type === 'document' || media.type === 'audio') {
47 return (
48 <a
49 key={idx}
50 href={getOssUrl(media.url)}
51 target="_blank"
52 rel="noopener noreferrer"
53 className="flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-muted hover:bg-muted/80 transition-colors"
54 >
55 {media.type === 'audio'
56 ? <Music className="w-4 h-4 text-muted-foreground" />
57 : <FileText className="w-4 h-4 text-muted-foreground" />}
58 <span className="text-sm text-foreground truncate max-w-[200px]">
59 {media.name || (media.type === 'audio' ? 'Audio' : 'Document')}
60 </span>
61 </a>
62 )
63 }
64
65 // 通过 URL 扩展名判断是否为视频(比 media.type 更准确)
66 const isVideo = isVideoUrl(media.url)
67 const url = getOssUrl(media.url)
68
69 return (
70 <button
71 key={idx}
72 type="button"
73 onClick={() => {
74 if (onPreviewByIndex) {
75 onPreviewByIndex(idx)
76 }
77 else if (onPreviewByUrl) {
78 onPreviewByUrl(media.url)
79 }
80 }}
81 className={cn(
82 SIZE_CLASSES[size],
83 'rounded-lg overflow-hidden border border-border bg-muted relative',
84 'flex items-center justify-center p-0 cursor-pointer',
85 )}
86 >
87 {isVideo ? (
88 <>
89 <video src={url} className="w-full h-full object-cover" preload="metadata" muted />
90 <span className="absolute inset-0 flex items-center justify-center pointer-events-none">
91 <Play className="w-6 h-6 text-white/90" />
92 </span>
93 </>
94 ) : (
95 <img
96 src={url}
97 alt={media.name || `media-${idx}`}
98 className="w-full h-full object-cover"
99 />
100 )}
101 </button>
102 )
103 })}
104 </div>
105 )
106 }
107
108 export default MediaGallery
109
109 lines Plain Text