react+ts手写cron表达式转换组件

276 阅读4分钟

前言

最近在写的一个分布式调度系统,后端同学需要让我传入cron表达式,给调度接口传参。我去学习,了解了cron表达式的用法,发现有3个通用的表达式刚好符合我们的需求:

需求

  1. 每天 xx 的时间:

0 11 20 * * ?

上面是每天20:11的cron表达式

  1. 每周的 xxx 星期 的 xxx 时间

0 14 20 * * WED,THU

上面是 每周星期三,星期四20:14的cron表达式

  1. 每周的 xxx号 的 xxx时间

0 15 20 3,7 * ?

上面是 每月的3,7号20:15的cron表达式

这三个表达式刚好符合我们的需求,并且每个符号表达的意思也很直观。那么,话不多说,直接开写!

环境

  • react

    • 我的版本:"react": "18.2.0",用的函数组件+react hooks
  • moment

    • npm install moment
  • semi-design(组件库)

    • npm install semi-design
  • typeScript

实现

数据

utils下创建cron.ts,对组件所用到的数据进行统一的管理:

export const dayOfTheWeekData = [
  { key: 'MON', label: '星期一' },
  { key: 'TUE', label: '星期二' },
  { key: 'WED', label: '星期三' },
  { key: 'THU', label: '星期四' },
  { key: 'FRI', label: '星期五' },
  { key: 'SAT', label: '星期六' },
  { key: 'SUN', label: '星期天' }
];

export const dayOfTheWeekOption = [
  { key: '1', label: '星期一' },
  { key: '2', label: '星期二' },
  { key: '3', label: '星期三' },
  { key: '4', label: '星期四' },
  { key: '5', label: '星期五' },
  { key: '6', label: '星期六' },
  { key: '7', label: '星期天' }
];

export const monthOption = [
  { key: '1', label: '一月' },
  { key: '2', label: '二月' },
  { key: '3', label: '三月' },
  { key: '4', label: '四月' },
  { key: '5', label: '五月' },
  { key: '6', label: '六月' },
  { key: '7', label: '七月' },
  { key: '8', label: '八月' },
  { key: '9', label: '九月' },
  { key: '10', label: '十月' },
  { key: '11', label: '十一月' },
  { key: '12', label: '十二月' }
];

//获取dayOfTheMonthOption的每月对象
function getDayOfTheMonthOption() {
  const days = [];
  for (let i = 1; i < 32; i += 1) {
    days.push({ key: i.toString(), label: i.toString().concat('号') });
  }
  return days;
}

export const dayOfTheMonthOption = getDayOfTheMonthOption();

组件

到了组件的具体实现,个人感觉我写的注释挺全的,就单挑几个核心重点讲下:

时间转换函数(handleTimeChange)

  //时间选择函数
  const handleTimeChange = (time: moment.Moment | null) => {
    setSelectTime(time);
    if (!time) return;
    const currentCron = expression ? expression.split(' ') : [];
    const [seconds, , , dayOfMonth, month1, dayOfWeek] = currentCron;
    const minutes = moment(time).minutes().toString(); //获取分钟
    const hours = moment(time).hours().toString(); //获取小时
    let result = null;
    if (!Number.isNaN(Number(hours)) && !Number.isNaN(Number(minutes))) {
      const minutesAndHour = seconds
        .concat(space)
        .concat(minutes)
        .concat(space)
        .concat(hours)
        .concat(space);
      if (defaultTimeType === 'everyDay') result = minutesAndHour.concat('* * ?');
      if (defaultTimeType !== 'everyDay')
        result = minutesAndHour
          .concat(dayOfMonth)
          .concat(space)
          .concat(month1)
          .concat(space)
          .concat(dayOfWeek);
    }
    if (result) onChange?.(result);
    setExpression(result);
  };
  1. 使用moment函数将time转成数字类型
    1. minutes = moment(time).minutes().toString(); //获取分钟
    2. hours = moment(time).hours().toString(); //获取小时
  2. 获取时间cron字符串minutesAndHour:
