Browse Source

[Feature][UI Next] Add Crontab Component (#8409)

* support crontab component

* fix time-modal form data init bug
Devosend 3 years ago
parent
commit
53869cb0eb

+ 193 - 0
dolphinscheduler-ui-next/src/components/crontab/common.ts

@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash'
+
+const timeI18n = {
+  second: {
+    everyTime: 'crontab.every_second',
+    every: 'crontab.every',
+    timeCarriedOut: 'crontab.second_carried_out',
+    timeStart: 'crontab.second_start',
+    cycleFrom: 'crontab.cycle_from',
+    specificTime: 'crontab.specific_second',
+    specificTimeTip: 'crontab.specific_second_tip',
+    to: 'crontab.to',
+    time: 'crontab.second'
+  },
+  minute: {
+    everyTime: 'crontab.every_minute',
+    every: 'crontab.every',
+    timeCarriedOut: 'crontab.minute_carried_out',
+    timeStart: 'crontab.minute_start',
+    cycleFrom: 'crontab.cycle_from',
+    specificTime: 'crontab.specific_minute',
+    specificTimeTip: 'crontab.specific_minute_tip',
+    to: 'crontab.to',
+    time: 'crontab.minute'
+  },
+  hour: {
+    everyTime: 'crontab.every_hour',
+    every: 'crontab.every',
+    timeCarriedOut: 'crontab.hour_carried_out',
+    timeStart: 'crontab.hour_start',
+    cycleFrom: 'crontab.cycle_from',
+    specificTime: 'crontab.specific_hour',
+    specificTimeTip: 'crontab.specific_hour_tip',
+    to: 'crontab.to',
+    time: 'crontab.hour'
+  },
+  month: {
+    everyTime: 'crontab.every_month',
+    every: 'crontab.every',
+    timeCarriedOut: 'crontab.month_carried_out',
+    timeStart: 'crontab.month_start',
+    cycleFrom: 'crontab.cycle_from',
+    specificTime: 'crontab.specific_month',
+    specificTimeTip: 'crontab.specific_month_tip',
+    to: 'crontab.to',
+    time: 'crontab.month'
+  },
+  year: {
+    everyTime: 'crontab.every_year',
+    every: 'crontab.every',
+    timeCarriedOut: 'crontab.year_carried_out',
+    timeStart: 'crontab.year_start',
+    cycleFrom: 'crontab.cycle_from',
+    specificTime: 'crontab.specific_year',
+    specificTimeTip: 'crontab.specific_year_tip',
+    to: 'crontab.to',
+    time: 'crontab.year'
+  }
+}
+
+const week = [
+  {
+    label: 'crontab.sunday',
+    value: 1
+  },
+  {
+    label: 'crontab.monday',
+    value: 2
+  },
+  {
+    label: 'crontab.tuesday',
+    value: 3
+  },
+  {
+    label: 'crontab.wednesday',
+    value: 4
+  },
+  {
+    label: 'crontab.thursday',
+    value: 5
+  },
+  {
+    label: 'crontab.friday',
+    value: 6
+  },
+  {
+    label: 'crontab.saturday',
+    value: 7
+  }
+]
+
+const specificWeek = [
+  {
+    label: 'SUN',
+    value: 'SUN'
+  },
+  {
+    label: 'MON',
+    value: 'MON'
+  },
+  {
+    label: 'TUE',
+    value: 'TUE'
+  },
+  {
+    label: 'WED',
+    value: 'WED'
+  },
+  {
+    label: 'THU',
+    value: 'THU'
+  },
+  {
+    label: 'FRI',
+    value: 'FRI'
+  },
+  {
+    label: 'SAT',
+    value: 'SAT'
+  }
+]
+
+const lastWeeks = [
+  {
+    label: 'crontab.sunday',
+    value: '?'
+  },
+  {
+    label: 'crontab.monday',
+    value: '2L'
+  },
+  {
+    label: 'crontab.tuesday',
+    value: '3L'
+  },
+  {
+    label: 'crontab.wednesday',
+    value: '4L'
+  },
+  {
+    label: 'crontab.thursday',
+    value: '5L'
+  },
+  {
+    label: 'crontab.friday',
+    value: '6L'
+  },
+  {
+    label: 'crontab.saturday',
+    value: '7L'
+  }
+]
+
+const isStr = (str: string, v: string) => {
+  let flag
+  if (str.indexOf(v) !== -1) {
+    flag = str.split(v)
+  }
+  return flag
+}
+
+const isWeek = (str: string) => {
+  let flag = false
+  const data = str.split(',')
+  const isSpecificWeek = (key: string) => {
+    return _.findIndex(specificWeek, (v) => v.value === key) !== -1
+  }
+  _.map(data, (v) => {
+    if (isSpecificWeek(v)) {
+      flag = true
+    }
+  })
+  return flag
+}
+
+export { isStr, isWeek, timeI18n, week, specificWeek, lastWeeks }

+ 29 - 0
dolphinscheduler-ui-next/src/components/crontab/index.module.scss

@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ .crontab-list {
+   display: flex;
+   .crontab-list-item {
+    display: flex;
+    vertical-align: middle;
+    align-items: center;
+    width: 460px;
+    > div {
+      margin: 5px;
+    }
+   }
+ }

+ 131 - 0
dolphinscheduler-ui-next/src/components/crontab/index.tsx

@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { computed, defineComponent, ref, watch, PropType } from 'vue'
+import { NTabPane, NTabs } from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import CrontabTime from './modules/time'
+import CrontabDay from './modules/day'
+import { timeI18n } from './common'
+
+const props = {
+  value: {
+    type: String as PropType<String>,
+    default: '* * * * * ? *'
+  }
+}
+
+export default defineComponent({
+  name: 'Crontab',
+  props,
+  emits: ['update:value'],
+  setup(props, ctx) {
+    const cron = props.value.split(' ')
+    const secondRef = ref(cron[0])
+    const minuteRef = ref(cron[1])
+    const hourRef = ref(cron[2])
+    const dayRef = ref(cron[3])
+    const monthRef = ref(cron[4])
+    const weekRef = ref(cron[5])
+    const yearRef = ref(cron[6])
+
+    const crontabValue = computed(
+      () =>
+        `${secondRef.value} ${minuteRef.value} ${hourRef.value} ${dayRef.value} ${monthRef.value} ${weekRef.value} ${yearRef.value}`
+    )
+
+    const reset = () => {
+      const cron = props.value.split(' ')
+      secondRef.value = cron[0]
+      minuteRef.value = cron[1]
+      hourRef.value = cron[2]
+      dayRef.value = cron[3]
+      monthRef.value = cron[4]
+      weekRef.value = cron[5]
+      yearRef.value = cron[6]
+    }
+
+    watch(
+      () => crontabValue.value,
+      () => {
+        ctx.emit('update:value', crontabValue.value)
+      }
+    )
+
+    watch(
+      () => props.value,
+      () => {
+        reset()
+      }
+    )
+
+    return {
+      secondRef,
+      minuteRef,
+      hourRef,
+      dayRef,
+      weekRef,
+      monthRef,
+      yearRef,
+      crontabValue
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      <NTabs type='line'>
+        <NTabPane name='seconde' tab={t('crontab.second')}>
+          <CrontabTime
+            v-model:timeValue={this.secondRef}
+            timeI18n={timeI18n.second}
+          />
+        </NTabPane>
+        <NTabPane name='minute' tab={t('crontab.minute')}>
+          <CrontabTime
+            v-model:timeValue={this.minuteRef}
+            timeI18n={timeI18n.minute}
+          />
+        </NTabPane>
+        <NTabPane name='hour' tab={t('crontab.hour')}>
+          <CrontabTime
+            v-model:timeValue={this.hourRef}
+            timeI18n={timeI18n.hour}
+          />
+        </NTabPane>
+        <NTabPane name='day' tab={t('crontab.day')}>
+          <CrontabDay
+            v-model:dayValue={this.dayRef}
+            v-model:weekValue={this.weekRef}
+          />
+        </NTabPane>
+        <NTabPane name='month' tab={t('crontab.month')}>
+          <CrontabTime
+            v-model:timeValue={this.monthRef}
+            timeI18n={timeI18n.month}
+          />
+        </NTabPane>
+        <NTabPane name='year' tab={t('crontab.year')}>
+          <CrontabTime
+            v-model:timeValue={this.yearRef}
+            timeI18n={timeI18n.year}
+          />
+        </NTabPane>
+      </NTabs>
+    )
+  }
+})

+ 640 - 0
dolphinscheduler-ui-next/src/components/crontab/modules/day.tsx

@@ -0,0 +1,640 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { defineComponent, onMounted, PropType, ref, watch } from 'vue'
+import { NInputNumber, NRadio, NRadioGroup, NSelect } from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import { isStr, isWeek, week, specificWeek, lastWeeks } from '../common'
+import styles from '../index.module.scss'
+
+const props = {
+  dayValue: {
+    type: String as PropType<string>,
+    default: '*'
+  },
+  weekValue: {
+    type: String as PropType<string>,
+    default: '?'
+  }
+}
+
+export default defineComponent({
+  name: 'CrontabDay',
+  props,
+  emits: ['update:dayValue', 'update:weekValue'],
+  setup(props, ctx) {
+    const { t } = useI18n()
+
+    const options = Array.from({ length: 60 }, (x, i) => ({
+      label: i.toString(),
+      value: i
+    }))
+
+    const weekOptions = week.map((v) => ({
+      label: t(v.label),
+      value: v.value
+    }))
+
+    const lastWeekOptions = lastWeeks.map((v) => ({
+      label: t(v.label),
+      value: v.value
+    }))
+
+    const radioRef = ref()
+    const dayRef = ref()
+    const weekRef = ref()
+    const WkintervalWeekStartRef = ref(2)
+    const WkintervalWeekPerformRef = ref(2)
+    const intervalDayStartRef = ref(1)
+    const intervalDayPerformRef = ref(1)
+    const WkspecificDayRef = ref<Array<number>>([])
+    const WkspecificWeekRef = ref<Array<number>>([])
+    const monthLastDaysRef = ref('L')
+    const monthLastWorkingDaysRef = ref('LW')
+    const monthLastWeeksRef = ref('?')
+    const monthTailBeforeRef = ref(1)
+    const recentlyWorkingDaysMonthRef = ref(1)
+    const WkmonthNumWeeksDayRef = ref(1)
+    const WkmonthNumWeeksWeekRef = ref(1)
+
+    /**
+     * Parse parameter value
+     */
+    const analyticalValue = () => {
+      const $dayVal = props.dayValue
+      const $weekVal = props.weekValue
+      const isWeek1 = $weekVal.indexOf('/') !== -1
+      const isWeek2 = $weekVal.indexOf('#') !== -1
+
+      // Initialization
+      if ($dayVal === '*' && $weekVal === '?') {
+        radioRef.value = 'everyDay'
+        return
+      }
+
+      // week
+      if (isWeek1 || isWeek2 || isWeek($weekVal)) {
+        dayRef.value = '?'
+
+        /**
+         * Processing by sequence number (excluding days)
+         * @param [
+         * WkintervalWeek=>(/),
+         * WkspecificWeek=>(TUE,WED),
+         * WkmonthNumWeeks=>(#)
+         * ]
+         */
+        const hanleWeekOne = () => {
+          const a = isStr($weekVal, '/') as string[]
+          WkintervalWeekStartRef.value = parseInt(a[0])
+          WkintervalWeekPerformRef.value = parseInt(a[1])
+          dayRef.value = '?'
+          weekRef.value = `${WkintervalWeekPerformRef.value}/${WkintervalWeekStartRef.value}`
+          radioRef.value = 'WkintervalWeek'
+        }
+
+        const hanleWeekTwo = () => {
+          WkspecificWeekRef.value = $weekVal
+            .split(',')
+            .map((item) => parseInt(item))
+          radioRef.value = 'WkspecificWeek'
+        }
+
+        const hanleWeekThree = () => {
+          const a = isStr($weekVal, '#') as string[]
+          WkmonthNumWeeksDayRef.value = parseInt(a[0])
+          WkmonthNumWeeksDayRef.value = parseInt(a[1])
+          radioRef.value = 'WkmonthNumWeeks'
+        }
+
+        // Processing week
+        if (isStr($weekVal, '/')) {
+          hanleWeekOne()
+        } else if (isStr($weekVal, '#')) {
+          hanleWeekThree()
+        } else if (isWeek($weekVal)) {
+          hanleWeekTwo()
+        }
+      } else {
+        weekRef.value = '?'
+
+        /**
+         * Processing by sequence number (excluding week)
+         * @param [
+         * everyDay=>(*),
+         * intervalDay=>(1/1),
+         * specificDay=>(1,2,5,3,4),
+         * monthLastDays=>(L),
+         * monthLastWorkingDays=>(LW),
+         * monthLastWeeks=>(3L),
+         * monthTailBefore=>(L-4),
+         * recentlyWorkingDaysMonth=>(6W)
+         * ]
+         */
+        const hanleDayOne = () => {
+          radioRef.value = 'everyDay'
+        }
+
+        const hanleDayTwo = () => {
+          const a = isStr($dayVal, '/') as string[]
+          intervalDayStartRef.value = parseInt(a[0])
+          intervalDayPerformRef.value = parseInt(a[1])
+          radioRef.value = 'intervalDay'
+        }
+
+        const hanleDayThree = () => {
+          WkspecificDayRef.value = $dayVal
+            .split(',')
+            .map((item) => parseInt(item))
+          radioRef.value = 'specificDay'
+        }
+
+        const hanleDayFour = () => {
+          radioRef.value = 'monthLastDays'
+        }
+
+        const hanleDayFive = () => {
+          radioRef.value = 'monthLastWorkingDays'
+        }
+
+        const hanleDaySix = () => {
+          monthLastWeeksRef.value = $dayVal
+          radioRef.value = 'monthLastWeeks'
+        }
+
+        const hanleDaySeven = () => {
+          const a = isStr($dayVal, '-') as string[]
+          monthTailBeforeRef.value = parseInt(a[1])
+          radioRef.value = 'monthTailBefore'
+        }
+
+        const hanleDayEight = () => {
+          recentlyWorkingDaysMonthRef.value = parseInt(
+            $dayVal.slice(0, $dayVal.length - 1)
+          )
+          radioRef.value = 'recentlyWorkingDaysMonth'
+        }
+
+        if ($dayVal === '*') {
+          hanleDayOne()
+        } else if (isStr($dayVal, '/')) {
+          hanleDayTwo()
+        } else if ($dayVal === 'L') {
+          hanleDayFour()
+        } else if ($dayVal === 'LW') {
+          hanleDayFive()
+        } else if ($dayVal.charAt($dayVal.length - 1) === 'L') {
+          hanleDaySix()
+        } else if (isStr($dayVal, '-')) {
+          hanleDaySeven()
+        } else if ($dayVal.charAt($dayVal.length - 1) === 'W') {
+          hanleDayEight()
+        } else {
+          hanleDayThree()
+        }
+      }
+    }
+
+    // Every few weeks
+    const onWkintervalWeekPerform = (value: number | null) => {
+      WkintervalWeekPerformRef.value = value || 0
+      if (radioRef.value === 'WkintervalWeek') {
+        dayRef.value = '?'
+        weekRef.value = `${WkintervalWeekStartRef.value}/${WkintervalWeekPerformRef.value}`
+      }
+    }
+
+    // Every few weeks
+    const onWkintervalWeekStart = (value: number | null) => {
+      WkintervalWeekStartRef.value = value || 0
+      if (radioRef.value === 'WkintervalWeek') {
+        dayRef.value = '?'
+        weekRef.value = `${WkintervalWeekStartRef.value}/${WkintervalWeekPerformRef.value}`
+      }
+    }
+
+    // Interval start time(1)
+    const onIntervalDayStart = (value: number | null) => {
+      intervalDayStartRef.value = value || 0
+      if (radioRef.value === 'intervalDay') {
+        intervalDaySet()
+      }
+    }
+
+    // Interval execution time(2)
+    const onIntervalDayPerform = (value: number | null) => {
+      intervalDayPerformRef.value = value || 0
+      if (radioRef.value === 'intervalDay') {
+        intervalDaySet()
+      }
+    }
+
+    // Specific day of the week (multiple choice)
+    const onWkspecificWeek = (arr: Array<number>) => {
+      WkspecificWeekRef.value = arr
+      if (radioRef.value === 'WkspecificWeek') {
+        dayRef.value = '?'
+        weekRef.value = arr.join(',')
+      }
+    }
+
+    // Specific days (multiple choices)
+    const onWkspecificDay = (arr: Array<number>) => {
+      WkspecificDayRef.value = arr
+      if (radioRef.value === 'specificDay') {
+        weekRef.value = '?'
+        dayRef.value = arr.join(',')
+      }
+    }
+
+    const onMonthLastWeeks = (value: string | null) => {
+      monthLastWeeksRef.value = value || '?'
+      if (radioRef.value === 'monthLastWeeks') {
+        weekRef.value = value
+        dayRef.value = '?'
+      }
+    }
+
+    // Specific days
+    const onSpecificDays = (arr: Array<number>) => {
+      WkspecificDayRef.value = arr
+      if (radioRef.value === 'specificDay') {
+        specificSet()
+      }
+    }
+
+    // By the end of this month
+    const onMonthTailBefore = (value: number | null) => {
+      monthTailBeforeRef.value = value || 0
+      if (radioRef.value === 'monthTailBefore') {
+        dayRef.value = `L-${monthTailBeforeRef.value}`
+      }
+    }
+
+    // Last working day
+    const onRecentlyWorkingDaysMonth = (value: number | null) => {
+      recentlyWorkingDaysMonthRef.value = value || 0
+      if (radioRef.value === 'recentlyWorkingDaysMonth') {
+        dayRef.value = `${recentlyWorkingDaysMonthRef.value}W`
+      }
+    }
+
+    // On the day of this month
+    const onWkmonthNumWeeksDay = (value: number | null) => {
+      WkmonthNumWeeksDayRef.value = value || 0
+      if (radioRef.value === 'WkmonthNumWeeks') {
+        weekRef.value = `${WkmonthNumWeeksWeekRef.value}#${WkmonthNumWeeksDayRef.value}`
+      }
+    }
+
+    // On the week of this month
+    const onWkmonthNumWeeksWeek = (value: number | null) => {
+      if (radioRef.value === 'WkmonthNumWeeks') {
+        dayRef.value = '?'
+        weekRef.value = `${value}#${WkmonthNumWeeksDayRef.value}`
+      }
+    }
+
+    // Reset every day
+    const everyDaySet = () => {
+      dayRef.value = '*'
+    }
+
+    // Reset interval week starts from *
+    const WkintervalWeekReset = () => {
+      weekRef.value = `${WkintervalWeekStartRef.value}/${WkintervalWeekPerformRef.value}`
+    }
+
+    // Reset interval days
+    const intervalDaySet = () => {
+      dayRef.value = `${intervalDayStartRef.value}/${intervalDayPerformRef.value}`
+    }
+
+    // Specific week (multiple choices)
+    const WkspecificWeekReset = () => {
+      weekRef.value = WkspecificWeekRef.value.length
+        ? WkspecificWeekRef.value.join(',')
+        : '*'
+    }
+
+    // Reset specific days
+    const specificSet = () => {
+      if (WkspecificDayRef.value.length) {
+        dayRef.value = WkspecificDayRef.value.join(',')
+      } else {
+        dayRef.value = '*'
+      }
+    }
+
+    // On the last day of the month
+    const monthLastDaysReset = () => {
+      dayRef.value = monthLastDaysRef.value
+    }
+
+    // On the last working day of the month
+    const monthLastWorkingDaysReset = () => {
+      dayRef.value = monthLastWorkingDaysRef.value
+    }
+
+    // At the end of the month*
+    const monthLastWeeksReset = () => {
+      dayRef.value = monthLastWeeksRef.value
+    }
+
+    // By the end of this month
+    const monthTailBeforeReset = () => {
+      dayRef.value = `L-${monthTailBeforeRef.value}`
+    }
+
+    // Last working day (Monday to Friday) to this month
+    const recentlyWorkingDaysMonthReset = () => {
+      dayRef.value = `${recentlyWorkingDaysMonthRef.value}W`
+    }
+
+    // On the day of this month
+    const WkmonthNumReset = () => {
+      weekRef.value = `${WkmonthNumWeeksWeekRef.value}#${WkmonthNumWeeksDayRef.value}`
+    }
+
+    const updateRadioDay = (value: string) => {
+      switch (value) {
+        case 'everyDay':
+          weekRef.value = '?'
+          everyDaySet()
+          break
+        case 'WkintervalWeek':
+          dayRef.value = '?'
+          WkintervalWeekReset()
+          break
+        case 'intervalDay':
+          weekRef.value = '?'
+          intervalDaySet()
+          break
+        case 'WkspecificWeek':
+          dayRef.value = '?'
+          WkspecificWeekReset()
+          break
+        case 'specificDay':
+          weekRef.value = '?'
+          specificSet()
+          break
+        case 'monthLastDays':
+          weekRef.value = '?'
+          monthLastDaysReset()
+          break
+        case 'monthLastWorkingDays':
+          weekRef.value = '?'
+          monthLastWorkingDaysReset()
+          break
+        case 'monthLastWeeks':
+          weekRef.value = '1L'
+          monthLastWeeksReset()
+          break
+        case 'monthTailBefore':
+          weekRef.value = '?'
+          monthTailBeforeReset()
+          break
+        case 'recentlyWorkingDaysMonth':
+          weekRef.value = '?'
+          recentlyWorkingDaysMonthReset()
+          break
+        case 'WkmonthNumWeeks':
+          dayRef.value = '?'
+          WkmonthNumReset()
+          break
+      }
+    }
+
+    watch(
+      () => dayRef.value,
+      () => ctx.emit('update:dayValue', dayRef.value.toString())
+    )
+
+    watch(
+      () => weekRef.value,
+      () => ctx.emit('update:weekValue', weekRef.value.toString())
+    )
+
+    onMounted(() => analyticalValue())
+
+    return {
+      options,
+      weekOptions,
+      lastWeekOptions,
+      radioRef,
+      WkintervalWeekStartRef,
+      WkintervalWeekPerformRef,
+      intervalDayStartRef,
+      intervalDayPerformRef,
+      WkspecificWeekRef,
+      WkspecificDayRef,
+      monthLastWeeksRef,
+      monthTailBeforeRef,
+      recentlyWorkingDaysMonthRef,
+      WkmonthNumWeeksDayRef,
+      WkmonthNumWeeksWeekRef,
+      updateRadioDay,
+      onWkintervalWeekStart,
+      onWkintervalWeekPerform,
+      onIntervalDayStart,
+      onIntervalDayPerform,
+      onSpecificDays,
+      onWkspecificWeek,
+      onWkspecificDay,
+      onMonthLastWeeks,
+      onMonthTailBefore,
+      onRecentlyWorkingDaysMonth,
+      onWkmonthNumWeeksDay,
+      onWkmonthNumWeeksWeek
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      <NRadioGroup
+        v-model:value={this.radioRef}
+        onUpdateValue={this.updateRadioDay}
+      >
+        <NRadio class={styles['crontab-list']} value={'everyDay'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.every_day')}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'WkintervalWeek'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.every')}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={7}
+                v-model:value={this.WkintervalWeekPerformRef}
+                onUpdateValue={this.onWkintervalWeekPerform}
+              />
+            </div>
+            <div>{t('crontab.day_carried_out')}</div>
+            <div>
+              <NSelect
+                style={{ width: '150px' }}
+                options={this.weekOptions}
+                defaultValue={this.WkintervalWeekStartRef}
+                v-model:value={this.WkintervalWeekStartRef}
+                onUpdateValue={this.onWkintervalWeekStart}
+              />
+            </div>
+            <div>{t('crontab.start')}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'intervalDay'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.every')}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={31}
+                v-model:value={this.intervalDayPerformRef}
+                onUpdateValue={this.onIntervalDayPerform}
+              />
+            </div>
+            <div>{t('crontab.day_carried_out')}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={1}
+                max={31}
+                v-model:value={this.intervalDayStartRef}
+                onUpdateValue={this.onIntervalDayStart}
+              />
+            </div>
+            <div>{t('crontab.day_start')}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'WkspecificWeek'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.specific_week')}</div>
+            <div>
+              <NSelect
+                style={{ width: '300px' }}
+                multiple
+                options={specificWeek}
+                placeholder={t('crontab.specific_week_tip')}
+                v-model:value={this.WkspecificWeekRef}
+                onUpdateValue={this.onWkspecificWeek}
+              />
+            </div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'specificDay'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.specific_day')}</div>
+            <div>
+              <NSelect
+                style={{ width: '300px' }}
+                multiple
+                options={this.options}
+                placeholder={t('crontab.specific_day_tip')}
+                v-model:value={this.WkspecificDayRef}
+                onUpdateValue={this.onWkspecificDay}
+              />
+            </div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'monthLastDays'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.last_day_of_month')}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'monthLastWorkingDays'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.last_work_day_of_month')}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'monthLastWeeks'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.last_of_month')}</div>
+            <div>
+              <NSelect
+                style={{ width: '150px' }}
+                options={this.lastWeekOptions}
+                defaultValue={this.monthLastWeeksRef}
+                v-model:value={this.monthLastWeeksRef}
+                onUpdateValue={this.onMonthLastWeeks}
+              />
+            </div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'monthTailBefore'}>
+          <div class={styles['crontab-list-item']}>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={31}
+                v-model:value={this.monthTailBeforeRef}
+                onUpdateValue={this.onMonthTailBefore}
+              />
+            </div>
+            <div>{t('crontab.before_end_of_month')}</div>
+          </div>
+        </NRadio>
+        <NRadio
+          class={styles['crontab-list']}
+          value={'recentlyWorkingDaysMonth'}
+        >
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.recent_business_day_to_month')}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={31}
+                v-model:value={this.recentlyWorkingDaysMonthRef}
+                onUpdateValue={this.onRecentlyWorkingDaysMonth}
+              />
+            </div>
+            <div>{t('crontab.one_day')}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'WkmonthNumWeeks'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t('crontab.in_this_months')}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={31}
+                v-model:value={this.WkmonthNumWeeksDayRef}
+                onUpdateValue={this.onWkmonthNumWeeksDay}
+              />
+            </div>
+            <div>
+              <NSelect
+                style={{ width: '150px' }}
+                options={this.weekOptions}
+                defaultValue={this.WkmonthNumWeeksWeekRef}
+                v-model:value={this.WkmonthNumWeeksWeekRef}
+                onUpdateValue={this.onWkmonthNumWeeksWeek}
+              />
+            </div>
+          </div>
+        </NRadio>
+      </NRadioGroup>
+    )
+  }
+})

