在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,407 @@
/**
* 调度管理 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,
} from 'antd';
import { PlusOutlined, ReloadOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import type { ScheduledTask, ScheduleConfig } from '../types';
import {
fetchSchedules,
createSchedule,
updateSchedule,
deleteSchedule,
toggleSchedule,
} from '../api/schedules';
/* ------------------------------------------------------------------ */
/* 常量 & 工具 */
/* ------------------------------------------------------------------ */
const STATUS_COLOR: Record<string, string> = {
success: 'success',
failed: 'error',
running: 'processing',
cancelled: 'warning',
};
const SCHEDULE_TYPE_LABEL: Record<string, string> = {
once: '一次性',
interval: '固定间隔',
daily: '每日',
weekly: '每周',
cron: 'Cron',
};
const INTERVAL_UNIT_LABEL: Record<string, string> = {
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 (
<Space>
<Form.Item name={['schedule_config', 'interval_value']} noStyle rules={[{ required: true }]}>
<InputNumber min={1} placeholder="间隔值" />
</Form.Item>
<Form.Item name={['schedule_config', 'interval_unit']} noStyle rules={[{ required: true }]}>
<Select style={{ width: 100 }} options={[
{ label: '分钟', value: 'minutes' },
{ label: '小时', value: 'hours' },
{ label: '天', value: 'days' },
]} />
</Form.Item>
</Space>
);
case 'daily':
return (
<Form.Item name={['schedule_config', 'daily_time']} label="执行时间" rules={[{ required: true }]}>
<TimePicker format="HH:mm" />
</Form.Item>
);
case 'weekly':
return (
<>
<Form.Item name={['schedule_config', 'weekly_days']} label="星期" rules={[{ required: true }]}>
<Checkbox.Group options={WEEKDAY_OPTIONS} />
</Form.Item>
<Form.Item name={['schedule_config', 'weekly_time']} label="执行时间" rules={[{ required: true }]}>
<TimePicker format="HH:mm" />
</Form.Item>
</>
);
case 'cron':
return (
<Form.Item name={['schedule_config', 'cron_expression']} label="Cron 表达式" rules={[{ required: true }]}>
<Input placeholder="0 4 * * *" />
</Form.Item>
);
default:
return null;
}
};
/* ------------------------------------------------------------------ */
/* 主组件 */
/* ------------------------------------------------------------------ */
const ScheduleTab: React.FC = () => {
const [data, setData] = useState<ScheduledTask[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<ScheduledTask | null>(null);
const [submitting, setSubmitting] = useState(false);
const [scheduleType, setScheduleType] = useState<string>('daily');
const [form] = Form.useForm();
/* 加载列表 */
const load = useCallback(async () => {
setLoading(true);
try {
setData(await fetchSchedules());
} catch {
message.error('加载调度任务失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
/* 打开创建 Modal */
const openCreate = () => {
setEditing(null);
form.resetFields();
form.setFieldsValue({
schedule_config: {
schedule_type: 'daily',
interval_value: 1,
interval_unit: 'hours',
daily_time: dayjs('04:00', 'HH:mm'),
weekly_days: [1],
weekly_time: dayjs('04:00', 'HH:mm'),
cron_expression: '0 4 * * *',
},
});
setScheduleType('daily');
setModalOpen(true);
};
/* 打开编辑 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 handleSubmit = async () => {
try {
const values = await form.validateFields();
setSubmitting(true);
// 将 dayjs 对象转为字符串
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,
};
if (editing) {
await updateSchedule(editing.id, {
name: values.name,
schedule_config: scheduleConfig,
});
message.success('调度任务已更新');
} else {
// 创建时使用默认 task_config简化实现
await createSchedule({
name: values.name,
task_codes: [],
task_config: {
tasks: [],
pipeline: 'api_full',
processing_mode: 'increment_only',
pipeline_flow: 'FULL',
dry_run: false,
window_mode: 'lookback',
window_start: null,
window_end: null,
window_split: null,
window_split_days: null,
lookback_hours: 24,
overlap_seconds: 600,
fetch_before_verify: false,
skip_ods_when_fetch_before_verify: false,
ods_use_local_json: false,
store_id: null,
dwd_only_tables: null,
force_full: false,
extra_args: {},
},
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 columns: ColumnsType<ScheduledTask> = [
{
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) => (
<Switch checked={enabled} onChange={() => 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 ? <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag> : '—',
},
{
title: '操作',
key: 'action',
width: 140,
render: (_: unknown, record: ScheduledTask) => (
<Space size="small">
<Button type="link" icon={<EditOutlined />} size="small" onClick={() => openEdit(record)}>
</Button>
<Popconfirm title="确认删除该调度任务?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />} size="small">
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<>
<div style={{ marginBottom: 12 }}>
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>
</Button>
</Space>
</div>
<Table<ScheduledTask>
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
pagination={false}
size="middle"
/>
{/* 创建/编辑 Modal */}
<Modal
title={editing ? '编辑调度任务' : '新建调度任务'}
open={modalOpen}
onOk={handleSubmit}
onCancel={() => setModalOpen(false)}
confirmLoading={submitting}
destroyOnClose
>
<Form form={form} layout="vertical" preserve={false}>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入调度任务名称' }]}>
<Input placeholder="例如:每日全量同步" />
</Form.Item>
<Form.Item name={['schedule_config', 'schedule_type']} label="调度类型" rules={[{ required: true }]}>
<Select
options={Object.entries(SCHEDULE_TYPE_LABEL).map(([value, label]) => ({ value, label }))}
onChange={(v: string) => setScheduleType(v)}
/>
</Form.Item>
<ScheduleConfigFields scheduleType={scheduleType} />
</Form>
</Modal>
</>
);
};
export default ScheduleTab;