返回 AiToEarn
1 /**
2 * AgentAssetCard - AI 生成素材卡片组件
3 * 展示单个 AI 生成素材
4 * 特点:
5 * - 支持图片和视频类型
6 * - 视频显示播放图标和时长
7 * - 视频无封面时显示占位图
8 * - 只读模式(无删除按钮)
9 */
10
11 'use client'
12
13 import type { AssetVo } from '@/types/agent-asset'
14 import { Play } from 'lucide-react'
15 import { memo, useCallback } from 'react'
16 import { OssImage } from '@/components/common/OssImage'
17 import { useVideoThumbnail } from '@/hooks/useVideoThumbnail'
18 import { getAssetMediaType, getAssetThumbUrl } from '@/utils/agent/asset'
19 import { cn } from '@/utils/className'
20 import { formatSeconds } from '@/utils/format'
21 import { VideoPlaceholder } from './VideoPlaceholder'
22
23 interface AgentAssetCardProps {
24 /** 素材数据 */
25 asset: AssetVo
26 /** 预览按钮无文件名时的可访问性文案 */
27 previewLabel: string
28 /** 点击回调 */
29 onClick?: (asset: AssetVo) => void
30 }
31
32 export const AgentAssetCard = memo(({ asset, previewLabel, onClick }: AgentAssetCardProps) => {
33 const mediaType = getAssetMediaType(asset)
34 const rawThumbUrl = getAssetThumbUrl(asset)
35 const isVideo = mediaType === 'video'
36 const autoThumbUrl = useVideoThumbnail(isVideo && !rawThumbUrl ? asset.url : null)
37 const thumbUrl = rawThumbUrl || autoThumbUrl
38 const hasThumb = Boolean(thumbUrl)
39 const duration = asset.metadata?.duration
40
41 // 处理点击
42 const handleClick = useCallback(() => {
43 onClick?.(asset)
44 }, [asset, onClick])
45
46 return (
47 <button
48 type="button"
49 aria-label={asset.filename || previewLabel}
50 className={cn(
51 'group relative block w-full rounded-lg overflow-hidden bg-muted cursor-pointer min-h-[120px] text-left',
52 'transition-all duration-200',
53 'hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
54 )}
55 onClick={handleClick}
56 >
57 {hasThumb ? (
58 // 有封面:图片自然撑开高度,实现瀑布流错落效果
59 <div className="relative">
60 <OssImage
61 src={thumbUrl}
62 alt={asset.filename || ''}
63 width={400}
64 height={300}
65 className="w-full h-auto block"
66 sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 33vw, (max-width: 1280px) 25vw, 20vw"
67 />
68
69 {/* 视频播放图标 */}
70 {isVideo && (
71 <div className="absolute inset-0 flex items-center justify-center">
72 <div className="w-10 h-10 rounded-full bg-foreground/60 flex items-center justify-center group-hover:bg-foreground/80 transition-colors">
73 <Play className="w-5 h-5 text-background fill-current ml-0.5" />
74 </div>
75 </div>
76 )}
77
78 {/* 视频时长标签 */}
79 {isVideo && duration && (
80 <div className="absolute bottom-2 right-2 px-1.5 py-0.5 bg-foreground/75 rounded text-xs text-background">
81 {formatSeconds(Math.floor(duration))}
82 </div>
83 )}
84
85 {/* 悬浮遮罩 */}
86 <div className="absolute inset-0 bg-foreground/0 group-hover:bg-foreground/10 transition-colors" />
87 </div>
88 ) : (
89 // 无封面:固定 16:9 占位
90 <div className="relative w-full" style={{ paddingBottom: '56.25%' }}>
91 <VideoPlaceholder />
92
93 {/* 视频播放图标 */}
94 {isVideo && (
95 <div className="absolute inset-0 flex items-center justify-center">
96 <div className="w-10 h-10 rounded-full bg-foreground/60 flex items-center justify-center group-hover:bg-foreground/80 transition-colors">
97 <Play className="w-5 h-5 text-background fill-current ml-0.5" />
98 </div>
99 </div>
100 )}
101
102 {/* 悬浮遮罩 */}
103 <div className="absolute inset-0 bg-foreground/0 group-hover:bg-foreground/10 transition-colors" />
104 </div>
105 )}
106 </button>
107 )
108 })
109
110 AgentAssetCard.displayName = 'AgentAssetCard'
111
111 lines Plain Text