+ 296 - 0
dolphinscheduler-ui-next/src/components/crontab/modules/time.tsx

@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash'
+import { defineComponent, onMounted, PropType, ref, toRefs, watch } from 'vue'
+import { NInputNumber, NRadio, NRadioGroup, NSelect } from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import { ICrontabI18n } from '../types'
+import { isStr } from '../common'
+import styles from '../index.module.scss'
+
+const props = {
+  timeValue: {
+    type: String as PropType<string>,
+    default: '*'
+  },
+  timeI18n: {
+    type: Object as PropType<ICrontabI18n>,
+    require: true
+  }
+}
+
+export default defineComponent({
+  name: 'CrontabTime',
+  props,
+  emits: ['update:timeValue'],
+  setup(props, ctx) {
+    const options = Array.from({ length: 60 }, (x, i) => ({
+      label: i.toString(),
+      value: i
+    }))
+
+    const timeRef = ref()
+    const radioRef = ref()
+    const intervalStartRef = ref(0)
+    const intervalPerformRef = ref(0)
+    const specificTimesRef = ref<Array<number>>([])
+    const cycleStartRef = ref(0)
+    const cycleEndRef = ref(0)
+
+    /**
+     * Parse parameter value
+     */
+    const analyticalValue = () => {
+      const $timeVal = props.timeValue
+      // Interval time
+      const $interval = isStr($timeVal, '/')
+      // Specific time
+      const $specific = isStr($timeVal, ',')
+      // Cycle time
+      const $cycle = isStr($timeVal, '-')
+
+      // Every time
+      if ($timeVal === '*') {
+        radioRef.value = 'everyTime'
+        timeRef.value = '*'
+        return
+      }
+
+      // Positive integer (times)
+      if (
+        ($timeVal.length === 1 && _.isInteger(parseInt($timeVal))) ||
+        ($timeVal.length === 2 && _.isInteger(parseInt($timeVal)))
+      ) {
+        radioRef.value = 'specificTime'
+        specificTimesRef.value = [parseInt($timeVal)]
+        return
+      }
+
+      // Interval times
+      if ($interval) {
+        radioRef.value = 'intervalTime'
+        intervalStartRef.value = parseInt($interval[0])
+        intervalPerformRef.value = parseInt($interval[1])
+        timeRef.value = `${intervalStartRef.value}/${intervalPerformRef.value}`
+        return
+      }
+
+      // Specific times
+      if ($specific) {
+        radioRef.value = 'specificTime'
+        specificTimesRef.value = $specific.map((item) => parseInt(item))
+        return
+      }
+
+      // Cycle time
+      if ($cycle) {
+        radioRef.value = 'cycleTime'
+        cycleStartRef.value = parseInt($cycle[0])
+        cycleEndRef.value = parseInt($cycle[1])
+        timeRef.value = `${cycleStartRef.value}-${cycleEndRef.value}`
+        return
+      }
+    }
+
+    // Interval start time(1)
+    const onIntervalStart = (value: number | null) => {
+      intervalStartRef.value = value || 0
+      if (radioRef.value === 'intervalTime') {
+        timeRef.value = `${intervalStartRef.value}/${intervalPerformRef.value}`
+      }
+    }
+
+    // Interval execution time(2)
+    const onIntervalPerform = (value: number | null) => {
+      intervalPerformRef.value = value || 0
+      if (radioRef.value === 'intervalTime') {
+        timeRef.value = `${intervalStartRef.value}/${intervalPerformRef.value}`
+      }
+    }
+
+    // Specific time
+    const onSpecificTimes = (arr: Array<number>) => {
+      specificTimesRef.value = arr
+      if (radioRef.value === 'specificTime') {
+        specificReset()
+      }
+    }
+
+    // Cycle start value
+    const onCycleStart = (value: number | null) => {
+      cycleStartRef.value = value || 0
+      if (radioRef.value === 'cycleTime') {
+        timeRef.value = `${cycleStartRef.value}-${cycleEndRef.value}`
+      }
+    }
+
+    // Cycle end value
+    const onCycleEnd = (value: number | null) => {
+      cycleEndRef.value = value || 0
+      if (radioRef.value === 'cycleTime') {
+        timeRef.value = `${cycleStartRef.value}-${cycleEndRef.value}`
+      }
+    }
+
+    // Reset every time
+    const everyReset = () => {
+      timeRef.value = '*'
+    }
+
+    // Reset interval time
+    const intervalReset = () => {
+      timeRef.value = `${intervalStartRef.value}/${intervalPerformRef.value}`
+    }
+
+    // Reset specific time
+    const specificReset = () => {
+      let timeValue = '*'
+      if (specificTimesRef.value.length) {
+        timeValue = specificTimesRef.value.join(',')
+      }
+      timeRef.value = timeValue
+    }
+
+    // Reset cycle time
+    const cycleReset = () => {
+      timeRef.value = `${cycleStartRef.value}-${cycleEndRef.value}`
+    }
+
+    const updateRadioTime = (value: string) => {
+      switch (value) {
+        case 'everyTime':
+          everyReset()
+          break
+        case 'intervalTime':
+          intervalReset()
+          break
+        case 'specificTime':
+          specificReset()
+          break
+        case 'cycleTime':
+          cycleReset()
+          break
+      }
+    }
+
+    watch(
+      () => timeRef.value,
+      () => ctx.emit('update:timeValue', timeRef.value.toString())
+    )
+
+    onMounted(() => analyticalValue())
+
+    return {
+      options,
+      radioRef,
+      intervalStartRef,
+      intervalPerformRef,
+      specificTimesRef,
+      cycleStartRef,
+      cycleEndRef,
+      updateRadioTime,
+      onIntervalStart,
+      onIntervalPerform,
+      onSpecificTimes,
+      onCycleStart,
+      onCycleEnd,
+      ...toRefs(props)
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      <NRadioGroup
+        v-model:value={this.radioRef}
+        onUpdateValue={this.updateRadioTime}
+      >
+        <NRadio class={styles['crontab-list']} value={'everyTime'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t(this.timeI18n!.everyTime)}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'intervalTime'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t(this.timeI18n!.every)}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={59}
+                v-model:value={this.intervalStartRef}
+                onUpdateValue={this.onIntervalStart}
+              />
+            </div>
+            <div>{t(this.timeI18n!.timeCarriedOut)}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={59}
+                v-model:value={this.intervalPerformRef}
+                onUpdateValue={this.onIntervalPerform}
+              />
+            </div>
+            <div>{t(this.timeI18n!.timeStart)}</div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'specificTime'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t(this.timeI18n!.specificTime)}</div>
+            <div>
+              <NSelect
+                style={{ width: '300px' }}
+                multiple
+                options={this.options}
+                placeholder={t(this.timeI18n!.specificTimeTip)}
+                v-model:value={this.specificTimesRef}
+                onUpdateValue={this.onSpecificTimes}
+              />
+            </div>
+          </div>
+        </NRadio>
+        <NRadio class={styles['crontab-list']} value={'cycleTime'}>
+          <div class={styles['crontab-list-item']}>
+            <div>{t(this.timeI18n!.cycleFrom)}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={59}
+                v-model:value={this.cycleStartRef}
+                onUpdateValue={this.onCycleStart}
+              />
+            </div>
+            <div>{t(this.timeI18n!.to)}</div>
+            <div>
+              <NInputNumber
+                defaultValue={0}
+                min={0}
+                max={59}
+                v-model:value={this.cycleEndRef}
+                onUpdateValue={this.onCycleEnd}
+              />
+            </div>
+            <div>{t(this.timeI18n!.time)}</div>
+          </div>
+        </NRadio>
+      </NRadioGroup>
+    )
+  }
+})

