feat: cron

pull/12/head
xingyu 2023-05-12 15:39:02 +08:00
parent fa49d7a988
commit 81d24264d5
20 changed files with 1286 additions and 1 deletions

View File

@ -42,6 +42,7 @@
"ant-design-vue": "^3.2.20",
"axios": "^1.4.0",
"codemirror": "^5.65.3",
"cron-parser": "^4.8.1",
"cropperjs": "^1.5.13",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",

View File

@ -28,6 +28,9 @@ dependencies:
codemirror:
specifier: ^5.65.3
version: 5.65.3
cron-parser:
specifier: ^4.8.1
version: 4.8.1
cropperjs:
specifier: ^1.5.13
version: 1.5.13
@ -3867,6 +3870,13 @@ packages:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
/cron-parser@4.8.1:
resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==}
engines: {node: '>=12.0.0'}
dependencies:
luxon: 3.3.0
dev: false
/cropperjs@1.5.13:
resolution: {integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==}
dev: false
@ -6064,6 +6074,11 @@ packages:
engines: {node: '>=16.14'}
dev: true
/luxon@3.3.0:
resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
engines: {node: '>=12'}
dev: false
/magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies:

View File

@ -0,0 +1,4 @@
export { default as CronTab } from './src/CronTabInput.vue'
export { default as CronTabInner } from './src/CronTabInner.vue'
export { default as CronTabModal } from './src/CronTabModal.vue'
export { default as CronValidator } from './src/validator'

View File

