likes
comments
collection
share

因为找了半天都没找到合适的,索性自己写了个Cron表达式组件

作者站长头像
站长
· 阅读数 16

因为找了半天都没找到合适的,索性自己写了个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.5typescript;

思路核心:就是根据用户点击不同的按钮来判断生成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[]
}

四、效果图片

因为找了半天都没找到合适的,索性自己写了个Cron表达式组件

五、结语

因为代码确实有点长,字数已经快到两万四千个字符了,所以将最近运行时间的封装函数放到第二篇文章中😘

Cron表达式,如何生成最近运行时间?(开箱即用、无需配置