const minutesAndHour = seconds.concat(space).concat(minutes).concat(space).concat(hours).concat(space);

3. 拼接得到完整的cron表达式: 1. defaultTimeType === 'everyDay'

result = minutesAndHour.concat('* * ?');

  1. defaultTimeType !== 'everyDay'
result = minutesAndHour.concat(dayOfMonth).concat(space).concat(month1).concat(space).concat(dayOfWeek);

日期转换函数(handleSelectChange)

setSelectedValue(data);
const selectValues = data.join(',');
const currentCron = expression ? expression.split(' ') : [];
const [seconds, minutes, hours, dayOfMonth, month1, dayOfWeek] = currentCron;
let result = '';
if (defaultTimeType === 'everyWeek') {
  result = seconds
    .concat(space)
    .concat(minutes)
    .concat(space)
    .concat(hours)
    .concat(space)
    .concat(dayOfMonth)
    .concat(space)
    .concat(month1)
    .concat(space)
    .concat(selectValues);
}
if (defaultTimeType === 'everyMonth') {
  result = seconds
    .concat(space)
    .concat(minutes)
    .concat(space)
    .concat(hours)
    .concat(space)
    .concat(data.length ? selectValues : '*')
    .concat(space)
    .concat(month1)
    .concat(space)
    .concat(dayOfWeek);
}
if (selectTime) onChange?.(result);
setExpression(result);
  1. defaultTimeType === 'everyWeek'
result = seconds.concat(space).concat(minutes).concat(space).concat(hours).concat(space).concat(dayOfMonth).concat(space).concat(month1).concat(space).concat(selectValues);
  1. defaultTimeType === 'everyMonth'
result = seconds.concat(space).concat(minutes).concat(space).concat(hours).concat(space).concat(data.length ? selectValues : '*').concat(space).concat(month1).concat(space).concat(dayOfWeek);

组件全部代码(CronInput)

import { ConfigProvider, TimePicker } from '@douyinfe/semi-ui';
import { Fragment, useState } from 'react';
import { Select } from '@douyinfe/semi-ui';
import moment from 'moment';
//引入数据
import { dayOfTheMonthOption, dayOfTheWeekData } from '@/utils/cron';

const { Option } = Select;
const format = 'HH:mm';
const defaultCron = '0 * * * * ?';
const space = ' '; //空格
//类型选择
const timeTypes = [
  { key: 'everyDay', label: '每天' },
  { key: 'everyWeek', label: '每周' },
  { key: 'everyMonth', label: '每月' }
];