@ -0,0 +1,345 @@
<template>
<div :class="`${prefixCls}`">
<div class="content">
<Tabs :size="`small`" v-model:activeKey="activeKey">
<TabPane tab="秒" key="second" v-if="!hideSecond">
<SecondUI v-model:value="second" :disabled="disabled" />
</TabPane>
<TabPane tab="分" key="minute">
<MinuteUI v-model:value="minute" :disabled="disabled" />
</TabPane>
<TabPane tab="时" key="hour">
<HourUI v-model:value="hour" :disabled="disabled" />
</TabPane>
<TabPane tab="日" key="day">
<DayUI v-model:value="day" :week="week" :disabled="disabled" />
</TabPane>
<TabPane tab="月" key="month">
<MonthUI v-model:value="month" :disabled="disabled" />
</TabPane>
<TabPane tab="周" key="week">
<WeekUI v-model:value="week" :day="day" :disabled="disabled" />
</TabPane>
<TabPane tab="年" key="year" v-if="!hideYear && !hideSecond">
<YearUI v-model:value="year" :disabled="disabled" />
</TabPane>
</Tabs>
<Divider />
<!-- 执行时间预览 -->
<Row :gutter="8">
<Col :span="18" style="margin-top: 22px">
<Row :gutter="8">
<Col :span="8" style="margin-bottom: 12px">
<Input v-model:value="inputValues.second" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'second'"></span>
</template>
</Input>
</Col>
<Col :span="8" style="margin-bottom: 12px">
<Input v-model:value="inputValues.minute" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'minute'"></span>
</template>
</Input>
</Col>
<Col :span="8" style="margin-bottom: 12px">
<Input v-model:value="inputValues.hour" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'hour'"></span>
</template>
</Input>
</Col>
<Col :span="8" style="margin-bottom: 12px">
<Input v-model:value="inputValues.day" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'day'"></span>
</template>
</Input>
</Col>
<Col :span="8" style="margin-bottom: 12px">
<Input v-model:value="inputValues.month" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'month'"></span>
</template>
</Input>
</Col>
<Col :span="8" style="margin-bottom: 12px">
<Input v-model:value="inputValues.week" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'week'"></span>
</template>
</Input>
</Col>
<Col :span="8">
<Input v-model:value="inputValues.year" @blur="onInputBlur">
<template #addonBefore>
<span class="allow-click" @click="activeKey = 'year'"></span>
</template>
</Input>
</Col>
<Col :span="16">
<Input v-model:value="inputValues.cron" @blur="onInputCronBlur">
<template #addonBefore>
<Tooltip title="Cron表达式"></Tooltip>
</template>
</Input>
</Col>
</Row>
</Col>
<Col :span="6">
<div>近十次执行时间不含年</div>
<Textarea type="textarea" :value="preTimeList" :rows="5" />
</Col>
</Row>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch, provide } from 'vue'
import { Input, Tooltip, Row, Col, Divider, Tabs, TabPane, Textarea } from 'ant-design-vue'
import { useDesign } from '@/hooks/web/useDesign'
import CronParser from 'cron-parser'
import SecondUI from './tabs/SecondUI.vue'
import MinuteUI from './tabs/MinuteUI.vue'
import HourUI from './tabs/HourUI.vue'
import DayUI from './tabs/DayUI.vue'
import MonthUI from './tabs/MonthUI.vue'
import WeekUI from './tabs/WeekUI.vue'
import YearUI from './tabs/YearUI.vue'
import { cronEmits, cronProps } from './cron.data'
import { dateFormat } from '@/utils/dateUtil'
import { simpleDebounce } from '@/utils'
const { prefixCls } = useDesign('cron-inner')
provide('prefixCls', prefixCls)
const emit = defineEmits([...cronEmits])
const props = defineProps({ ...cronProps })
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 inputValues = reactive({
second: '',
minute: '',
hour: '',
day: '',
month: '',
week: '',
year: '',
cron: ''
})
const preTimeList = ref('执行预览,会忽略年份参数。')
// cron
const cronValueInner = computed(() => {
let 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(' ')
})
//
const cronValueNoYear = computed(() => {
const v = cronValueInner.value
if (props.hideYear || props.hideSecond) return v
const vs = v.split(' ')
if (vs.length >= 6) {
// Quartz
vs[5] = convertWeekToQuartz(vs[5])
}
return vs.slice(0, vs.length - 1).join(' ')
})
const calTriggerList = simpleDebounce(calTriggerListInner, 500)
watch(
() => props.value,
(newVal) => {
if (newVal === cronValueInner.value) {
return
}
formatValue()
}
)
watch(cronValueInner, (newValue) => {
calTriggerList()
emitValue(newValue)
assignInput()
})
assignInput()
formatValue()
calTriggerListInner()
function assignInput() {
inputValues.second = second.value
inputValues.minute = minute.value
inputValues.hour = hour.value
inputValues.day = day.value
inputValues.month = month.value
inputValues.week = week.value
inputValues.year = year.value
inputValues.cron = cronValueInner.value
}
function formatValue() {
if (!props.value) return
const values = props.value.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()
}
// Quartz
// 1 = 2 = 3 = 4 = 5 = 6 = 7 =
function convertWeekToQuartz(week: string) {
let convert = (v: string) => {
if (v === '0') {
return '1'
}
if (v === '1') {
return '0'
}
return (Number.parseInt(v) - 1).toString()
}
// 1-7 or 1/7
let patten1 = /^([0-7])([-/])([0-7])$/
// 1,4,7
let patten2 = /^([0-7])(,[0-7])+$/
if (/^[0-7]$/.test(week)) {
return convert(week)
} else if (patten1.test(week)) {
return week.replace(patten1, (_$0, before, separator, after) => {
if (separator === '/') {
return convert(before) + separator + after
} else {
return convert(before) + separator + convert(after)
}
})
} else if (patten2.test(week)) {
return week
.split(',')
.map((v) => convert(v))
.join(',')
}
return week
}
function calTriggerListInner() {
//
if (props.remote) {
props.remote(cronValueInner.value, +new Date(), (v) => {
preTimeList.value = v
})
return
}
const format = 'yyyy-MM-dd hh:mm:ss'
const options = {
currentDate: dateFormat(new Date(), format)
}
const iter = CronParser.parseExpression(cronValueNoYear.value, options)
const result: string[] = []
for (let i = 1; i <= 10; i++) {
result.push(dateFormat(new Date(iter.next() as any), format))
}
preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间'
}
function onInputBlur() {
second.value = inputValues.second
minute.value = inputValues.minute
hour.value = inputValues.hour
day.value = inputValues.day
month.value = inputValues.month
week.value = inputValues.week
year.value = inputValues.year
}
function onInputCronBlur(event) {
emitValue(event.target.value)
}
function emitValue(value) {
emit('change', value)
emit('update:value', value)
}
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-cron-inner';
.@{prefix-cls} {
.content {
.ant-checkbox-wrapper + .ant-checkbox-wrapper {
margin-left: 0;
}
}
&-config-list {
text-align: left;
margin: 0 10px 10px;
.item {
margin-top: 5px;
font-size: 14px;
span {
padding: 0 2px;
}
}
.choice {
padding: 5px 8px;
}
.w60 {
width: 60px;
min-width: 60px;
}
.w80 {
width: 80px;
min-width: 80px;
}
.list {
margin: 0 20px;
}
.list-check-item {
padding: 1px 3px;
width: 4em;
}
.list-cn .list-check-item {
width: 5em;
}
.tip-info {
color: #999;
}
}
.allow-click {
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<div :class="`${prefixCls}`">
<Input :placeholder="placeholder" v-model:value="editCronValue" :disabled="disabled">
<template #addonAfter>
<a class="open-btn" :disabled="disabled ? 'disabled' : null" @click="showConfigModal">
<Icon icon="ant-design:setting-outlined" />
<span>选择</span>
</a>
</template>
</Input>
<CronTabModal
@register="registerModal"
v-model:value="editCronValue"
:exeStartTime="exeStartTime"
:hideYear="hideYear"
:remote="remote"
:hideSecond="hideSecond"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Input } from 'ant-design-vue'
import { useDesign } from '@/hooks/web/useDesign'
import { useModal } from '@/components/Modal'
import { propTypes } from '@/utils/propTypes'
import { Icon } from '@/components/Icon'
import CronTabModal from './CronTabModal.vue'
import { cronEmits, cronProps } from './cron.data'
const { prefixCls } = useDesign('cron-input')
const emit = defineEmits([...cronEmits])
const props = defineProps({
...cronProps,
placeholder: propTypes.string.def('请输入cron表达式'),
exeStartTime: propTypes.oneOfType([propTypes.number, propTypes.string, propTypes.object]).def(0)
})
const [registerModal, { openModal }] = useModal()
const editCronValue = ref(props.value)
watch(
() => props.value,
(newVal) => {
if (newVal !== editCronValue.value) {
editCronValue.value = newVal
}
}
)
watch(editCronValue, (newVal) => {
emit('change', newVal)
emit('update:value', newVal)
})
function showConfigModal() {
if (!props.disabled) {
openModal()
}
}
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-cron-input';
.@{prefix-cls} {
a.open-btn {
cursor: pointer;
.app-iconify {
position: relative;
top: 1px;
right: 2px;
}
}
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<BasicModal @register="registerModal" title="Cron表达式" width="800px" @ok="onOk">
<CronTab v-bind="attrs" />
</BasicModal>
</template>
<script lang="ts" setup>
import { useAttrs } from '@/hooks/core/useAttrs'
import { BasicModal, useModalInner } from '@/components/Modal'
import CronTab from './CronTabInner.vue'
defineOptions({ name: 'CronTabModal', inheritAttrs: false })
const attrs = useAttrs()
const [registerModal, { closeModal }] = useModalInner()
function onOk() {
closeModal()
}
</script>

View File

@ -0,0 +1,10 @@
import { propTypes } from '@/utils/propTypes'
export const cronEmits = ['change', 'update:value']
export const cronProps = {
value: propTypes.string.def(''),
disabled: propTypes.bool.def(false),
hideSecond: propTypes.bool.def(false),
hideYear: propTypes.bool.def(false),
remote: propTypes.func
}

View File

@ -0,0 +1,87 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs"></Radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" class="w-4" v-bind="typeLoopAttrs" />
<span> 日开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.last" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"></Radio>
<div class="list">
<CheckboxGroup v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<Checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</Checkbox>
</template>
</CheckboxGroup>
</div>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue'
import { InputNumber, Radio, Checkbox } from 'ant-design-vue'
import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin'
export default defineComponent({
name: 'DayUI',
components: { InputNumber, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '*',
props: {
week: { type: String, default: '?' }
}
}),
emits: useTabEmits(),
setup(props, context) {
const disabledChoice = computed(() => {
return (props.week && props.week !== '?') || props.disabled
})
const setup = useTabSetup(props, context, {
defaultValue: '*',
valueWork: 1,
minValue: 1,
maxValue: 31,
valueRange: { start: 1, end: 31 },
valueLoop: { start: 1, interval: 1 },
disabled: disabledChoice
})
const typeWorkAttrs = computed(() => ({
disabled: setup.type.value !== TypeEnum.work || props.disabled || disabledChoice.value,
...setup.inputNumberAttrs.value
}))
watch(
() => props.week,
() => {
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value)
}
)
return { ...setup, typeWorkAttrs }
}
})
</script>

