import { useQueryClient } from "@tanstack/react-query"; import { SCHEDULE_CHANGED_EVENT, type ScheduledJob, type ScheduleGroup } from "@umt/contracts"; import { AlertTriangle, CalendarClock, CircleCheck, LoaderCircle, Plus, RotateCw, XCircle } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useScheduleBoard } from "~/api/schedules.ts"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatTile } from "~/components/console/stat-tile.tsx"; import { AgentCheckDialog } from "~/components/schedules/agent-check-dialog.tsx"; import { GROUP_LABEL, GROUP_ORDER } from "~/components/schedules/labels.ts"; import { RunsSheet } from "~/components/schedules/runs-sheet.tsx"; import { ScheduleCard } from "~/components/schedules/schedule-card.tsx"; import { Button } from "~/components/ui/button.tsx"; import { qk } from "~/lib/query-keys.ts"; import { useSse } from "~/lib/sse.ts"; export const meta = () => [{ title: "定时任务 · UHD 媒体自动化控制台" }]; /** 每秒一跳的当前时间(倒计时用) */ function useNow(ms = 1000) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const t = setInterval(() => setNow(Date.now()), ms); return () => clearInterval(t); }, [ms]); return now; } export default function SchedulesPage() { const qc = useQueryClient(); const board = useScheduleBoard(); const now = useNow(); const [history, setHistory] = useState(null); const [editing, setEditing] = useState<{ job: ScheduledJob | null; key: number } | null>(null); // SSE:调度器每次开始/结束一次运行都会发 schedule.changed;1 秒节流 const timer = useRef | null>(null); useSse([SCHEDULE_CHANGED_EVENT], () => { if (timer.current) return; timer.current = setTimeout(() => { timer.current = null; qc.invalidateQueries({ queryKey: qk.all("schedules") }); }, 1000); }); useEffect( () => () => { if (timer.current) clearTimeout(timer.current); }, [], ); const data = board.data; const items = data?.items ?? []; const byGroup = new Map(); for (const g of GROUP_ORDER) byGroup.set(g, []); for (const j of items) byGroup.get(j.group)?.push(j); const stats = data?.stats; const val = (n: number | undefined) => (board.isPending ? "…" : board.isError ? "—" : (n ?? 0)); return ( <> } />
0 ? "destructive" : "neutral"} value={val(stats?.failed)} />
{data && !data.schedulerOnline ? (

调度器未在线,任务不会按时运行。

) : (
)} {board.isPending ? ( ) : board.isError ? ( board.refetch()} /> ) : items.length === 0 ? ( ) : (
{GROUP_ORDER.map((g) => { const jobs = byGroup.get(g) ?? []; return (

{GROUP_LABEL[g]} {jobs.length}

{jobs.length === 0 ? (

{g === "agent" ? "还没有巡检。" : "还没有定时任务。"}

) : ( jobs.map((j) => ( setEditing({ job, key: Date.now() })} /> )) )}
); })}
)} setHistory(null)} /> {editing ? ( !o && setEditing(null)} /> ) : null} ); }