返回 AiToEarn
TasksHistoryPageContent.tsx
根目录 / project / aitoearn-web / src / app / [lng] / tasks-history / TasksHistoryPageContent.tsx
1 /**
2 * 任务记录页内容组件 - Tasks History Content
3 * 客户端组件,包含所有交互逻辑
4 * 支持搜索任务标题和收藏筛选功能
5 */
6 'use client'
7
8 import type { TaskListItem } from '@/api/ai/ai.types'
9 import { ArrowLeft, FileText, FileVideo, History, RefreshCw, Search, Star, X } from 'lucide-react'
10 import { useParams, useRouter } from 'next/navigation'
11 import { useCallback, useEffect, useRef, useState } from 'react'
12 import { agentApi } from '@/api/ai/ai.api'
13 import { useTransClient } from '@/app/i18n/client'
14 import { TaskCardSkeleton } from '@/components/Chat'
15 import TaskHistoryList from '@/components/Chat/TaskHistoryList'
16 import { Button } from '@/components/ui/button'
17 import { Input } from '@/components/ui/input'
18 import { Pagination } from '@/components/ui/pagination'
19 import { cn } from '@/utils/className'
20 import { toast } from '@/utils/ui/toast'
21 import UserLogsModal from './components/UserLogsModal'
22 import VideoHistoryModal from './components/VideoHistoryModal'
23
24 export function TasksHistoryPageContent() {
25 const { t } = useTransClient('chat')
26 const router = useRouter()
27 const { lng } = useParams()
28
29 // 状态
30 const [tasks, setTasks] = useState<TaskListItem[]>([])
31 const [isLoading, setIsLoading] = useState(true)
32 const [page, setPage] = useState(1)
33 const [total, setTotal] = useState(0)
34 const [logsModalOpen, setLogsModalOpen] = useState(false)
35 const [videoHistoryModalOpen, setVideoHistoryModalOpen] = useState(false)
36
37 // 搜索和筛选状态
38 const [searchKeyword, setSearchKeyword] = useState('')
39 const [favoriteOnly, setFavoriteOnly] = useState(false)
40 const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
41
42 // 每页 16 条
43 const pageSize = 16
44
45 const totalPages = Math.max(1, Math.ceil(total / pageSize || 1))
46
47 // 判断是否有筛选条件
48 const hasFilters = searchKeyword.trim() || favoriteOnly
49
50 /** 加载任务列表(分页) */
51 const loadTasks = useCallback(
52 async (pageNum: number, keyword?: string, onlyFavorites?: boolean) => {
53 setIsLoading(true)
54 try {
55 const result = await agentApi.getTaskList({
56 page: pageNum,
57 pageSize,
58 keyword: keyword?.trim() || undefined,
59 favoriteOnly: onlyFavorites || undefined,
60 })
61 if (result && result.code === 0 && result.data) {
62 const newTasks = result.data.list || []
63 setTasks(newTasks)
64 setTotal(result.data.total || 0)
65 setPage(pageNum)
66 }
67 }
68 catch (error) {
69 console.error('Load task list failed:', error)
70 toast.error(t('message.error'))
71 }
72 finally {
73 setIsLoading(false)
74 }
75 },
76 [pageSize, t],
77 )
78
79 /** 初始加载 */
80 useEffect(() => {
81 loadTasks(1, searchKeyword, favoriteOnly)
82 }, [])
83
84 /** 搜索防抖处理 */
85 const handleSearchChange = (value: string) => {
86 setSearchKeyword(value)
87
88 // 清除之前的定时器
89 if (debounceTimerRef.current) {
90 clearTimeout(debounceTimerRef.current)
91 }
92
93 // 设置 500ms 防抖
94 debounceTimerRef.current = setTimeout(() => {
95 loadTasks(1, value, favoriteOnly)
96 }, 500)
97 }
98
99 /** 清除搜索 */
100 const handleClearSearch = () => {
101 setSearchKeyword('')
102 if (debounceTimerRef.current) {
103 clearTimeout(debounceTimerRef.current)
104 }
105 loadTasks(1, '', favoriteOnly)
106 }
107
108 /** 切换收藏筛选 */
109 const handleToggleFavoriteOnly = () => {
110 const newValue = !favoriteOnly
111 setFavoriteOnly(newValue)
112 loadTasks(1, searchKeyword, newValue)
113 }
114
115 /** 清除所有筛选 */
116 const handleClearFilters = () => {
117 setSearchKeyword('')
118 setFavoriteOnly(false)
119 if (debounceTimerRef.current) {
120 clearTimeout(debounceTimerRef.current)
121 }
122 loadTasks(1, '', false)
123 }
124
125 /** 刷新列表,保持当前筛选条件 */
126 const handleRefresh = () => {
127 loadTasks(1, searchKeyword, favoriteOnly)
128 }
129
130 /** 分页切换 */
131 const handlePageChange = (nextPage: number) => {
132 if (nextPage < 1 || nextPage > totalPages)
133 return
134 loadTasks(nextPage, searchKeyword, favoriteOnly)
135 }
136
137 /** 返回首页 */
138 const handleBack = () => {
139 router.push(`/ai-social`)
140 }
141
142 /** 打开日志弹窗 */
143 const handleOpenLogs = () => {
144 setLogsModalOpen(true)
145 }
146
147 /** 打开视频历史弹窗 */
148 const handleOpenVideoHistory = () => {
149 setVideoHistoryModalOpen(true)
150 }
151
152 return (
153 <div className="flex flex-col min-h-screen bg-background">
154 {/* 顶部导航 */}
155 <header className="sticky top-0 z-10 flex items-center justify-between px-4 py-3 bg-card border-b border-border">
156 <div className="flex items-center gap-3">
157 <Button variant="ghost" size="icon" onClick={handleBack} className="w-8 h-8">
158 <ArrowLeft className="w-5 h-5" />
159 </Button>
160 <div className="flex items-center gap-2">
161 <History className="w-5 h-5 text-primary" />
162 <h1 className="text-lg font-semibold text-foreground">{t('history.title')}</h1>
163 </div>
164 {total > 0 && (
165 <span className="text-sm text-muted-foreground">
166 (
167 {total}
168 )
169 </span>
170 )}
171 </div>
172 <div className="flex items-center gap-2">
173 <Button
174 variant="ghost"
175 size="icon"
176 onClick={handleOpenLogs}
177 className="w-8 h-8"
178 title={t('history.logs')}
179 >
180 <FileText className="w-4 h-4" />
181 </Button>
182 <Button
183 variant="ghost"
184 size="icon"
185 onClick={handleOpenVideoHistory}
186 className="w-8 h-8"
187 title={t('history.videoHistory')}
188 >
189 <FileVideo className="w-4 h-4" />
190 </Button>
191 <Button
192 variant="ghost"
193 size="icon"
194 onClick={handleRefresh}
195 disabled={isLoading}
196 className="w-8 h-8"
197 >
198 <RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
199 </Button>
200 </div>
201 </header>
202
203 {/* 搜索和筛选区域 */}
204 <div className="sticky top-[57px] z-10 px-4 py-3 bg-card border-b border-border">
205 <div className="flex items-center gap-3 max-w-6xl mx-auto">
206 {/* 搜索框 */}
207 <div className="relative flex-1">
208 <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
209 <Input
210 type="text"
211 placeholder={t('history.searchPlaceholder')}
212 value={searchKeyword}
213 onChange={e => handleSearchChange(e.target.value)}
214 className="pl-9 pr-9 border-0 bg-muted/50 focus-visible:ring-0 focus-visible:bg-muted"
215 />
216 {searchKeyword && (
217 <button
218 type="button"
219 onClick={handleClearSearch}
220 className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground cursor-pointer"
221 >
222 <X className="w-4 h-4" />
223 </button>
224 )}
225 </div>
226
227 {/* 筛选按钮区域 */}
228 <div className="flex items-center gap-2 shrink-0">
229 {/* 收藏筛选按钮 */}
230 <Button
231 variant={favoriteOnly ? 'default' : 'outline'}
232 size="sm"
233 onClick={handleToggleFavoriteOnly}
234 className="gap-1.5 cursor-pointer"
235 >
236 <Star className={cn('w-4 h-4', favoriteOnly && 'fill-current')} />
237 <span className="hidden sm:inline">{t('history.favoriteOnly')}</span>
238 </Button>
239
240 {/* 清除筛选按钮 */}
241 {hasFilters && (
242 <Button
243 variant="ghost"
244 size="sm"
245 onClick={handleClearFilters}
246 className="text-muted-foreground hover:text-foreground cursor-pointer hidden sm:flex"
247 >
248 {t('history.clearFilters')}
249 </Button>
250 )}
251 </div>
252 </div>
253 </div>
254
255 {/* 任务列表 */}
256 <main className="flex-1 px-4 py-6">
257 <div className="w-full max-w-6xl mx-auto">
258 {isLoading ? (
259 // 加载骨架屏
260 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
261 {Array.from({ length: pageSize }).map((_, index) => (
262 <TaskCardSkeleton key={index} />
263 ))}
264 </div>
265 ) : tasks.length === 0 ? (
266 // 空状态 - 根据筛选条件显示不同提示
267 <div className="flex flex-col items-center justify-center py-16">
268 {favoriteOnly ? (
269 // 收藏筛选的空状态
270 <>
271 <Star className="w-16 h-16 text-muted-foreground/30 mb-4" />
272 <p className="text-muted-foreground mb-4">{t('history.emptyFavorites')}</p>
273 <Button onClick={handleClearFilters} className="cursor-pointer">
274 {t('history.clearFilters')}
275 </Button>
276 </>
277 ) : searchKeyword.trim() ? (
278 // 搜索的空状态
279 <>
280 <Search className="w-16 h-16 text-muted-foreground/30 mb-4" />
281 <p className="text-muted-foreground mb-4">{t('history.emptySearch')}</p>
282 <Button onClick={handleClearSearch} className="cursor-pointer">
283 {t('history.clearFilters')}
284 </Button>
285 </>
286 ) : (
287 // 无任何筛选的空状态
288 <>
289 <History className="w-16 h-16 text-muted-foreground/30 mb-4" />
290 <p className="text-muted-foreground mb-4">{t('history.empty')}</p>
291 <Button onClick={handleBack} className="cursor-pointer">
292 {t('home.startChat')}
293 </Button>
294 </>
295 )}
296 </div>
297 ) : (
298 // 任务卡片网格 + 分页器
299 <>
300 <div>
301 <TaskHistoryList
302 tasks={tasks}
303 isLoading={isLoading}
304 onRefresh={() => loadTasks(page, searchKeyword, favoriteOnly)}
305 />
306 </div>
307
308 {/* 分页器(使用项目内的 Pagination 组件) */}
309 {totalPages > 1 && (
310 <div className="mt-8">
311 <Pagination
312 current={page}
313 pageSize={pageSize}
314 total={total}
315 onChange={handlePageChange}
316 showTotal={(totalCount, [_start, _end]) => (
317 <span className="text-sm text-muted-foreground">
318 {page}
319 {' '}
320 /
321 {totalPages}
322 </span>
323 )}
324 className="w-full"
325 />
326 </div>
327 )}
328 </>
329 )}
330 </div>
331 </main>
332
333 {/* 用户日志弹窗 */}
334 <UserLogsModal open={logsModalOpen} onClose={() => setLogsModalOpen(false)} />
335
336 {/* 视频历史弹窗 */}
337 <VideoHistoryModal
338 open={videoHistoryModalOpen}
339 onClose={() => setVideoHistoryModalOpen(false)}
340 />
341 </div>
342 )
343 }
344
344 lines Plain Text