View File

@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 时开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"></Radio>
<div class="list">
<CheckboxGroup v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<Checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</Checkbox>
</template>
</CheckboxGroup>
</div>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { InputNumber, Radio, Checkbox } from 'ant-design-vue'
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
export default defineComponent({
name: 'HourUI',
components: { InputNumber, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '*'
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 23,
valueRange: { start: 0, end: 23 },
valueLoop: { start: 0, interval: 1 }
})
}
})
</script>

View File

@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 分开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"></Radio>
<div class="list">
<CheckboxGroup v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<Checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</Checkbox>
</template>
</CheckboxGroup>
</div>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { InputNumber, Radio, Checkbox } from 'ant-design-vue'
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
export default defineComponent({
name: 'MinuteUI',
components: { InputNumber, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '*'
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 }
})
}
})
</script>

View File

@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 月开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"></Radio>
<div class="list">
<CheckboxGroup v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<Checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</Checkbox>
</template>
</CheckboxGroup>
</div>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { InputNumber, Radio, Checkbox } from 'ant-design-vue'
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
export default defineComponent({
name: 'MonthUI',
components: { InputNumber, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '*'
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 1,
maxValue: 12,
valueRange: { start: 1, end: 12 },
valueLoop: { start: 1, interval: 1 }
})
}
})
</script>

