mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: 新的Cron表达式生成器
This commit is contained in:
155
web/src/components/common/CronPreview.vue
Normal file
155
web/src/components/common/CronPreview.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const props = defineProps({
|
||||
cron: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 星期名称映射
|
||||
const weekdayNames: Record<string, string> = {
|
||||
'0': $gettext('Sunday'),
|
||||
'1': $gettext('Monday'),
|
||||
'2': $gettext('Tuesday'),
|
||||
'3': $gettext('Wednesday'),
|
||||
'4': $gettext('Thursday'),
|
||||
'5': $gettext('Friday'),
|
||||
'6': $gettext('Saturday'),
|
||||
'7': $gettext('Sunday') // 有些系统用 7 表示周日
|
||||
}
|
||||
|
||||
// 格式化时间为 HH:MM
|
||||
const formatTime = (hour: string, minute: string): string => {
|
||||
const h = hour.padStart(2, '0')
|
||||
const m = minute.padStart(2, '0')
|
||||
return `${h}:${m}`
|
||||
}
|
||||
|
||||
// 解析 Cron 表达式并生成人类可读描述
|
||||
const parseDescription = computed((): string => {
|
||||
const cron = props.cron.trim()
|
||||
const parts = cron.split(/\s+/)
|
||||
|
||||
// Cron 表达式应该有 5 个部分:分 时 日 月 周
|
||||
if (parts.length !== 5) {
|
||||
return $gettext('Cron expression: %{cron}', { cron })
|
||||
}
|
||||
|
||||
const [minute, hour, day, month, weekday] = parts
|
||||
|
||||
try {
|
||||
// 每 N 分钟:*/N * * * *
|
||||
if (
|
||||
minute.startsWith('*/') &&
|
||||
hour === '*' &&
|
||||
day === '*' &&
|
||||
month === '*' &&
|
||||
weekday === '*'
|
||||
) {
|
||||
const n = minute.slice(2)
|
||||
return $gettext('Run every %{n} minutes', { n })
|
||||
}
|
||||
|
||||
// 每 N 小时的某分钟:M */N * * *
|
||||
if (
|
||||
!minute.includes('*') &&
|
||||
hour.startsWith('*/') &&
|
||||
day === '*' &&
|
||||
month === '*' &&
|
||||
weekday === '*'
|
||||
) {
|
||||
const n = hour.slice(2)
|
||||
const m = minute.padStart(2, '0')
|
||||
return $gettext('Run every %{n} hours at minute %{m}', { n, m })
|
||||
}
|
||||
|
||||
// 每 N 天的某时某分:M H */N * *
|
||||
if (
|
||||
!minute.includes('*') &&
|
||||
!hour.includes('*') &&
|
||||
day.startsWith('*/') &&
|
||||
month === '*' &&
|
||||
weekday === '*'
|
||||
) {
|
||||
const n = day.slice(2)
|
||||
const time = formatTime(hour, minute)
|
||||
return $gettext('Run every %{n} days at %{time}', { n, time })
|
||||
}
|
||||
|
||||
// 每小时的某分钟:M * * * *
|
||||
if (!minute.includes('*') && hour === '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
const m = minute.padStart(2, '0')
|
||||
return $gettext('Run hourly at minute %{m}', { m })
|
||||
}
|
||||
|
||||
// 每天的某时某分:M H * * *
|
||||
if (
|
||||
!minute.includes('*') &&
|
||||
!hour.includes('*') &&
|
||||
day === '*' &&
|
||||
month === '*' &&
|
||||
weekday === '*'
|
||||
) {
|
||||
const time = formatTime(hour, minute)
|
||||
return $gettext('Run daily at %{time}', { time })
|
||||
}
|
||||
|
||||
// 每周某天的某时某分:M H * * W
|
||||
if (
|
||||
!minute.includes('*') &&
|
||||
!hour.includes('*') &&
|
||||
day === '*' &&
|
||||
month === '*' &&
|
||||
!weekday.includes('*')
|
||||
) {
|
||||
const time = formatTime(hour, minute)
|
||||
const weekdayName = weekdayNames[weekday] || weekday
|
||||
return $gettext('Run weekly on %{weekday} at %{time}', { weekday: weekdayName, time })
|
||||
}
|
||||
|
||||
// 每月某日的某时某分:M H D * *
|
||||
if (
|
||||
!minute.includes('*') &&
|
||||
!hour.includes('*') &&
|
||||
!day.includes('*') &&
|
||||
month === '*' &&
|
||||
weekday === '*'
|
||||
) {
|
||||
const time = formatTime(hour, minute)
|
||||
return $gettext('Run monthly on day %{day} at %{time}', { day, time })
|
||||
}
|
||||
|
||||
// 每年某月某日的某时某分:M H D Mon *
|
||||
if (
|
||||
!minute.includes('*') &&
|
||||
!hour.includes('*') &&
|
||||
!day.includes('*') &&
|
||||
!month.includes('*') &&
|
||||
weekday === '*'
|
||||
) {
|
||||
const time = formatTime(hour, minute)
|
||||
return $gettext('Run yearly on month %{month} day %{day} at %{time}', { month, day, time })
|
||||
}
|
||||
|
||||
// 每分钟:* * * * *
|
||||
if (minute === '*' && hour === '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
return $gettext('Run every minute')
|
||||
}
|
||||
|
||||
// 无法解析,返回原始表达式
|
||||
return $gettext('Cron expression: %{cron}', { cron })
|
||||
} catch {
|
||||
return $gettext('Cron expression: %{cron}', { cron })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>{{ parseDescription }}</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
260
web/src/components/common/CronSelector.vue
Normal file
260
web/src/components/common/CronSelector.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<script setup lang="ts">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import CronPreview from './CronPreview.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// 生成的 Cron 表达式值
|
||||
const value = defineModel<string>('value', {
|
||||
type: String,
|
||||
required: true
|
||||
})
|
||||
|
||||
// 当前选择的周期类型
|
||||
const selectedOption = ref<string>('every-n-minutes')
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
// Every N 系列
|
||||
nMinutes: 30, // 每 N 分钟
|
||||
nHours: 2, // 每 N 小时
|
||||
nDays: 3, // 每 N 天
|
||||
|
||||
// 时间配置
|
||||
minute: 30, // 分钟
|
||||
hour: 1, // 小时
|
||||
day: 3, // 日期(1-31)
|
||||
month: 1, // 月份(1-12)
|
||||
weekday: 1, // 星期(0-6,0=周日)
|
||||
|
||||
// 自定义表达式
|
||||
customCron: '* * * * *'
|
||||
})
|
||||
|
||||
// 周期选项
|
||||
const options = [
|
||||
{ label: $gettext('Every N Minutes'), value: 'every-n-minutes' },
|
||||
{ label: $gettext('Every N Hours'), value: 'every-n-hours' },
|
||||
{ label: $gettext('Every N Days'), value: 'every-n-days' },
|
||||
{ label: $gettext('Hourly'), value: 'every-hour' },
|
||||
{ label: $gettext('Daily'), value: 'every-day' },
|
||||
{ label: $gettext('Weekly'), value: 'every-week' },
|
||||
{ label: $gettext('Monthly'), value: 'every-month' },
|
||||
{ label: $gettext('Yearly'), value: 'every-year' },
|
||||
{ label: $gettext('Custom'), value: 'custom' }
|
||||
]
|
||||
|
||||
// 星期选项
|
||||
const weekdayOptions = [
|
||||
{ label: $gettext('Sunday'), value: 0 },
|
||||
{ label: $gettext('Monday'), value: 1 },
|
||||
{ label: $gettext('Tuesday'), value: 2 },
|
||||
{ label: $gettext('Wednesday'), value: 3 },
|
||||
{ label: $gettext('Thursday'), value: 4 },
|
||||
{ label: $gettext('Friday'), value: 5 },
|
||||
{ label: $gettext('Saturday'), value: 6 }
|
||||
]
|
||||
|
||||
// 月份选项
|
||||
const monthOptions = Array.from({ length: 12 }, (_, i) => ({
|
||||
label: $gettext('Month %{month}', { month: String(i + 1) }),
|
||||
value: i + 1
|
||||
}))
|
||||
|
||||
// 生成 Cron 表达式
|
||||
const generateCron = (): string => {
|
||||
const { minute, hour, day, month, weekday, nMinutes, nHours, nDays, customCron } = formData.value
|
||||
|
||||
switch (selectedOption.value) {
|
||||
case 'every-n-minutes':
|
||||
// 每 N 分钟:*/N * * * *
|
||||
return `*/${nMinutes} * * * *`
|
||||
|
||||
case 'every-n-hours':
|
||||
// 每 N 小时的第 M 分钟:M */N * * *
|
||||
return `${minute} */${nHours} * * *`
|
||||
|
||||
case 'every-n-days':
|
||||
// 每 N 天的 H 时 M 分:M H */N * *
|
||||
return `${minute} ${hour} */${nDays} * *`
|
||||
|
||||
case 'every-hour':
|
||||
// 每小时的第 M 分钟:M * * * *
|
||||
return `${minute} * * * *`
|
||||
|
||||
case 'every-day':
|
||||
// 每天 H 时 M 分:M H * * *
|
||||
return `${minute} ${hour} * * *`
|
||||
|
||||
case 'every-week':
|
||||
// 每周几的 H 时 M 分:M H * * W
|
||||
return `${minute} ${hour} * * ${weekday}`
|
||||
|
||||
case 'every-month':
|
||||
// 每月 D 日 H 时 M 分:M H D * *
|
||||
return `${minute} ${hour} ${day} * *`
|
||||
|
||||
case 'every-year':
|
||||
// 每年 Mon 月 D 日 H 时 M 分:M H D Mon *
|
||||
return `${minute} ${hour} ${day} ${month} *`
|
||||
|
||||
case 'custom':
|
||||
return customCron
|
||||
|
||||
default:
|
||||
return '* * * * *'
|
||||
}
|
||||
}
|
||||
|
||||
// 监听变化,更新 Cron 表达式
|
||||
watch(
|
||||
[selectedOption, formData],
|
||||
() => {
|
||||
value.value = generateCron()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// 判断是否显示某个输入框
|
||||
const showMonth = computed(() => selectedOption.value === 'every-year')
|
||||
|
||||
const showDay = computed(() =>
|
||||
['every-n-days', 'every-month', 'every-year'].includes(selectedOption.value)
|
||||
)
|
||||
|
||||
const showWeekday = computed(() => selectedOption.value === 'every-week')
|
||||
|
||||
const showHour = computed(() =>
|
||||
['every-n-days', 'every-day', 'every-week', 'every-month', 'every-year'].includes(
|
||||
selectedOption.value
|
||||
)
|
||||
)
|
||||
|
||||
const showMinute = computed(() =>
|
||||
[
|
||||
'every-n-minutes',
|
||||
'every-n-hours',
|
||||
'every-n-days',
|
||||
'every-hour',
|
||||
'every-day',
|
||||
'every-week',
|
||||
'every-month',
|
||||
'every-year'
|
||||
].includes(selectedOption.value)
|
||||
)
|
||||
|
||||
const showNDays = computed(() => selectedOption.value === 'every-n-days')
|
||||
const showNHours = computed(() => selectedOption.value === 'every-n-hours')
|
||||
const showNMinutes = computed(() => selectedOption.value === 'every-n-minutes')
|
||||
const showCustom = computed(() => selectedOption.value === 'custom')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical :size="12">
|
||||
<n-flex align="center" :wrap="false">
|
||||
<!-- 周期类型选择 -->
|
||||
<n-select
|
||||
v-model:value="selectedOption"
|
||||
:options="options"
|
||||
:style="{ width: '160px', flexShrink: 0 }"
|
||||
/>
|
||||
|
||||
<!-- 每 N 分钟 -->
|
||||
<n-input-number
|
||||
v-if="showNMinutes"
|
||||
v-model:value="formData.nMinutes"
|
||||
:min="1"
|
||||
:max="59"
|
||||
:style="{ width: '140px' }"
|
||||
>
|
||||
<template #suffix>{{ $gettext('Minutes') }}</template>
|
||||
</n-input-number>
|
||||
|
||||
<!-- 每 N 小时 -->
|
||||
<n-input-number
|
||||
v-if="showNHours"
|
||||
v-model:value="formData.nHours"
|
||||
:min="1"
|
||||
:max="23"
|
||||
:style="{ width: '140px' }"
|
||||
>
|
||||
<template #suffix>{{ $gettext('Hours') }}</template>
|
||||
</n-input-number>
|
||||
|
||||
<!-- 每 N 天 -->
|
||||
<n-input-number
|
||||
v-if="showNDays"
|
||||
v-model:value="formData.nDays"
|
||||
:min="1"
|
||||
:max="31"
|
||||
:style="{ width: '140px' }"
|
||||
>
|
||||
<template #suffix>{{ $gettext('Days') }}</template>
|
||||
</n-input-number>
|
||||
|
||||
<!-- 月份选择(每年) -->
|
||||
<n-select
|
||||
v-if="showMonth"
|
||||
v-model:value="formData.month"
|
||||
:options="monthOptions"
|
||||
:style="{ width: '140px' }"
|
||||
/>
|
||||
|
||||
<!-- 日期选择(每月、每年) -->
|
||||
<n-input-number
|
||||
v-if="showDay && !showNDays"
|
||||
v-model:value="formData.day"
|
||||
:min="1"
|
||||
:max="31"
|
||||
:style="{ width: '140px' }"
|
||||
>
|
||||
<template #suffix>{{ $gettext('Day') }}</template>
|
||||
</n-input-number>
|
||||
|
||||
<!-- 星期选择(每周) -->
|
||||
<n-select
|
||||
v-if="showWeekday"
|
||||
v-model:value="formData.weekday"
|
||||
:options="weekdayOptions"
|
||||
:style="{ width: '140px' }"
|
||||
/>
|
||||
|
||||
<!-- 小时选择 -->
|
||||
<n-input-number
|
||||
v-if="showHour"
|
||||
v-model:value="formData.hour"
|
||||
:min="0"
|
||||
:max="23"
|
||||
:style="{ width: '140px' }"
|
||||
>
|
||||
<template #suffix>{{ $gettext('Hour') }}</template>
|
||||
</n-input-number>
|
||||
|
||||
<!-- 分钟选择 -->
|
||||
<n-input-number
|
||||
v-if="showMinute && !showNMinutes"
|
||||
v-model:value="formData.minute"
|
||||
:min="0"
|
||||
:max="59"
|
||||
:style="{ width: '140px' }"
|
||||
>
|
||||
<template #suffix>{{ $gettext('Minute') }}</template>
|
||||
</n-input-number>
|
||||
|
||||
<!-- 自定义 Cron 表达式 -->
|
||||
<n-input
|
||||
v-if="showCustom"
|
||||
v-model:value="formData.customCron"
|
||||
:placeholder="$gettext('Enter Cron expression')"
|
||||
:style="{ width: '240px' }"
|
||||
/>
|
||||
</n-flex>
|
||||
|
||||
<!-- 预览 -->
|
||||
<n-text depth="3">
|
||||
<cron-preview :cron="value" />
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -3,6 +3,7 @@ import app from '@/api/panel/app'
|
||||
import cron from '@/api/panel/cron'
|
||||
import home from '@/api/panel/home'
|
||||
import website from '@/api/panel/website'
|
||||
import CronSelector from '@/components/common/CronSelector.vue'
|
||||
import { NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
@@ -106,7 +107,7 @@ onMounted(() => {
|
||||
<n-input v-model:value="createModel.name" :placeholder="$gettext('Task Name')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Task Schedule')">
|
||||
<!-- <cron-naive v-model="createModel.time" locale="zh-cn"></cron-naive>-->
|
||||
<cron-selector v-model:value="createModel.time" />
|
||||
</n-form-item>
|
||||
<div v-if="createModel.type === 'shell'">
|
||||
<n-text>{{ $gettext('Script Content') }}</n-text>
|
||||
@@ -152,13 +153,9 @@ onMounted(() => {
|
||||
<n-input-number v-model:value="createModel.save" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]" pt-20>
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block :loading="loading" @click="handleSubmit">
|
||||
{{ $gettext('Submit') }}
|
||||
</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" :loading="loading" @click="handleSubmit" mt-10 block>
|
||||
{{ $gettext('Submit') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import cron from '@/api/panel/cron'
|
||||
import file from '@/api/panel/file'
|
||||
import CronPreview from '@/components/common/CronPreview.vue'
|
||||
import { decodeBase64, formatDateTime } from '@/utils'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
@@ -71,7 +72,7 @@ const columns: any = [
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return row.time
|
||||
return h(CronPreview, { cron: row.time })
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -196,6 +197,7 @@ const saveTaskEdit = async () => {
|
||||
useRequest(
|
||||
cron.update(editTask.value.id, editTask.value.name, editTask.value.time, editTask.value.script)
|
||||
).onSuccess(() => {
|
||||
editModal.value = false
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
window.$bus.emit('task:refresh-cron')
|
||||
})
|
||||
@@ -239,20 +241,22 @@ onUnmounted(() => {
|
||||
v-model:show="editModal"
|
||||
preset="card"
|
||||
:title="$gettext('Edit Task')"
|
||||
style="width: 80vw"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="saveTaskEdit"
|
||||
>
|
||||
<n-form inline>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Task Name')">
|
||||
<n-input v-model:value="editTask.name" :placeholder="$gettext('Task Name')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Task Schedule')">
|
||||
<!-- <cron-naive v-model="editTask.time" locale="zh-cn"></cron-naive>-->
|
||||
<cron-selector v-model:value="editTask.time"></cron-selector>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<common-editor v-model:value="editTask.script" height="60vh" />
|
||||
<common-editor v-model:value="editTask.script" lang="sh" height="40vh" />
|
||||
<n-button type="info" @click="saveTaskEdit" mt-10 block>
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user