interface Props {
  onChange?: (cron?: string) => void;
}
const CronInput: React.FC<Props> = ({ onChange }) => {
  const [defaultTimeType, setDefaultTimeType] = useState(timeTypes[0].key); //选择类型
  const [selectedValue, setSelectedValue] = useState<[]>([]); //日期,多选数组
  const [selectTime, setSelectTime] = useState<any>(null); //时间
  const [expression, setExpression] = useState<string | null>(defaultCron); //bzd

  //类型选择函数
  const handleTimeTypeChange = (selectValue: string) => {
    setDefaultTimeType(selectValue);
    setSelectTime(null);
    setSelectedValue([]);
    setExpression(defaultCron);
  };

  //时间选择函数
  const handleTimeChange = (time: moment.Moment | null) => {
    setSelectTime(time);
    if (!time) return;
    const currentCron = expression ? expression.split(' ') : [];
    const [seconds, , , dayOfMonth, month1, dayOfWeek] = currentCron;
    const minutes = moment(time).minutes().toString(); //获取分钟
    const hours = moment(time).hours().toString(); //获取小时
    let result = null;
    if (!Number.isNaN(Number(hours)) && !Number.isNaN(Number(minutes))) {
      const minutesAndHour = seconds
        .concat(space)
        .concat(minutes)
        .concat(space)
        .concat(hours)
        .concat(space);
      if (defaultTimeType === 'everyDay') result = minutesAndHour.concat('* * ?');
      if (defaultTimeType !== 'everyDay')
        result = minutesAndHour
          .concat(dayOfMonth)
          .concat(space)
          .concat(month1)
          .concat(space)
          .concat(dayOfWeek);
    }
    if (result) onChange?.(result);
    setExpression(result);
  };

  const handleSelectChange = (data: []) => {
    setSelectedValue(data);
    const selectValues = data.join(',');
    const currentCron = expression ? expression.split(' ') : [];
    const [seconds, minutes, hours, dayOfMonth, month1, dayOfWeek] = currentCron;
    let result = '';
    if (defaultTimeType === 'everyWeek') {
      result = seconds
        .concat(space)
        .concat(minutes)
        .concat(space)
        .concat(hours)
        .concat(space)
        .concat(dayOfMonth)
        .concat(space)
        .concat(month1)
        .concat(space)
        .concat(selectValues);
    }
    if (defaultTimeType === 'everyMonth') {
      result = seconds
        .concat(space)
        .concat(minutes)
        .concat(space)
        .concat(hours)
        .concat(space)
        .concat(data.length ? selectValues : '*')
        .concat(space)
        .concat(month1)
        .concat(space)
        .concat(dayOfWeek);
    }
    if (selectTime) onChange?.(result);
    setExpression(result);
  };

  const RenderSelect = ({
    placeholder,
    data = []
  }: {
    placeholder: string;
    data: { key: string; label: string }[];
  }) => {
    return (
      <Fragment>
        <Select
          multiple
          placeholder={placeholder}
          onChange={(val: any) => handleSelectChange(val)}
          style={{ marginRight: '16px', width: 'auto' }}
          value={selectedValue}
        >
          {data.map((item: { key: string; label: string }) => (
            <Option key={item.key} value={item.key}>
              {item.label}
            </Option>
          ))}
        </Select>
        <ConfigProvider>
          <TimePicker
            value={selectTime && moment(selectTime, format).toDate()}
            format={format}
            placeholder="请选择时间"
            onChange={(val: any) => handleTimeChange(val)}
          />
        </ConfigProvider>
      </Fragment>
    );
  };
  return (
    <>
      <div className={'cron'}>
        <Select
          // role="cron-type"
          style={{ marginRight: '16px', width: 'auto' }}
          placeholder="请选择类型"
          onChange={(val: any) => handleTimeTypeChange(val)}
          value={defaultTimeType}
        >
          {timeTypes.map((item) => (
            <Option key={item.key} value={item.key}>
              {' '}
              {item.label}
            </Option>
          ))}
        </Select>
        {defaultTimeType === 'everyDay' && (
          <ConfigProvider>
            <TimePicker
              value={selectTime && moment(selectTime, format).toDate()}
              format={format}
              placeholder="请选择时间"
              onChange={(val: any) => handleTimeChange(val)}
            />
          </ConfigProvider>
        )}
        {defaultTimeType === 'everyWeek' && (
          <RenderSelect data={dayOfTheWeekData} placeholder="请选择星期" />
        )}
        {defaultTimeType === 'everyMonth' && (
          <RenderSelect data={dayOfTheMonthOption} placeholder="请选择日期" />
        )}
      </div>
    </>
  );
};

export default CronInput;

使用与效果

使用

使用方法很简单,接收onChange传来的cron表达式即可:

const App: FC<IProps> = (props) => {
  const { datas = [] } = props;
  let [value, setValue] = useState<string>();
  return (
    <div>
      <CronInput onChange={(cron) => setValue(cron)} />
      <div>{value}</div>
    </div>
  );
};

效果

  1. 每天

image.png

  1. 每周

image.png

  1. 每月

image.png