diff --git a/package.json b/package.json index 22e9b588..287198cc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4410b3d8..18c4cbb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/CronTab/index.ts b/src/components/CronTab/index.ts new file mode 100644 index 00000000..691818c4 --- /dev/null +++ b/src/components/CronTab/index.ts @@ -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' diff --git a/src/components/CronTab/src/CronTabInner.vue b/src/components/CronTab/src/CronTabInner.vue new file mode 100644 index 00000000..cf3def38 --- /dev/null +++ b/src/components/CronTab/src/CronTabInner.vue @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 秒 + + + + + + + 分 + + + + + + + 时 + + + + + + + 日 + + + + + + + 月 + + + + + + + 周 + + + + + + + 年 + + + + + + + 式 + + + + + + + 近十次执行时间(不含年) + + + + + + + + + diff --git a/src/components/CronTab/src/CronTabInput.vue b/src/components/CronTab/src/CronTabInput.vue new file mode 100644 index 00000000..d870d79e --- /dev/null +++ b/src/components/CronTab/src/CronTabInput.vue @@ -0,0 +1,76 @@ + + + + + + + 选择 + + + + + + + + + + diff --git a/src/components/CronTab/src/CronTabModal.vue b/src/components/CronTab/src/CronTabModal.vue new file mode 100644 index 00000000..9e2df421 --- /dev/null +++ b/src/components/CronTab/src/CronTabModal.vue @@ -0,0 +1,20 @@ + + + + + + + diff --git a/src/components/CronTab/src/cron.data.ts b/src/components/CronTab/src/cron.data.ts new file mode 100644 index 00000000..d377eacb --- /dev/null +++ b/src/components/CronTab/src/cron.data.ts @@ -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 +} diff --git a/src/components/CronTab/src/tabs/DayUI.vue b/src/components/CronTab/src/tabs/DayUI.vue new file mode 100644 index 00000000..c11b5150 --- /dev/null +++ b/src/components/CronTab/src/tabs/DayUI.vue @@ -0,0 +1,87 @@ + + + + + 不设置 + 日和周只能设置其中之一 + + + 每日 + + + 区间 + 从 + + 日 至 + + 日 + + + 循环 + 从 + + 日开始,间隔 + + 日 + + + 最后一日 + + + 指定 + + + + {{ i }} + + + + + + + + + diff --git a/src/components/CronTab/src/tabs/HourUI.vue b/src/components/CronTab/src/tabs/HourUI.vue new file mode 100644 index 00000000..c95fbec4 --- /dev/null +++ b/src/components/CronTab/src/tabs/HourUI.vue @@ -0,0 +1,59 @@ + + + + + 每时 + + + 区间 + 从 + + 时 至 + + 时 + + + 循环 + 从 + + 时开始,间隔 + + 时 + + + 指定 + + + + {{ i }} + + + + + + + + + diff --git a/src/components/CronTab/src/tabs/MinuteUI.vue b/src/components/CronTab/src/tabs/MinuteUI.vue new file mode 100644 index 00000000..6fed4c56 --- /dev/null +++ b/src/components/CronTab/src/tabs/MinuteUI.vue @@ -0,0 +1,59 @@ + + + + + 每分 + + + 区间 + 从 + + 分 至 + + 分 + + + 循环 + 从 + + 分开始,间隔 + + 分 + + + 指定 + + + + {{ i }} + + + + + + + + + diff --git a/src/components/CronTab/src/tabs/MonthUI.vue b/src/components/CronTab/src/tabs/MonthUI.vue new file mode 100644 index 00000000..945bf488 --- /dev/null +++ b/src/components/CronTab/src/tabs/MonthUI.vue @@ -0,0 +1,59 @@ + + + + + 每月 + + + 区间 + 从 + + 月 至 + + 月 + + + 循环 + 从 + + 月开始,间隔 + + 月 + + + 指定 + + + + {{ i }} + + + + + + + + + diff --git a/src/components/CronTab/src/tabs/SecondUI.vue b/src/components/CronTab/src/tabs/SecondUI.vue new file mode 100644 index 00000000..6f08ba8a --- /dev/null +++ b/src/components/CronTab/src/tabs/SecondUI.vue @@ -0,0 +1,59 @@ + + + + + 每秒 + + + 区间 + 从 + + 秒 至 + + 秒 + + + 循环 + 从 + + 秒开始,间隔 + + 秒 + + + 指定 + + + + {{ i }} + + + + + + + + + diff --git a/src/components/CronTab/src/tabs/WeekUI.vue b/src/components/CronTab/src/tabs/WeekUI.vue new file mode 100644 index 00000000..e8c65d19 --- /dev/null +++ b/src/components/CronTab/src/tabs/WeekUI.vue @@ -0,0 +1,125 @@ + + + + + 不设置 + 日和周只能设置其中之一 + + + 区间 + 从 + + 至 + + + + 循环 + 从 + + 开始,间隔 + + 天 + + + 指定 + + + + {{ opt.label }} + + + + + + + + + diff --git a/src/components/CronTab/src/tabs/YearUI.vue b/src/components/CronTab/src/tabs/YearUI.vue new file mode 100644 index 00000000..5b57c0d8 --- /dev/null +++ b/src/components/CronTab/src/tabs/YearUI.vue @@ -0,0 +1,49 @@ + + + + + 每年 + + + 区间 + 从 + + 年 至 + + 年 + + + 循环 + 从 + + 年开始,间隔 + + 年 + + + + + + diff --git a/src/components/CronTab/src/tabs/useTabMixin.ts b/src/components/CronTab/src/tabs/useTabMixin.ts new file mode 100644 index 00000000..48b2fc44 --- /dev/null +++ b/src/components/CronTab/src/tabs/useTabMixin.ts @@ -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([]) + // 对于不同的类型,所定义的值也有所不同 + 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 + } +} diff --git a/src/components/CronTab/src/validator.ts b/src/components/CronTab/src/validator.ts new file mode 100644 index 00000000..d0e54437 --- /dev/null +++ b/src/components/CronTab/src/validator.ts @@ -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 diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts index ad36ebf9..dbf21c8c 100644 --- a/src/components/Form/src/types/index.ts +++ b/src/components/Form/src/types/index.ts @@ -117,3 +117,4 @@ export type ComponentType = | 'ApiTransfer' | 'Editor' | 'FileUpload' + | 'CronTab' diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts index 98feef10..c95a8545 100644 --- a/src/utils/dateUtil.ts +++ b/src/utils/dateUtil.ts @@ -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 分钟 * diff --git a/src/utils/index.ts b/src/utils/index.ts index cb7a2ab4..898758e6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -113,3 +113,29 @@ export const withInstall = (component: T, alias?: str } return component as WithInstall } + +/** + * 简单实现防抖方法 + * + * 防抖(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) + } +} diff --git a/src/views/infra/job/job.data.ts b/src/views/infra/job/job.data.ts index f7b2dfdc..922f6c7e 100644 --- a/src/views/infra/job/job.data.ts +++ b/src/views/infra/job/job.data.ts @@ -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: '重试次数',