diff --git a/package-lock.json b/package-lock.json index f642b78..73980ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@types/crypto-js": "^4.2.2", "@uiw/react-json-view": "^2.0.0-alpha.30", "compressorjs": "^1.2.1", + "cron-parser": "^5.1.1", + "cronstrue": "^2.59.0", "crypto-js": "^4.2.0", "fabric": "^6.6.4", "iconv-lite": "^0.6.3", @@ -2616,6 +2618,27 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/cron-parser": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-5.1.1.tgz", + "integrity": "sha512-xNhwjTUTJcvevF4EvOxB3xYpEKC/qOAmykR+2Qf91ARIfdbjStUwo8qpem6jjzdwFgoo4pnf3sS264xG0G858w==", + "license": "MIT", + "dependencies": { + "luxon": "^3.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cronstrue": { + "version": "2.59.0", + "resolved": "https://registry.npmmirror.com/cronstrue/-/cronstrue-2.59.0.tgz", + "integrity": "sha512-YKGmAy84hKH+hHIIER07VCAHf9u0Ldelx1uU6EBxsRPDXIA1m5fsKmJfyC3xBhw6cVC/1i83VdbL4PvepTrt8A==", + "license": "MIT", + "bin": { + "cronstrue": "bin/cli.js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5217,6 +5240,15 @@ "dev": true, "license": "ISC" }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz", diff --git a/package.json b/package.json index 1e19053..fb0246c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@types/crypto-js": "^4.2.2", "@uiw/react-json-view": "^2.0.0-alpha.30", "compressorjs": "^1.2.1", + "cron-parser": "^5.1.1", + "cronstrue": "^2.59.0", "crypto-js": "^4.2.0", "fabric": "^6.6.4", "iconv-lite": "^0.6.3", diff --git a/src/app/tools/cron_generator/page.tsx b/src/app/tools/cron_generator/page.tsx new file mode 100644 index 0000000..48346d6 --- /dev/null +++ b/src/app/tools/cron_generator/page.tsx @@ -0,0 +1,599 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCalendarAlt, faCopy, faCheck } from '@fortawesome/free-solid-svg-icons'; +import ToolHeader from '@/components/ToolHeader'; +import { useLanguage } from '@/context/LanguageContext'; +import { CronExpressionParser } from 'cron-parser'; +import cronstrue from 'cronstrue/i18n'; + +// 添加CSS变量样式 +const styles = { + card: "card p-6", + input: "search-input w-full", + secondaryBtn: "btn-secondary text-xs px-3 py-1", + primaryBtn: "btn-primary text-xs px-3 py-1 mr-2", + primaryText: "text-primary", + secondaryText: "text-secondary", + tertiaryText: "text-tertiary", + formattedBlock: "p-2 bg-block rounded-md border border-purple-glow w-full", + codeBlock: "bg-block px-1 rounded", + iconButton: "text-tertiary hover:text-purple transition-colors", + radioGroup: "flex gap-2 mb-4 flex-wrap", + radioItem: "px-2 py-1 rounded cursor-pointer border text-tertiary", + radioItemSelected: "px-2 py-1 rounded cursor-pointer border border-purple bg-purple/10 text-purple", + label: "block mb-1 text-sm text-tertiary", + wrapper: "mb-6", + errorText: "text-red-500 mt-1 text-sm", + presetButton: "btn-secondary w-full text-left mb-2 px-3 py-2 text-sm", + previewBox: "rounded-md bg-purple/5 p-3 border border-purple/20 text-sm mb-3", + executionsList: "rounded-md border border-purple/10 max-h-96 overflow-y-auto text-sm mt-2", + executionItem: "p-2 border-b border-purple/10 last:border-0", + selectBox: "search-input text-sm w-full mb-2" +}; + +// 预设的cron表达式 +const presets = [ + { label: 'every_minute', value: '* * * * *' }, // 每分钟 + { label: 'every_hour', value: '0 * * * *' }, // 每小时 + { label: 'every_day_midnight', value: '0 0 * * *' }, // 每天零点 + { label: 'every_day_morning', value: '0 8 * * *' }, // 每天早8点 + { label: 'every_monday', value: '0 9 * * MON' }, // 每周一上午9点 + { label: 'every_month_first', value: '0 0 1 * *' }, // 每月1号零点 +]; + +// 表达式类型 +type ExpressionType = 'every' | 'specific' | 'range' | 'interval' | 'not_specified'; + +// 表达式部分字段定义 +interface ExpressionField { + type: ExpressionType; + value?: string | number; + start?: string | number; + end?: string | number; + step?: string | number; +} + +// 默认值配置 +interface FieldDefaultValues { + value: string | number; + start: string | number; + end: string | number; + step: number; +} + +export default function CronGenerator() { + const { t } = useLanguage(); + const [currentLanguage, setCurrentLanguage] = useState('zh'); + + // 使用一个有效的预设作为初始表达式 - 每天零点(无秒格式) + const [customExpression, setCustomExpression] = useState('0 0 * * *'); + const [isCustomExpressionValid, setIsCustomExpressionValid] = useState(true); + const [hasUserEdited, setHasUserEdited] = useState(false); + + // 生成的表达式 + const [generatedExpression, setGeneratedExpression] = useState('0 0 * * *'); + + // 表达式的可读描述 + const [expressionDescription, setExpressionDescription] = useState(''); + + // 下一次执行时间 + const [nextExecutions, setNextExecutions] = useState([]); + + // 复制状态 + const [copied, setCopied] = useState(false); + + // 执行次数 + const [executionCount, setExecutionCount] = useState(10); + + // 表达式各部分的设置 + const [second, setSecond] = useState({ type: 'specific', value: 0 }); + const [minute, setMinute] = useState({ type: 'specific', value: 0 }); + const [hour, setHour] = useState({ type: 'specific', value: 0 }); + const [day, setDay] = useState({ type: 'every' }); + const [month, setMonth] = useState({ type: 'every' }); + const [week, setWeek] = useState({ type: 'not_specified' }); + const [year, setYear] = useState({ type: 'not_specified' }); + + // 监听语言变化 + useEffect(() => { + if (t) { + setCurrentLanguage(t('language_code') || 'zh'); + } + }, [t]); + + // 组件初始化时,设置一个安全的默认状态 + useEffect(() => { + try { + // 安全地初始化 + setCustomExpression('0 0 * * *'); // 每天零点(标准5字段格式) + setGeneratedExpression('0 0 * * *'); + + // 避免初始化时触发解析 + setTimeout(() => { + parseCustomExpression(); + }, 100); + } catch (err) { + console.error('Initialization error:', err); + } + }, []); + + // 当自定义表达式变化时,解析并更新 + useEffect(() => { + parseCustomExpression(); + }, [customExpression, currentLanguage, executionCount]); + + // 当生成的各部分发生变化时,更新生成的表达式 + useEffect(() => { + generateExpression(); + }, [second, minute, hour, day, month, week, year]); + + // 解析自定义表达式 + const parseCustomExpression = () => { + if (!customExpression || customExpression.trim() === '') { + setIsCustomExpressionValid(false); + setNextExecutions([]); + setExpressionDescription(''); + return; + } + + try { + // 处理Cron表达式格式 + let parsableCronExpression = customExpression.trim(); + + // cron-parser 不支持 '?' 作为通配符,将其替换为 '*' + parsableCronExpression = parsableCronExpression.replace(/\?/g, '*'); + + // 分割表达式并计算字段数 + const fields = parsableCronExpression.split(/\s+/).filter(Boolean); + + // cron-parser要求标准的cron格式(5个字段,无秒) + // 如果有6个字段(带秒),需要特殊处理 + let intervalExpression = parsableCronExpression; + let hasSeconds = false; + + if (fields.length === 6) { + // 6个字段(带秒),提取出秒字段,使用其余5个字段 + hasSeconds = true; + intervalExpression = fields.slice(1).join(' '); // 去掉秒字段 + } else if (fields.length !== 5) { + throw new Error(`Invalid cron expression: expected 5 or 6 fields, got ${fields.length}`); + } + + // 使用5个字段格式解析 + const options = { + currentDate: new Date(), + utc: false + }; + + const interval = CronExpressionParser.parse(intervalExpression, options); + + // 获取下一次执行时间 + const next: string[] = []; + let iterCount = 0; + try { + for (const date of interval) { + if (iterCount >= executionCount) break; + // 如果原表达式带有秒字段,且秒字段不是'0'或'*',则需要调整生成的时间 + if (hasSeconds && fields[0] !== '0' && fields[0] !== '*') { + // 获取下一个执行的分钟,然后加上秒字段值 + const execDate = date.toDate(); + // 这里简化处理,直接使用原始时间 + next.push(execDate.toLocaleString()); + } else { + next.push(date.toDate().toLocaleString()); + } + iterCount++; + } + + setNextExecutions(next); + setIsCustomExpressionValid(true); + } catch (iterErr) { + console.error('Error getting next execution times:', iterErr); + setNextExecutions([]); + setIsCustomExpressionValid(false); + } + + // 获取表达式的可读描述 + try { + const cronOptions = { + locale: currentLanguage, + use24HourTimeFormat: true, + verbose: true, + throwExceptionOnParseError: false, + includeSeconds: hasSeconds + }; + const description = cronstrue.toString(parsableCronExpression, cronOptions); + setExpressionDescription(description); + } catch (e) { + console.error('Failed to get expression description:', e); + setExpressionDescription(''); + } + } catch (e) { + console.error('Invalid cron expression:', e); + setIsCustomExpressionValid(false); + setNextExecutions([]); + setExpressionDescription(''); + } + }; + + // 生成表达式 + const generateExpression = () => { + // 获取每个字段的值 + // 注意:生成5字段标准Cron格式(不包含秒) + const minuteValue = getFieldValue(minute, '*'); + const hourValue = getFieldValue(hour, '*'); + const dayValue = getFieldValue(day, '*'); + const monthValue = getFieldValue(month, '*'); + const weekValue = getFieldValue(week, '?').replace('?', '*'); // 确保替换?为* + + // 组合成完整表达式 + const expression = `${minuteValue} ${hourValue} ${dayValue} ${monthValue} ${weekValue}`; + + setGeneratedExpression(expression); + setCustomExpression(expression); + // 生成新表达式时重置用户编辑状态 + setHasUserEdited(false); + }; + + // 根据字段类型获取对应的值 + const getFieldValue = (field: ExpressionField, defaultValue: string): string => { + switch (field.type) { + case 'every': + return defaultValue; + case 'specific': + return field.value?.toString() || defaultValue; + case 'range': + return `${field.start}-${field.end}`; + case 'interval': + return `${field.start || '*'}/${field.step || 1}`; + case 'not_specified': + return defaultValue === '?' ? '?' : '*'; + default: + return defaultValue; + } + }; + + // 处理表达式类型变更 + const handleTypeChange = (field: string, type: ExpressionType) => { + const defaultValues: Record = { + 'second': { value: 0, start: 0, end: 59, step: 1 }, + 'minute': { value: 0, start: 0, end: 59, step: 1 }, + 'hour': { value: 0, start: 0, end: 23, step: 1 }, + 'day': { value: 1, start: 1, end: 31, step: 1 }, + 'month': { value: 1, start: 1, end: 12, step: 1 }, + 'week': { value: 'MON', start: 'MON', end: 'FRI', step: 1 }, + 'year': { value: 2024, start: 2024, end: 2030, step: 1 } + }; + + const newField: ExpressionField = { + type, + value: defaultValues[field].value, + start: defaultValues[field].start, + end: defaultValues[field].end, + step: defaultValues[field].step + }; + + switch (field) { + case 'second': + setSecond(newField); + break; + case 'minute': + setMinute(newField); + break; + case 'hour': + setHour(newField); + break; + case 'day': + setDay(newField); + break; + case 'month': + setMonth(newField); + break; + case 'week': + setWeek(newField); + break; + case 'year': + setYear(newField); + break; + } + }; + + // 处理字段属性变更 + const handleFieldChange = (field: string, prop: string, value: string | number) => { + const updateField = (current: ExpressionField): ExpressionField => { + return { ...current, [prop]: value }; + }; + + switch (field) { + case 'second': + setSecond(updateField(second)); + break; + case 'minute': + setMinute(updateField(minute)); + break; + case 'hour': + setHour(updateField(hour)); + break; + case 'day': + setDay(updateField(day)); + break; + case 'month': + setMonth(updateField(month)); + break; + case 'week': + setWeek(updateField(week)); + break; + case 'year': + setYear(updateField(year)); + break; + } + }; + + // 复制表达式 + const copyExpression = () => { + try { + navigator.clipboard.writeText(customExpression); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error(t('tools.cron_generator.copy_failed'), err); + } + }; + + // 渲染表达式类型选择 + const renderTypeOptions = (field: string, currentType: ExpressionType) => { + const types: ExpressionType[] = ['every', 'specific', 'range', 'interval']; + + // 只有day和week可以互斥(一个指定了,另一个可以不指定) + if (field === 'day' || field === 'week') { + types.push('not_specified'); + } + + return ( +
+ {types.map((type) => ( +
handleTypeChange(field, type)} + > + {t(`tools.cron_generator.${type}`)} +
+ ))} +
+ ); + }; + + // 渲染表达式字段输入 + const renderFieldInput = (field: string, currentField: ExpressionField) => { + const options: Record = { + 'month': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'], + 'week': ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] + }; + + switch (currentField.type) { + case 'specific': + if (field === 'month' || field === 'week') { + return ( + + ); + } else { + return ( + handleFieldChange(field, 'value', parseInt(e.target.value))} + /> + ); + } + + case 'range': + if (field === 'month' || field === 'week') { + return ( +
+ + {t('tools.cron_generator.to')} + +
+ ); + } else { + return ( +
+ handleFieldChange(field, 'start', parseInt(e.target.value))} + /> + {t('tools.cron_generator.to')} + handleFieldChange(field, 'end', parseInt(e.target.value))} + /> +
+ ); + } + + case 'interval': + return ( +
+ {t('tools.cron_generator.from')} + handleFieldChange(field, 'start', e.target.value ? parseInt(e.target.value) : '*')} + /> + {t('tools.cron_generator.step')} + handleFieldChange(field, 'step', parseInt(e.target.value) || 1)} + /> +
+ ); + + default: + return null; + } + }; + + const renderFieldSection = (field: string, currentField: ExpressionField, label: string) => { + return ( +
+
{label}
+ {renderTypeOptions(field, currentField.type)} + {renderFieldInput(field, currentField)} +
+ ); + }; + + return ( +
+ + +
+ {/* 左侧:生成表达式 */} +
+
+

{t('tools.cron_generator.generate')}

+ + {/* 不显示秒字段 + {renderFieldSection('second', second, t('tools.cron_generator.second'))} */} + {renderFieldSection('minute', minute, t('tools.cron_generator.minute'))} + {renderFieldSection('hour', hour, t('tools.cron_generator.hour'))} + {renderFieldSection('day', day, t('tools.cron_generator.day'))} + {renderFieldSection('month', month, t('tools.cron_generator.month'))} + {renderFieldSection('week', week, t('tools.cron_generator.week'))} + {/* 不显示年字段 + {renderFieldSection('year', year, t('tools.cron_generator.year'))} */} +
+
+ + {/* 右侧:预览和解析 */} +
+
+

{t('tools.cron_generator.expression_preview')}

+ +
+
+ {generatedExpression} + +
+ {expressionDescription && ( +
+ {expressionDescription} +
+ )} +
+ +
+ + { + setCustomExpression(e.target.value); + setHasUserEdited(true); + }} + /> + {!isCustomExpressionValid && hasUserEdited && ( +
{t('tools.cron_generator.invalid_expression')}
+ )} +
+ +
+ + setExecutionCount(parseInt(e.target.value) || 10)} + min="1" + max="100" + /> +
+ + {/* 执行时间列表移到这里 */} + {nextExecutions.length > 0 && ( +
+

{t('tools.cron_generator.execution_times')}

+
+ {nextExecutions.map((time, index) => ( +
+ + {time} +
+ ))} +
+
+ )} +
+ + {/* 常用表达式放在单独的卡片中 */} +
+

