| 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 |