vue项目cron组件

2,696 阅读2分钟

vue项目中需要用到cron组件,在网上找了好多资料,但是都不符合我们要求。有的比较复杂,有的文档不太友好,有的不支持自定义,后面通过AI写了一个简单的cron组件。

先推荐一下网上的cron组件:

1.vcrontab:支持解析/反解析 cron 表达式,生成最近五次的符合条件时间,依赖 vue2 和 element-ui 效果图:

1.png

2.vue-cron

效果图:

2.png

3.vue-js-cron

效果图: 3.png

AI编写的cron组件:使用cron-parser

效果图:

4.png

代码:

组件:

<template>
  <div class="simple-cron">
    <!-- 模式切换 -->
    <el-radio-group v-model="mode" class="mode-switch">
      <el-radio-button label="hourly">时间段模式</el-radio-button>
      <el-radio-button label="daily">间隔天数模式</el-radio-button>
    </el-radio-group>

    <!-- 时间段模式配置 -->
    <div v-if="mode === 'hourly'" class="config-section">
      <div class="config-item">
        <span></span>
        <el-input-number 
          v-model="hourly.interval" 
          :min="1" 
          :max="24"
          controls-position="right"
        ></el-input-number>
        <span>小时执行一次</span>
      </div>

      <div class="config-item">
        <span>时间段:</span>
        <el-time-select
          v-model="hourly.startHour"
          :picker-options="startTimeOptions"
          placeholder="开始时间"
        />
        <span class="time-separator"></span>
        <el-time-select
          v-model="hourly.endHour"
          :picker-options="endTimeOptions"
          placeholder="结束时间"
        />
      </div>
    </div>

    <!-- 间隔天数模式配置 -->
    <div v-else class="config-section">
      <div class="config-item">
        <span>每隔</span>
        <el-input-number 
          v-model="daily.interval" 
          :min="1" 
          :max="31"
          controls-position="right"
        ></el-input-number>
        <span>天执行一次</span>
      </div>

      <div class="config-item">
        <span>开始时间:</span>
        <el-time-picker
          v-model="daily.startTime"
          :picker-options="{
            selectableRange: '00:00:00 - 23:59:59'
          }"
          format="HH:mm:ss"
          value-format="HH:mm:ss"
          placeholder="选择时间"
        />
      </div>
    </div>

    <!-- 预览区域 -->
    <div class="preview-section">
      <el-alert
        :title="`Cron表达式:${cronExpression}`"
        type="info"
        :closable="false"
      />
      <el-divider content-position="left">最近5次执行时间</el-divider>
      <el-table :data="executionTimes" size="mini" empty-text="无有效时间">
        <el-table-column prop="time" label="执行时间" width="240"></el-table-column>
      </el-table>
    </div>

    <!-- 清空按钮 -->
    <el-button 
      @click="handleReset"
      class="reset-btn"
      size="mini"
    >
      清空配置
    </el-button>
  </div>
</template>

<script>
import cronParser from 'cron-parser';

