返回 AiToEarn
index.tsx
1 /**
2 * TaskPreview - 任务预览组件
3 * 功能:显示最近的任务卡片列表、AI 生成素材,支持 Tab 切换和"浏览全部"跳转
4 */
5
6 'use client'
7
8 import type { TaskListItem } from '@/api/ai/ai.types'
9 import type { MediaItem } from '@/api/materials/material.types'
10 import type { MediaPreviewItem } from '@/components/common/MediaPreview'
11 import { ArrowRight, Bot, History } from 'lucide-react'
12 import Link from 'next/link'
13 import { useParams } from 'next/navigation'
14 import { startTransition, useCallback, useEffect, useState } from 'react'
15 import { useShallow } from 'zustand/react/shallow'
16 import { agentApi, getAgentAssets } from '@/api/ai/ai.api'
17
18 import { useTransClient } from '@/app/i18n/client'
19 import TaskHistoryList from '@/components/Chat/TaskHistoryList'
20 import { MediaPreview } from '@/components/common/MediaPreview'
21 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
22 import { useUserStore } from '@/store'
23 import { convertAssetToMediaItem } from '@/utils/agent/asset'
24 import { getOssUrl } from '@/utils/oss'
25 import { MediaPreviewList } from './components/MediaPreviewList'
26
27 export interface ITaskPreviewProps {
28 /** 显示数量 */
29 limit?: number
30 /** 自定义类名 */
31 className?: string
32 }
33
34 /** Tab 类型 */
35 type TabValue = 'tasks' | 'agent'
36
37 /**
38 * TaskPreview - 任务预览组件
39 */
40 export function TaskPreview({ limit = 4, className }: ITaskPreviewProps) {
41 const { t } = useTransClient('chat')
42 const { lng } = useParams()
43
44 const { _hasHydrated, token } = useUserStore(
45 useShallow(state => ({
46 _hasHydrated: state._hasHydrated,
47 token: state.token,
48 })),
49 )
50
51 // 当前选中的 Tab
52 const [activeTab, setActiveTab] = useState<TabValue>('tasks')
53
54 // 任务状态
55 const [taskState, setTaskState] = useState<{
56 tasks: TaskListItem[]
57 isLoading: boolean
58 }>({
59 tasks: [],
60 isLoading: true,
61 })
62
63 // AI 生成素材状态
64 const [agentState, setAgentState] = useState<{
65 mediaList: MediaItem[]
66 isLoading: boolean
67 }>({
68 mediaList: [],
69 isLoading: true,
70 })
71
72 // 媒体预览弹窗状态
73 const [previewOpen, setPreviewOpen] = useState(false)
74 const [previewIndex, setPreviewIndex] = useState(0)
75 const [previewItems, setPreviewItems] = useState<MediaPreviewItem[]>([])
76
77 /** 加载任务列表 */
78 const loadTasks = useCallback(async () => {
79 try {
80 const result = await agentApi.getTaskList({ page: 1, pageSize: limit })
81 if (result && result.code === 0 && result.data) {
82 startTransition(() => {
83 setTaskState({
84 tasks: result.data.list || [],
85 isLoading: false,
86 })
87 })
88 }
89 else {
90 startTransition(() => {
91 setTaskState(prev => ({ ...prev, isLoading: false }))
92 })
93 }
94 }
95 catch (error) {
96 console.error('Load task list failed:', error)
97 startTransition(() => {
98 setTaskState(prev => ({ ...prev, isLoading: false }))
99 })
100 }
101 }, [limit])
102
103 /** 加载 AI 生成素材 */
104 const loadAgentAssets = useCallback(async () => {
105 try {
106 const result = await getAgentAssets({ page: 1, pageSize: limit })
107 if (result && result.data) {
108 // 转换为 MediaItem 格式
109 const convertedList = (result.data.list || []).map(convertAssetToMediaItem)
110 startTransition(() => {
111 setAgentState({
112 mediaList: convertedList,
113 isLoading: false,
114 })
115 })
116 }
117 else {
118 startTransition(() => {
119 setAgentState(prev => ({ ...prev, isLoading: false }))
120 })
121 }
122 }
123 catch (error) {
124 console.error('Load agent assets failed:', error)
125 startTransition(() => {
126 setAgentState(prev => ({ ...prev, isLoading: false }))
127 })
128 }
129 }, [limit])
130
131 /** 处理 AI 生成素材点击 - 打开预览弹窗 */
132 const handleAgentAssetClick = useCallback(
133 (index: number) => {
134 const items: MediaPreviewItem[] = agentState.mediaList.map((media) => {
135 // AI 生成素材可能是视频或图片
136 const isVideo = media.type === 'video'
137 return {
138 type: isVideo ? ('video' as const) : ('image' as const),
139 src: getOssUrl(media.url),
140 title: media.title,
141 }
142 })
143 setPreviewItems(items)
144 setPreviewIndex(index)
145 setPreviewOpen(true)
146 },
147 [agentState.mediaList],
148 )
149
150 // 初始化加载
151 useEffect(() => {
152 if (!_hasHydrated)
153 return
154
155 if (token) {
156 // 加载任务列表
157 loadTasks()
158 // 加载 AI 生成素材
159 loadAgentAssets()
160 }
161 else {
162 // 未登录时停止 loading
163 startTransition(() => {
164 setTaskState(prev => ({ ...prev, isLoading: false }))
165 setAgentState(prev => ({ ...prev, isLoading: false }))
166 })
167 }
168 }, [_hasHydrated, token, loadTasks, loadAgentAssets])
169
170 /** 获取浏览全部链接 */
171 const getViewAllLink = (tab: TabValue): string => {
172 switch (tab) {
173 case 'tasks':
174 return '/tasks-history'
175 case 'agent':
176 return '/agent-assets'
177 default:
178 return '/tasks-history'
179 }
180 }
181
182 // 判断是否有数据(任一 Tab 有数据即显示)
183 const hasAnyData
184 = !taskState.isLoading
185 && !agentState.isLoading
186 && (taskState.tasks.length > 0
187 || agentState.mediaList.length > 0)
188
189 // 判断是否全部加载完成且无数据
190 const allLoadedNoData
191 = !taskState.isLoading
192 && !agentState.isLoading
193 && taskState.tasks.length === 0
194 && agentState.mediaList.length === 0
195
196 // 如果全部加载完成且无数据,不显示该区域
197 if (allLoadedNoData) {
198 return null
199 }
200
201 return (
202 <section className={className}>
203 <div className="w-full max-w-5xl mx-auto">
204 <Tabs
205 value={activeTab}
206 onValueChange={value => setActiveTab(value as TabValue)}
207 className="w-full"
208 >
209 {/* Tab 标题栏 */}
210 <div className="flex items-center justify-between mb-6">
211 <TabsList className="h-11 p-1 bg-muted/50 rounded-lg">
212 <TabsTrigger
213 value="tasks"
214 className="gap-2 px-4 h-9 text-sm font-medium rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm transition-all"
215 >
216 <History className="w-4 h-4" />
217 <span className="hidden sm:inline">{t('home.recentTasks')}</span>
218 </TabsTrigger>
219 <TabsTrigger
220 value="agent"
221 className="gap-2 px-4 h-9 text-sm font-medium rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm transition-all"
222 >
223 <Bot className="w-4 h-4" />
224 <span className="hidden sm:inline">{t('home.agentAssets')}</span>
225 </TabsTrigger>
226 </TabsList>
227
228 {/* 浏览全部按钮 */}
229 <Link
230 href={getViewAllLink(activeTab)}
231 className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-primary transition-colors cursor-pointer group"
232 >
233 {t('home.viewAll')}
234 <ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
235 </Link>
236 </div>
237
238 {/* Tab 内容区域 - 固定最小高度避免抖动 */}
239 <div className="min-h-[200px]">
240 {/* 最近任务 */}
241 <TabsContent value="tasks" className="mt-0">
242 <TaskHistoryList
243 tasks={taskState.tasks}
244 isLoading={taskState.isLoading}
245 skeletonCount={limit}
246 onRefresh={() => loadTasks()}
247 className="grid-cols-2 md:grid-cols-4"
248 />
249 </TabsContent>
250
251 {/* AI 生成素材 */}
252 <TabsContent value="agent" className="mt-0">
253 <MediaPreviewList
254 mediaList={agentState.mediaList}
255 isLoading={agentState.isLoading}
256 skeletonCount={limit}
257 onItemClick={handleAgentAssetClick}
258 />
259 </TabsContent>
260 </div>
261 </Tabs>
262 </div>
263
264 {/* 媒体预览弹窗 */}
265 <MediaPreview
266 open={previewOpen}
267 items={previewItems}
268 initialIndex={previewIndex}
269 onClose={() => setPreviewOpen(false)}
270 />
271 </section>
272 )
273 }
274
275 export default TaskPreview
276
276 lines Plain Text