'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 type { CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest, } from '@/lib/services/admin'; import services from '@/lib/services'; 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([ services.adminTask.listSchedules(), services.adminTask.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 services.adminTask.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 services.adminTask.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 services.adminTask.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 services.adminTask.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' ? (