View File

@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 秒开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"></a-radio>
<div class="list">
<CheckboxGroup v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<Checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</Checkbox>
</template>
</CheckboxGroup>
</div>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { InputNumber, Radio, Checkbox } from 'ant-design-vue'
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
export default defineComponent({
name: 'SecondUI',
components: { InputNumber, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '*'
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 }
})
}
})
</script>

View File

@ -0,0 +1,125 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs"></Radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<Select v-model:value="valueRange.start" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
<span> </span>
<Select v-model:value="valueRange.end" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<Select v-model:value="valueLoop.start" :options="weekOptions" v-bind="typeLoopSelectAttrs" />
<span> 开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs"></a-radio>
<div class="list list-cn">
<CheckboxGroup v-model:value="valueList">
<template v-for="opt in weekOptions" :key="i">
<Checkbox :value="opt.value" v-bind="typeSpecifyAttrs">{{ opt.label }}</Checkbox>
</template>
</CheckboxGroup>
</div>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { computed, watch, defineComponent } from 'vue'
import { InputNumber, Radio, Checkbox, Select } from 'ant-design-vue'
import { useTabProps, useTabEmits, useTabSetup, TypeEnum } from './useTabMixin'
const WEEK_MAP_EN = {
'1': 'SUN',
'2': 'MON',
'3': 'TUE',
'4': 'WED',
'5': 'THU',
'6': 'FRI',
'7': 'SAT'
}
const WEEK_MAP_CN = {
'1': '周日',
'2': '周一',
'3': '周二',
'4': '周三',
'5': '周四',
'6': '周五',
'7': '周六'
}
export default defineComponent({
name: 'WeekUI',
components: { InputNumber, Select, Checkbox, CheckboxGroup: Checkbox.Group, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '?',
props: {
day: { type: String, default: '*' }
}
}),
emits: useTabEmits(),
setup(props, context) {
const disabledChoice = computed(() => {
return (props.day && props.day !== '?') || props.disabled
})
const setup = useTabSetup(props, context, {
defaultType: TypeEnum.unset,
defaultValue: '?',
minValue: 1,
maxValue: 7,
// 0,7 1
valueRange: { start: 1, end: 7 },
valueLoop: { start: 2, interval: 1 },
disabled: disabledChoice
})
const weekOptions = computed(() => {
let options: { label: string; value: number }[] = []
for (let weekKey of Object.keys(WEEK_MAP_CN)) {
let weekName: string = WEEK_MAP_CN[weekKey]
options.push({
value: Number.parseInt(weekKey),
label: weekName
})
}
return options
})
const typeRangeSelectAttrs = computed(() => ({
class: ['w80'],
disabled: setup.typeRangeAttrs.value.disabled
}))
const typeLoopSelectAttrs = computed(() => ({
class: ['w80'],
disabled: setup.typeLoopAttrs.value.disabled
}))
watch(
() => props.day,
() => {
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value)
}
)
return {
...setup,
weekOptions,
typeLoopSelectAttrs,
typeRangeSelectAttrs,
WEEK_MAP_CN,
WEEK_MAP_EN
}
}
})
</script>

