cron 生成器.

This commit is contained in:
lijiahang
2024-05-21 11:24:27 +08:00
parent 2fed2aaa34
commit 4f0f320fcd
21 changed files with 9239 additions and 9178 deletions

View File

@@ -68,15 +68,47 @@
// -- modal
.modal-form-small {
padding: 20px 20px 2px 20px;
.arco-modal-header {
height: 40px;
padding: 0 14px;
}
.arco-modal-body {
padding: 20px 20px 2px 20px;
}
.arco-modal-footer {
padding: 8px 12px;
}
}
.modal-form-medium {
padding: 20px 28px 4px 28px;
.arco-modal-header {
height: 40px;
padding: 0 16px;
}
.arco-modal-body {
padding: 20px 28px 4px 28px;
}
.arco-modal-footer {
padding: 10px 12px;
}
}
.modal-form-large {
padding: 24px 36px 4px 36px;
.arco-modal-header {
height: 48px;
}
.arco-modal-body {
padding: 24px 36px 4px 36px;
}
.arco-modal-footer {
padding: 10px 14px;
}
}
// -- card-view

View File

@@ -0,0 +1,61 @@
export const cronEmits = ['change', 'update:modelValue'];
// cron 参数类型
export interface CronPropType {
modelValue: string;
disabled: boolean;
hideSecond: boolean;
hideYear: boolean;
placeholder: string;
callback: (expression: string, timestamp: number, validated: boolean) => void;
}
// cron 参数默认值
export const cronDefaultProps: Partial<CronPropType> = {
disabled: false,
hideSecond: false,
hideYear: false,
placeholder: '请输入 cron 表达式',
};
/**
* 转化为 quartz 周
* 1 = 周日
* 2 = 周一
* 3 = 周二
* 4 = 周三
* 5 = 周四
* 6 = 周五
* 7 = 周六
*/
export const convertWeekToQuartz = (week: string) => {
const convert = (v: string) => {
if (v === '0') {
return '1';
}
if (v === '1') {
return '0';
}
return (Number.parseInt(v) - 1).toString();
};
// 匹配示例 1-7 or 1/7
const patten1 = /^([0-7])([-/])([0-7])$/;
// 匹配示例 1,4,7
const 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;
};

View File

@@ -0,0 +1,89 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每日</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-input-number v-model="valueRange.start" v-bind="inputNumberAttrs" />
<span> </span>
<a-input-number v-model="valueRange.end" v-bind="inputNumberAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-input-number v-model="valueLoop.start" v-bind="typeLoopAttrs" />
<span>日开始, 间隔</span>
<a-input-number v-model="valueLoop.interval" v-bind="typeLoopAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.last" v-bind="beforeRadioAttrs">最后一日</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<a-grid :cols="11">
<a-grid-item v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">
{{ i }}
</a-checkbox>
</a-grid-item>
</a-grid>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue';
import { TypeEnum, useFormProps, useFromEmits, useFormSetup } from './use-mixin';
export default defineComponent({
name: 'DayForm',
props: useFormProps({
defaultValue: '*',
props: {
week: { type: String, default: '?' },
},
}),
emits: useFromEmits(),
setup(props, context) {
const disabledChoice = computed(() => {
return (props.week && props.week !== '?') || props.disabled;
});
const setup = useFormSetup(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,61 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每时</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-input-number v-model="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<a-input-number v-model="valueRange.end" v-bind="typeRangeAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-input-number v-model="valueLoop.start" v-bind="typeLoopAttrs" />
<span>时开始, 间隔</span>
<a-input-number v-model="valueLoop.interval" v-bind="typeLoopAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<a-grid :cols="12">
<a-grid-item v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">
{{ i }}
</a-checkbox>
</a-grid-item>
</a-grid>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFromEmits, useFormProps, useFormSetup } from './use-mixin';
export default defineComponent({
name: 'HourForm',
props: useFormProps({
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
return useFormSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 23,
valueRange: { start: 0, end: 23 },
valueLoop: { start: 0, interval: 1 },
});
},
});
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每分</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-input-number v-model="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<a-input-number v-model="valueRange.end" v-bind="typeRangeAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-input-number v-model="valueLoop.start" v-bind="typeLoopAttrs" />
<span>分开始, 间隔</span>
<a-input-number v-model="valueLoop.interval" v-bind="typeLoopAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<a-grid :cols="10">
<a-grid-item v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">
{{ i }}
</a-checkbox>
</a-grid-item>
</a-grid>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFromEmits, useFormProps, useFormSetup } from './use-mixin';
export default defineComponent({
name: 'MinuteForm',
props: useFormProps({
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
return useFormSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 },
});
},
});
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每月</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-input-number v-model="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<a-input-number v-model="valueRange.end" v-bind="typeRangeAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-input-number v-model="valueLoop.start" v-bind="typeLoopAttrs" />
<span>月开始, 间隔</span>
<a-input-number v-model="valueLoop.interval" v-bind="typeLoopAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<a-grid :cols="12">
<a-grid-item v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">
{{ i }}
</a-checkbox>
</a-grid-item>
</a-grid>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFromEmits, useFormProps, useFormSetup } from './use-mixin';
export default defineComponent({
name: 'MonthForm',
props: useFormProps({
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
return useFormSetup(props, context, {
defaultValue: '*',
minValue: 1,
maxValue: 12,
valueRange: { start: 1, end: 12 },
valueLoop: { start: 1, interval: 1 },
});
},
});
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每秒</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-input-number v-model="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<a-input-number v-model="valueRange.end" v-bind="typeRangeAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-input-number v-model="valueLoop.start" v-bind="typeLoopAttrs" />
<span>秒开始, 间隔</span>
<a-input-number v-model="valueLoop.interval" v-bind="typeLoopAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<a-grid :cols="10">
<a-grid-item v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">
{{ i }}
</a-checkbox>
</a-grid-item>
</a-grid>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFromEmits, useFormProps, useFormSetup } from './use-mixin';
export default defineComponent({
name: 'SecondForm',
props: useFormProps({
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
return useFormSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 },
});
},
});
</script>

