返回 AiToEarn
index.tsx
1 /**
2 * TaskCard - 任务卡片组件
3 * 功能:显示任务简要信息,支持点击跳转到对话详情
4 */
5
6 'use client'
7
8 import type React from 'react'
9 import {
10 AlertCircle,
11 CheckCircle2,
12 Heart,
13 Loader2,
14 MessageSquare,
15 Share2,
16 Star,
17 Trash2,
18 } from 'lucide-react'
19 import { useRouter } from 'next/navigation'
20 import { useState } from 'react'
21 import { useTransClient } from '@/app/i18n/client'
22 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
23 import { cn } from '@/utils/className'
24 import { formatRelativeTime } from '@/utils/format'
25
26 export interface ITaskCardProps {
27 /** 任务ID */
28 id: string
29 /** 任务标题 */
30 title: string
31 /** 任务状态(英文原始状态字符串) */
32 status?: string
33 /** 创建时间 */
34 createdAt: string | number
35 /** 更新时间 */
36 updatedAt?: string | number
37 /** 任务评分(1-5) */
38 rating?: number | null
39 /** 是否已收藏 */
40 isFavorited?: boolean
41 /** 收藏加载中 */
42 isFavoriteLoading?: boolean
43 /** 删除回调 */
44 onDelete?: (id: string) => void | Promise<void>
45 /** 评分回调(用于历史列表触发外部评分弹窗) */
46 onRateClick?: (taskId: string) => void
47 /** 选择回调(如果提供,点击卡片将触发选择而不是跳转) */
48 onSelect?: (id: string) => void
49 /** 在主页展示内联评分控件 */
50 showInlineRating?: boolean
51 /** 自定义类名 */
52 className?: string
53 /** 分享回调(由父组件触发 ShareModal) */
54 onShare?: (id: string) => void
55 /** 收藏切换回调 */
56 onFavoriteToggle?: (id: string, isFavorited: boolean) => void | Promise<void>
57 }
58
59 /** 获取状态显示配置 */
60 function getStatusConfig(status: string | undefined, t: (key: string) => string) {
61 const normalizedStatus = status?.toLowerCase()
62
63 switch (normalizedStatus) {
64 case 'requires_action':
65 return {
66 label: t('task.status.requiresAction') || 'Requires Action',
67 className:
68 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-800',
69 icon: AlertCircle,
70 }
71 case 'completed':
72 return {
73 label: t('task.status.completed') || 'Completed',
74 className:
75 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800',
76 icon: CheckCircle2,
77 }
78 case 'running':
79 return {
80 label: t('task.status.running') || 'Running',
81 className:
82 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800',
83 icon: Loader2,
84 }
85 case 'error':
86 case 'failed':
87 return {
88 label: t('task.status.failed') || 'Failed',
89 className:
90 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800',
91 icon: AlertCircle,
92 }
93 default:
94 return {
95 label: status || '',
96 className: 'bg-muted text-muted-foreground border-border',
97 icon: null,
98 }
99 }
100 }
101
102 /**
103 * TaskCard - 任务卡片组件
104 */
105 export function TaskCard({
106 id,
107 title,
108 status,
109 createdAt,
110 updatedAt,
111 rating,
112 isFavorited = false,
113 isFavoriteLoading = false,
114 onDelete,
115 className,
116 onSelect,
117 onRateClick,
118 onShare,
119 onFavoriteToggle,
120 }: ITaskCardProps) {
121 const router = useRouter()
122 const { t } = useTransClient('chat')
123 const [isDeleting, setIsDeleting] = useState(false)
124
125 const statusConfig = getStatusConfig(status, t as (key: string) => string)
126
127 /** 跳转到对话详情页 */
128 const handleClick = () => {
129 if (isDeleting)
130 return
131 if (onSelect) {
132 onSelect(id)
133 return
134 }
135 router.push(`/chat/${id}`)
136 }
137
138 /** 处理删除 */
139 const handleDelete = async (e: React.MouseEvent) => {
140 e.stopPropagation()
141 e.preventDefault()
142 if (!onDelete || isDeleting)
143 return
144
145 try {
146 setIsDeleting(true)
147 await onDelete(id)
148 }
149 finally {
150 setIsDeleting(false)
151 }
152 }
153
154 /** 触发评分回调(由历史列表等外部组件使用) */
155 const handleRateClick = (e?: React.MouseEvent) => {
156 e?.stopPropagation()
157 e?.preventDefault()
158 if (!onRateClick)
159 return
160 onRateClick(id)
161 }
162
163 const handleShareClick = (e?: React.MouseEvent) => {
164 e?.stopPropagation()
165 e?.preventDefault()
166 if (typeof onShare === 'function')
167 onShare(id)
168 }
169
170 /** 处理收藏切换 */
171 const handleFavoriteToggle = (e: React.MouseEvent) => {
172 e.stopPropagation()
173 e.preventDefault()
174 if (isFavoriteLoading)
175 return
176 onFavoriteToggle?.(id, !isFavorited)
177 }
178
179 return (
180 <div
181 onClick={handleClick}
182 className={cn(
183 'flex flex-col p-4 rounded-xl border border-border bg-card cursor-pointer transition-all',
184 'hover:border-border hover:shadow-md',
185 isDeleting && 'opacity-60 cursor-wait',
186 className,
187 )}
188 >
189 {/* 图标 + 标题 */}
190 <div className="flex items-start gap-3 mb-2">
191 <div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
192 <MessageSquare className="w-4 h-4 text-muted-foreground" />
193 </div>
194 <h4 title={title} className="text-sm font-medium text-foreground truncate flex-1 pt-1">
195 {title || 'New Chat'}
196 </h4>
197 </div>
198
199 {/* 时间 & 状态 */}
200 <div className="mt-1 flex items-center justify-between gap-2">
201 <span className="text-xs text-muted-foreground">
202 {formatRelativeTime(new Date(updatedAt || createdAt))}
203 </span>
204 {status && statusConfig.label && (
205 <span
206 className={cn(
207 'inline-flex items-center gap-1 rounded-full border px-1.5 sm:px-2 py-0.5 text-[11px] font-medium shrink-0',
208 statusConfig.className,
209 )}
210 title={statusConfig.label}
211 >
212 {statusConfig.icon && (
213 <statusConfig.icon
214 className={cn('w-3 h-3', status?.toLowerCase() === 'running' && 'animate-spin')}
215 />
216 )}
217 {/* 移动端只显示图标,桌面端显示文字 */}
218 <span className="hidden sm:inline">{statusConfig.label}</span>
219 </span>
220 )}
221 </div>
222
223 {/* Action buttons: 收藏 + 评分 + 分享 + 删除 - 只显示图标 + Tooltip */}
224 <TooltipProvider delayDuration={300}>
225 <div className="mt-3 flex items-center justify-end gap-1">
226 {/* 收藏按钮 */}
227 <Tooltip>
228 <TooltipTrigger asChild>
229 <button
230 onClick={handleFavoriteToggle}
231 disabled={isFavoriteLoading}
232 className="flex items-center justify-center w-7 h-7 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors opacity-60 hover:opacity-100 cursor-pointer"
233 aria-label={isFavorited ? t('task.unfavorite') : t('task.favorite')}
234 >
235 {isFavoriteLoading ? (
236 <Loader2 className="w-4 h-4 animate-spin" />
237 ) : (
238 <Heart
239 className={cn(
240 'w-4 h-4 transition-colors',
241 isFavorited && 'text-red-500 fill-red-500',
242 )}
243 />
244 )}
245 </button>
246 </TooltipTrigger>
247 <TooltipContent>
248 {isFavorited ? t('task.unfavorite') : t('task.favorite')}
249 </TooltipContent>
250 </Tooltip>
251
252 {/* 评分按钮 */}
253 <Tooltip>
254 <TooltipTrigger asChild>
255 <button
256 onClick={handleRateClick}
257 className="flex items-center justify-center w-7 h-7 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors opacity-60 hover:opacity-100 cursor-pointer"
258 aria-label={t('task.rate')}
259 >
260 <Star
261 className={cn(
262 'w-4 h-4 transition-colors',
263 rating && 'text-amber-400 fill-amber-400',
264 )}
265 />
266 </button>
267 </TooltipTrigger>
268 <TooltipContent>{t('task.rate')}</TooltipContent>
269 </Tooltip>
270
271 {/* 分享按钮 */}
272 <Tooltip>
273 <TooltipTrigger asChild>
274 <button
275 onClick={handleShareClick}
276 className="flex items-center justify-center w-7 h-7 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors opacity-60 hover:opacity-100 cursor-pointer"
277 aria-label={t('task.share')}
278 >
279 <Share2 className="w-4 h-4" />
280 </button>
281 </TooltipTrigger>
282 <TooltipContent>{t('task.share')}</TooltipContent>
283 </Tooltip>
284
285 {/* 删除按钮 */}
286 <Tooltip>
287 <TooltipTrigger asChild>
288 <button
289 onClick={handleDelete}
290 disabled={isDeleting}
291 className="flex items-center justify-center w-7 h-7 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors opacity-60 hover:opacity-100 cursor-pointer"
292 aria-label={t('task.delete')}
293 >
294 {isDeleting ? (
295 <Loader2 className="w-4 h-4 animate-spin" />
296 ) : (
297 <Trash2 className="w-4 h-4" />
298 )}
299 </button>
300 </TooltipTrigger>
301 <TooltipContent>{t('task.delete')}</TooltipContent>
302 </Tooltip>
303 </div>
304 </TooltipProvider>
305 </div>
306 )
307 }
308
309 export default TaskCard
310
310 lines Plain Text