View File

@ -0,0 +1,49 @@
<template>
<div :class="`${prefixCls}-config-list`">
<RadioGroup v-model:value="type">
<div class="item">
<Radio :value="TypeEnum.every" v-bind="beforeRadioAttrs"></Radio>
</div>
<div class="item">
<Radio :value="TypeEnum.range" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber class="w80" v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber class="w80" v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<Radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs"></Radio>
<span> </span>
<InputNumber class="w80" v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 年开始间隔 </span>
<InputNumber class="w80" v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
</RadioGroup>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { InputNumber, Radio } from 'ant-design-vue'
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
export default defineComponent({
name: 'YearUI',
components: { InputNumber, Radio, RadioGroup: Radio.Group },
props: useTabProps({
defaultValue: '*'
}),
emits: useTabEmits(),
setup(props, context) {
const nowYear = new Date().getFullYear()
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
valueRange: { start: nowYear, end: nowYear + 100 },
valueLoop: { start: nowYear, interval: 1 }
})
}
})
</script>

View File

@ -0,0 +1,199 @@
// 主要用于日和星期的互斥使用
import { computed, inject, reactive, ref, unref, watch } from 'vue'
import { propTypes } from '@/utils/propTypes'
export enum TypeEnum {
unset = 'UNSET',
every = 'EVERY',
range = 'RANGE',
loop = 'LOOP',
work = 'WORK',
last = 'LAST',
specify = 'SPECIFY'
}
// use 公共 props
export function useTabProps(options) {
const defaultValue = options?.defaultValue ?? '?'
return {
value: propTypes.string.def(defaultValue),
disabled: propTypes.bool.def(false),
...options?.props
}
}
// use 公共 emits
export function useTabEmits() {
return ['change', 'update:value']
}
// use 公共 setup
export function useTabSetup(props, context, options) {
const { emit } = context
const prefixCls = inject('prefixCls')
const defaultValue = ref(options?.defaultValue ?? '?')
// 类型
const type = ref(options.defaultType ?? TypeEnum.every)
const valueList = ref<any[]>([])
// 对于不同的类型,所定义的值也有所不同
const valueRange = reactive(options.valueRange)
const valueLoop = reactive(options.valueLoop)
const valueWeek = reactive(options.valueWeek)
const valueWork = ref(options.valueWork)
const maxValue = ref(options.maxValue)
const minValue = ref(options.minValue)
// 根据不同的类型计算出的value
const computeValue = computed(() => {
const valueArray: any[] = []
switch (type.value) {
case TypeEnum.unset:
valueArray.push('?')
break
case TypeEnum.every:
valueArray.push('*')
break
case TypeEnum.range:
valueArray.push(`${valueRange.start}-${valueRange.end}`)
break
case TypeEnum.loop:
valueArray.push(`${valueLoop.start}/${valueLoop.interval}`)
break
case TypeEnum.work:
valueArray.push(`${valueWork.value}W`)
break
case TypeEnum.last:
valueArray.push('L')
break
case TypeEnum.specify:
if (valueList.value.length === 0) {
valueList.value.push(minValue.value)
}
valueArray.push(valueList.value.join(','))
break
default:
valueArray.push(defaultValue.value)
break
}
return valueArray.length > 0 ? valueArray.join('') : defaultValue.value
})
// 指定值范围区间,介于最小值和最大值之间
const specifyRange = computed(() => {
const range: number[] = []
if (maxValue.value != null) {
for (let i = minValue.value; i <= maxValue.value; i++) {
range.push(i)
}
}
return range
})
watch(
() => props.value,
(val) => {
if (val !== computeValue.value) {
parseValue(val)
}
},
{ immediate: true }
)
watch(computeValue, (v) => updateValue(v))
function updateValue(value) {
emit('change', value)
emit('update:value', value)
}
/**
* parseValue
* @param value
*/
function parseValue(value) {
if (value === computeValue.value) {
return
}
try {
if (!value || value === defaultValue.value) {
type.value = TypeEnum.every
} else if (value.indexOf('?') >= 0) {
type.value = TypeEnum.unset
} else if (value.indexOf('-') >= 0) {
type.value = TypeEnum.range
const values = value.split('-')
if (values.length >= 2) {
valueRange.start = parseInt(values[0])
valueRange.end = parseInt(values[1])
}
} else if (value.indexOf('/') >= 0) {
type.value = TypeEnum.loop
const values = value.split('/')
if (values.length >= 2) {
valueLoop.start = value[0] === '*' ? 0 : parseInt(values[0])
valueLoop.interval = parseInt(values[1])
}
} else if (value.indexOf('W') >= 0) {
type.value = TypeEnum.work
const values = value.split('W')
if (!values[0] && !isNaN(values[0])) {
valueWork.value = parseInt(values[0])
}
} else if (value.indexOf('L') >= 0) {
type.value = TypeEnum.last
} else if (value.indexOf(',') >= 0 || !isNaN(value)) {
type.value = TypeEnum.specify
valueList.value = value.split(',').map((item) => parseInt(item))
} else {
type.value = TypeEnum.every
}
} catch (e) {
type.value = TypeEnum.every
}
}
const beforeRadioAttrs = computed(() => ({
class: ['choice'],
disabled: props.disabled || unref(options.disabled)
}))
const inputNumberAttrs = computed(() => ({
class: ['w60'],
max: maxValue.value,
min: minValue.value,
precision: 0
}))
const typeRangeAttrs = computed(() => ({
disabled: type.value !== TypeEnum.range || props.disabled || unref(options.disabled),
...inputNumberAttrs.value
}))
const typeLoopAttrs = computed(() => ({
disabled: type.value !== TypeEnum.loop || props.disabled || unref(options.disabled),
...inputNumberAttrs.value
}))
const typeSpecifyAttrs = computed(() => ({
disabled: type.value !== TypeEnum.specify || props.disabled || unref(options.disabled),
class: ['list-check-item']
}))
return {
type,
TypeEnum,
prefixCls,
defaultValue,
valueRange,
valueLoop,
valueWeek,
valueList,
valueWork,
maxValue,
minValue,
computeValue,
specifyRange,
updateValue,
parseValue,
beforeRadioAttrs,
inputNumberAttrs,
typeRangeAttrs,
typeLoopAttrs,
typeSpecifyAttrs
}
}

