Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | import { useEffect } from 'react'; import { useConnectionStore } from '../store/connectionStore'; import { useConfigStore } from '../store/configStore'; import { Clock, RefreshCw, Play, Pause, AlertCircle, CheckCircle2 } from 'lucide-react'; const STATUS_CONFIG: Record<string, { icon: typeof Play; color: string; label: string }> = { active: { icon: Play, color: 'text-green-500', label: '运行中' }, paused: { icon: Pause, color: 'text-yellow-500', label: '已暂停' }, completed: { icon: CheckCircle2, color: 'text-blue-500', label: '已完成' }, error: { icon: AlertCircle, color: 'text-red-500', label: '错误' }, }; export function TaskList() { const scheduledTasks = useConfigStore((s) => s.scheduledTasks); const connectionState = useConnectionStore((s) => s.connectionState); const loadScheduledTasks = useConfigStore((s) => s.loadScheduledTasks); const connected = connectionState === 'connected'; useEffect(() => { if (connected) { loadScheduledTasks(); } }, [connected]); if (!connected) { return ( <div className="flex flex-col items-center justify-center h-full text-gray-400 text-xs px-4 text-center"> <Clock className="w-8 h-8 mb-2 opacity-30" /> <p>定时任务</p> <p className="mt-1">连接 Gateway 后可用</p> </div> ); } return ( <div className="h-full flex flex-col"> {/* Header */} <div className="flex items-center justify-between px-3 py-2 border-b border-gray-200"> <span className="text-xs font-medium text-gray-500">Heartbeat 任务</span> <button onClick={loadScheduledTasks} className="p-1 text-gray-400 hover:text-orange-500 rounded" title="刷新" > <RefreshCw className="w-3.5 h-3.5" /> </button> </div> <div className="flex-1 overflow-y-auto custom-scrollbar"> {scheduledTasks.length > 0 ? ( scheduledTasks.map((task) => { const cfg = STATUS_CONFIG[task.status] || STATUS_CONFIG.active; const StatusIcon = cfg.icon; return ( <div key={task.id} className="px-3 py-3 border-b border-gray-50 hover:bg-gray-50" > <div className="flex items-center gap-2 mb-1"> <StatusIcon className={`w-3.5 h-3.5 flex-shrink-0 ${cfg.color}`} /> <span className="text-xs font-medium text-gray-900 truncate">{task.name}</span> </div> <div className="pl-5.5 space-y-0.5"> <div className="text-[11px] text-gray-500 font-mono">{task.schedule}</div> {task.description && ( <div className="text-[11px] text-gray-400 truncate">{task.description}</div> )} <div className="flex gap-3 text-[10px] text-gray-400"> {task.lastRun && <span>上次: {formatTaskTime(task.lastRun)}</span>} {task.nextRun && <span>下次: {formatTaskTime(task.nextRun)}</span>} </div> </div> </div> ); }) ) : ( <div className="flex flex-col items-center justify-center h-full text-gray-400 text-xs px-4 text-center"> <Clock className="w-8 h-8 mb-2 opacity-30" /> <p>暂无定时任务</p> <p className="mt-1">Heartbeat 引擎管理的定时任务</p> <p className="mt-0.5 text-[11px]">默认心跳周期: 1h</p> </div> )} </div> </div> ); } function formatTaskTime(timeStr: string): string { try { const d = new Date(timeStr); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const future = diffMs < 0; const absDiff = Math.abs(diffMs); const mins = Math.floor(absDiff / 60000); if (mins < 1) return future ? '即将' : '刚刚'; if (mins < 60) return future ? `${mins}分钟后` : `${mins}分钟前`; const hrs = Math.floor(mins / 60); if (hrs < 24) return future ? `${hrs}小时后` : `${hrs}小时前`; return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; } catch { return timeStr; } } |