cron_generator

This commit is contained in:
star7th
2025-04-23 20:11:08 +08:00
parent bbfd4d9f93
commit 260536c962
10 changed files with 737 additions and 3 deletions
+32
View File
@@ -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",
+2
View File
@@ -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",
+599
View File
@@ -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<string[]>([]);
// 复制状态
const [copied, setCopied] = useState(false);
// 执行次数
const [executionCount, setExecutionCount] = useState(10);
// 表达式各部分的设置
const [second, setSecond] = useState<ExpressionField>({ type: 'specific', value: 0 });
const [minute, setMinute] = useState<ExpressionField>({ type: 'specific', value: 0 });
const [hour, setHour] = useState<ExpressionField>({ type: 'specific', value: 0 });
const [day, setDay] = useState<ExpressionField>({ type: 'every' });
const [month, setMonth] = useState<ExpressionField>({ type: 'every' });
const [week, setWeek] = useState<ExpressionField>({ type: 'not_specified' });
const [year, setYear] = useState<ExpressionField>({ 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<string, FieldDefaultValues> = {
'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 (
<div className={styles.radioGroup}>
{types.map((type) => (
<div
key={type}
className={currentType === type ? styles.radioItemSelected : styles.radioItem}
onClick={() => handleTypeChange(field, type)}
>
{t(`tools.cron_generator.${type}`)}
</div>
))}
</div>
);
};
// 渲染表达式字段输入
const renderFieldInput = (field: string, currentField: ExpressionField) => {
const options: Record<string, string[]> = {
'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 (
<select
className={styles.selectBox}
value={currentField.value?.toString()}
onChange={(e) => handleFieldChange(field, 'value', e.target.value)}
>
{options[field].map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
} else {
return (
<input
type="number"
className={styles.input}
value={currentField.value}
onChange={(e) => handleFieldChange(field, 'value', parseInt(e.target.value))}
/>
);
}
case 'range':
if (field === 'month' || field === 'week') {
return (
<div className="flex items-center gap-2">
<select
className={styles.selectBox}
value={currentField.start?.toString()}
onChange={(e) => handleFieldChange(field, 'start', e.target.value)}
>
{options[field].map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<span>{t('tools.cron_generator.to')}</span>
<select
className={styles.selectBox}
value={currentField.end?.toString()}
onChange={(e) => handleFieldChange(field, 'end', e.target.value)}
>
{options[field].map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
} else {
return (
<div className="flex items-center gap-2">
<input
type="number"
className={styles.input}
value={currentField.start}
onChange={(e) => handleFieldChange(field, 'start', parseInt(e.target.value))}
/>
<span>{t('tools.cron_generator.to')}</span>
<input
type="number"
className={styles.input}
value={currentField.end}
onChange={(e) => handleFieldChange(field, 'end', parseInt(e.target.value))}
/>
</div>
);
}
case 'interval':
return (
<div className="flex items-center gap-2">
<span>{t('tools.cron_generator.from')}</span>
<input
type="number"
className={styles.input}
value={currentField.start === '*' ? '' : currentField.start}
placeholder="*"
onChange={(e) => handleFieldChange(field, 'start', e.target.value ? parseInt(e.target.value) : '*')}
/>
<span>{t('tools.cron_generator.step')}</span>
<input
type="number"
className={styles.input}
value={currentField.step}
onChange={(e) => handleFieldChange(field, 'step', parseInt(e.target.value) || 1)}
/>
</div>
);
default:
return null;
}
};
const renderFieldSection = (field: string, currentField: ExpressionField, label: string) => {
return (
<div className={styles.wrapper}>
<div className="font-medium mb-2">{label}</div>
{renderTypeOptions(field, currentField.type)}
{renderFieldInput(field, currentField)}
</div>
);
};
return (
<div className="container mx-auto px-4 py-8 max-w-5xl">
<ToolHeader
toolCode="cron_generator"
icon={faCalendarAlt}
title={t('tools.cron_generator.title')}
description={t('tools.cron_generator.description')}
/>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧:生成表达式 */}
<div className="lg:col-span-2">
<div className={styles.card}>
<h2 className="text-xl font-medium mb-4">{t('tools.cron_generator.generate')}</h2>
{/* 不显示秒字段
{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'))} */}
</div>
</div>
{/* 右侧:预览和解析 */}
<div>
<div className={styles.card}>
<h2 className="text-xl font-medium mb-4">{t('tools.cron_generator.expression_preview')}</h2>
<div className={styles.previewBox}>
<div className="flex items-center justify-between">
<span className="font-mono">{generatedExpression}</span>
<button
className={styles.iconButton}
onClick={copyExpression}
title={t('tools.cron_generator.copy_expression')}
>
<FontAwesomeIcon icon={copied ? faCheck : faCopy} />
</button>
</div>
{expressionDescription && (
<div className="mt-2 text-tertiary">
{expressionDescription}
</div>
)}
</div>
<div className="mb-4">
<label className={styles.label}>{t('tools.cron_generator.custom_expression')}</label>
<input
type="text"
className={styles.input}
value={customExpression}
onChange={(e) => {
setCustomExpression(e.target.value);
setHasUserEdited(true);
}}
/>
{!isCustomExpressionValid && hasUserEdited && (
<div className={styles.errorText}>{t('tools.cron_generator.invalid_expression')}</div>
)}
</div>
<div className="mb-4">
<label className={styles.label}>{t('tools.cron_generator.next_executions_count')}</label>
<input
type="number"
className={styles.input}
value={executionCount}
onChange={(e) => setExecutionCount(parseInt(e.target.value) || 10)}
min="1"
max="100"
/>
</div>
{/* 执行时间列表移到这里 */}
{nextExecutions.length > 0 && (
<div className="mb-4">
<h2 className="text-xl font-medium mb-4">{t('tools.cron_generator.execution_times')}</h2>
<div className={styles.executionsList}>
{nextExecutions.map((time, index) => (
<div key={index} className={styles.executionItem}>
<FontAwesomeIcon icon={faCalendarAlt} className="mr-2 text-purple" />
{time}
</div>
))}
</div>
</div>
)}
</div>
{/* 常用表达式放在单独的卡片中 */}
<div className={`${styles.card} mt-4`}>
<h2 className="text-xl font-medium mb-4">{t('tools.cron_generator.presets')}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{presets.map((preset) => (
<button
key={preset.value}
className={styles.presetButton}
onClick={() => {
setCustomExpression(preset.value);
setHasUserEdited(false);
}}
>
{t(`tools.cron_generator.${preset.label}`)}
<div className="text-xs text-tertiary mt-1">{preset.value}</div>
</button>
))}
</div>
</div>
</div>
</div>
</div>
);
}
+2 -1
View File
@@ -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
}
};
@@ -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;
@@ -0,0 +1,9 @@
import cronGeneratorZh from './zh';
import cronGeneratorEn from './en';
export const cronGenerator = {
zh: cronGeneratorZh,
en: cronGeneratorEn
};
export default cronGenerator;
@@ -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;
+3 -1
View File
@@ -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;
+2 -1
View File
@@ -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
}
};
+6
View File
@@ -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[];