View File

@ -0,0 +1,48 @@
import CronParser from 'cron-parser'
import type { ValidatorRule } from 'ant-design-vue/lib/form/interface'
const cronRule: ValidatorRule = {
validator({}, value) {
// 没填写就不校验
if (!value) {
return Promise.resolve()
}
const values: string[] = value.split(' ').filter((item) => !!item)
if (values.length > 7) {
return Promise.reject('Cron表达式最多7项')
}
// 检查第7项
let val: string = value
if (values.length === 7) {
const year = values[6]
if (year !== '*' && year !== '?') {
let yearValues: string[] = []
if (year.indexOf('-') >= 0) {
yearValues = year.split('-')
} else if (year.indexOf('/')) {
yearValues = year.split('/')
} else {
yearValues = [year]
}
// 判断是否都是数字
const checkYear = yearValues.some((item) => isNaN(Number(item)))
if (checkYear) {
return Promise.reject('Cron表达式参数[年]错误:' + year)
}
}
// 取其中的前六项
val = values.slice(0, 6).join(' ')
}
// 6位 没有年
// 5位没有秒、年
try {
const iter = CronParser.parseExpression(val)
iter.next()
return Promise.resolve()
} catch (e) {
return Promise.reject('Cron表达式错误' + e)
}
}
}
export default cronRule.validator

