import { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Form, Tag, Tooltip } from 'antd'; import { ClockCircleOutlined, PlayCircleOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { DataTable } from '../../components/admin/DataTable'; import { AdminPage } from '../../components/admin/AdminPage'; import { useListPageSize } from './useListPageSize'; import { useToast } from '../../contexts/ToastContext'; import { request } from '../../utils/request'; import { ScheduledJobModal } from './scheduled-jobs/ScheduledJobModal'; import type { AvailableTask, ScheduledJob } from './scheduled-jobs/types'; function getErrorMessage(error: unknown) { if (typeof error !== 'object' || error === null || !('response' in error)) return null; const detail = (error as { response?: { data?: { detail?: unknown } } }).response?.data?.detail; if (typeof detail === 'string') return detail; if (typeof detail === 'object' && detail !== null && 'message' in detail) return String(detail.message); return null; } export function ScheduledJobs() { // 每页数量由系统参数 page_size 控制 const systemPageSize = useListPageSize(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [keyword, setKeyword] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [activeTabKey, setActiveTabKey] = useState('basic'); const [availableTasks, setAvailableTasks] = useState([]); const [selectedTask, setSelectedTask] = useState(null); const [form] = Form.useForm(); const toast = useToast(); const jobType = Form.useWatch('job_type', form); const predefinedFunction = Form.useWatch('predefined_function', form); const filteredData = useMemo(() => { const normalized = keyword.toLowerCase(); return data.filter((item) => item.name.toLowerCase().includes(normalized) || item.description?.toLowerCase().includes(normalized)); }, [data, keyword]); const loadData = useCallback(async () => { setLoading(true); try { const { data: result } = await request.get('/scheduled-jobs'); setData(result || []); } catch { toast.error('加载数据失败'); } finally { setLoading(false); } }, [toast]); const loadAvailableTasks = useCallback(async () => { try { const { data: result } = await request.get('/scheduled-jobs/available-tasks'); setAvailableTasks(result || []); } catch { toast.error('加载可用任务列表失败'); } }, [toast]); useEffect(() => { void loadData(); void loadAvailableTasks(); }, [loadAvailableTasks, loadData]); useEffect(() => { if (!predefinedFunction || availableTasks.length === 0) { setSelectedTask(null); return; } const task = availableTasks.find((item) => item.name === predefinedFunction) || null; setSelectedTask(task); if (task && !editingRecord) { const defaults: Record = {}; task.parameters.forEach((parameter) => { if (parameter.default !== null && parameter.default !== undefined) defaults[parameter.name] = parameter.default; }); form.setFieldsValue({ function_params: defaults }); } }, [availableTasks, editingRecord, form, predefinedFunction]); const handleAdd = () => { setEditingRecord(null); setSelectedTask(null); form.resetFields(); form.setFieldsValue({ job_type: 'predefined', is_active: true, function_params: {} }); setActiveTabKey('basic'); setIsModalOpen(true); }; const handleEdit = (record: ScheduledJob) => { setEditingRecord(record); form.setFieldsValue({ ...record, function_params: record.function_params || {} }); setActiveTabKey('basic'); setIsModalOpen(true); }; const handleDelete = async (record: ScheduledJob) => { try { await request.delete(`/scheduled-jobs/${record.id}`); toast.success('删除成功'); await loadData(); } catch { toast.error('删除失败'); } }; const handleRunNow = async (record: ScheduledJob) => { try { await request.post(`/scheduled-jobs/${record.id}/run`); toast.success('定时任务已触发,请前往"系统任务"中查看进度'); window.setTimeout(() => void loadData(), 1000); } catch { toast.error('触发失败'); } }; /** 与其它列表一致:状态列使用统一的启用/停用开关 */ const handleStatusChange = async (record: ScheduledJob, checked: boolean) => { try { await request.put(`/scheduled-jobs/${record.id}`, { is_active: checked }); setData((current) => current.map((item) => (item.id === record.id ? { ...item, is_active: checked } : item))); toast.success(`${record.name} 已${checked ? '启用' : '禁用'}`); } catch { toast.error('状态更新失败'); } }; const handleModalOk = async () => { try { const values = await form.validateFields(); if (values.job_type === 'predefined') { delete values.python_code; } else { delete values.predefined_function; delete values.function_params; } if (editingRecord) { await request.put(`/scheduled-jobs/${editingRecord.id}`, values); toast.success('更新成功'); } else { await request.post('/scheduled-jobs', values); toast.success('创建成功'); } setIsModalOpen(false); await loadData(); } catch (error) { const message = getErrorMessage(error); if (message) toast.error(message); } }; const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', width: 60 }, { title: '任务名称', dataIndex: 'name', width: 200, render: (text, record) => (
{text}
{record.description ?
{record.description}
: null}
), }, { title: '类型', dataIndex: 'job_type', width: 120, render: (type) => {type === 'predefined' ? '内置任务' : '自定义代码'}, }, { title: '任务函数', dataIndex: 'predefined_function', width: 200, render: (func, record) => record.job_type === 'predefined' ? {func} : -, }, { title: 'Cron 表达式', dataIndex: 'cron_expression', width: 130, render: (text) => {text} }, { title: '上次执行', width: 200, render: (_, record) => record.last_run_at ? (
{new Date(record.last_run_at).toLocaleString()}
{record.last_run_status === 'success' ? '成功' : '失败'}
) : 从未执行, }, ]; return ( } title="定时任务管理" description="按 Cron 表达式周期性执行内置任务或自定义代码"> void loadData()} onSearch={setKeyword} searchPlaceholder="搜索任务名称 / 描述" onAdd={handleAdd} addText="新增定时任务" onEdit={handleEdit} onDelete={handleDelete} onStatusChange={handleStatusChange} statusField="is_active" deleteConfirmTitle="确认删除该定时任务?" deleteConfirmDescription="删除后该任务不再自动执行" rowKey="id" pageSize={systemPageSize} customActions={(record) => (