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