/** * 调度管理 Tab 组件。 * * 功能: * - 调度任务列表(名称、调度类型、启用 Switch、下次执行、执行次数、最近状态、操作) * - 创建/编辑调度任务 Modal(名称 + 调度配置) * - 删除确认 */ import React, { useEffect, useState, useCallback } from 'react'; import { Table, Tag, Button, Switch, Popconfirm, Space, Modal, Form, Input, Select, InputNumber, TimePicker, Checkbox, message, Typography, } from 'antd'; import { ReloadOutlined, EditOutlined, DeleteOutlined, HistoryOutlined, PlayCircleOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; import type { ScheduledTask, ScheduleConfig } from '../types'; import { fetchSchedules, updateSchedule, deleteSchedule, toggleSchedule, runScheduleNow, } from '../api/schedules'; import ScheduleHistoryDrawer from './ScheduleHistoryDrawer'; const { Text } = Typography; /* ------------------------------------------------------------------ */ /* 常量 & 工具 */ /* ------------------------------------------------------------------ */ const STATUS_COLOR: Record = { success: 'success', failed: 'error', running: 'processing', cancelled: 'warning', }; const SCHEDULE_TYPE_LABEL: Record = { once: '一次性', interval: '固定间隔', daily: '每日', weekly: '每周', cron: 'Cron', }; const INTERVAL_UNIT_LABEL: Record = { minutes: '分钟', hours: '小时', days: '天', }; const WEEKDAY_OPTIONS = [ { label: '周一', value: 1 }, { label: '周二', value: 2 }, { label: '周三', value: 3 }, { label: '周四', value: 4 }, { label: '周五', value: 5 }, { label: '周六', value: 6 }, { label: '周日', value: 0 }, ]; /** 格式化时间 */ function fmtTime(iso: string | null | undefined): string { if (!iso) return '—'; return new Date(iso).toLocaleString('zh-CN'); } /** 根据调度配置生成可读描述 */ function describeSchedule(cfg: ScheduleConfig): string { switch (cfg.schedule_type) { case 'once': return '一次性'; case 'interval': return `每 ${cfg.interval_value} ${INTERVAL_UNIT_LABEL[cfg.interval_unit] ?? cfg.interval_unit}`; case 'daily': return `每日 ${cfg.daily_time}`; case 'weekly': { const days = (cfg.weekly_days ?? []) .map((d) => WEEKDAY_OPTIONS.find((o) => o.value === d)?.label ?? `${d}`) .join('、'); return `每周 ${days} ${cfg.weekly_time}`; } case 'cron': return `Cron: ${cfg.cron_expression}`; default: return cfg.schedule_type; } } /* ------------------------------------------------------------------ */ /* 调度配置表单子组件 */ /* ------------------------------------------------------------------ */ /** 根据调度类型动态渲染配置项 */ const ScheduleConfigFields: React.FC<{ scheduleType: string }> = ({ scheduleType }) => { switch (scheduleType) { case 'interval': return ( ); default: return null; } }; /* ------------------------------------------------------------------ */ /* 主组件 */ /* ------------------------------------------------------------------ */ const ScheduleTab: React.FC = () => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [submitting, setSubmitting] = useState(false); const [scheduleType, setScheduleType] = useState('daily'); const [form] = Form.useForm(); /* 执行历史抽屉状态 */ const [historyOpen, setHistoryOpen] = useState(false); const [historyScheduleId, setHistoryScheduleId] = useState(null); const [historyScheduleName, setHistoryScheduleName] = useState(''); /* 加载列表 */ const load = useCallback(async () => { setLoading(true); try { setData(await fetchSchedules()); } catch { message.error('加载调度任务失败'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); /* 打开编辑 Modal */ const openEdit = (record: ScheduledTask) => { setEditing(record); const cfg = record.schedule_config; form.setFieldsValue({ name: record.name, schedule_config: { ...cfg, daily_time: cfg.daily_time ? dayjs(cfg.daily_time, 'HH:mm') : undefined, weekly_time: cfg.weekly_time ? dayjs(cfg.weekly_time, 'HH:mm') : undefined, }, }); setScheduleType(cfg.schedule_type); setModalOpen(true); }; /* 打开执行历史 */ const openHistory = (record: ScheduledTask) => { setHistoryScheduleId(record.id); setHistoryScheduleName(record.name); setHistoryOpen(true); }; /* 提交编辑 */ const handleSubmit = async () => { if (!editing) return; try { const values = await form.validateFields(); setSubmitting(true); const cfg = { ...values.schedule_config }; if (cfg.daily_time && typeof cfg.daily_time !== 'string') { cfg.daily_time = cfg.daily_time.format('HH:mm'); } if (cfg.weekly_time && typeof cfg.weekly_time !== 'string') { cfg.weekly_time = cfg.weekly_time.format('HH:mm'); } const scheduleConfig: ScheduleConfig = { schedule_type: cfg.schedule_type ?? 'daily', interval_value: cfg.interval_value ?? 1, interval_unit: cfg.interval_unit ?? 'hours', daily_time: cfg.daily_time ?? '04:00', weekly_days: cfg.weekly_days ?? [1], weekly_time: cfg.weekly_time ?? '04:00', cron_expression: cfg.cron_expression ?? '0 4 * * *', enabled: true, start_date: null, end_date: null, }; await updateSchedule(editing.id, { name: values.name, schedule_config: scheduleConfig, }); message.success('调度任务已更新'); setModalOpen(false); load(); } catch { // 表单验证失败 } finally { setSubmitting(false); } }; /* 删除 */ const handleDelete = async (id: string) => { try { await deleteSchedule(id); message.success('已删除'); load(); } catch { message.error('删除失败'); } }; /* 启用/禁用 */ const handleToggle = async (id: string) => { try { await toggleSchedule(id); load(); } catch { message.error('切换状态失败'); } }; /* 手动执行一次(不更新调度间隔) */ const handleRunNow = async (id: string) => { try { await runScheduleNow(id); message.success('已提交到执行队列'); } catch { message.error('执行失败'); } }; /* 表格列定义 */ const columns: ColumnsType = [ { title: '调度 ID', dataIndex: 'id', key: 'id', width: 120, render: (id: string) => ( {id.slice(0, 8)}… ), }, { title: '名称', dataIndex: 'name', key: 'name', }, { title: '调度类型', key: 'schedule_type', render: (_: unknown, record: ScheduledTask) => describeSchedule(record.schedule_config), }, { title: '启用', dataIndex: 'enabled', key: 'enabled', width: 80, render: (enabled: boolean, record: ScheduledTask) => ( handleToggle(record.id)} size="small" /> ), }, { title: '下次执行', dataIndex: 'next_run_at', key: 'next_run_at', render: fmtTime, }, { title: '执行次数', dataIndex: 'run_count', key: 'run_count', width: 90, }, { title: '最近状态', dataIndex: 'last_status', key: 'last_status', width: 100, render: (s: string | null) => s ? {s} : '—', }, { title: '操作', key: 'action', width: 300, render: (_: unknown, record: ScheduledTask) => ( handleRunNow(record.id)}> handleDelete(record.id)}> ), }, ]; return ( <>
共 {data.length} 个调度任务
rowKey="id" columns={columns} dataSource={data} loading={loading} pagination={false} size="middle" /> {/* 编辑 Modal */} setModalOpen(false)} confirmLoading={submitting} destroyOnClose >