2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 04:22:33 +08:00

feat: 新的Cron表达式生成器

This commit is contained in:
2025-12-31 17:38:08 +08:00
parent b86fc187d7
commit 80fde60526
4 changed files with 430 additions and 14 deletions

View 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>

View 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-60=周日)
// 自定义表达式
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>

View File

@@ -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>

View File

@@ -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>