/** * Scheduled Jobs Management Page */ import { useState, useEffect } from 'react'; import { Modal, Form, Input, Switch, Button, Space, Popconfirm, Tag, Tooltip, Badge, Tabs, Select, Row, Col, Card, Alert } from 'antd'; import { PlayCircleOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined, InfoCircleOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; import { useToast } from '../../contexts/ToastContext'; interface ScheduledJob { id: number; name: string; job_type: 'predefined' | 'custom_code'; predefined_function?: string; function_params?: Record; cron_expression: string; python_code?: string; is_active: boolean; description: string; last_run_at: string | null; last_run_status: 'success' | 'failed' | null; next_run_at: string | null; created_at: string; updated_at: string; } interface AvailableTask { name: string; description: string; category: string; parameters: Array<{ name: string; type: string; description: string; required: boolean; default: any; }>; } export function ScheduledJobs() { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [filteredData, setFilteredData] = 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); useEffect(() => { loadData(); loadAvailableTasks(); }, []); useEffect(() => { // When predefined function changes, update selected task if (predefinedFunction && availableTasks.length > 0) { const task = availableTasks.find(t => t.name === predefinedFunction); setSelectedTask(task || null); // Set default parameter values only if not editing if (task && !editingRecord) { const defaultParams: Record = {}; task.parameters.forEach(param => { if (param.default !== null && param.default !== undefined) { defaultParams[param.name] = param.default; } }); form.setFieldsValue({ function_params: defaultParams }); } else if (task && editingRecord) { // When editing, just set the selected task, don't override params setSelectedTask(task); } } else { setSelectedTask(null); } }, [predefinedFunction, availableTasks]); const loadData = async () => { setLoading(true); try { const { data: result } = await request.get('/scheduled-jobs'); setData(result || []); setFilteredData(result || []); } catch (error) { toast.error('加载数据失败'); } finally { setLoading(false); } }; const loadAvailableTasks = async () => { try { const { data: result } = await request.get('/scheduled-jobs/available-tasks'); setAvailableTasks(result || []); } catch (error) { toast.error('加载可用任务列表失败'); } }; const handleSearch = (keyword: string) => { const lowerKeyword = keyword.toLowerCase(); const filtered = data.filter( (item) => item.name.toLowerCase().includes(lowerKeyword) || item.description?.toLowerCase().includes(lowerKeyword) ); setFilteredData(filtered); }; 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('删除成功'); loadData(); } catch (error) { toast.error('删除失败'); } }; const handleRunNow = async (record: ScheduledJob) => { try { await request.post(`/scheduled-jobs/${record.id}/run`); toast.success('定时任务已触发,请前往"系统任务"中查看进度'); setTimeout(loadData, 1000); } catch (error) { toast.error('触发失败'); } }; const handleModalOk = async () => { try { const values = await form.validateFields(); // Clean up data based on job_type 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); loadData(); } catch (error: any) { if (error.response?.data?.detail) { const detail = error.response.data.detail; if (typeof detail === 'object' && detail.message) { toast.error(detail.message); } else { toast.error(detail); } } } }; const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', width: 60, }, { title: '任务名称', dataIndex: 'name', width: 200, render: (text, record) => (
{text}
{record.description && (
{record.description}
)}
), }, { title: '类型', dataIndex: 'job_type', width: 120, render: (type) => ( {type === 'predefined' ? '内置任务' : '自定义代码'} ), }, { title: '任务函数', dataIndex: 'predefined_function', width: 200, render: (func, record) => { if (record.job_type === 'predefined') { return {func}; } return -; }, }, { title: 'Cron 表达式', dataIndex: 'cron_expression', width: 130, render: (text) => {text}, }, { title: '状态', dataIndex: 'is_active', width: 80, render: (active) => ( ), }, { title: '上次执行', width: 200, render: (_, record) => (
{record.last_run_at ? ( <>
{new Date(record.last_run_at).toLocaleString()}
{record.last_run_status === 'success' ? '成功' : '失败'} ) : ( 从未执行 )}
), }, { title: '操作', key: 'action', width: 150, render: (_, record) => (