View File

@ -117,3 +117,4 @@ export type ComponentType =
| 'ApiTransfer'
| 'Editor'
| 'FileUpload'
| 'CronTab'

View File

@ -64,6 +64,45 @@ export function convertDate(date) {
return date
}
/**
*
* @param date
* @param block
*/
export function dateFormat(date, block) {
if (!date) {
return ''
}
let format = block || 'yyyy-MM-dd'
date = new Date(date)
const map = {
M: date.getMonth() + 1, // 月份
d: date.getDate(), // 日
h: date.getHours(), // 小时
m: date.getMinutes(), // 分
s: date.getSeconds(), // 秒
q: Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds() // 毫秒
}
format = format.replace(/([yMdhmsqS])+/g, (all, t) => {
let v = map[t]
if (v !== undefined) {
if (all.length > 1) {
v = `0${v}`
v = v.substr(v.length - 2)
}
return v
} else if (t === 'y') {
return date
.getFullYear()
.toString()
.substr(4 - all.length)
}
return all
})
return format
}
/**
* xx
*

View File

@ -113,3 +113,29 @@ export const withInstall = <T extends CustomComponent>(component: T, alias?: str
}
return component as WithInstall<T>
}
/**
*
*
* (debounce)(delay)100ms
* 100ms
*
*
* @param fn
* @param delay
* @returns {Function}
*/
export function simpleDebounce(fn, delay = 100) {
let timer: any | null = null
return function () {
// eslint-disable-next-line prefer-rest-params
const args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
// @ts-ignore
fn.apply(this, args)
}, delay)
}
}

View File

@ -1,6 +1,10 @@
import { DescItem } from '@/components/Description'
import { BasicColumn, FormSchema, useRender } from '@/components/Table'
import { DICT_TYPE, getDictOpts } from '@/utils/dict'
import { useComponentRegister } from '@/components/Form'
import { CronTab } from '@/components/CronTab'
useComponentRegister('CronTab', CronTab)
export const columns: BasicColumn[] = [
{
@ -91,7 +95,7 @@ export const formSchema: FormSchema[] = [
label: 'CRON 表达式',
field: 'cronExpression',
required: true,
component: 'Input'
component: 'CronTab'
},
{
label: '重试次数',