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

17 篇文章 0 订阅
9 篇文章 0 订阅

前言

最近在写的一个分布式调度系统,后端同学需要让我传入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);
  1. 拼接得到完整的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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
要在 React + TypeScript 中通过 JSON 动态加载组件并生成页面,你可以按照以下步骤进行操作: 1. 创建一个 JSON 文件,包含页面的结构和组件信息。例如,可以使用类似以下结构的 JSON: ```json { "pages": [ { "name": "Page1", "components": [ { "name": "Component1", "props": { "text": "Hello, World!" } }, { "name": "Component2", "props": { "data": [1, 2, 3, 4, 5] } } ] }, { "name": "Page2", "components": [ { "name": "Component3", "props": { "title": "Welcome to Page 2" } } ] } ] } ``` 2. 在你的 React + TypeScript 应用程序中,创建一个组件来加载和渲染 JSON 文件中定义的页面和组件。例如,可以使用以下代码: ```tsx import React, { useEffect, useState, ReactNode } from 'react'; import { ComponentType } from 'react'; type ComponentProps = { name: string; props: object; }; type Page = { name: string; components: ComponentProps[]; }; type PageData = { pages: Page[]; }; const DynamicPage: React.FC = () => { const [pageData, setPageData] = useState<PageData | null>(null); useEffect(() => { // 使用异步请求加载 JSON 文件 const fetchData = async () => { const response = await fetch('path/to/your/json/file.json'); const data = await response.json(); setPageData(data); }; fetchData(); }, []); const renderComponent = (component: ComponentProps): ReactNode => { const Component: ComponentType<any> = require(`./components/${component.name}`).default; return <Component {...component.props} />; }; if (!pageData) { return <div>Loading...</div>; } return ( <> {pageData.pages.map((page, index) => ( <div key={index}> <h1>{page.name}</h1> {page.components.map((component, componentIndex) => ( <div key={componentIndex}>{renderComponent(component)}</div> ))} </div> ))} </> ); }; export default DynamicPage; ``` 在上面的代码中,我们首先使用 `useEffect` 和 `fetch` 异步加载 JSON 文件,并将其保存在 `pageData` 状态中。 然后,我们定义了一个 `renderComponent` 函数,根据组件的名称动态导入相应的组件,并传递组件的属性。这里我们假设组件文件位于 `./components` 目录下。 最后,我们通过遍历 `pageData.pages` 和 `page.components` 来渲染页面和组件。 请确保根据你的实际情况修改代码中的路径和组件导入逻辑。 希望这对你有所帮助!如果有任何进一步的问题,请随时问我。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿泽不会飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值