View File

@@ -0,0 +1,222 @@
import { computed, reactive, ref, unref, watch } from 'vue';
// 类型定义
export enum TypeEnum {
unset = 'UNSET',
every = 'EVERY',
range = 'RANGE',
loop = 'LOOP',
work = 'WORK',
last = 'LAST',
specify = 'SPECIFY',
}
// 周定义
export const WEEK_MAP: any = {
1: '周日',
2: '周一',
3: '周二',
4: '周三',
5: '周四',
6: '周五',
7: '周六',
};
// use 公共 props
export function useFormProps(options: any) {
const defaultValue = options?.defaultValue ?? '?';
return {
modelValue: {
type: String,
default: defaultValue,
},
disabled: {
type: Boolean,
default: false,
},
...options?.props,
};
}
// use 公共 emits
export function useFromEmits() {
return ['change', 'update:modelValue'];
}
// use 公共 setup
export function useFormSetup(props: any, context: any, options: any) {
const { emit } = context;
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 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;
});
// 更新值
const updateValue = (value: any) => {
emit('change', value);
emit('update:modelValue', value);
};
// 解析值
const parseValue = (value: any) => {
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: any) => parseInt(item));
} else {
type.value = TypeEnum.every;
}
} catch (e) {
type.value = TypeEnum.every;
}
};
// 更新值
watch(() => props.modelValue, (val) => {
if (val !== computeValue.value) {
parseValue(val);
}
}, { immediate: true });
// 更新值
watch(computeValue, (v) => updateValue(v));
// 单选框属性
const beforeRadioAttrs = computed(() => ({
class: ['choice'],
disabled: props.disabled || unref(options.disabled),
size: 'small',
}));
// 输入框属性
const inputNumberAttrs = computed(() => ({
max: maxValue.value,
min: minValue.value,
precision: 0,
size: 'small',
hideButton: true,
class: 'w60'
}));
// 区间属性
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'],
size: 'small',
}));
return {
type,
TypeEnum,
defaultValue,
valueRange,
valueLoop,
valueList,
valueWork,
maxValue,
minValue,
computeValue,
specifyRange,
updateValue,
// parseValue,
beforeRadioAttrs,
inputNumberAttrs,
typeRangeAttrs,
typeLoopAttrs,
typeSpecifyAttrs,
};
}

View File

