"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Download, Edit3, Eye, Pause, Play, RefreshCw, Search, Trash2, } from "lucide-react"; import { useSearchParams } from "react-router-dom"; import { useRouteNavigate as useNavigate } from "@/core/route-base"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { useTheme } from "@/contexts/ThemeContext"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Textarea } from "@/components/ui/textarea"; import { WorkspaceBody, WorkspaceContainer, WorkspaceHeader, } from "@/components/workspace/workspace-container"; import { buildCron, DEFAULT_SCHEDULE_CONFIG, parseCron, ScheduleBuilder, type ScheduleConfig, statusLabel, statusVariant, } from "@/components/workspace/scheduled-tasks/schedule-builder"; import { downloadScheduledTaskRun, type ScheduledTask, useAdminScheduledTaskActions, useAdminScheduledTaskRuns, useAllScheduledTasks, } from "@/core/scheduled-tasks"; import { cn } from "@/lib/utils"; function formatDate(value?: string | null) { return value ? new Date(value).toLocaleString() : "暂无"; } /** Render a creator-like identifier without the email domain part. */ function displayUsername(value?: string | null): string { const raw = (value ?? "").trim(); if (!raw) return ""; const at = raw.indexOf("@"); return at === -1 ? raw : raw.slice(0, at); } const emptyForm = { name: "", timezone: "Asia/Shanghai", prompt: "", }; export default function AdminScheduledTasksPage() { const queryClient = useQueryClient(); const { theme } = useTheme(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); // ?task= carries the task to re-select when returning from a run detail. const initialTaskId = searchParams.get("task"); const [searchInput, setSearchInput] = useState(""); const [search, setSearch] = useState(""); const { data: tasks = [], isLoading } = useAllScheduledTasks({ search }); const actions = useAdminScheduledTaskActions(); const [selectedTaskId, setSelectedTaskId] = useState( initialTaskId, ); // One-shot scroll target after a back-navigation. const pendingScrollRef = useRef(initialTaskId); const [editing, setEditing] = useState(null); const [form, setForm] = useState(emptyForm); const [scheduleConfig, setScheduleConfig] = useState( DEFAULT_SCHEDULE_CONFIG, ); const sortedTasks = useMemo( () => [...tasks].sort((a, b) => String(a.next_run_at ?? "").localeCompare(String(b.next_run_at ?? "")), ), [tasks], ); const selectedTask = sortedTasks.find((task) => task.task_id === selectedTaskId) ?? null; const { data: runs = [], isLoading: runsLoading } = useAdminScheduledTaskRuns(selectedTaskId); useEffect(() => { if (!selectedTaskId && sortedTasks[0]) { setSelectedTaskId(sortedTasks[0].task_id); } if ( selectedTaskId && !sortedTasks.some((task) => task.task_id === selectedTaskId) ) { setSelectedTaskId(sortedTasks[0]?.task_id ?? null); } }, [selectedTaskId, sortedTasks]); const openEdit = (task: ScheduledTask) => { setEditing(task); setForm({ name: task.name, timezone: task.timezone, prompt: task.prompt, }); setScheduleConfig( parseCron(task.cron_expr) ?? parseCron(task.schedule_text) ?? DEFAULT_SCHEDULE_CONFIG, ); }; const closeDialog = () => { setEditing(null); setForm(emptyForm); setScheduleConfig(DEFAULT_SCHEDULE_CONFIG); }; const saveTask = async () => { if (!editing) return; try { await actions.update.mutateAsync({ taskId: editing.task_id, data: { name: form.name, schedule: buildCron(scheduleConfig), timezone: form.timezone, prompt: form.prompt, }, }); toast.success("任务已更新"); closeDialog(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } }; const toggleEnabled = async (task: ScheduledTask) => { try { await actions.update.mutateAsync({ taskId: task.task_id, data: { enabled: !task.enabled }, }); toast.success(task.enabled ? "任务已暂停" : "任务已启用"); } catch (error) { toast.error(error instanceof Error ? error.message : "操作失败"); } }; const removeTask = async (task: ScheduledTask) => { if (!window.confirm(`确认删除任务「${task.name}」?该操作不可撤销。`)) return; try { await actions.remove.mutateAsync(task.task_id); toast.success("任务已删除"); } catch (error) { toast.error(error instanceof Error ? error.message : "删除失败"); } }; const triggerTask = async (task: ScheduledTask) => { setSelectedTaskId(task.task_id); try { await actions.trigger.mutateAsync(task.task_id); await queryClient.invalidateQueries({ queryKey: ["scheduled-tasks", "admin", task.task_id, "runs"], }); await queryClient.invalidateQueries({ queryKey: ["scheduled-tasks", "admin", "list"], }); toast.success("已触发执行"); } catch (error) { toast.error(error instanceof Error ? error.message : "触发失败"); } }; const downloadRun = async (runId: string) => { try { await downloadScheduledTaskRun(runId); } catch (error) { toast.error(error instanceof Error ? error.message : "下载失败"); } }; const deleteRun = async (runId: string) => { if (!window.confirm("确认删除这条执行记录?该操作不可撤销。")) return; try { await actions.removeRun.mutateAsync(runId); toast.success("执行记录已删除"); } catch (error) { toast.error(error instanceof Error ? error.message : "删除失败"); } }; return (
定时任务管理

管理员视图:查看、编辑、删除全部用户的定时任务

{ event.preventDefault(); setSearch(searchInput.trim()); }} > setSearchInput(event.target.value)} placeholder="按任务名称搜索" />
{isLoading && (
正在加载任务...
)} {!isLoading && sortedTasks.length === 0 && (
没有匹配的定时任务。
)} {sortedTasks.map((task) => (
))}
执行记录

{selectedTask ? selectedTask.name : "请选择左侧任务"}

{runsLoading && (
正在加载执行记录...
)} {!selectedTask && (
请先选择一个定时任务。
)} {!runsLoading && selectedTask && runs.length === 0 && (
这个任务还没有执行记录。
)} {runs.map((run) => (
{run.result?.title || formatDate(run.scheduled_for)}
计划:{formatDate(run.scheduled_for)} · 开始: {formatDate(run.started_at)}
{(run.retry_count ?? 0) > 0 && ( 已重试 {run.retry_count} )} {statusLabel(run.status)}
{run.error && (
{run.error}
)}
))}
!open && closeDialog()}> 编辑定时任务
setForm((value) => ({ ...value, name: event.target.value })) } placeholder="任务名称" />

执行频率

setForm((value) => ({ ...value, timezone: event.target.value, })) } placeholder="时区,例如:Asia/Shanghai" />