"use client" import {useCallback, useEffect, useState} from "react" import {toast} from "sonner" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" import {Label} from "@/components/ui/label" import {Textarea} from "@/components/ui/textarea" import {Switch} from "@/components/ui/switch" import {Spinner} from "@/components/ui/spinner" import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" import {Clock, Edit2, Info, Plus, RefreshCw, Trash2} from "lucide-react" import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services/admin" import {buildTaskPayload} from "@/lib/task-param-utils" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" export function TaskSchedulesManager() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [schedules, setSchedules] = useState([]) const [taskTypes, setTaskTypes] = useState([]) // Modal States const [dialogOpen, setDialogOpen] = useState(false) const [submitLoading, setSubmitLoading] = useState(false) const [editingSchedule, setEditingSchedule] = useState(null) // Delete States const [deleteOpen, setDeleteOpen] = useState(false) const [deletingId, setDeletingId] = useState(null) const [deleteLoading, setDeleteLoading] = useState(false) // Form Fields const [name, setName] = useState("") const [selectedTaskType, setSelectedTaskType] = useState("") const [cron, setCron] = useState("") const [isActive, setIsActive] = useState(true) const [paramValues, setParamValues] = useState>({}) // Fetch Schedules & Task Types const fetchData = useCallback(async () => { try { setLoading(true) setError(null) const [schedulesData, taskTypesData] = await Promise.all([ AdminService.listSchedules(), AdminService.getTaskTypes() ]) setSchedules(schedulesData || []) setTaskTypes(taskTypesData || []) } catch (err) { setError(err instanceof Error ? err : new Error("加载定时任务配置失败")) } finally { setLoading(false) } }, []) useEffect(() => { fetchData() }, [fetchData]) // Track task type selection to initialize parameters useEffect(() => { if (selectedTaskType) { const targetTask = taskTypes.find(t => t.type === selectedTaskType) if (editingSchedule && editingSchedule.task_type === selectedTaskType) { // Parse existing payload when editing the same task type try { const parsed = JSON.parse(editingSchedule.payload || "{}") const values: Record = {} targetTask?.params?.forEach(p => { values[p.name] = String(parsed[p.name] ?? "") }) setParamValues(values) } catch { const initialValues: Record = {} targetTask?.params?.forEach(p => { initialValues[p.name] = "" }) setParamValues(initialValues) } } else { const initialValues: Record = {} targetTask?.params?.forEach(p => { initialValues[p.name] = "" }) setParamValues(initialValues) } } else { setParamValues({}) } }, [selectedTaskType, taskTypes, editingSchedule]) const handleToggleActive = async (schedule: Schedule) => { try { const updated = await AdminService.updateSchedule(schedule.id, { name: schedule.name, task_type: schedule.task_type, cron: schedule.cron, payload: schedule.payload, is_active: !schedule.is_active }) setSchedules(prev => prev.map(s => s.id === schedule.id ? updated : s)) toast.success(updated.is_active ? `已启用定时任务:${updated.name}` : `已停用定时任务:${updated.name}`) } catch (err) { toast.error("修改任务状态失败", { description: err instanceof Error ? err.message : "未知错误" }) } } const openCreateDialog = () => { setEditingSchedule(null) setName("") setSelectedTaskType(taskTypes[0]?.type || "") setCron("0 */2 * * *") setIsActive(true) setParamValues({}) setDialogOpen(true) } const openEditDialog = (schedule: Schedule) => { setEditingSchedule(schedule) setName(schedule.name) setSelectedTaskType(schedule.task_type) setCron(schedule.cron) setIsActive(schedule.is_active) setDialogOpen(true) } const handleSubmit = async () => { if (!name.trim()) { toast.error("任务名称不能为空") return } if (!cron.trim()) { toast.error("Cron 表达式不能为空") return } try { setSubmitLoading(true) const targetTask = taskTypes.find(t => t.type === selectedTaskType) let payloadStr = '{}' if (targetTask?.params && targetTask.params.length > 0) { const { payload, error } = buildTaskPayload(targetTask.params, paramValues) if (error) { toast.error(error) setSubmitLoading(false) return } payloadStr = payload ?? '{}' } if (editingSchedule) { const req: UpdateScheduleRequest = { name, task_type: selectedTaskType, cron, payload: payloadStr, is_active: isActive } const updated = await AdminService.updateSchedule(editingSchedule.id, req) setSchedules(prev => prev.map(s => s.id === editingSchedule.id ? updated : s)) toast.success("定时任务更新成功") } else { const req: CreateScheduleRequest = { name, task_type: selectedTaskType, cron, payload: payloadStr, is_active: isActive } const created = await AdminService.createSchedule(req) setSchedules(prev => [created, ...prev]) toast.success("定时任务创建成功") } setDialogOpen(false) } catch (err) { toast.error("提交失败", { description: err instanceof Error ? err.message : "未知错误" }) } finally { setSubmitLoading(false) } } const openDeleteDialog = (id: string) => { setDeletingId(id) setDeleteOpen(true) } const handleDelete = async () => { if (!deletingId) return try { setDeleteLoading(true) await AdminService.deleteSchedule(deletingId) setSchedules(prev => prev.filter(s => s.id !== deletingId)) toast.success("定时任务删除成功") setDeleteOpen(false) } catch (err) { toast.error("删除失败", { description: err instanceof Error ? err.message : "未知错误" }) } finally { setDeleteLoading(false) } } const getSelectedTaskMeta = () => { return taskTypes.find(t => t.type === selectedTaskType) } const getTaskName = (type: string) => { const meta = taskTypes.find(t => t.type === type) return meta ? meta.name : type } return (

{error ? (
) : loading && schedules.length === 0 ? ( ) : schedules.length === 0 ? ( ) : (
任务名称 关联异步任务 Cron 表达式 执行参数 (Payload) 启用状态 操作 {schedules.map((schedule) => ( {schedule.name}
{getTaskName(schedule.task_type)} {schedule.task_type}
{schedule.cron} {schedule.payload || "{}"}
handleToggleActive(schedule)} />
))}
)} {/* Add / Edit Dialog */} {editingSchedule ? "修改定时任务" : "新增定时任务"} 配置定时任务的调度参数和运行载荷。
setName(e.target.value)} className="text-xs" />
setCron(e.target.value)} className="text-xs font-mono" />

使用标准 5 位 Cron 字段格式(分钟、小时、日期、月份、星期几)。

决定此定时任务是否会定时触发执行。

{(() => { const targetTask = getSelectedTaskMeta() if (!targetTask || !targetTask.params || targetTask.params.length === 0) { return (
所选任务类型不需要运行参数参数。
) } return (
{targetTask.params.map((param) => (
{param.type === 'text' ? (