@@ -0,0 +1,109 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-select v-model="valueRange.start" v-bind="typeRangeSelectAttrs">
<a-option v-for="item in weekOptions" :key="item.value" :label="item.label" :value="item.value" />
</a-select>
<span></span>
<a-select v-model="valueRange.end" v-bind="typeRangeSelectAttrs">
<a-option v-for="item in weekOptions" :key="item.value" :label="item.label" :value="item.value" />
</a-select>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-select v-model="valueLoop.start" v-bind="typeLoopSelectAttrs">
<a-option v-for="item in weekOptions" :key="item.value" :label="item.label" :value="item.value" />
</a-select>
<span>开始, 间隔</span>
<a-input-number v-model="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">
<a-checkbox-group v-model="valueList">
<template v-for="opt in weekOptions" :key="opt">
<a-checkbox :value="opt.value" v-bind="typeSpecifyAttrs">
{{ opt.label }}
</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue';
import { TypeEnum, useFormProps, useFormSetup, useFromEmits, WEEK_MAP } from './use-mixin';
export default defineComponent({
name: 'WeekForm',
props: useFormProps({
defaultValue: '?',
props: {
day: { type: String, default: '*' },
},
}),
emits: useFromEmits(),
setup(props, context) {
const disabledChoice = computed(() => {
return (props.day && props.day !== '?') || props.disabled;
});
const setup = useFormSetup(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(() => {
const options: { label: string; value: number }[] = [];
for (const weekKey of Object.keys(WEEK_MAP)) {
const weekName: string = WEEK_MAP[weekKey];
options.push({
value: Number.parseInt(weekKey),
label: weekName,
});
}
return options;
});
const typeRangeSelectAttrs = computed(() => ({
disabled: setup.typeRangeAttrs.value.disabled,
size: 'small',
class: ['w80'],
}));
const typeLoopSelectAttrs = computed(() => ({
disabled: setup.typeLoopAttrs.value.disabled,
size: 'small',
class: ['w80'],
}));
watch(() => props.day, () => {
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
});
return {
...setup,
weekOptions,
typeLoopSelectAttrs,
typeRangeSelectAttrs,
WEEK_MAP,
};
},
});
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="cron-inner-config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每年</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span></span>
<a-input-number v-model="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<a-input-number v-model="valueRange.end" v-bind="typeRangeAttrs" />
<span></span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span></span>
<a-input-number v-model="valueLoop.start" v-bind="typeLoopAttrs" />
<span>年开始, 间隔</span>
<a-input-number v-model="valueLoop.interval" v-bind="typeLoopAttrs" />
<span></span>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFormProps, useFormSetup, useFromEmits } from './use-mixin';
export default defineComponent({
name: 'YearForm',
props: useFormProps({
defaultValue: '*',
}),
emits: useFromEmits(),
setup(props, context) {
const nowYear = new Date().getFullYear();
return useFormSetup(props, context, {
defaultValue: '*',
minValue: 0,
valueRange: { start: nowYear, end: nowYear + 100 },
valueLoop: { start: nowYear, interval: 1 },
});
},
});
</script>

View File

