| 1 | /** |
| 2 | * PromptGallery 组件 |
| 3 | * 功能:展示视频提示词画廊,支持点击查看详情和应用提示词 |
| 4 | */ |
| 5 | 'use client' |
| 6 | |
| 7 | import type { PromptGalleryItem } from './types' |
| 8 | import { memo, useCallback, useMemo, useState } from 'react' |
| 9 | import { useTransClient } from '@/app/i18n/client' |
| 10 | import { cn } from '@/utils/className' |
| 11 | import { VideoCard, VideoDetailModal } from './components' |
| 12 | import { promptGalleryAssets } from './data' |
| 13 | |
| 14 | /** 应用提示词回调参数 */ |
| 15 | export interface ApplyPromptData { |
| 16 | /** 提示词内容 */ |
| 17 | prompt: string |
| 18 | /** 参考素材图片列表(可选) */ |
| 19 | materials?: string[] |
| 20 | /** 模式类型 */ |
| 21 | mode: 'edit' | 'generate' |
| 22 | } |
| 23 | |
| 24 | /** PromptGallery 组件属性 */ |
| 25 | export interface PromptGalleryProps { |
| 26 | /** 应用提示词回调(点击视频卡片时触发) */ |
| 27 | onApplyPrompt?: (data: ApplyPromptData) => void |
| 28 | /** 自定义类名 */ |
| 29 | className?: string |
| 30 | } |
| 31 | |
| 32 | const PromptGallery = memo(({ onApplyPrompt, className }: PromptGalleryProps) => { |
| 33 | const { t } = useTransClient('promptGallery') |
| 34 | const [selectedItem, setSelectedItem] = useState<PromptGalleryItem | null>(null) |
| 35 | const [modalOpen, setModalOpen] = useState(false) |
| 36 | |
| 37 | // 组合数据 - title 和 prompt 直接使用 data.ts 中的英文,不走翻译 |
| 38 | const promptGalleryItems = useMemo<PromptGalleryItem[]>( |
| 39 | () => |
| 40 | promptGalleryAssets.map(asset => ({ |
| 41 | ...asset, |
| 42 | title: asset.title, |
| 43 | prompt: asset.prompt, |
| 44 | })), |
| 45 | [], |
| 46 | ) |
| 47 | |
| 48 | // 点击卡片只打开详情弹窗,不应用提示词 |
| 49 | const handleCardClick = useCallback((item: PromptGalleryItem) => { |
| 50 | setSelectedItem(item) |
| 51 | setModalOpen(true) |
| 52 | }, []) |
| 53 | |
| 54 | // 关闭弹窗 |
| 55 | const handleModalClose = useCallback((open: boolean) => { |
| 56 | setModalOpen(open) |
| 57 | if (!open) { |
| 58 | // 延迟清除选中项,避免弹窗关闭动画中内容消失 |
| 59 | setTimeout(() => setSelectedItem(null), 300) |
| 60 | } |
| 61 | }, []) |
| 62 | |
| 63 | // 在弹窗中点击应用提示词按钮 |
| 64 | const handleApplyPrompt = useCallback( |
| 65 | (data: { prompt: string, materials?: string[] }) => { |
| 66 | if (onApplyPrompt) { |
| 67 | onApplyPrompt({ |
| 68 | prompt: data.prompt, |
| 69 | materials: data.materials, |
| 70 | mode: 'generate', |
| 71 | }) |
| 72 | } |
| 73 | }, |
| 74 | [onApplyPrompt], |
| 75 | ) |
| 76 | |
| 77 | return ( |
| 78 | <section className="mb-10 px-4 md:px-6 lg:px-8"> |
| 79 | <div className={cn('w-full max-w-5xl mx-auto', className)}> |
| 80 | {/* 标题区域 */} |
| 81 | <div className="mb-6 text-center sm:mb-8"> |
| 82 | <h2 className="mb-2 text-2xl font-bold text-foreground sm:text-3xl">{t('title')}</h2> |
| 83 | <p className="text-sm text-muted-foreground sm:text-base">{t('subtitle')}</p> |
| 84 | </div> |
| 85 | |
| 86 | {/* 视频卡片网格 - 3列竖屏布局 */} |
| 87 | {/* |
| 88 | PC 端 (>= 1024px) 3 列布局: |
| 89 | ┌─────────┬─────────┬─────────┐ |
| 90 | │ │ │ │ |
| 91 | │ 竖视频1 │ 竖视频2 │ 竖视频3 │ |
| 92 | │ │ │ │ |
| 93 | └─────────┴─────────┴─────────┘ |
| 94 | |
| 95 | 平板 (640px - 1024px) 2 列布局: |
| 96 | ┌─────────┬─────────┐ |
| 97 | │ 竖视频1 │ 竖视频2 │ |
| 98 | ├─────────┼─────────┤ |
| 99 | │ 竖视频3 │ │ |
| 100 | └─────────┴─────────┘ |
| 101 | |
| 102 | 移动端 (< 640px) 1 列布局 |
| 103 | */} |
| 104 | <div |
| 105 | className={cn( |
| 106 | 'grid gap-2 sm:gap-4', |
| 107 | // 移动端 1 列 |
| 108 | 'grid-cols-1', |
| 109 | // 平板 2 列 |
| 110 | 'sm:grid-cols-2', |
| 111 | // PC 端 3 列 |
| 112 | 'lg:grid-cols-3', |
| 113 | )} |
| 114 | > |
| 115 | {promptGalleryItems.slice(0, 3).map((item, index) => ( |
| 116 | <div key={index} className="aspect-[9/12]"> |
| 117 | <VideoCard |
| 118 | item={item} |
| 119 | onClick={() => handleCardClick(item)} |
| 120 | size="vertical" |
| 121 | /> |
| 122 | </div> |
| 123 | ))} |
| 124 | </div> |
| 125 | |
| 126 | {/* 视频详情弹窗 */} |
| 127 | <VideoDetailModal |
| 128 | open={modalOpen} |
| 129 | onOpenChange={handleModalClose} |
| 130 | item={selectedItem} |
| 131 | onApplyPrompt={handleApplyPrompt} |
| 132 | /> |
| 133 | </div> |
| 134 | </section> |
| 135 | ) |
| 136 | }) |
| 137 | |
| 138 | export default PromptGallery |
| 139 |