mirror of
				https://github.com/continew-org/continew-admin-ui.git
				synced 2025-10-31 20:58:40 +08:00 
			
		
		
		
	feat(job): 支持可视化生成 CRON 表达式
This commit is contained in:
		
							
								
								
									
										88
									
								
								src/components/GenCron/CronForm/component/day-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/components/GenCron/CronForm/component/day-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| <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 { TypeEnum, useFormProps, useFormSetup, useFromEmits } from './use-mixin' | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'DayForm', | ||||
|   props: useFormProps({ | ||||
|     defaultValue: '*', | ||||
|     props: { | ||||
|       week: { type: String, default: '?' } | ||||
|     } | ||||
|   }), | ||||
|   emits: useFromEmits(), | ||||
|   setup(props, context) { | ||||
|     const isDisabled = 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: isDisabled | ||||
|     }) | ||||
|     const typeWorkAttrs = computed(() => ({ | ||||
|       disabled: setup.type.value !== TypeEnum.work || props.disabled || isDisabled.value, | ||||
|       ...setup.inputNumberAttrs.value | ||||
|     })) | ||||
|  | ||||
|     watch( | ||||
|       () => props.week, | ||||
|       () => { | ||||
|         setup.updateValue(isDisabled.value ? '?' : setup.computeValue.value) | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     return { ...setup, typeWorkAttrs } | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
							
								
								
									
										60
									
								
								src/components/GenCron/CronForm/component/hour-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/components/GenCron/CronForm/component/hour-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <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 { useFormProps, useFormSetup, useFromEmits } 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> | ||||
							
								
								
									
										60
									
								
								src/components/GenCron/CronForm/component/minute-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/components/GenCron/CronForm/component/minute-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <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 { useFormProps, useFormSetup, useFromEmits } 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> | ||||
							
								
								
									
										60
									
								
								src/components/GenCron/CronForm/component/month-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/components/GenCron/CronForm/component/month-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <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 { useFormProps, useFormSetup, useFromEmits } 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> | ||||
							
								
								
									
										60
									
								
								src/components/GenCron/CronForm/component/second-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/components/GenCron/CronForm/component/second-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <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 { useFormProps, useFormSetup, useFromEmits } 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> | ||||
							
								
								
									
										220
									
								
								src/components/GenCron/CronForm/component/use-mixin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/components/GenCron/CronForm/component/use-mixin.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,220 @@ | ||||
| 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.includes('?')) { | ||||
|         type.value = TypeEnum.unset | ||||
|       } else if (value.includes('-')) { | ||||
|         type.value = TypeEnum.range | ||||
|         const values = value.split('-') | ||||
|         if (values.length >= 2) { | ||||
|           valueRange.start = Number.parseInt(values[0]) | ||||
|           valueRange.end = Number.parseInt(values[1]) | ||||
|         } | ||||
|       } else if (value.includes('/')) { | ||||
|         type.value = TypeEnum.loop | ||||
|         const values = value.split('/') | ||||
|         if (values.length >= 2) { | ||||
|           valueLoop.start = value[0] === '*' ? 0 : Number.parseInt(values[0]) | ||||
|           valueLoop.interval = Number.parseInt(values[1]) | ||||
|         } | ||||
|       } else if (value.includes('W')) { | ||||
|         type.value = TypeEnum.work | ||||
|         const values = value.split('W') | ||||
|         if (!values[0] && !Number.isNaN(values[0])) { | ||||
|           valueWork.value = Number.parseInt(values[0]) | ||||
|         } | ||||
|       } else if (value.includes('L')) { | ||||
|         type.value = TypeEnum.last | ||||
|       } else if (value.includes(',') || !Number.isNaN(value)) { | ||||
|         type.value = TypeEnum.specify | ||||
|         valueList.value = value.split(',').map((item: any) => Number.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, | ||||
|     beforeRadioAttrs, | ||||
|     inputNumberAttrs, | ||||
|     typeRangeAttrs, | ||||
|     typeLoopAttrs, | ||||
|     typeSpecifyAttrs | ||||
|   } | ||||
| } | ||||
							
								
								
									
										108
									
								
								src/components/GenCron/CronForm/component/week-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/components/GenCron/CronForm/component/week-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| <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 { TypeEnum, WEEK_MAP, useFormProps, useFormSetup, useFromEmits } 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> | ||||
							
								
								
									
										46
									
								
								src/components/GenCron/CronForm/component/year-form.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/GenCron/CronForm/component/year-form.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <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 { 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> | ||||
							
								
								
									
										378
									
								
								src/components/GenCron/CronForm/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								src/components/GenCron/CronForm/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,378 @@ | ||||
| <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 lang="ts" setup> | ||||
| 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> | ||||
							
								
								
									
										9
									
								
								src/components/GenCron/CronForm/type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/components/GenCron/CronForm/type.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| // cron 参数类型 | ||||
| export interface CronPropType { | ||||
|   modelValue: string | ||||
|   disabled: boolean | ||||
|   hideSecond: boolean | ||||
|   hideYear: boolean | ||||
|   placeholder: string | ||||
|   callback: (expression: string, timestamp: number, validated: boolean) => void | ||||
| } | ||||
							
								
								
									
										67
									
								
								src/components/GenCron/CronModel/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/components/GenCron/CronModel/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| <template> | ||||
|   <a-modal v-model:visible="visible" | ||||
|            modal-class="modal-form-small" | ||||
|            title-align="start" | ||||
|            title="CRON 生成器" | ||||
|            :top="32" | ||||
|            :width="780" | ||||
|            :align-center="false" | ||||
|            :draggable="true" | ||||
|            :mask-closable="false" | ||||
|            :unmount-on-close="true" | ||||
|            :body-style="{ padding: '4px 16px 8px 16px' }"> | ||||
|     <!-- cron 输入框 --> | ||||
|     <CronGeneratorInput ref="cronInputRef" 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"> | ||||
| </script> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { Message } from '@arco-design/web-vue' | ||||
| import CronGeneratorInput from '@/components/GenCron/CronForm/index.vue' | ||||
|  | ||||
| const emits = defineEmits(['ok']) | ||||
|  | ||||
| const visible = ref<boolean>(false) | ||||
|  | ||||
| const cronInputRef = ref<InstanceType<typeof CronGeneratorInput>>() | ||||
|  | ||||
| const cronExpression = ref('') | ||||
|  | ||||
| // 打开新增 | ||||
| const open = (cron: string = '') => { | ||||
|   cronExpression.value = cron | ||||
|   visible.value = true | ||||
| } | ||||
|  | ||||
| defineExpose({ open }) | ||||
|  | ||||
| // 确定 | ||||
| const handlerOk = () => { | ||||
|   if (cronInputRef.value?.checkCron()) { | ||||
|     Message.error('日和周只能有一个为 [不设置]') | ||||
|     return | ||||
|   } | ||||
|   visible.value = false | ||||
|   emits('ok', cronExpression.value) | ||||
| } | ||||
|  | ||||
| // 关闭 | ||||
| const handlerClose = () => { | ||||
|   visible.value = false | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| </style> | ||||
| @@ -11,18 +11,15 @@ | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import type { LabelValueState } from '@/types/global' | ||||
| import type { GiCellTagType } from '@/components/GiCell/type' | ||||
|  | ||||
| defineOptions({ name: 'GiCellTag' }) | ||||
|  | ||||
| const props = defineProps({ | ||||
|   dict: { | ||||
|     type: Array<LabelValueState>, | ||||
|     required: true | ||||
|   }, | ||||
|   value: { | ||||
|     type: [Number, String], | ||||
|     required: true | ||||
|   } | ||||
| const props = withDefaults(defineProps<Partial<GiCellTagType>>(), { | ||||
|   dict: [{ | ||||
|     label: '', | ||||
|     value: '' | ||||
|   }], | ||||
|   value: '' | ||||
| }) | ||||
|  | ||||
| const dictItem = computed((): LabelValueState => { | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/components/GiCell/type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/components/GiCell/type.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| import type { LabelValueState } from '@/types/global' | ||||
|  | ||||
| export interface GiCellTagType { | ||||
|   dict: LabelValueState[] | any[] | ||||
|   value: number | string | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { browse, mapTree } from 'xe-utils' | ||||
| import { camelCase, upperFirst } from 'lodash-es' | ||||
| import { Message } from '@arco-design/web-vue' | ||||
| import CronParser from 'cron-parser' | ||||
| import { isExternal } from '@/utils/validate' | ||||
|  | ||||
| export function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { | ||||
| @@ -279,3 +280,75 @@ export const fileToBase64 = (file: File): Promise<string> => { | ||||
|     reader.readAsDataURL(file) | ||||
|   }) | ||||
| } | ||||
| 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(), | ||||
|     'H+': date.getHours(), | ||||
|     'm+': date.getMinutes(), | ||||
|     's+': date.getSeconds(), | ||||
|     'q+': Math.floor((date.getMonth() + 3) / 3), | ||||
|     'S+': date.getMilliseconds() | ||||
|   } | ||||
|  | ||||
|   let formattedDate = pattern // Start with the pattern | ||||
|  | ||||
|   // Year Handling | ||||
|   const yearMatch = formattedDate.match(/(y+)/) | ||||
|   if (yearMatch) { | ||||
|     formattedDate = formattedDate.replace(yearMatch[0], (`${date.getFullYear()}`).substring(4 - yearMatch[0].length)) | ||||
|   } | ||||
|  | ||||
|   // Other Formatters | ||||
|   for (const k in o) { | ||||
|     const reg = new RegExp(`(${k})`) | ||||
|     const match = formattedDate.match(reg) | ||||
|     if (match) { | ||||
|       formattedDate = formattedDate.replace(match[0], (match[0].length === 1) ? o[k] : (`00${o[k]}`).substring((`${o[k]}`).length)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return formattedDate | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 不含年的 cron 表达式 | ||||
|  * @param cron | ||||
|  */ | ||||
| const expressionNoYear = (cron: string) => { | ||||
|   const vs = cron.split(' ') | ||||
|   return vs.slice(0, vs.length - 1).join(' ') | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 解析cron表达式预计未来运行时间 | ||||
|  * @param cron cron表达式 | ||||
|  */ | ||||
| export function parseCron(cron: string) { | ||||
|   try { | ||||
|     const parse = expressionNoYear(cron) | ||||
|     const iter = CronParser.parseExpression(parse, { | ||||
|       currentDate: dateFormat(new Date()) | ||||
|     }) | ||||
|     const result: string[] = [] | ||||
|     for (let i = 1; i <= 5; i++) { | ||||
|       const nextDate = iter.next() | ||||
|       if (nextDate) { | ||||
|         result.push(dateFormat(new Date(nextDate as any))) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return result.length > 0 ? result.join('\n') : '无执行时间' | ||||
|   } catch (e) { | ||||
|     return '表达式错误' | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -58,11 +58,17 @@ | ||||
|               > | ||||
|                 <template #suffix>秒</template> | ||||
|               </a-input-number> | ||||
|               <a-input | ||||
|                 v-else | ||||
|                 v-model="form.triggerInterval" | ||||
|                 placeholder="请输入CRON表达式" | ||||
|               /> | ||||
|               <div v-else style="display: flex;"> | ||||
|                 <a-input | ||||
|                     v-model="form.triggerInterval" | ||||
|                     placeholder="请输入CRON表达式" | ||||
|                 /> | ||||
|                 <a-button @click="openGeneratorCron(form.triggerInterval)"> | ||||
|                   <template #icon> | ||||
|                     <icon-history /> | ||||
|                   </template> | ||||
|                 </a-button> | ||||
|               </div> | ||||
|             </a-form-item> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
| @@ -150,6 +156,7 @@ | ||||
|         </a-row> | ||||
|       </fieldset> | ||||
|     </a-form> | ||||
|     <CronGeneratorModal ref="genModal" @ok="(e) => form.triggerInterval = e" /> | ||||
|   </a-modal> | ||||
| </template> | ||||
|  | ||||
| @@ -159,6 +166,7 @@ import { useWindowSize } from '@vueuse/core' | ||||
| import { addJob, listGroup, updateJob } from '@/apis/schedule' | ||||
| import { useForm } from '@/hooks' | ||||
| import { useDict } from '@/hooks/app' | ||||
| import CronGeneratorModal from '@/components/GenCron/CronModel/index.vue' | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'save-success'): void | ||||
| @@ -176,7 +184,7 @@ const dataId = ref() | ||||
| const isUpdate = computed(() => !!dataId.value) | ||||
| const title = computed(() => (isUpdate.value ? '修改任务' : '新增任务')) | ||||
| const formRef = ref<FormInstance>() | ||||
|  | ||||
| const genModal = ref() | ||||
| const rules: FormInstance['rules'] = { | ||||
|   groupName: [{ required: true, message: '请选择任务组' }], | ||||
|   jobName: [{ required: true, message: '请输入任务名称' }], | ||||
| @@ -302,6 +310,11 @@ const onDeleteArgs = (index) => { | ||||
|   args.value.splice(index, 1) | ||||
| } | ||||
|  | ||||
| // 打开生成表达式 | ||||
| const openGeneratorCron = (cron: string) => { | ||||
|   genModal.value.open(cron) | ||||
| } | ||||
|  | ||||
| defineExpose({ onAdd, onUpdate }) | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -36,7 +36,14 @@ | ||||
|       <template #triggerType="{ record }"> | ||||
|         <GiCellTag :value="record.triggerType" :dict="job_trigger_type_enum" />:  | ||||
|         <span v-if="record.triggerType === 2">{{ record.triggerInterval }} 秒</span> | ||||
|         <span v-else>{{ record.triggerInterval }}</span> | ||||
|         <span v-else> | ||||
|           <a-popover title="最近5次运行时间" position="bottom"> | ||||
|             <template #content> | ||||
|               <a-textarea :model-value="parseCron(record.triggerInterval)" :auto-size="true" style="margin-top: 10px" /> | ||||
|             </template> | ||||
|              <a-link>{{ record.triggerInterval }}</a-link> | ||||
|           </a-popover> | ||||
|          </span> | ||||
|       </template> | ||||
|       <template #taskType="{ record }"> | ||||
|         <GiCellTag :value="record.taskType" :dict="job_task_type_enum" /> | ||||
| @@ -77,7 +84,7 @@ import { type JobQuery, type JobResp, deleteJob, listGroup, listJob, triggerJob, | ||||
| import type { TableInstanceColumns } from '@/components/GiTable/type' | ||||
| import { useTable } from '@/hooks' | ||||
| import { useDict } from '@/hooks/app' | ||||
| import { isMobile } from '@/utils' | ||||
| import { isMobile, parseCron } from '@/utils' | ||||
| import has from '@/utils/has' | ||||
|  | ||||
| defineOptions({ name: 'ScheduleJob' }) | ||||
| @@ -180,13 +187,11 @@ const JobDetailDrawerRef = ref<InstanceType<typeof JobDetailDrawer>>() | ||||
| const onDetail = (record: JobResp) => { | ||||
|   JobDetailDrawerRef.value?.onDetail(record) | ||||
| } | ||||
|  | ||||
| const router = useRouter() | ||||
| // 日志 | ||||
| const onLog = (record: JobResp) => { | ||||
|   router.push({ path: '/schedule/log', query: { jobId: record.id, jobName: record.jobName, groupName: record.groupName } }) | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|   getGroupList() | ||||
| }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 KAI
					KAI