Files
wms-ui/src/components/GenCron/CronForm/index.vue
2026-02-26 17:31:18 +08:00

381 lines
10 KiB
Vue

<template>
<div class="cron-inner">
<div class="content">
<!-- 设置表单 -->
<a-tabs v-model:active-key="activeKey" size="small">
<!-- -->
<a-tab-pane v-if="!hideSecond" key="second" title="秒">
<SecondForm v-model="second" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane key="minute" title="分">
<MinuteForm v-model="minute" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane key="hour" title="时">
<HourForm v-model="hour" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane key="day" title="日">
<DayForm v-model="day" :week="week" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane key="month" title="月">
<MonthForm v-model="month" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane key="week" title="周">
<WeekForm v-model="week" :day="day" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane v-if="!hideYear && !hideSecond" key="year" title="年">
<YearForm v-model="year" :disabled="disabled" />
</a-tab-pane>
</a-tabs>
<!-- 执行时间预览 -->
<a-row :gutter="8">
<!-- 快捷修改 -->
<a-col :span="18" style="margin-top: 28px">
<a-row :gutter="[12, 12]">
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.second" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'second'"></span>
</template>
</a-input>
</a-col>
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.minute" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'minute'"></span>
</template>
</a-input>
</a-col>
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.hour" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'hour'"></span>
</template>
</a-input>
</a-col>
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.day" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'day'"></span>
</template>
</a-input>
</a-col>
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.month" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'month'"></span>
</template>
</a-input>
</a-col>
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.week" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'week'"></span>
</template>
</a-input>
</a-col>
<!-- -->
<a-col :span="8">
<a-input v-model="cronInputs.year" @change="onInputChange">
<template #prepend>
<span class="allow-click" @click="activeKey = 'year'"></span>
</template>
</a-input>
</a-col>
<!-- 表达式 -->
<a-col :span="16">
<a-input
v-model="cronInputs.cron"
:placeholder="placeholder"
@change="onInputCronChange"
>
<template #prepend>
<span class="allow-click">表达式</span>
</template>
</a-input>
</a-col>
</a-row>
</a-col>
<!-- 执行时间 -->
<a-col :span="6">
<div class="preview-times usn">近五次执行时间 (不解析年)</div>
<a-textarea v-model="previewTimes" :auto-size="{ minRows: 5, maxRows: 5 }" />
</a-col>
</a-row>
</div>
</div>
</template>
<script lang="ts">
</script>
<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'
import CronParser from 'cron-parser'
import SecondForm from '@/components/GenCron/CronForm/component/second-form.vue'
import MinuteForm from '@/components/GenCron/CronForm/component/minute-form.vue'
import HourForm from '@/components/GenCron/CronForm/component/hour-form.vue'
import DayForm from '@/components/GenCron/CronForm/component/day-form.vue'
import MonthForm from '@/components/GenCron/CronForm/component/month-form.vue'
import WeekForm from '@/components/GenCron/CronForm/component/week-form.vue'
import YearForm from '@/components/GenCron/CronForm/component/year-form.vue'
import { dateFormat } from '@/utils'
import type { CronPropType } from '@/components/GenCron/CronForm/type'
const props = withDefaults(defineProps<Partial<CronPropType>>(), {
disabled: false,
hideSecond: false,
hideYear: false,
placeholder: '请输入 Cron 表达式',
})
const emit = defineEmits(['change', 'update:modelValue'])
const activeKey = ref(props.hideSecond ? 'minute' : 'second')
const second = ref('*')
const minute = ref('*')
const hour = ref('*')
const day = ref('*')
const month = ref('*')
const week = ref('?')
const year = ref('*')
const cronInputs = reactive({
second: '',
minute: '',
hour: '',
day: '',
month: '',
week: '',
year: '',
cron: '',
})
const previewTimes = ref('执行预览')
// cron 表达式
const cronExpression = computed(() => {
const result: string[] = []
if (!props.hideSecond) {
result.push(second.value ? second.value : '*')
}
result.push(minute.value ? minute.value : '*')
result.push(hour.value ? hour.value : '*')
result.push(day.value ? day.value : '*')
result.push(month.value ? month.value : '*')
result.push(week.value ? week.value : '?')
if (!props.hideYear && !props.hideSecond) {
result.push(year.value ? year.value : '*')
}
return result.join(' ')
})
// 不含年的 cron 表达式
const expressionNoYear = (corn: string) => {
if (props.hideYear || props.hideSecond) return corn
const vs = corn.split(' ')
return vs.slice(0, vs.length - 1).join(' ')
}
// 计算触发时间
const calculateNextExecutionTimes = (corn: string = cronExpression.value) => {
try {
const parse = expressionNoYear(corn)
// 解析表达式
const date = dateFormat(new Date())
const iter = CronParser.parseExpression(parse, {
currentDate: date,
})
const result: string[] = []
for (let i = 1; i <= 5; i++) {
result.push(dateFormat(new Date(iter.next() as any)))
}
previewTimes.value = result.length > 0 ? result.join('\n') : '无执行时间'
// 回调
if (props.callback) {
props.callback(cronExpression.value, +new Date(), true)
}
} catch (e) {
previewTimes.value = '表达式错误'
// 回调
if (props.callback) {
props.callback(cronExpression.value, +new Date(), false)
}
}
}
const calcTriggerTimeList = useDebounceFn(calculateNextExecutionTimes, 500)
// 监听 cron 修改
watch(() => props.modelValue, (newVal) => {
if (newVal === cronExpression.value) {
return
}
parseCron()
})
// 监听 cron 修改
watch(cronExpression, (newValue) => {
calcTriggerTimeList()
emitValue(newValue)
assignInput()
})
// 根据 cron 解析
const parseCron = () => {
// 计算执行时间
calcTriggerTimeList()
if (!props.modelValue) {
return
}
const values = props.modelValue.split(' ').filter((item) => !!item)
if (!values || values.length <= 0) {
return
}
let i = 0
if (!props.hideSecond) second.value = values[i++]
if (values.length > i) minute.value = values[i++]
if (values.length > i) hour.value = values[i++]
if (values.length > i) day.value = values[i++]
if (values.length > i) month.value = values[i++]
if (values.length > i) week.value = values[i++]
if (values.length > i) year.value = values[i]
// 重新分配
assignInput()
}
// 重新分配
const assignInput = () => {
cronInputs.second = second.value
cronInputs.minute = minute.value
cronInputs.hour = hour.value
cronInputs.day = day.value
cronInputs.month = month.value
cronInputs.week = week.value
cronInputs.year = year.value
cronInputs.cron = cronExpression.value
}
// 修改 cron 解析内容
const onInputChange = () => {
second.value = cronInputs.second
minute.value = cronInputs.minute
hour.value = cronInputs.hour
day.value = cronInputs.day
month.value = cronInputs.month
week.value = cronInputs.week
year.value = cronInputs.year
}
// 修改 cron 输入框
const onInputCronChange = (value: string) => {
emitValue(value)
}
// 修改 cron
const emitValue = (value: string) => {
emit('change', value)
emit('update:modelValue', value)
}
onMounted(() => {
assignInput()
parseCron()
// 如果 modelValue 没有值则更新为 cronExpression
if (!props.modelValue) {
emitValue(cronExpression.value)
}
})
const checkCron = () => {
return (day.value === '?' && week.value === '?')
}
defineExpose({ checkCron })
</script>
<style lang="less" scoped>
.cron-inner {
user-select: none;
:deep(.arco-tabs-content) {
padding-top: 6px;
}
:deep(.cron-inner-config-list) {
text-align: left;
margin: 0 12px 4px 12px;
.item {
margin-top: 6px;
font-size: 14px;
width: 100%;
}
.choice {
padding: 4px 8px 4px 0;
}
.w60 {
margin: 0 8px !important;
padding: 0 8px !important;
width: 60px !important;
}
.w80 {
margin: 0 8px !important;
padding: 0 8px !important;
width: 80px !important;
}
.list {
margin: 0 20px;
}
.list-check-item {
padding: 1px 3px;
width: 4em;
}
.list-cn .list-check-item {
width: 5em;
}
.tip-info {
color: var(--color-text-3);
}
}
}
:deep(.arco-input-prepend) {
padding: 0 !important;
}
:deep(.arco-input-append) {
padding: 0 !important;
}
.preview-times {
color: var(--color-text-3);
margin: 2px 0 4px 0;
}
.allow-click {
width: 100%;
height: 100%;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
}
</style>