返回 AiToEarn
index.tsx
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
139 lines Plain Text