定时任务支持 @once 和 @boot 任务

This commit is contained in:
whyour
2025-02-20 02:18:59 +08:00
parent 496918131f
commit 8173075b67
11 changed files with 242 additions and 118 deletions
+4 -1
View File
@@ -496,5 +496,8 @@
"NPM 镜像源": "NPM Mirror Source",
"PyPI 镜像源": "PyPI Mirror Source",
"alpine linux 镜像源": "Alpine Linux Mirror Source",
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute"
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute",
"常规定时": "Normal Timing",
"手动运行": "Manual Run",
"开机运行": "Boot Run"
}
+5 -1
View File
@@ -496,5 +496,9 @@
"NPM 镜像源": "NPM 镜像源",
"PyPI 镜像源": "PyPI 镜像源",
"alpine linux 镜像源": "alpine linux 镜像源",
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行",
"常规定时": "常规定时",
"手动运行": "手动运行",
"开机运行": "开机运行"
}
+8 -2
View File
@@ -263,7 +263,9 @@ const Crontab = () => {
},
},
render: (text, record) => {
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
return record.nextRunTime
? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')
: '-';
},
},
{
@@ -396,9 +398,13 @@ const Crontab = () => {
setValue(
data.map((x) => {
const specialSchedules = ['@once', '@boot'];
const nextRunTime = specialSchedules.includes(x.schedule)
? null
: getCrontabsNextDate(x.schedule, x.extra_schedules);
return {
...x,
nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
nextRunTime,
subscription: subscriptionMap?.[x.sub_id],
};
}),
+112 -60
View File
@@ -1,12 +1,30 @@
import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Button, Space } from 'antd';
import { Modal, message, Input, Form, Button, Space, Select } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import cronParse from 'cron-parser';
import EditableTagGroup from '@/components/tag';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
enum ScheduleType {
Normal = 'normal',
Once = 'once',
Boot = 'boot',
}
const scheduleTypeMap = {
[ScheduleType.Normal]: '',
[ScheduleType.Once]: '@once',
[ScheduleType.Boot]: '@boot',
};
const getScheduleType = (schedule?: string): ScheduleType => {
if (schedule?.startsWith('@once')) return ScheduleType.Once;
if (schedule?.startsWith('@boot')) return ScheduleType.Boot;
return ScheduleType.Normal;
};
const CronModal = ({
cron,
handleCancel,
@@ -18,15 +36,26 @@ const CronModal = ({
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [scheduleType, setScheduleType] = useState<ScheduleType>(
cron ? getScheduleType(cron.schedule) : ScheduleType.Normal,
);
const handleOk = async (values: any) => {
setLoading(true);
const method = cron?.id ? 'put' : 'post';
const payload = { ...values };
if (cron?.id) {
payload.id = cron.id;
}
try {
const method = cron?.id ? 'put' : 'post';
const payload = {
...values,
schedule:
scheduleType !== ScheduleType.Normal
? scheduleTypeMap[scheduleType]
: values.schedule,
};
if (cron?.id) {
payload.id = cron.id;
}
const { code, data } = await request[method](
`${config.apiPrefix}crons`,
payload,
@@ -38,74 +67,53 @@ const CronModal = ({
);
handleCancel(data);
}
setLoading(false);
} catch (error: any) {
console.error(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
setScheduleType(getScheduleType(cron?.schedule));
}, [cron, visible]);
return (
<Modal
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}
open={visible}
forceRender
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={cron}
>
<Form.Item
name="name"
label={intl.get('名称')}
rules={[{ required: true, whitespace: true }]}
>
<Input placeholder={intl.get('请输入任务名称')} />
</Form.Item>
<Form.Item
name="command"
label={intl.get('命令/脚本')}
rules={[{ required: true, whitespace: true }]}
>
<Input.TextArea
rows={4}
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get(
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
)}
/>
</Form.Item>
const handleScheduleTypeChange = (type: ScheduleType) => {
setScheduleType(type);
form.setFieldValue('schedule', '');
};
const renderScheduleOptions = () => (
<Select defaultValue={scheduleType} value={scheduleType} onChange={handleScheduleTypeChange}>
<Select.Option value={ScheduleType.Normal}>
{intl.get('常规定时')}
</Select.Option>
<Select.Option value={ScheduleType.Once}>
{intl.get('手动运行')}
</Select.Option>
<Select.Option value={ScheduleType.Boot}>
{intl.get('开机运行')}
</Select.Option>
</Select>
);
const renderScheduleFields = () => {
if (scheduleType !== ScheduleType.Normal) return null;
return (
<>
<Form.Item
name="schedule"
label={intl.get('定时规则')}
rules={[
{ required: true },
{
validator: (rule, value) => {
validator: (_, value) => {
if (!value || cronParse.parseExpression(value).hasNext()) {
return Promise.resolve();
} else {
return Promise.reject(intl.get('Cron表达式格式有误'));
}
return Promise.reject(intl.get('Cron表达式格式有误'));
},
},
]}
@@ -136,14 +144,58 @@ const CronModal = ({
))}
<Form.Item>
<a onClick={() => add({ schedule: '' })}>
<PlusOutlined />
{intl.get('新增定时规则')}
<PlusOutlined /> {intl.get('新增定时规则')}
</a>
</Form.Item>
<Form.ErrorList errors={errors} />
</>
)}
</Form.List>
</>
);
};
return (
<Modal
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}
open={visible}
forceRender
centered
maskClosable={false}
onOk={() => form.validateFields().then(handleOk)}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={cron}
>
<Form.Item
name="name"
label={intl.get('名称')}
rules={[{ required: true, whitespace: true }]}
>
<Input placeholder={intl.get('请输入任务名称')} />
</Form.Item>
<Form.Item
name="command"
label={intl.get('命令/脚本')}
rules={[{ required: true, whitespace: true }]}
>
<Input.TextArea
rows={4}
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get(
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
)}
/>
</Form.Item>
<Form.Item label={intl.get('定时类型')} required>
{renderScheduleOptions()}
</Form.Item>
{renderScheduleFields()}
<Form.Item name="labels" label={intl.get('标签')}>
<EditableTagGroup />
</Form.Item>
@@ -155,7 +207,7 @@ const CronModal = ({
)}
rules={[
{
validator(rule, value) {
validator(_, value) {
if (
value &&
(value.includes(' task ') || value.startsWith('task '))
@@ -183,7 +235,7 @@ const CronModal = ({
)}
rules={[
{
validator(rule, value) {
validator(_, value) {
if (
value &&
(value.includes(' task ') || value.startsWith('task '))