export default {
  name: 'SimpleCron',
  data() {
    return {
      // 定义默认值常量
      DEFAULT_START: '00:00',
      DEFAULT_END: '23:00',
      DEFAULT_TIME: '00:00:00',
      // 数据初始化
      mode: 'hourly',
      hourly: {
        interval: 1,
        startHour: this.DEFAULT_START,
        endHour: this.DEFAULT_END
      },
      daily: {
        interval: 1,
        startTime: this.DEFAULT_TIME
      }
    }
  },
  mounted(){
    this.handleReset()
  },
  computed: {
    // 时间段模式配置
    startTimeOptions() {
      return {
        start: '00:00',
        step: '01:00',
        end: '23:00'
      }
    },
    endTimeOptions() {
      return {
        start: '00:00',
        step: '01:00',
        end: '23:00',
        minTime: this.hourly.startHour
      }
    },

    // 日期选择限制
    dateOptions() {
      return {
        disabledDate(time) {
          return time.getTime() < Date.now() - 8.64e7 // 禁止选择昨天之前的日期
        }
      }
    },
    cronExpression() {
      // 添加安全校验
      try {
        if (this.mode === 'hourly') {
          const start = this.parseHour(this.hourly.startHour)
          const end = this.parseHour(this.hourly.endHour)
          return `0 0 ${start}-${end}/${this.hourly.interval} * * *`
        } else {
          const [hour, minute, second] = this.safeSplit(this.daily.startTime)
          return `${second} ${minute} ${hour} */${this.daily.interval} * * *`
        }
      } catch {
        return '无效配置'
      }
    },

    // 执行时间计算
    executionTimes() {
      return this.mode === 'hourly' 
        ? this.calculateHourlyTimes() 
        : this.calculateDailyTimes()
    }
  },
  methods: {
    // 安全分割方法
    safeSplit(str, separator = ':', fallback = ['00', '00', '00']) {
      return (str || '').split(separator).map(p => p || fallback[i])
    },
    // 解析小时数(带校验)
    parseHour(timeStr) {
      const [hour] = this.safeSplit(timeStr, ':', [this.DEFAULT_START])
      return parseInt(hour) || 0
    },
    // 清空操作处理
    handleReset() {
      this.hourly = {
        interval: 1,
        startHour: this.DEFAULT_START,
        endHour: this.DEFAULT_END
      }
      this.daily = {
        interval: 1,
        startTime: this.DEFAULT_TIME
      }

      // if (this.mode === 'hourly') {
      //   this.hourly = {
      //     interval: 1,
      //     startHour: this.DEFAULT_START,
      //     endHour: this.DEFAULT_END
      //   }
      // } else {
      //   this.daily = {
      //     interval: 1,
      //     startTime: this.DEFAULT_TIME
      //   }
      // }
      // this.$nextTick(() => {
      //   this.$message.success('配置已重置')
      // })
    },
    // 新增Cron表达式验证方法
    validateCron() {
      try {
        cronParser.parseExpression(this.cronExpression)
        return true
      } catch {
        return false
      }
    },
    // 新增获取当天日期方法
    getTodayString() {
      const today = new Date()
      const year = today.getFullYear()
      const month = String(today.getMonth() + 1).padStart(2, '0')
      const day = String(today.getDate()).padStart(2, '0')
      return `${year}-${month}-${day}`
    },
    // 格式化时间(新增秒显示)
    formatTime(date) {
      const pad = n => n.toString().padStart(2, '0')
      return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} 
              ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
    },
    // 时间计算添加校验
    calculateHourlyTimes() {
      try {
        if (!this.hourly.startHour || !this.hourly.endHour) {
          throw new Error('时间段未配置')
        }
        const interval = cronParser.parseExpression(this.cronExpression, {
          currentDate: new Date(),
          iterator: true
        })
        
        return Array.from({ length: 5 }, () => {
          const next = interval.next()
          return { time: this.formatTime(next.value.toDate()) }
        })
      } catch (e) {
        return [{ time: e.message || '配置错误' }]
      }
    },

    calculateDailyTimes() {
      try {
        const now = new Date()
        // 解析时分秒
        const [hour, minute, second] = this.daily.startTime.split(':')
        
        // 创建基准时间(今日 + 用户选择的时间)
        let baseDate = new Date(
          now.getFullYear(),
          now.getMonth(),
          now.getDate(),
          hour,
          minute,
          second
        )

        // 如果已过当前时间,跳到下个周期
        if (baseDate < now) {
          baseDate.setDate(baseDate.getDate() + this.daily.interval)
        }

        // 生成5次执行时间
        return Array.from({ length: 5 }, (_, i) => {
          const time = new Date(baseDate)
          time.setDate(time.getDate() + i * this.daily.interval)
          return { time: this.formatTime(time) }
        })
      } catch {
        return [{ time: '无效配置' }]
      }
    }
  },
  watch: {
    // 监视时间字段变化
    'hourly.startHour'(val) {
      if (!val) this.hourly.startHour = this.DEFAULT_START
    },
    'hourly.endHour'(val) {
      if (!val) this.hourly.endHour = this.DEFAULT_END
    },
    'daily.startTime'(val) {
      if (!val) this.daily.startTime = this.DEFAULT_TIME
    },
    // 监听所有配置项变化
    cronExpression: {
      handler: function(newVal){
        this.$emit('input', newVal) // 触发v-model更新
        this.$emit('change', newVal) // 同时触发自定义事件
      },
      immediate: true
    }
  },
  props: {
    value: {
      type: String,
      default: ''
    }
  },
}
</script>

<style scoped>
.simple-cron {
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background: #fff;
  max-width: 800px;
  position: relative;
}

.mode-switch {
  margin-bottom: 20px;
}

.config-section {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.config-item {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.time-separator {
  margin: 0 10px;
  color: #909399;
}

.ml-10 {
  margin-left: 10px;
}

.preview-section {
  margin-top: 20px;
}

.el-table {
  margin-top: 10px;
}
.el-time-picker {
  width: 160px !important;
}
.el-table .cell {
  white-space: nowrap;
}
.reset-btn {
  position: absolute;
  right: 20px;
  top: 20px;
  z-index: 1;
}

/* 错误状态高亮 */
.invalid .el-input__inner {
  border-color: #f56c6c !important;
}
</style>

组件使用:

<simple-cron v-model="rules_form.cron" @change="handleCronChange" />