+ 30 - 0
dolphinscheduler-ui-next/src/components/crontab/types.ts

@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+interface ICrontabI18n {
+  everyTime: string
+  every: string
+  timeCarriedOut: string
+  timeStart: string
+  cycleFrom: string
+  specificTime: string
+  specificTimeTip: string
+  to: string
+  time: string
+}
+
+export { ICrontabI18n }

+ 64 - 1
dolphinscheduler-ui-next/src/locales/modules/en_US.ts

@@ -951,6 +951,68 @@ const data_quality = {
   }
 }
 
+const crontab = {
+  second: 'second',
+  minute: 'minute',
+  hour: 'hour',
+  day: 'day',
+  month: 'month',
+  year: 'year',
+  monday: 'Monday',
+  tuesday: 'Tuesday',
+  wednesday: 'Wednesday',
+  thursday: 'Thursday',
+  friday: 'Friday',
+  saturday: 'Saturday',
+  sunday: 'Sunday',
+  every_second: 'Every second',
+  every: 'Every',
+  second_carried_out: 'second carried out',
+  second_start: 'Start',
+  specific_second: 'Specific second(multiple)',
+  specific_second_tip: 'Please enter a specific second',
+  cycle_from: 'Cycle from',
+  to: 'to',
+  every_minute: 'Every minute',
+  minute_carried_out: 'minute carried out',
+  minute_start: 'Start',
+  specific_minute: 'Specific minute(multiple)',
+  specific_minute_tip: 'Please enter a specific minute',
+  every_hour: 'Every hour',
+  hour_carried_out: 'hour carried out',
+  hour_start: 'Start',
+  specific_hour: 'Specific hour(multiple)',
+  specific_hour_tip: 'Please enter a specific hour',
+  every_day: 'Every day',
+  week_carried_out: 'week carried out',
+  start: 'Start',
+  day_carried_out: 'day carried out',
+  day_start: 'Start',
+  specific_week: 'Specific day of the week(multiple)',
+  specific_week_tip: 'Please enter a specific week',
+  specific_day: 'Specific days(multiple)',
+  specific_day_tip: 'Please enter a days',
+  last_day_of_month: 'On the last day of the month',
+  last_work_day_of_month: 'On the last working day of the month',
+  last_of_month: 'At the last of this month',
+  before_end_of_month: 'Before the end of this month',
+  recent_business_day_to_month:
+    'The most recent business day (Monday to Friday) to this month',
+  in_this_months: 'In this months',
+  every_month: 'Every month',
+  month_carried_out: 'month carried out',
+  month_start: 'Start',
+  specific_month: 'Specific months(multiple)',
+  specific_month_tip: 'Please enter a months',
+  every_year: 'Every year',
+  year_carried_out: 'year carried out',
+  year_start: 'Start',
+  specific_year: 'Specific year(multiple)',
+  specific_year_tip: 'Please enter a year',
+  one_hour: 'hour',
+  one_day: 'day'
+}
+
 export default {
   login,
   modal,
@@ -965,5 +1027,6 @@ export default {
   project,
   security,
   datasource,
-  data_quality
+  data_quality,
+  crontab
 }

+ 62 - 1
dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts

@@ -937,6 +937,66 @@ const data_quality = {
     TargetTableTotalRows: '目标表总行数'
   }
 }
