```
feat(scheduler): 添加高级调度配置界面 - 实现调度模式选择(每天、每周、每月、自定义Cron) - 添加时间选择器和日期相关配置选项 - 提供可视化调度预览功能 - 支持Cron表达式验证和实时更新 - 修改默认调度时间为早上8点40分 - 更新邮件接收者地址 ```
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"cronTime": "0 9 * * *",
|
||||
"cronTime": "40 08 * * *",
|
||||
"threshold": 100000,
|
||||
"description": "每天9点采集当日项目",
|
||||
"timeRange": "thisMonth"
|
||||
@@ -16,6 +16,6 @@
|
||||
"smtpPort": 587,
|
||||
"smtpUser": "1076597680@qq.com",
|
||||
"smtpPass": "nfrjdiraqddsjeeh",
|
||||
"recipients": "1650243281@qq.com"
|
||||
"recipients": "5482498@qq.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
disable-all-tasks.bat
Normal file
13
disable-all-tasks.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo ========================================
|
||||
echo 批量禁用所有任务
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
node disable-all-tasks.js
|
||||
|
||||
echo.
|
||||
pause
|
||||
65
disable-all-tasks.js
Normal file
65
disable-all-tasks.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// 批量禁用所有任务脚本
|
||||
// 用法: node disable-all-tasks.js
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const DEFAULT_DB_DIR = join(__dirname, 'data');
|
||||
const DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, 'results.db');
|
||||
const LEGACY_DB_PATH = join(DEFAULT_DB_DIR, 'results.sqlite');
|
||||
|
||||
// 确定数据库路径
|
||||
let DB_PATH = process.env.APP_DB_PATH || process.env.RESULTS_DB_PATH;
|
||||
if (!DB_PATH) {
|
||||
if (existsSync(DEFAULT_DB_PATH)) {
|
||||
DB_PATH = DEFAULT_DB_PATH;
|
||||
} else if (existsSync(LEGACY_DB_PATH)) {
|
||||
DB_PATH = LEGACY_DB_PATH;
|
||||
} else {
|
||||
DB_PATH = DEFAULT_DB_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existsSync(DB_PATH)) {
|
||||
console.error('数据库文件不存在:', DB_PATH);
|
||||
console.log('请确保项目已初始化并运行过至少一次');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
// 先查询当前启用的任务数量
|
||||
const enabledCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE enabled = 1').get();
|
||||
console.log(`当前启用的任务数量: ${enabledCount.count}`);
|
||||
|
||||
// 查询总任务数量
|
||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM tasks').get();
|
||||
console.log(`总任务数量: ${totalCount.count}`);
|
||||
|
||||
if (enabledCount.count === 0) {
|
||||
console.log('没有需要禁用的任务');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 执行批量禁用
|
||||
const result = db.prepare('UPDATE tasks SET enabled = 0').run();
|
||||
console.log(`已禁用 ${result.changes} 个任务`);
|
||||
|
||||
// 验证结果
|
||||
const newEnabledCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE enabled = 1').get();
|
||||
console.log(`禁用后启用的任务数量: ${newEnabledCount.count}`);
|
||||
|
||||
db.close();
|
||||
console.log('操作完成');
|
||||
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
22924
|
||||
15556
|
||||
|
||||
@@ -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, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user