因为找了半天都没找到合适的,索性自己写了个Cron表达式组件
因为找了半天都没找到合适的,索性自己写了个Cron表达式组件
最近项目中需要一个
corn
表达式的组件,然后去网上找了资源,发现个qnn-react-cron
,但是和我项目的环境不兼容,导入进来无法使用,后面又折腾了一阵时间,都没找到合适的,最终索性,自己写一个了,虽然浪费时间,但是也是无奈之举😂
一、了解Cron表达式
cron
表达式是用以控制固定时间、日期或者是间隔定期来执行任务,非常适合安排重复性任务,比如:发送消息通知、监控数据等等,一般的格式:0 0 18 ? * 4
(意思:周四晚上8点发通知)。
Cron字符
Cron
表达式的字符串一般都是由简单的几个字符和空格拼接而成,一般会有4-6
个空格,空格隔开的那部分就会形成时间子配置。
子配置 | 是否必填 | 范围 | 允许的特殊字符 | 说明 |
---|---|---|---|---|
秒 | 否 | 0-59 | * , - / | 不常用 |
分 | 是 | 0-59 | * , - / | |
小时 | 是 | 0-23 | * , - / | |
几号 | 是 | 1-31 | * , - / ? L W | |
月份 | 是 | 1-12或JAN - DEC | * , - / | |
星期几 | 是 | 0-7或SUN-SAT | * , - / ? L # | 0和7是周六,1是周日 |
年 | 否 | 1970-2099 | * , - / | 不常用 |
特殊字符说明:
*
:匹配任意的单个值,比如每秒、每分钟、每小时等。
,
:用以分隔列表项,比如星期:2,4,6
表示每周的一三五匹配。
-
:定义一个范围区间,小时:6-10
表示匹配早上6
点到早上10
点。
/
:定义间隔时间执行,小时:6/2
表示从早上6
点开始,每2
小时执行一次任务。
?
:只用于日期项和星期项,表示互斥关系,日期如果有值,星期则就要用?,反之同理,否则配置项会失效。
L
:只用于日期项和星期项,表示一个月倒数第几天或者一个星期倒数第几天,5L表示倒数第五天。
W
:只用于日期项,表示距离工作日(周一到周五)最近的一天。
#
:只用于星期项,5#3
对应于每个月的第三个星期五。
二、React Hooks中实现Cron表达式组件
本案例中的环境:React:18.2.0
; antd:4.24.5
;typescript
;
思路核心:就是根据用户点击不同的按钮来判断生成cron表达式,方法差不多就是根据上面的特殊字符来进行判断的,就是操作会很繁琐,我个人写的也没有将上面所有的情况全部囊括进去,仅满足的是日常非特殊业务完成而设计的,各位小伙伴可以依据个人实际情况,进行修改和添加,以创建更完善的版本;
废话不多说,直接上代码:
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { Input, InputNumber, Tabs, Radio, Row, Col, RadioChangeEvent, Card, Select } from 'antd'
import { CheckboxValueType } from 'antd/lib/checkbox/Group'
import classes from './index.module.scss'
import { cronRunTime } from './cronRunTime'
import { cronTimeEnum, IloopType, IperiodType, IpointType, IradioType } from './type'
// cron 类型
const cronType = ['second', 'minute', 'hour', 'day', 'month', 'week'] as cronTimeEnum[]
// 按钮基本样式
const radioStyle = {
display: 'block',
lineHeight: 2,
}
const Cron = (props: { cronExpression: string }, ref: any) => {
const { cronExpression } = props
const [cronText, setCronText] = useState('')
// 单选 选择执行类型
const [radioValue, setRadioValue] = useState<IradioType>({
second: 1,
minute: 1,
hour: 1,
day: 1,
month: 1,
week: 1,
})
// 周期
const [periodValue, setPeriodValue] = useState<IperiodType>({
second: { min: 1, max: 2 },
minute: { min: 1, max: 2 },
hour: { min: 0, max: 1 },
day: { min: 1, max: 2 },
month: { min: 1, max: 2 },
week: { min: 2, max: 3 },
})
// 从 ... 开始
const [loopValue, setLoopValue] = useState<IloopType>({
second: { start: 0, end: 1 },
minute: { start: 0, end: 1 },
hour: { start: 0, end: 1 },
day: { start: 1, end: 1 },
month: { start: 1, end: 1 },
week: { start: 1, end: 1 },
})
// 指定
const [pointValue, setPointValue] = useState<IpointType>({
second: [],
minute: [],
hour: [],
day: [],
month: [],
week: [],
})
// 最近运行时间
const [resultTime, setResultTime] = useState<string[]>([])
useEffect(() => {
if (cronExpression) {
cronComeShow(cronExpression)
} else {
resetCronState()
}
}, [props])
useEffect(() => {
createCron()
}, [radioValue, periodValue, loopValue])
// ref 上传函数
useImperativeHandle(ref, () => ({
refGetCron,
resetCronState,
}))
// ref 获取值
const refGetCron = () => cronText
// 重置或取消 -- 设置初始值
const resetCronState = () => {
setRadioValue({
second: 1,
minute: 1,
hour: 1,
day: 1,
month: 1,
week: 1,
})
setPeriodValue({
second: { min: 1, max: 2 },
minute: { min: 1, max: 2 },
hour: { min: 0, max: 1 },
day: { min: 1, max: 2 },
month: { min: 1, max: 2 },
week: { min: 2, max: 3 },
})
setLoopValue({
second: { start: 0, end: 1 },
minute: { start: 0, end: 1 },
hour: { start: 0, end: 1 },
day: { start: 1, end: 1 },
month: { start: 1, end: 1 },
week: { start: 1, end: 1 },
})
setPointValue({ second: [], minute: [], hour: [], day: [], month: [], week: [] })
setCronText('')
}
//生成cron
const createCron = () => {
let changeCron = {} as IradioType
cronType.forEach((item) => (changeCron = { ...changeCron, ...cronGenerator(item) }))
const { second, minute, hour, day, month, week } = changeCron
const cronText = second + ' ' + minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + week
setCronText(cronText)
setResultTime(cronRunTime(cronText))
}
/**
* cron生成器
* @param type
*/
const cronGenerator = (type: cronTimeEnum) => {
let srv = radioValue[type]
let period = periodValue[type]
let loop = loopValue[type]
let param = pointValue[type]
let data = ''
switch (srv) {
case 1:
data = '*'
break
case 2:
data = '?'
break
case 'point':
for (let v of param) {
data = data + v + ','
}
data = data.substring(0, data.length - 1)
break
case 'period':
data = period.min + '-' + period.max
break
case 'loop':
data = loop.start + '/' + loop.end
break
default:
data = '*'
}
return cronItemGenerator(type, data)
}
/**
* 对象生成器
* @param type
* @param data
* @returns {{second: *}|{minute: *}}
*/
const cronItemGenerator = (type: string, data: string): { [key: string]: string } => {
switch (type) {
case 'second':
return { second: data }
case 'minute':
return { minute: data }
case 'hour':
return { hour: data }
case 'day':
return { day: data }
case 'month':
return { month: data }
case 'week':
return { week: data }
default:
return {}
}
}
/**
* 生成 日期选择时间 options
* @param num
* @returns {lable:number,value:number}[]
*/
const createSelectOption = (num: number): { label: number; value: number }[] => {
let obj = [] as { label: number; value: number }[]
for (let i = 1; i <= num; i++) {
obj.push({
label: i,
value: i,
})
}
return obj
}
/**
* 生成 week options
* @param num
* @returns {lable:string,value:number}[]
*/
const createWeekSelect = (num: number): { label: string; value: number }[] => {
let obj = [] as { label: string; value: number }[]
const weeks = ['', '一', '二', '三', '四', '五', '六', '日']
for (let i = 1; i <= num; i++) {
obj.push({
label: `星期${weeks[i]}`,
value: i + 1 === 8 ? 1 : i + 1,
})
}
return obj
}
/**
* cron 回显
* @param cronText
*/
const cronComeShow = (cronText: string) => {
// 表达式回显
setCronText(cronText)
// 拆分表达式
const cronList = cronText.split(' ')
// 单选按钮
const changeRadio = {} as IradioType
// 周期数组范围回显
const initPeriodValue = {} as IperiodType
// 从...开始
const initLoopValue = {} as IloopType
// 指定
const initPointValue = {} as IpointType
// 单选按钮回显
// 按钮内部的内容回显
cronList.forEach((cron, index) => {
if (cron.indexOf('*') !== -1) {
} else if (cron.indexOf('-') !== -1) {
changeRadio[cronType[index]] = 'period'
initPeriodValue[cronType[index]] = {
min: Number(cron.split('-')[0]),
max: Number(cron.split('-')[1]),
}
} else if (cron.indexOf('/') !== -1) {
changeRadio[cronType[index]] = 'loop'
initLoopValue[cronType[index]] = {
start: Number(cron.split('/')[0]),
end: Number(cron.split('/')[1]),
}
} else if (cron.indexOf('?') !== -1) {
changeRadio[cronType[index]] = 2
} else {
// 指定
changeRadio[cronType[index]] = 'point'
initPointValue[cronType[index]] = cron.split(',').map((item) => Number(item))
}
})
setPeriodValue({
...periodValue,
...initPeriodValue,
})
setLoopValue({
...loopValue,
...initLoopValue,
})
setPointValue({
...pointValue,
...initPointValue,
})
setRadioValue({ ...radioValue, ...changeRadio })
}
/**
* 单选按钮选择
* @param e
* @param type
*/
const handleRadioChange = (e: RadioChangeEvent, type: string) => {
switch (type) {
case 'week':
setRadioValue({ ...radioValue, day: 2, ...cronItemGenerator(type, e.target.value) })
break
case 'day':
setRadioValue({ ...radioValue, week: 2, ...cronItemGenerator(type, e.target.value) })
break
default:
setRadioValue({
...radioValue,
...cronItemGenerator(type, e.target.value),
})
break
}
}
/**
* 指定时间选择
* @param checkedValues
* @param type
* @param fun
*/
const handleCheckboxChange = (
checkedValues: string | CheckboxValueType[],
type: string,
fun: Function,
) => {
// select双向绑定
fun({ ...pointValue, [type]: checkedValues })
// 选择时间自动跳到 指定
setRadioValue({
...radioValue,
[type]: 'point',
})
}
/**
* 周期 InputNumber按钮选择
* @param e
* @param type
* @param tar
*/
const handlePeriodChange = (e: number, type: cronTimeEnum, tar: string) => {
let data = periodValue
data[type] = tar === 'max' ? { max: e, min: data[type].min } : { max: data[type].max, min: e }
setPeriodValue({
...periodValue,
...cronItemGenerator(type, data[type] as unknown as string),
})
}
/**
* 从...开始 InputNumber按钮选择
* @param e
* @param type
* @param tar
*/
const handleLoopChange = (e: number, type: cronTimeEnum, tar: string) => {
let data = loopValue
data[type] =
tar == 'start' ? { start: e, end: data[type].end } : { start: data[type].start, end: e }
setLoopValue({
...loopValue,
...cronItemGenerator(type, data[type] as unknown as string),
})
}
return (
<div className={classes['cron-com']}>
<Tabs
type="card"
items={[
{
label: `秒`,
key: '1',
children: (
<Card className={classes['card-top']}>
<Radio.Group
onChange={(e) => {
handleRadioChange(e, 'second')
}}
value={radioValue['second']}
>
<Radio style={radioStyle} value={1}>
每秒执行
</Radio>
<Radio style={radioStyle} value="period">
周期从
<InputNumber
size="small"
min={0}
max={58}
value={periodValue.second.min}
onChange={(e) => handlePeriodChange(e as number, 'second', 'min')}
/>
-
<InputNumber
size="small"
min={1}
max={59}
value={periodValue.second.max}
onChange={(e) => handlePeriodChange(e as number, 'second', 'max')}
/>
秒
</Radio>
<Radio style={radioStyle} value="loop">
从
<InputNumber
size="small"
min={0}
max={58}
value={loopValue.second.start}
onChange={(e) => handleLoopChange(e as number, 'second', 'start')}
/>
秒开始,每
<InputNumber
size="small"
min={1}
max={59}
value={loopValue.second.end}
onChange={(e) => handleLoopChange(e as number, 'second', 'end')}
/>
秒执行一次
</Radio>
<Row>
<Radio style={radioStyle} value="point">
指定
</Radio>
<Col span={18}>
<Select
value={pointValue.second}
style={{ width: '80%', marginTop: 7 }}
mode="multiple"
allowClear
placeholder="请选择秒"
onChange={(value) => handleCheckboxChange(value, 'second', setPointValue)}
options={createSelectOption(59)}
/>
</Col>
</Row>
</Radio.Group>
</Card>
),
},
{
label: `分`,
key: '2',
children: (
<Card className={classes['card-top']}>
<Radio.Group
value={radioValue['minute']}
onChange={(e) => handleRadioChange(e, 'minute')}
>
<Radio style={radioStyle} value={1}>
每分执行
</Radio>
<Radio style={radioStyle} value="period">
周期从
<InputNumber
size="small"
min={0}
max={58}
value={periodValue.minute.min}
onChange={(e) => handlePeriodChange(e as number, 'minute', 'min')}
/>
-
<InputNumber
size="small"
min={2}
max={59}
value={periodValue.minute.max}
onChange={(e) => handlePeriodChange(e as number, 'minute', 'max')}
/>
分
</Radio>
<Radio style={radioStyle} value="loop">
从
<InputNumber
size="small"
min={0}
max={58}
value={loopValue.minute.start}
onChange={(e) => handleLoopChange(e as number, 'minute', 'start')}
/>
分开始,每
<InputNumber
size="small"
min={1}
max={58}
value={loopValue.minute.end}
onChange={(e) => handleLoopChange(e as number, 'minute', 'end')}
/>
分执行一次
</Radio>
<Row>
<Radio style={radioStyle} value="point">
指定
</Radio>
<Col span={18}>
<Select
value={pointValue.minute}
style={{ width: '80%', marginTop: 7 }}
mode="multiple"
allowClear
placeholder="请选择分钟"
onChange={(value) => handleCheckboxChange(value, 'minute', setPointValue)}
options={createSelectOption(59)}
/>
</Col>
</Row>
</Radio.Group>
</Card>
),
},
{
label: `时`,
key: '3',
children: (
<Card className={classes['card-top']}>
<Radio.Group
onChange={(e) => handleRadioChange(e, 'hour')}
value={radioValue['hour']}
>
<Radio style={radioStyle} value={1}>
每小时执行
</Radio>
<Radio style={radioStyle} value="period">
周期从
<InputNumber
size="small"
min={0}
max={22}
value={periodValue.hour.min}
onChange={(e) => handlePeriodChange(e as number, 'hour', 'min')}
/>
-
<InputNumber
size="small"
min={1}
max={23}
value={periodValue.hour.max}
onChange={(e) => handlePeriodChange(e as number, 'hour', 'max')}
/>
时
</Radio>
<Radio style={radioStyle} value="loop">
从
<InputNumber
size="small"
min={0}
max={22}
value={loopValue.hour.start}
onChange={(e) => handleLoopChange(e as number, 'hour', 'start')}
/>
时开始,每
<InputNumber
size="small"
min={1}
max={22}
value={loopValue.hour.end}
onChange={(e) => handleLoopChange(e as number, 'hour', 'end')}
/>
时执行一次
</Radio>
<Row>
<Radio style={radioStyle} value="point">
指定
</Radio>
<Col span={18}>
<Select
value={pointValue.hour}
style={{ width: '80%', marginTop: 7 }}
mode="multiple"
allowClear
placeholder="请选择小时"
onChange={(value) => handleCheckboxChange(value, 'hour', setPointValue)}
options={createSelectOption(23)}
/>
</Col>
</Row>
</Radio.Group>
</Card>
),
},
{
label: `日`,
key: '4',
children: (
<Card className={classes['card-top']}>
<Radio.Group
onChange={(e) => handleRadioChange(e, 'day')}
value={radioValue['day']}
>
<Radio style={radioStyle} value={1}>
每日执行
</Radio>
<Radio style={radioStyle} value={2}>
不指定
</Radio>
<Radio style={radioStyle} value="period">
周期从
<InputNumber
size="small"
min={1}
max={30}
value={periodValue.day.min}
onChange={(e) => handlePeriodChange(e as number, 'day', 'min')}
/>
-
<InputNumber
size="small"
min={2}
max={31}
value={periodValue.day.max}
onChange={(e) => handlePeriodChange(e as number, 'day', 'max')}
/>
日
</Radio>
<Radio style={radioStyle} value="loop">
从
<InputNumber
size="small"
min={1}
max={31}
value={loopValue.day.start}
onChange={(e) => handleLoopChange(e as number, 'day', 'start')}
/>
日开始,每
<InputNumber
size="small"
min={1}
max={31}
value={loopValue.day.end}
onChange={(e) => handleLoopChange(e as number, 'day', 'end')}
/>
日执行一次
</Radio>
<Row>
<Radio style={radioStyle} value="point">
指定
</Radio>
<Col span={18}>
<Select
value={pointValue.day}
style={{ width: '80%', marginTop: 7 }}
mode="multiple"
allowClear
placeholder="请选择日期"
onChange={(value) => handleCheckboxChange(value, 'day', setPointValue)}
options={createSelectOption(23)}
/>
</Col>
</Row>
</Radio.Group>
</Card>
),
},
{
label: `月`,
key: '5',
children: (
<Card className={classes['card-top']}>
<Radio.Group
onChange={(e) => handleRadioChange(e, 'month')}
value={radioValue['month']}
>
<Radio style={radioStyle} value={1}>
每月执行
</Radio>
<Radio style={radioStyle} value={2}>
不指定
</Radio>
<Radio style={radioStyle} value="period">
周期从
<InputNumber
size="small"
min={1}
max={11}
value={periodValue.month.min}
onChange={(e) => handlePeriodChange(e as number, 'month', 'min')}
/>
-
<InputNumber
size="small"
min={2}
max={12}
value={periodValue.month.max}
onChange={(e) => handlePeriodChange(e as number, 'month', 'max')}
/>
月
</Radio>
<Radio style={radioStyle} value="loop">
从
<InputNumber
size="small"
min={1}
max={12}
value={loopValue.month.start}
onChange={(e) => handleLoopChange(e as number, 'month', 'start')}
/>
月开始,每
<InputNumber
size="small"
min={1}
max={12}
value={loopValue.month.end}
onChange={(e) => handleLoopChange(e as number, 'month', 'end')}
/>
月执行一次
</Radio>
<Row>
<Radio style={radioStyle} value="point">
指定
</Radio>
<Col span={18}>
<Select
value={pointValue.month}
style={{ width: '80%', marginTop: 7 }}
mode="multiple"
allowClear
placeholder="请选择月份"
onChange={(value) => handleCheckboxChange(value, 'month', setPointValue)}
options={createSelectOption(12)}
/>
</Col>
</Row>
</Radio.Group>
</Card>
),
},
{
label: `周`,
key: '6',
children: (
<Card className={classes['card-top']}>
<Radio.Group
onChange={(e) => handleRadioChange(e, 'week')}
value={radioValue['week']}
>
<Radio style={radioStyle} value={1}>
每周执行
</Radio>
<Radio style={radioStyle} value={2}>
不指定
</Radio>
<Row>
<Col>
<Radio style={radioStyle} value="period">
周期从 周
</Radio>
</Col>
<Col>
<Select
style={{ marginTop: 7 }}
allowClear
value={periodValue.week.min}
placeholder="请选择周"
onChange={(e) => handlePeriodChange(e as number, 'week', 'min')}
options={createWeekSelect(7)}
/>
</Col>
<Col className={classes['week-font']}>- 周</Col>
<Col>
<Select
style={{ marginTop: 7 }}
allowClear
value={periodValue.week.max}
placeholder="请选择周"
onChange={(e) => handlePeriodChange(e as number, 'week', 'max')}
options={createWeekSelect(7)}
/>
</Col>
</Row>
<Row>
<Radio style={radioStyle} value="point">
指定
</Radio>
<Col span={18}>
<Select
value={pointValue.week}
style={{ width: '80%', marginTop: 7 }}
mode="multiple"
allowClear
placeholder="请选择周"
onChange={(value) => handleCheckboxChange(value, 'week', setPointValue)}
options={createWeekSelect(7)}
/>
</Col>
</Row>
</Radio.Group>
</Card>
),
},
]}
/>
<Card className={classes['corn-time']}>
<Row gutter={10}>
<Col className={classes['titile']}>时间表达式:</Col>
<Col>
<Input
placeholder="生成Cron"
style={{ width: 400, marginTop: 10 }}
readOnly
value={cronText}
/>
</Col>
</Row>
</Card>
<Card className={classes['run-time']}>
<p className="title">最近五次运行时间</p>
<ul>
{resultTime.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</Card>
</div>
)
}
export default forwardRef(Cron)
三、typescript类型
export interface IradioType {
second: number | string
minute: number | string
hour: number | string
day: number | string
month: number | string
week: number | string
}
// 基础 cron 数据类型
export type cronTimeEnum = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'week'
// 周期
export interface IperiodType {
second: minMaxType
minute: minMaxType
hour: minMaxType
day: minMaxType
month: minMaxType
week: minMaxType
}
interface minMaxType {
min: number
max: number
}
// 从 ... 开始
export interface IloopType {
second: startEndType
minute: startEndType
hour: startEndType
day: startEndType
month: startEndType
week: startEndType
}
interface startEndType {
start: number
end: number
}
// 指定
export interface IpointType {
second: number[]
minute: number[]
hour: number[]
day: number[]
month: number[]
week: number[]
}
四、效果图片

五、结语
因为代码确实有点长,字数已经快到两万四千个字符了,所以将最近运行时间的封装函数放到第二篇文章中😘
转载自:https://juejin.cn/post/7248249730478817337