+const crontab = {
+  second: '秒',
+  minute: '分',
+  hour: '时',
+  day: '天',
+  month: '月',
+  year: '年',
+  monday: '星期一',
+  tuesday: '星期二',
+  wednesday: '星期三',
+  thursday: '星期四',
+  friday: '星期五',
+  saturday: '星期六',
+  sunday: '星期天',
+  every_second: '每一秒钟',
+  every: '每隔',
+  second_carried_out: '秒执行 从',
+  second_start: '秒开始',
+  specific_second: '具体秒数(可多选)',
+  specific_second_tip: '请选择具体秒数',
+  cycle_from: '周期从',
+  to: '到',
+  every_minute: '每一分钟',
+  minute_carried_out: '分执行 从',
+  minute_start: '分开始',
+  specific_minute: '具体分钟数(可多选)',
+  specific_minute_tip: '请选择具体分钟数',
+  every_hour: '每一小时',
+  hour_carried_out: '小时执行 从',
+  hour_start: '小时开始',
+  specific_hour: '具体小时数(可多选)',
+  specific_hour_tip: '请选择具体小时数',
+  every_day: '每一天',
+  week_carried_out: '周执行 从',
+  start: '开始',
+  day_carried_out: '天执行 从',
+  day_start: '天开始',
+  specific_week: '具体星期几(可多选)',
+  specific_week_tip: '请选择具体周几',
+  specific_day: '具体天数(可多选)',
+  specific_day_tip: '请选择具体天数',
+  last_day_of_month: '在这个月的最后一天',
+  last_work_day_of_month: '在这个月的最后一个工作日',
+  last_of_month: '在这个月的最后一个',
+  before_end_of_month: '在本月底前',
+  recent_business_day_to_month: '最近的工作日(周一至周五)至本月',
+  in_this_months: '在这个月的第',
+  every_month: '每一月',
+  month_carried_out: '月执行 从',
+  month_start: '月开始',
+  specific_month: '具体月数(可多选)',
+  specific_month_tip: '请选择具体月数',
+  every_year: '每一年',
+  year_carried_out: '年执行 从',
+  year_start: '年开始',
+  specific_year: '具体年数(可多选)',
+  specific_year_tip: '请选择具体年数',
+  one_hour: '小时',
+  one_day: '日'
+}
 
 export default {
   login,
@@ -952,5 +1012,6 @@ export default {
   project,
   security,
   datasource,
-  data_quality
+  data_quality,
+  crontab
 }

+ 52 - 6
dolphinscheduler-ui-next/src/views/projects/workflow/definition/components/timing-modal.tsx

@@ -15,7 +15,15 @@
  * limitations under the License.
  */
 
-import { defineComponent, PropType, toRefs, h, onMounted, ref } from 'vue'
+import {
+  defineComponent,
+  PropType,
+  toRefs,
+  h,
+  onMounted,
+  ref,
+  watch
+} from 'vue'
 import { useI18n } from 'vue-i18n'
 import Modal from '@/components/modal'
 import { useForm } from './use-form'
@@ -34,10 +42,12 @@ import {
   NInputGroup,
   NList,
   NListItem,
-  NThing
+  NThing,
+  NPopover
 } from 'naive-ui'
 import { ArrowDownOutlined, ArrowUpOutlined } from '@vicons/antd'
 import { timezoneList } from '@/utils/timezone'
+import Crontab from '@/components/crontab'
 
 const props = {
   row: {
@@ -59,6 +69,7 @@ export default defineComponent({
   props,
   emits: ['update:show', 'update:row', 'updateList'],
   setup(props, ctx) {
+    const crontabRef = ref()
     const parallelismRef = ref(false)
     const { t } = useI18n()
     const { timingState } = useForm()
@@ -173,8 +184,28 @@ export default defineComponent({
       getEnvironmentList()
     })
 
+    watch(
+      () => props.row,
+      () => {
+        timingState.timingForm.startEndTime = [
+          new Date(props.row.startTime),
+          new Date(props.row.endTime)
+        ]
+        timingState.timingForm.crontab = props.row.crontab
+        timingState.timingForm.timezoneId = props.row.timezoneId
+        timingState.timingForm.failureStrategy = props.row.failureStrategy
+        timingState.timingForm.warningType = props.row.warningType
+        timingState.timingForm.processInstancePriority =
+          props.row.processInstancePriority
+        timingState.timingForm.warningGroupId = props.row.warningGroupId
+        timingState.timingForm.workerGroup = props.row.workerGroup
+        timingState.timingForm.environmentCode = props.row.environmentCode
+      }
+    )
+
     return {
       t,
+      crontabRef,
       parallelismRef,
       hideModal,
       handleTiming,
@@ -216,10 +247,25 @@ export default defineComponent({
           </NFormItem>
           <NFormItem label={t('project.workflow.timing')} path='crontab'>
             <NInputGroup>
-              <NInput
-                style={{ width: '80%' }}
-                v-model:value={this.timingForm.crontab}
-              ></NInput>
+              <NPopover
+                trigger='click'
+                showArrow={false}
+                placement='bottom'
+                style={{ width: '500px' }}
+              >
+                {{
+                  trigger: () => (
+                    <NInput
+                      style={{ width: '80%' }}
+                      readonly={true}
+                      v-model:value={this.timingForm.crontab}
+                    ></NInput>
+                  ),
+                  default: () => (
+                    <Crontab v-model:value={this.timingForm.crontab} />
+                  )
+                }}
+              </NPopover>
               <NButton type='primary' ghost onClick={this.handlePreview}>
                 {t('project.workflow.execute_time')}
               </NButton>

+ 3 - 2
dolphinscheduler-ui-next/src/views/projects/workflow/definition/components/use-modal.ts

@@ -160,7 +160,8 @@ export function useModal(
       schedule: JSON.stringify({
         startTime: start,
         endTime: end,
-        crontab: state.timingForm.crontab
+        crontab: state.timingForm.crontab,
+        timezoneId: state.timingForm.timezoneId
       }),
       failureStrategy: state.timingForm.failureStrategy,
       warningType: state.timingForm.warningType,
@@ -169,7 +170,7 @@ export function useModal(
         state.timingForm.warningGroupId === ''
           ? 0
           : state.timingForm.warningGroupId,
-      workerGroup: state.timingForm.workerGroups,
+      workerGroup: state.timingForm.workerGroup,
       environmentCode: state.timingForm.environmentCode
     }
     return data