From c25852d81026f11043645fc74feca18feee86338 Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Wed, 1 Apr 2026 00:12:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20Cron=20=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E5=BC=8F=E7=BC=96=E8=BE=91=E5=99=A8=E5=A2=9E=E5=8A=A0=E9=A2=84?= =?UTF-8?q?=E8=AE=BE=E5=92=8C=E4=B8=AD=E6=96=87=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 8 个常用预设按钮(每天 02:00、每 6 小时、每周日、每月 1 日等), 一键设置无需逐个 Tab 操作 2. 新增中文可读描述(如 "02 时 00 分 执行"),实时显示在表达式下方 3. 选中的预设按钮高亮显示 --- web/src/components/CronInput/CronInput.tsx | 95 ++++++++++++++++++++-- 1 file changed, 87 insertions(+), 8 deletions(-) diff --git a/web/src/components/CronInput/CronInput.tsx b/web/src/components/CronInput/CronInput.tsx index fc32fe8..7bb137c 100644 --- a/web/src/components/CronInput/CronInput.tsx +++ b/web/src/components/CronInput/CronInput.tsx @@ -1,5 +1,5 @@ -import { Input, Space, Switch, Tabs, Typography, Radio, Checkbox, Select } from '@arco-design/web-react' -import { useEffect, useState } from 'react' +import { Button, Input, Space, Switch, Tabs, Typography, Radio, Select } from '@arco-design/web-react' +import { useEffect, useMemo, useState } from 'react' export interface CronInputProps { value?: string @@ -18,6 +18,18 @@ interface CronState { week: string } +// 常用预设 +const PRESETS = [ + { label: '每天 02:00', value: '0 2 * * *' }, + { label: '每天 00:00', value: '0 0 * * *' }, + { label: '每 6 小时', value: '0 */6 * * *' }, + { label: '每 12 小时', value: '0 */12 * * *' }, + { label: '每周日 03:00', value: '0 3 * * 0' }, + { label: '每月 1 日 02:00', value: '0 2 1 * *' }, + { label: '每 30 分钟', value: '*/30 * * * *' }, + { label: '每小时', value: '0 * * * *' }, +] + function parseCron(expr: string): CronState { const parts = (expr || DEFAULT_CRON).trim().split(/\s+/) return { @@ -33,6 +45,43 @@ function stringifyCron(state: CronState): string { return `${state.minute} ${state.hour} ${state.day} ${state.month} ${state.week}` } +// 将 cron 表达式转为中文可读描述 +function describeCron(expr: string): string { + const parts = expr.trim().split(/\s+/) + if (parts.length !== 5) return '' + const [minute, hour, day, month, week] = parts + + const segments: string[] = [] + + // 月 + if (month !== '*') segments.push(`${month} 月`) + // 日 + if (day !== '*') segments.push(`${day} 日`) + // 周 + if (week !== '*') { + const weekNames: Record = { '0': '日', '1': '一', '2': '二', '3': '三', '4': '四', '5': '五', '6': '六', '7': '日' } + const weekDesc = week.split(',').map((w) => weekNames[w] || w).join('、') + segments.push(`星期${weekDesc}`) + } + // 小时 + if (hour.includes('/')) { + segments.push(`每 ${hour.split('/')[1]} 小时`) + } else if (hour !== '*') { + segments.push(`${hour.padStart(2, '0')} 时`) + } + // 分钟 + if (minute.includes('/')) { + segments.push(`每 ${minute.split('/')[1]} 分钟`) + } else if (minute !== '*') { + segments.push(`${minute.padStart(2, '0')} 分`) + } else if (hour !== '*' && !hour.includes('/')) { + segments.push('00 分') + } + + if (segments.length === 0) return '每分钟执行' + return segments.join(' ') + ' 执行' +} + function generateOptions(min: number, max: number) { return Array.from({ length: max - min + 1 }, (_, i) => ({ label: String(i + min), @@ -69,6 +118,8 @@ export function CronInput({ value, onChange }: CronInputProps) { } }, [value, isAdvanced, internalValue]) + const description = useMemo(() => describeCron(internalValue), [internalValue]) + const notifyChange = (nextValue: string) => { setInternalValue(nextValue) if (onChange) { @@ -82,6 +133,12 @@ export function CronInput({ value, onChange }: CronInputProps) { notifyChange(stringifyCron(nextState)) } + const handlePreset = (cronExpr: string) => { + setInternalValue(cronExpr) + setState(parseCron(cronExpr)) + if (onChange) onChange(cronExpr) + } + const renderPartTab = ( part: CronPart, title: string, @@ -91,8 +148,7 @@ export function CronInput({ value, onChange }: CronInputProps) { const currentVal = state[part] const isAny = currentVal === allowAnyVal || currentVal === '*' || currentVal === '?' const isSpecific = !isAny && !currentVal.includes('/') && !currentVal.includes('-') - - // For simplicity in this visual editor, we only support "every" (*) and "specific values" (1,2,3). + const type = isAny ? 'any' : 'specific' const specificValues = isSpecific ? currentVal.split(',') : [] @@ -105,7 +161,7 @@ export function CronInput({ value, onChange }: CronInputProps) { if (val === 'any') { handleStateChange(part, allowAnyVal) } else { - handleStateChange(part, options[0].value) // Default to first valid item + handleStateChange(part, options[0].value) } }} > @@ -128,7 +184,6 @@ export function CronInput({ value, onChange }: CronInputProps) { if (vals.length === 0) { handleStateChange(part, allowAnyVal) } else { - // Sort numerically to keep things neat const sorted = [...vals].sort((a, b) => Number(a) - Number(b)) handleStateChange(part, sorted.join(',')) } @@ -144,6 +199,24 @@ export function CronInput({ value, onChange }: CronInputProps) { return (
+ {/* 常用预设 */} +
+ 常用预设 + + {PRESETS.map((preset) => ( + + ))} + +
+ + {/* 表达式 + 可读描述 */}
- 高级模式 (手动输入) + 高级模式 { setIsAdvanced(checked) if (!checked) { - // When switching back to visual, parse the current raw value setState(parseCron(internalValue)) notifyChange(stringifyCron(parseCron(internalValue))) } @@ -173,6 +245,13 @@ export function CronInput({ value, onChange }: CronInputProps) {
+ {/* 中文可读描述 */} + {description && ( + + {description} + + )} + {!isAdvanced && (