- Add skill-adapter.ts to bridge configStore and UI skill formats - Refactor SkillMarket to use new skill-adapter instead of skill-discovery - Add health check state to connectionStore - Update multiple components with improved typing - Clean up test artifacts and add new test results - Update README and add skill-market-mvp plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
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;
|
|
}
|
|
}
|