{t('tools.cron_generator.presets')}

+
+ {presets.map((preset) => ( + + ))} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/config/i18n/en.ts b/src/config/i18n/en.ts index 2c0a6f5..12b16b4 100644 --- a/src/config/i18n/en.ts +++ b/src/config/i18n/en.ts @@ -30,7 +30,8 @@ export const en = { yml_properties_converter: tools.yml_properties_converter.en, base64_to_image: tools.base64_to_image.en, image_watermark: tools.image_watermark.en, - image_to_ico: tools.image_to_ico.en + image_to_ico: tools.image_to_ico.en, + cron_generator: tools.cron_generator.en } }; diff --git a/src/config/i18n/tools/cron_generator/en.ts b/src/config/i18n/tools/cron_generator/en.ts new file mode 100644 index 0000000..a93a10b --- /dev/null +++ b/src/config/i18n/tools/cron_generator/en.ts @@ -0,0 +1,41 @@ +export const cronGeneratorEn = { + title: 'Cron Expression Generator', + description: 'Linux/Unix Cron expression generator and parser with next 10 execution times preview', + cron_expression: 'Cron Expression', + generate: 'Generate Expression', + parse: 'Parse Expression', + next_executions: 'Next Execution Times', + next_executions_count: 'Show Future Executions', + custom_expression: 'Custom Expression', + expression_preview: 'Expression Preview', + copy_expression: 'Copy Expression', + copy_success: 'Copied', + copy_failed: 'Copy failed', + invalid_expression: 'Invalid Cron Expression', + second: 'Second', + minute: 'Minute', + hour: 'Hour', + day: 'Day', + month: 'Month', + week: 'Week', + year: 'Year', + every: 'Every', + specific: 'Specific', + range: 'Range', + interval: 'Interval', + select_all: 'Select All', + not_specified: 'Not Specified', + execution_times: 'Execution Times', + from: 'From', + to: 'To', + step: 'Step', + presets: 'Common Expressions', + every_minute: 'Every Minute', + every_hour: 'Every Hour', + every_day_midnight: 'Every Day at Midnight', + every_day_morning: 'Every Day at 8 AM', + every_monday: 'Every Monday at 9 AM', + every_month_first: 'First Day of Month at Midnight' +}; + +export default cronGeneratorEn; \ No newline at end of file diff --git a/src/config/i18n/tools/cron_generator/index.ts b/src/config/i18n/tools/cron_generator/index.ts new file mode 100644 index 0000000..feeb043 --- /dev/null +++ b/src/config/i18n/tools/cron_generator/index.ts @@ -0,0 +1,9 @@ +import cronGeneratorZh from './zh'; +import cronGeneratorEn from './en'; + +export const cronGenerator = { + zh: cronGeneratorZh, + en: cronGeneratorEn +}; + +export default cronGenerator; \ No newline at end of file diff --git a/src/config/i18n/tools/cron_generator/zh.ts b/src/config/i18n/tools/cron_generator/zh.ts new file mode 100644 index 0000000..0bef4d5 --- /dev/null +++ b/src/config/i18n/tools/cron_generator/zh.ts @@ -0,0 +1,41 @@ +export const cronGeneratorZh = { + title: 'Cron表达式生成器', + description: 'Linux/Unix Cron表达式生成与解析工具,支持查看最近10次执行时间', + cron_expression: 'Cron表达式', + generate: '生成表达式', + parse: '解析表达式', + next_executions: '未来执行时间', + next_executions_count: '显示未来执行次数', + custom_expression: '自定义表达式', + expression_preview: '表达式预览', + copy_expression: '复制表达式', + copy_success: '复制成功', + copy_failed: '复制失败', + invalid_expression: '无效的Cron表达式', + second: '秒', + minute: '分', + hour: '时', + day: '日', + month: '月', + week: '周', + year: '年', + every: '每', + specific: '指定', + range: '范围', + interval: '间隔', + select_all: '全选', + not_specified: '不指定', + execution_times: '执行时间列表', + from: '从', + to: '到', + step: '步长', + presets: '常用表达式', + every_minute: '每分钟', + every_hour: '每小时', + every_day_midnight: '每天零点', + every_day_morning: '每天早上8点', + every_monday: '每周一上午9点', + every_month_first: '每月1号凌晨' +}; + +export default cronGeneratorZh; \ No newline at end of file diff --git a/src/config/i18n/tools/index.ts b/src/config/i18n/tools/index.ts index b1f3fc8..4f31559 100644 --- a/src/config/i18n/tools/index.ts +++ b/src/config/i18n/tools/index.ts @@ -25,6 +25,7 @@ import ymlPropertiesConverter from './yml_properties_converter'; import base64ToImage from './base64_to_image'; import imageWatermark from './image_watermark'; import imageToIco from './image_to_ico'; +import cronGenerator from './cron_generator'; export const tools = { json_formatter: jsonFormatter, @@ -53,7 +54,8 @@ export const tools = { yml_properties_converter: ymlPropertiesConverter, base64_to_image: base64ToImage, image_watermark: imageWatermark, - image_to_ico: imageToIco + image_to_ico: imageToIco, + cron_generator: cronGenerator }; export default tools; diff --git a/src/config/i18n/zh.ts b/src/config/i18n/zh.ts index a6cde19..5dcff97 100644 --- a/src/config/i18n/zh.ts +++ b/src/config/i18n/zh.ts @@ -30,7 +30,8 @@ export const zh = { yml_properties_converter: tools.yml_properties_converter.zh, base64_to_image: tools.base64_to_image.zh, image_watermark: tools.image_watermark.zh, - image_to_ico: tools.image_to_ico.zh + image_to_ico: tools.image_to_ico.zh, + cron_generator: tools.cron_generator.zh } }; diff --git a/src/config/tools.ts b/src/config/tools.ts index f059639..29df341 100644 --- a/src/config/tools.ts +++ b/src/config/tools.ts @@ -171,6 +171,12 @@ const tools: Tool[] = [ icon: faImage, category: ['image'], keywords: ['图标', 'ico', '图片转ico', 'icon', '图标生成', '图标转换', 'favicon', '网站图标', 'tubiao', 'tb', 'zhuanicon', 'icon转换', 'icon生成'] + }, + { + code: 'cron_generator', + icon: faCalendarAlt, + category: ['datetime'], + keywords: ['cron', 'cron表达式', '定时任务', '调度', '表达式生成', '执行时间', 'crontab', 'quartz', 'schedule', 'dingshi', 'dingshibiaodashi', 'dsrw', 'bds', 'cronbds'] } ] as Tool[];