全新定时任务管理

This commit is contained in:
whyour 2021-04-03 16:39:10 +08:00
parent 32be5a6591
commit 8a45599919
9 changed files with 861 additions and 37 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@
/src/.umi-test
/.env.local
.env
.history
/config
/log

View File

@ -3,15 +3,180 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import CronService from '../services/cron';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/cron',
'/crons',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.crontabs();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/crons',
celebrate({
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/run',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.run(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/disable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.disabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/enable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.enabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/log',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.log(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons',
celebrate({
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string(),
_id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/crons/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.remove(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/import',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CronService);
const data = await cookieService.import_crontab();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);

27
back/data/cron.ts Normal file
View File

@ -0,0 +1,27 @@
export class Crontab {
name?: string;
command: string;
schedule: string;
timestamp?: string;
created?: number;
saved?: boolean;
_id?: string;
status?: CrontabStatus;
constructor(options: Crontab) {
this.name = options.name;
this.command = options.command;
this.schedule = options.schedule;
this.saved = options.saved;
this._id = options._id;
this.created = options.created;
this.status = options.status || CrontabStatus.idle;
this.timestamp = new Date().toString();
}
}
export enum CrontabStatus {
'idle',
'running',
'disabled',
}

183
back/services/cron.ts Normal file
View File

@ -0,0 +1,183 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import DataStore from 'nedb';
import config from '../config';
import { Crontab, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util';
@Service()
export default class CronService {
private cronDb = new DataStore({ filename: config.cronDbFile });
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public async create(payload: Crontab): Promise<void> {
const tab = new Crontab(payload);
tab.created = new Date().valueOf();
tab.saved = false;
this.cronDb.insert(tab);
await this.set_crontab();
}
public async update(payload: Crontab): Promise<void> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Crontab({ ...doc, ...other });
tab.saved = false;
this.cronDb.update({ _id }, tab, { returnUpdatedDocs: true });
await this.set_crontab();
}
public async status(_id: string, stopped: boolean) {
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
}
public async remove(_id: string) {
this.cronDb.remove({ _id }, {});
await this.set_crontab();
}
public async crontabs(): Promise<Crontab[]> {
return new Promise((resolve) => {
this.cronDb
.find({})
.sort({ created: -1 })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async run(_id: string) {
this.cronDb.find({ _id }).exec((err, docs) => {
let res = docs[0];
this.logger.silly('Running job');
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + res.command);
let logFile = `${config.manualLogPath}${res._id}.log`;
fs.writeFileSync(logFile, `${new Date().toString()}\n\n`);
const cmd = spawn(res.command, { shell: true });
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.error(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
});
cmd.on('error', (err) => {
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
});
}
public async disabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
await this.set_crontab();
}
public async enabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
}
public async log(_id: string) {
let logFile = `${config.manualLogPath}${_id}.log`;
return getFileContentByName(logFile);
}
private make_command(tab: Crontab) {
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
return crontab_job_string;
}
private async set_crontab() {
const tabs = await this.crontabs();
var crontab_string = '';
tabs.forEach((tab) => {
if (tab.status !== CrontabStatus.disabled) {
crontab_string += tab.schedule;
crontab_string += ' ';
crontab_string += this.make_command(tab);
crontab_string += '\n';
}
});
this.logger.silly(crontab_string);
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
}
private reload_db() {
this.cronDb.loadDatabase();
}
public import_crontab() {
exec('crontab -l', (error, stdout, stderr) => {
var lines = stdout.split('\n');
var namePrefix = new Date().getTime();
lines.reverse().forEach((line, index) => {
line = line.replace(/\t+/g, ' ');
var regex = /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
var command = line.replace(regex, '').trim();
var schedule = line.replace(command, '').trim();
var is_valid = false;
try {
is_valid = cron_parser.parseString(line).expressions.length > 0;
} catch (e) {}
if (command && schedule && is_valid) {
var name = namePrefix + '_' + index;
this.cronDb.findOne({ command, schedule }, (err, doc) => {
if (err) {
throw err;
}
if (!doc) {
this.create({ name, command, schedule });
} else {
doc.command = command;
doc.schedule = schedule;
this.update(doc);
}
});
}
});
});
}
public autosave_crontab() {
return this.set_crontab();
}
}

View File

@ -29,13 +29,14 @@
"celebrate": "^13.0.3",
"codemirror": "^5.59.4",
"cors": "^2.8.5",
"cron-parser": "^3.3.0",
"darkreader": "^4.9.27",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-jwt": "^6.0.0",
"got": "^11.8.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.10.6",
"nedb": "^1.8.0",
"node-fetch": "^2.6.1",
"qrcode.react": "^1.0.1",
"react-codemirror2": "^7.2.1",
@ -51,6 +52,7 @@
"@types/express": "^4.17.8",
"@types/express-jwt": "^6.0.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/nedb": "^1.8.11",
"@types/node": "^14.11.2",
"@types/node-fetch": "^2.5.8",
"@types/qrcode.react": "^1.0.1",

74
shell/api.sh Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env bash
get_token() {
echo $AuthConf
local authInfo=$(cat $AuthConf)
token=$(get_json_value "$authInfo" "token")
}
get_json_value() {
local json=$1
local key=$2
if [[ -z "$3" ]]; then
local num=1
else
local num=$3
fi
local value=$(echo "${json}" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'${key}'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p)
echo ${value}
}
add_cron_api() {
local currentTimeStamp=$(date +%s%3)
if [ $# -eq 1 ]; then
local schedule=$(echo "$1" | awk -F ": " '{print $1}')
local command=$(echo "$1" | awk -F ": " '{print $2}')
local name=$(echo "$1" | awk -F ": " '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local api=$(curl "http://localhost:5678/api/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed)
echo $api
code=$(get_json_value $api "code")
if [[ $code == 200 ]]; then
echo -e "$name 添加成功"
else
echo -e "$name 添加失败"
fi
}
del_cron_api() {
local id=$1
local currentTimeStamp=$(date +%s%3)
local api=$(curl "http://localhost:5678/api/crons/$id?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
echo $api
code=$(get_json_value $api "code")
if [[ $code == 200 ]]; then
echo -e "$name 删除成功"
else
echo -e "$name 删除失败"
fi
}

View File

@ -16,6 +16,7 @@ ScriptsDir=$ShellDir/scripts
ConfigDir=$ShellDir/config
FileConf=$ConfigDir/config.sh
CookieConf=$ConfigDir/cookie.sh
AuthConf=$ConfigDir/auth.json
ExtraShell=$ConfigDir/extra.sh
FileConfSample=$ShellDir/sample/config.sh.sample
ListCronSample=$ShellDir/sample/crontab.list.sample
@ -44,6 +45,8 @@ Import_Conf() {
fi
[ -f $CookieConf ] && . $CookieConf
[ -f $FileConf ] && . $FileConf
. $ShellDir/shell/api.sh
}
# 更新shell
@ -198,7 +201,7 @@ Git_Pull_Scripts_Next() {
Diff_Cron() {
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*$ShellJs (j[drx]_\w+)\.*|\1|" | sort -u >$ListCurrentTask
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*ID=(.*)$ShellJs (j[drx]_\w+)\.*|\1|" | sort -u >$ListCurrentTask
if [ -s $ListCurrentTask ]; then
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
else
@ -218,7 +221,7 @@ Del_Cron() {
echo
JsDrop=$(cat $ListJsDrop)
for Cron in $JsDrop; do
perl -i -ne "{print unless / $Cron( |$)/}" $ListCronCurrent
del_cron_api "$Cron"
done
crontab $ListCronCurrent
echo -e "成功删除失效的脚本与定时任务\n"
@ -234,7 +237,8 @@ Add_Cron() {
if [[ $Cron == jd_bean_sign ]]; then
echo "4 0,9 * * * $ShellJs $Cron" >> $ListCronCurrent
else
cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+)node */scripts/(j[drx]_\w+)\.js.+|\1$ShellJs \2|" >> $ListCronCurrent
param=$(cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+)node */scripts/(j[drx]_\w+)\.js.+|\1\: $ShellJs \2: \2|")
add_cron_api "$param"
fi
done
@ -274,6 +278,8 @@ echo -e "--------------------------------------------------------------\n"
Import_Conf
Random_Pull_Cron
get_token
# 更新shell
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
Git_Pull_Shell

View File

@ -1,39 +1,311 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { Button, notification, Modal } from 'antd';
import {
Button,
notification,
Modal,
Table,
Tag,
Space,
Tooltip,
Dropdown,
Menu,
Typography,
} from 'antd';
import {
ClockCircleOutlined,
SyncOutlined,
CloseCircleOutlined,
FileTextOutlined,
EllipsisOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
EditOutlined,
StopOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { request } from '@/utils/http';
import CronModal from './modal';
const { Text } = Typography;
enum CrontabStatus {
'idle',
'running',
'disabled',
}
const Crontab = () => {
const columns = [
{
title: '任务名',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
render: (text: string, record: any) => (
<span>{record.name || record._id}</span>
),
},
{
title: '任务',
dataIndex: 'command',
key: 'command',
align: 'center' as const,
},
{
title: '任务定时',
dataIndex: 'schedule',
key: 'schedule',
align: 'center' as const,
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any) => (
<>
{record.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
</Tag>
)}
{record.status === CrontabStatus.running && (
<Tag icon={<SyncOutlined spin />} color="processing">
</Tag>
)}
{record.status === CrontabStatus.disabled && (
<Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
)}
</>
),
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => (
<Space size="middle">
<Tooltip title="运行">
<a
onClick={() => {
runCron(record);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
<Tooltip title="日志">
<a
onClick={() => {
logCron(record);
}}
>
<FileTextOutlined />
</a>
</Tooltip>
<MoreBtn key="more" record={record} />
</Space>
),
},
];
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState('');
const [value, setValue] = useState();
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedCron, setEditedCron] = useState();
const getConfig = () => {
const getCrons = () => {
setLoading(true);
request
.get(`${config.apiPrefix}config/crontab`)
.then((data) => {
.get(`${config.apiPrefix}crons`)
.then((data: any) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const updateConfig = () => {
const addCron = () => {
setIsModalVisible(true);
};
const editCron = (record: any) => {
setEditedCron(record);
setIsModalVisible(true);
};
const delCron = (record: any) => {
Modal.confirm({
title: '确认删除',
content: (
<>
<Text type="warning">{record.name}</Text>
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons`, {
data: { _id: record._id },
})
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: '删除成功',
});
getCrons();
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const runCron = (record: any) => {
Modal.confirm({
title: '确认运行',
content: (
<>
<Text type="warning">{record.name}</Text>
</>
),
onOk() {
request
.get(`${config.apiPrefix}crons/${record._id}/run`)
.then((data: any) => {
getCrons();
});
},
onCancel() {
console.log('Cancel');
},
});
};
const enabledOrDisabledCron = (record: any) => {
Modal.confirm({
title: '确认禁用',
content: (
<>
<Text type="warning">{record.name}</Text>
</>
),
onOk() {
request
.get(
`${config.apiPrefix}crons/${record._id}/${
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
}`,
{
data: { _id: record._id },
},
)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: '禁用成功',
});
getCrons();
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const logCron = (record: any) => {
request
.post(`${config.apiPrefix}save`, {
data: { content: value, name: 'crontab.list' },
})
.then((data) => {
notification.success({
message: data.msg,
.get(`${config.apiPrefix}crons/${record._id}/log`)
.then((data: any) => {
Modal.info({
width: 650,
title: `${record.name || record._id}`,
content: (
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
{data.data || '暂无日志'}
</pre>
),
onOk() {},
});
});
};
const MoreBtn: React.FC<{
record: any;
}> = ({ record }) => (
<Dropdown
arrow
trigger={['click', 'hover']}
overlay={
<Menu onClick={({ key }) => action(key, record)}>
<Menu.Item key="edit" icon={<EditOutlined />}>
</Menu.Item>
<Menu.Item
key="enableordisable"
icon={
record.status === CrontabStatus.disabled ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)
}
>
{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
</Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />}>
</Menu.Item>
</Menu>
}
>
<a>
<EllipsisOutlined />
</a>
</Dropdown>
);
const action = (key: string | number, record: any) => {
switch (key) {
case 'edit':
editCron(record);
break;
case 'enableordisable':
enabledOrDisabledCron(record);
break;
case 'delete':
delCron(record);
break;
default:
break;
}
};
const handleCancel = (needUpdate?: boolean) => {
setIsModalVisible(false);
if (needUpdate) {
getCrons();
}
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
@ -44,16 +316,17 @@ const Crontab = () => {
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
getCrons();
}, []);
return (
<PageContainer
className="code-mirror-wrapper"
title="crontab.list"
title="定时任务"
loading={loading}
extra={[
<Button key="1" type="primary" onClick={updateConfig}>
<Button key="2" type="primary" onClick={() => addCron()}>
</Button>,
]}
header={{
@ -68,23 +341,24 @@ const Crontab = () => {
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
<Table
columns={columns}
pagination={{
hideOnSinglePage: true,
showSizeChanger: true,
defaultPageSize: 20,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
dataSource={value}
rowKey="pin"
size="middle"
bordered
scroll={{ x: 768 }}
/>
<CronModal
visible={isModalVisible}
handleCancel={handleCancel}
cron={editedCron}
/>
</PageContainer>
);

View File

@ -0,0 +1,92 @@
import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import cronParse from 'cron-parser';
const CronModal = ({
cron,
handleCancel,
visible,
}: {
cron?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const handleOk = async (values: any) => {
const method = cron ? 'put' : 'post';
const payload = { ...values };
if (cron) {
payload._id = cron._id;
}
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
if (code === 200) {
notification.success({
message: cron ? '更新Cron成功' : '添加Cron成功',
});
} else {
notification.error({
message: data,
});
}
handleCancel(true);
};
useEffect(() => {
if (cron) {
form.setFieldsValue(cron);
}
}, [cron]);
return (
<Modal
title={cron ? '编辑定时' : '新建定时'}
visible={visible}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
destroyOnClose
>
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}>
<Form.Item name="name" label="名称">
<Input />
</Form.Item>
<Form.Item name="command" label="任务" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
name="schedule"
label="时间"
rules={[
{ required: true },
{
validator: (rule, value) => {
if (cronParse.parseString(value).expressions.length > 0) {
return Promise.resolve();
} else {
return Promise.reject('Cron表达式格式有误');
}
},
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default CronModal;