feat(scheduler): 添加高级调度配置界面

- 实现调度模式选择(每天、每周、每月、自定义Cron)
- 添加时间选择器和日期相关配置选项
- 提供可视化调度预览功能
- 支持Cron表达式验证和实时更新
- 修改默认调度时间为早上8点40分
- 更新邮件接收者地址
```
This commit is contained in:
2026-03-20 16:28:27 +08:00
parent 89d0abd44c
commit 0d74cfe754
5 changed files with 255 additions and 8 deletions

View File

@@ -688,14 +688,51 @@
</select>
</div>
<div class="form-group">
<label>Cron 表达</label>
<input type="text" id="cfgCronTime" placeholder="0 9 * * *">
<label>执行方</label>
<select id="cfgScheduleMode" onchange="handleSchedulerModeChange()">
<option value="daily">每天</option>
<option value="weekly">每周</option>
<option value="monthly">每月</option>
<option value="custom">高级 Cron</option>
</select>
</div>
<div class="form-group">
<label>描述</label>
<input type="text" id="cfgDescription" placeholder="每天9点采集">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>执行时间</label>
<input type="time" id="cfgScheduleTime" value="09:00" onchange="updateSchedulerCronPreview()">
</div>
<div class="form-group" id="cfgWeekdayGroup" style="display:none;">
<label>星期</label>
<select id="cfgWeekday" onchange="updateSchedulerCronPreview()">
<option value="1">周一</option>
<option value="2">周二</option>
<option value="3">周三</option>
<option value="4">周四</option>
<option value="5">周五</option>
<option value="6">周六</option>
<option value="0">周日</option>
</select>
</div>
<div class="form-group" id="cfgMonthDayGroup" style="display:none;">
<label>每月几号</label>
<input type="number" id="cfgMonthDay" min="1" max="31" value="1" onchange="updateSchedulerCronPreview()">
</div>
</div>
<div class="form-row">
<div class="form-group" id="cfgCustomCronGroup" style="display:none;">
<label>高级 Cron 表达式</label>
<input type="text" id="cfgCronTime" placeholder="0 9 * * *" oninput="updateSchedulerCronPreview()">
</div>
<div class="form-group">
<label>计划预览</label>
<input type="text" id="cfgCronPreview" readonly placeholder="每天 09:00">
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="triggerScheduledTask()">立即执行定时任务</button>
<span id="schedulerStatus" style="margin-left:12px;font-size:13px;color:#888;"></span>
</div>
@@ -1433,6 +1470,123 @@
.replace(/'/g, '&#39;');
}
function padSchedulerNumber(value) {
return String(value).padStart(2, '0');
}
function normalizeSchedulerTime(value) {
const match = /^(\d{1,2}):(\d{1,2})$/.exec(String(value || '').trim());
if (!match) return '09:00';
const hour = Math.min(23, Math.max(0, parseInt(match[1], 10)));
const minute = Math.min(59, Math.max(0, parseInt(match[2], 10)));
return `${padSchedulerNumber(hour)}:${padSchedulerNumber(minute)}`;
}
function splitSchedulerTime(value) {
const [hour, minute] = normalizeSchedulerTime(value).split(':');
return { hour, minute };
}
function describeScheduler(mode, time, weekday, monthDay, cronTime) {
const normalizedTime = normalizeSchedulerTime(time);
const weekdayMap = {
'0': '周日',
'1': '周一',
'2': '周二',
'3': '周三',
'4': '周四',
'5': '周五',
'6': '周六',
};
if (mode === 'daily') return `每天 ${normalizedTime}`;
if (mode === 'weekly') return `每周${weekdayMap[String(weekday)] || '周一'} ${normalizedTime}`;
if (mode === 'monthly') return `每月 ${monthDay}${normalizedTime}`;
return cronTime || '0 9 * * *';
}
function parseSchedulerCron(cronTime) {
const normalizedCron = String(cronTime || '0 9 * * *').trim().replace(/\s+/g, ' ');
const parts = normalizedCron.split(' ');
const fallback = {
mode: 'custom',
time: '09:00',
weekday: '1',
monthDay: '1',
cronTime: normalizedCron || '0 9 * * *',
};
if (parts.length !== 5) return fallback;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
if (!/^\d+$/.test(minute) || !/^\d+$/.test(hour)) return fallback;
const time = `${padSchedulerNumber(parseInt(hour, 10))}:${padSchedulerNumber(parseInt(minute, 10))}`;
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return { mode: 'daily', time, weekday: '1', monthDay: '1', cronTime: normalizedCron };
}
if (dayOfMonth === '*' && month === '*' && /^(0|1|2|3|4|5|6|7)$/.test(dayOfWeek)) {
return {
mode: 'weekly',
time,
weekday: dayOfWeek === '7' ? '0' : dayOfWeek,
monthDay: '1',
cronTime: normalizedCron,
};
}
if (/^\d+$/.test(dayOfMonth) && month === '*' && dayOfWeek === '*') {
const normalizedMonthDay = Math.min(31, Math.max(1, parseInt(dayOfMonth, 10)));
return {
mode: 'monthly',
time,
weekday: '1',
monthDay: String(normalizedMonthDay),
cronTime: normalizedCron,
};
}
return fallback;
}
function buildSchedulerCronFromInputs() {
const mode = document.getElementById('cfgScheduleMode').value;
const time = normalizeSchedulerTime(document.getElementById('cfgScheduleTime').value);
const { hour, minute } = splitSchedulerTime(time);
const weekday = document.getElementById('cfgWeekday').value || '1';
const monthDayValue = parseInt(document.getElementById('cfgMonthDay').value, 10);
const monthDay = Number.isFinite(monthDayValue) ? Math.min(31, Math.max(1, monthDayValue)) : 1;
if (mode === 'daily') return `${minute} ${hour} * * *`;
if (mode === 'weekly') return `${minute} ${hour} * * ${weekday}`;
if (mode === 'monthly') return `${minute} ${hour} ${monthDay} * *`;
return document.getElementById('cfgCronTime').value.trim() || '0 9 * * *';
}
function updateSchedulerCronPreview() {
const cronTime = buildSchedulerCronFromInputs();
document.getElementById('cfgCronTime').value = cronTime;
document.getElementById('cfgCronPreview').value = describeScheduler(
document.getElementById('cfgScheduleMode').value,
document.getElementById('cfgScheduleTime').value,
document.getElementById('cfgWeekday').value,
document.getElementById('cfgMonthDay').value,
cronTime
);
}
function handleSchedulerModeChange() {
const mode = document.getElementById('cfgScheduleMode').value;
document.getElementById('cfgWeekdayGroup').style.display = mode === 'weekly' ? '' : 'none';
document.getElementById('cfgMonthDayGroup').style.display = mode === 'monthly' ? '' : 'none';
document.getElementById('cfgCustomCronGroup').style.display = mode === 'custom' ? '' : 'none';
updateSchedulerCronPreview();
}
// ===== 设置 =====
async function loadSettings() {
try {
@@ -1448,8 +1602,14 @@
// Scheduler
document.getElementById('cfgSchedulerEnabled').value = String(cfg.scheduler?.enabled ?? false);
document.getElementById('cfgCronTime').value = cfg.scheduler?.cronTime || '0 9 * * *';
const schedulerUi = parseSchedulerCron(cfg.scheduler?.cronTime || '0 9 * * *');
document.getElementById('cfgScheduleMode').value = schedulerUi.mode;
document.getElementById('cfgScheduleTime').value = schedulerUi.time;
document.getElementById('cfgWeekday').value = schedulerUi.weekday;
document.getElementById('cfgMonthDay').value = schedulerUi.monthDay;
document.getElementById('cfgCronTime').value = schedulerUi.cronTime;
document.getElementById('cfgDescription').value = cfg.scheduler?.description || '';
handleSchedulerModeChange();
// Email
document.getElementById('cfgSmtpHost').value = cfg.email?.smtpHost || '';
@@ -1473,6 +1633,14 @@
async function saveSettings() {
try {
const cronTime = buildSchedulerCronFromInputs();
if (document.getElementById('cfgScheduleMode').value === 'custom') {
const cronParts = cronTime.trim().split(/\s+/);
if (cronParts.length !== 5) {
throw new Error('Cron 表达式格式错误,请填写 5 段');
}
}
const cfg = {
agent: {
baseUrl: document.getElementById('cfgBaseUrl').value.trim(),
@@ -1481,7 +1649,7 @@
},
scheduler: {
enabled: document.getElementById('cfgSchedulerEnabled').value === 'true',
cronTime: document.getElementById('cfgCronTime').value.trim() || '0 9 * * *',
cronTime,
description: document.getElementById('cfgDescription').value.trim(),
},
email: {
@@ -1545,6 +1713,7 @@
// ===== 初始化 =====
initProjectSearchInputs();
loadTasks();
handleSchedulerModeChange();
loadSettings();
</script>
</body>