@@ -0,0 +1,386 @@
<template>
<div class="cron-inner">
<div class="content">
<!-- 设置表单 -->
<a-tabs v-model:active-key="activeKey" size="small">
<!-- -->
<a-tab-pane title="秒" key="second" v-if="!hideSecond">
<second-form v-model="second" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane title="分" key="minute">
<minute-form v-model="minute" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane title="时" key="hour">
<hour-form v-model="hour" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane title="日" key="day">
<day-form v-model="day" :week="week" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane title="月" key="month">
<month-form v-model="month" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane title="周" key="week">
<week-form v-model="week" :day="day" :disabled="disabled" />
</a-tab-pane>
<!-- -->
<a-tab-pane title="年" key="year" v-if="!hideYear && !hideSecond">
<year-form 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="inputValues.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="inputValues.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="inputValues.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="inputValues.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="inputValues.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="inputValues.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="inputValues.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="inputValues.cron"
:placeholder="placeholder"
@change="onInputCronChange">
<template #prepend>
<span class="allow-click">Expression</span>
</template>
<template #append>
<span class="allow-click span-blue"
title="点击复制"
@click="copy(inputValues.cron,'已复制')">
<icon-copy />
</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">
export default {
name: 'cronInput'
};
</script>
<script lang="ts" setup>
import type { CronPropType } from './const.types';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { cronEmits, cronDefaultProps, convertWeekToQuartz } from './const.types';
import { useDebounceFn } from '@vueuse/core';
import { dateFormat } from '@/utils';
import { copy } from '@/hooks/copy';
import CronParser from 'cron-parser';
import SecondForm from './form/second-form.vue';
import MinuteForm from './form/minute-form.vue';
import HourForm from './form/hour-form.vue';
import DayForm from './form/day-form.vue';
import MonthForm from './form/month-form.vue';
import WeekForm from './form/week-form.vue';
import YearForm from './form/year-form.vue';
const emit = defineEmits([...cronEmits]);
const props = withDefaults(defineProps<Partial<CronPropType>>(), { ...cronDefaultProps });
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 previewTimes = ref('执行预览');
// cron 表达式
const expression = 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 = computed(() => {
const v = expression.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 calcTriggerTime = () => {
try {
// 解析表达式
const iter = CronParser.parseExpression(expressionNoYear.value, {
currentDate: dateFormat(new 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(expression.value, +new Date(), true);
}
} catch (e) {
previewTimes.value = '表达式错误';
// 回调
if (props.callback) {
props.callback(expression.value, +new Date(), false);
}
}
};
const calcTriggerTimeList = useDebounceFn(calcTriggerTime, 500);
// 监听 cron 修改
watch(() => props.modelValue, (newVal) => {
if (newVal === expression.value) {
return;
}
parseCron();
});
// 监听 cron 修改
watch(expression, (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 = () => {
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 = expression.value;
};
// 修改 cron 解析内容
const onInputChange = () => {
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;
};
// 修改 cron 输入框
const onInputCronChange = (value: string) => {
emitValue(value);
};
// 修改 cron
const emitValue = (value: string) => {
emit('change', value);
emit('update:modelValue', value);
};
onMounted(() => {
assignInput();
parseCron();
// 如果 modelValue 没有值则更新为 expression
if (!props.modelValue) {
emitValue(expression.value);
}
});
</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>

View File

@@ -0,0 +1,68 @@
<template>
<a-modal v-model:visible="visible"
modal-class="modal-form-small"
title-align="start"
title="cron 生成器"
:top="16"
:width="780"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:body-style="{ padding: '4px 16px 8px 16px' }">
<!-- cron 输入框 -->
<cron-input v-model="cronExpression" />
<!-- 页脚-->
<template #footer>
<a-button size="small" @click="handlerClose">关闭</a-button>
<a-button size="small"
type="primary"
@click="handlerOk">
确定
</a-button>
</template>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'cronModal'
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import useVisible from '@/hooks/visible';
import CronInput from '@/components/data/cron/input/index.vue';
const { visible, setVisible } = useVisible();
const cronExpression = ref('');
const emits = defineEmits(['ok']);
// 打开新增
const open = (cron: string = '') => {
cronExpression.value = cron;
setVisible(true);
};
defineExpose({ open });
// 确定
const handlerOk = () => {
setVisible(false);
console.log(cronExpression.value);
console.log('ok', cronExpression.value);
emits('ok', cronExpression.value);
};
// 关闭
const handlerClose = () => {
setVisible(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,45 @@
import CronParser from 'cron-parser';
// 验证器
export const cronValidator = ({}, value: any) => {
// 没填写就不校验
if (!value) {
return Promise.resolve();
}
const values: string[] = value.split(' ').filter((item: any) => !!item);
if (values.length > 7) {
return Promise.reject('表达式最多 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: any) => isNaN(Number(item)));
if (checkYear) {
return Promise.reject('表达式参数[年]错误: ' + 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('表达式错误: ' + e);
}
};
export default cronValidator;

View File

@@ -4,7 +4,7 @@
<a-spin class="message-classify-container"
:hide-icon="true"
:loading="fetchLoading">
<a-tabs v-model:activeKey="currentClassify"
<a-tabs v-model:active-key="currentClassify"
type="rounded"
:hide-content="true"
@change="loadClassifyMessage">

View File

@@ -64,6 +64,9 @@ export const YMD_HMS = 'yyyy-MM-dd HH:mm:ss';
* 格式化时间
*/
export function dateFormat(date = new Date(), pattern = YMD_HMS) {
if (!date) {
return '';
}
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),

View File

@@ -2,7 +2,7 @@ export default {
'login.form.title': '登录 Orion Visor',
'login.form.userName.errMsg': '用户名不能为空',
'login.form.password.errMsg': '密码不能为空',
'login.form.login.errMsg': '登录出错轻刷新重试',
'login.form.login.errMsg': '登录出错, 轻刷新重试',
'login.form.login.success': '欢迎使用',
'login.form.userName.placeholder': '用户名',
'login.form.password.placeholder': '密码',