cosmo/frontend/src/pages/admin/ScheduledJobs.tsx

208 lines
7.9 KiB
TypeScript
Raw Normal View History

2026-07-21 12:09:51 +00:00
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Badge, Button, Form, Popconfirm, Space, Tag, Tooltip } from 'antd';
import { DeleteOutlined, EditOutlined, PlayCircleOutlined } from '@ant-design/icons';
2025-12-11 08:31:26 +00:00
import type { ColumnsType } from 'antd/es/table';
2026-07-21 12:09:51 +00:00
2025-12-11 08:31:26 +00:00
import { DataTable } from '../../components/admin/DataTable';
import { useToast } from '../../contexts/ToastContext';
2026-07-21 12:09:51 +00:00
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;
2025-12-11 08:31:26 +00:00
}
export function ScheduledJobs() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<ScheduledJob[]>([]);
2026-07-21 12:09:51 +00:00
const [keyword, setKeyword] = useState('');
2025-12-11 08:31:26 +00:00
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<ScheduledJob | null>(null);
const [activeTabKey, setActiveTabKey] = useState('basic');
const [availableTasks, setAvailableTasks] = useState<AvailableTask[]>([]);
const [selectedTask, setSelectedTask] = useState<AvailableTask | null>(null);
const [form] = Form.useForm();
const toast = useToast();
2026-07-21 12:09:51 +00:00
const jobType = Form.useWatch<ScheduledJob['job_type']>('job_type', form);
const predefinedFunction = Form.useWatch<string>('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]);
2025-12-11 08:31:26 +00:00
2026-07-21 12:09:51 +00:00
const loadData = useCallback(async () => {
2025-12-11 08:31:26 +00:00
setLoading(true);
try {
const { data: result } = await request.get('/scheduled-jobs');
setData(result || []);
2026-07-21 12:09:51 +00:00
} catch {
2025-12-11 08:31:26 +00:00
toast.error('加载数据失败');
} finally {
setLoading(false);
}
2026-07-21 12:09:51 +00:00
}, [toast]);
2025-12-11 08:31:26 +00:00
2026-07-21 12:09:51 +00:00
const loadAvailableTasks = useCallback(async () => {
2025-12-11 08:31:26 +00:00
try {
const { data: result } = await request.get('/scheduled-jobs/available-tasks');
setAvailableTasks(result || []);
2026-07-21 12:09:51 +00:00
} catch {
2025-12-11 08:31:26 +00:00
toast.error('加载可用任务列表失败');
}
2026-07-21 12:09:51 +00:00
}, [toast]);
2025-12-11 08:31:26 +00:00
2026-07-21 12:09:51 +00:00
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<string, unknown> = {};
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]);
2025-12-11 08:31:26 +00:00
const handleAdd = () => {
setEditingRecord(null);
setSelectedTask(null);
form.resetFields();
2026-07-21 12:09:51 +00:00
form.setFieldsValue({ job_type: 'predefined', is_active: true, function_params: {} });
2025-12-11 08:31:26 +00:00
setActiveTabKey('basic');
setIsModalOpen(true);
};
const handleEdit = (record: ScheduledJob) => {
setEditingRecord(record);
2026-07-21 12:09:51 +00:00
form.setFieldsValue({ ...record, function_params: record.function_params || {} });
2025-12-11 08:31:26 +00:00
setActiveTabKey('basic');
setIsModalOpen(true);
};
const handleDelete = async (record: ScheduledJob) => {
try {
await request.delete(`/scheduled-jobs/${record.id}`);
toast.success('删除成功');
2026-07-21 12:09:51 +00:00
await loadData();
} catch {
2025-12-11 08:31:26 +00:00
toast.error('删除失败');
}
};
const handleRunNow = async (record: ScheduledJob) => {
try {
await request.post(`/scheduled-jobs/${record.id}/run`);
toast.success('定时任务已触发,请前往"系统任务"中查看进度');
2026-07-21 12:09:51 +00:00
window.setTimeout(() => void loadData(), 1000);
} catch {
2025-12-11 08:31:26 +00:00
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);
2026-07-21 12:09:51 +00:00
await loadData();
} catch (error) {
const message = getErrorMessage(error);
if (message) toast.error(message);
2025-12-11 08:31:26 +00:00
}
};
const columns: ColumnsType<ScheduledJob> = [
2026-07-21 12:09:51 +00:00
{ title: 'ID', dataIndex: 'id', width: 60 },
2025-12-11 08:31:26 +00:00
{
2026-07-21 12:09:51 +00:00
title: '任务名称', dataIndex: 'name', width: 200,
render: (text, record) => <div><div style={{ fontWeight: 500 }}>{text}</div>{record.description && <div style={{ fontSize: 12, color: '#888' }}>{record.description}</div>}</div>,
2025-12-11 08:31:26 +00:00
},
{
2026-07-21 12:09:51 +00:00
title: '类型', dataIndex: 'job_type', width: 120,
render: (type) => <Tag color={type === 'predefined' ? 'blue' : 'purple'}>{type === 'predefined' ? '内置任务' : '自定义代码'}</Tag>,
2025-12-11 08:31:26 +00:00
},
{
2026-07-21 12:09:51 +00:00
title: '任务函数', dataIndex: 'predefined_function', width: 200,
render: (func, record) => record.job_type === 'predefined' ? <Tag color="cyan">{func}</Tag> : <span style={{ color: '#ccc' }}>-</span>,
2025-12-11 08:31:26 +00:00
},
2026-07-21 12:09:51 +00:00
{ title: 'Cron 表达式', dataIndex: 'cron_expression', width: 130, render: (text) => <Tag color="green">{text}</Tag> },
{ title: '状态', dataIndex: 'is_active', width: 80, render: (active) => <Badge status={active ? 'success' : 'default'} text={active ? '启用' : '禁用'} /> },
2025-12-11 08:31:26 +00:00
{
2026-07-21 12:09:51 +00:00
title: '上次执行', width: 200,
render: (_, record) => record.last_run_at ? (
<div><div>{new Date(record.last_run_at).toLocaleString()}</div><Tag color={record.last_run_status === 'success' ? 'green' : 'red'}>{record.last_run_status === 'success' ? '成功' : '失败'}</Tag></div>
) : <span style={{ color: '#ccc' }}></span>,
2025-12-11 08:31:26 +00:00
},
{
2026-07-21 12:09:51 +00:00
title: '操作', key: 'action', width: 150,
2025-12-11 08:31:26 +00:00
render: (_, record) => (
<Space size="small">
2026-07-21 12:09:51 +00:00
<Tooltip title="立即执行"><Button type="text" icon={<PlayCircleOutlined />} onClick={() => handleRunNow(record)} style={{ color: '#52c41a' }} /></Tooltip>
<Tooltip title="编辑"><Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} style={{ color: '#1890ff' }} /></Tooltip>
<Popconfirm title="确认删除该任务?" onConfirm={() => handleDelete(record)} okText="删除" cancelText="取消">
<Tooltip title="删除"><Button type="text" danger icon={<DeleteOutlined />} /></Tooltip>
2025-12-11 08:31:26 +00:00
</Popconfirm>
</Space>
),
},
];
return (
<>
<DataTable
title="定时任务管理"
columns={columns}
dataSource={filteredData}
loading={loading}
total={filteredData.length}
2026-07-21 12:09:51 +00:00
onSearch={setKeyword}
2025-12-11 08:31:26 +00:00
onAdd={handleAdd}
rowKey="id"
pageSize={10}
/>
2026-07-21 12:09:51 +00:00
<ScheduledJobModal
form={form}
record={editingRecord}
2025-12-11 08:31:26 +00:00
open={isModalOpen}
onOk={handleModalOk}
onCancel={() => setIsModalOpen(false)}
2026-07-21 12:09:51 +00:00
activeTab={activeTabKey}
onActiveTabChange={setActiveTabKey}
jobType={jobType}
availableTasks={availableTasks}
selectedTask={selectedTask}
onClearSelectedTask={() => setSelectedTask(null)}
/>
2025-12-11 08:31:26 +00:00
</>
);
}