提交 28f55365 authored 作者: 陈泽健's avatar 陈泽健

feat(service-monitor): 定时任务弹窗交互优化(友好调度配置替代 Cron 表达式)

- 删除 Cron 表达式输入,改为重复周期单选组(每天/工作日/每周)
- 新增时:分下拉选择器 + 星期多选按钮组
- 新增生效日期/失效日期选择器
- 列表显示自然语言描述(如'工作日 09:30')替代 Cron 表达式
- 后端新增 _build_cron/_describe_schedule 自动转换
- 数据模型新增 repeat_mode/hour/minute/weekdays/start_date/end_date 字段
- 新增 PRD 文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 65a88a6d
......@@ -23,6 +23,98 @@ logger = logging.getLogger("service_monitor.schedule")
_scheduler = None
_scheduler_lock = threading.Lock()
# 星期映射:数字 → 中文名
WEEKDAY_NAMES = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "日"}
def _build_cron(repeat_mode: str, hour: int, minute: int, weekdays: list | None = None) -> str:
"""将友好调度字段转换为 Cron 表达式。
Args:
repeat_mode: daily / weekday / weekly
hour: 0-23
minute: 0-59
weekdays: 星期几列表(1=周一…7=周日),仅 weekly 模式
Returns:
标准 5 段 Cron 表达式
"""
h = max(0, min(23, int(hour)))
m = max(0, min(59, int(minute)))
if repeat_mode == "daily":
return f"{m} {h} * * *"
elif repeat_mode == "weekday":
return f"{m} {h} * * 1-5"
elif repeat_mode == "weekly":
if not weekdays:
raise ValueError("每周模式请选择至少一个星期")
# 排序去重,确保合法
days = sorted(set(int(d) for d in weekdays if 1 <= int(d) <= 7))
if not days:
raise ValueError("每周模式请选择至少一个星期")
dow = ",".join(str(d) for d in days)
return f"{m} {h} * * {dow}"
else:
raise ValueError(f"不支持的重复周期: {repeat_mode}")
def _describe_schedule(sched: dict) -> str:
"""生成定时任务的自然语言描述。"""
mode = sched.get("repeat_mode", "")
hour = sched.get("hour")
minute = sched.get("minute", 0)
weekdays = sched.get("weekdays", [])
start_date = sched.get("start_date")
end_date = sched.get("end_date")
# 兼容旧数据:无 repeat_mode 时从 cron 反推
if not mode:
return _describe_cron(sched.get("cron", ""))
time_str = f"{int(hour):02d}:{int(minute):02d}" if hour is not None else "???"
if mode == "daily":
desc = f"每天 {time_str}"
elif mode == "weekday":
desc = f"工作日 {time_str}"
elif mode == "weekly":
if weekdays:
day_names = "、".join(f"周{WEEKDAY_NAMES.get(d, str(d))}" for d in sorted(weekdays))
desc = f"每{day_names} {time_str}"
else:
desc = f"每周 {time_str}"
else:
desc = sched.get("cron", "")
# 追加起止日期
date_parts = []
if start_date:
date_parts.append(start_date)
if end_date:
date_parts.append(end_date)
if date_parts:
desc += f"({' 至 '.join(date_parts)})"
return desc
def _describe_cron(cron_expr: str) -> str:
"""Best-effort 从 Cron 表达式生成描述(兼容旧数据)。"""
if not cron_expr:
return "-"
parts = cron_expr.split()
if len(parts) != 5:
return cron_expr
minute, hour, _, _, dow = parts
time_str = f"{int(hour):02d}:{int(minute):02d}" if hour.isdigit() and minute.isdigit() else f"{hour}:{minute}"
if dow == "*":
return f"每天 {time_str}"
elif dow == "1-5":
return f"工作日 {time_str}"
else:
return cron_expr
def _load_schedules() -> list:
"""加载定时任务列表。"""
......@@ -85,21 +177,45 @@ def create_schedule(data: dict, created_by: str) -> dict:
name = data.get("name", "").strip()
target_id = data.get("target_id", "").strip()
suite = data.get("suite", "quick")
cron_expr = data.get("cron", "").strip()
repeat_mode = data.get("repeat_mode", "daily")
hour = data.get("hour", 8)
minute = data.get("minute", 0)
weekdays = data.get("weekdays", [])
start_date = data.get("start_date") or None
end_date = data.get("end_date") or None
if not name:
raise ValueError("任务名称不能为空")
if not target_id:
raise ValueError("请选择监测目标")
if not cron_expr:
raise ValueError("请输入 Cron 表达式")
# 校验 cron 表达式
# 校验 repeat_mode
if repeat_mode not in ("daily", "weekday", "weekly"):
raise ValueError("重复周期仅支持:每天、工作日、每周")
# 校验时间
try:
from croniter import croniter
croniter(cron_expr) # 不抛异常即合法
except Exception as e:
raise ValueError(f"Cron 表达式无效: {e}")
hour = int(hour)
minute = int(minute)
if not (0 <= hour <= 23 and 0 <= minute <= 59):
raise ValueError
except (ValueError, TypeError):
raise ValueError("时间格式无效,小时 0-23,分钟 0-59")
# 校验星期(weekly 模式)
if repeat_mode == "weekly":
if not weekdays:
raise ValueError("每周模式请选择至少一个星期")
weekdays = [int(d) for d in weekdays if 1 <= int(d) <= 7]
if not weekdays:
raise ValueError("每周模式请选择至少一个星期")
# 校验起止日期
if end_date and start_date and end_date < start_date:
raise ValueError("失效日期必须晚于生效日期")
# 生成 Cron 表达式
cron_expr = _build_cron(repeat_mode, hour, minute, weekdays)
# 生成 ID
schedule_id = "sched_" + str(uuid.uuid4())[:12]
......@@ -116,6 +232,12 @@ def create_schedule(data: dict, created_by: str) -> dict:
"target_name": target_name,
"suite": suite,
"cron": cron_expr,
"repeat_mode": repeat_mode,
"hour": hour,
"minute": minute,
"weekdays": weekdays,
"start_date": start_date,
"end_date": end_date,
"enabled": True,
"last_run_at": None,
"last_run_status": None,
......@@ -144,17 +266,49 @@ def update_schedule(schedule_id: str, data: dict) -> dict:
if not name:
raise ValueError("任务名称不能为空")
schedules[i]["name"] = name
if "cron" in data:
cron_expr = data["cron"].strip()
if not cron_expr:
raise ValueError("Cron 表达式不能为空")
# 调度字段更新:只要传了 repeat_mode 就重算 cron
repeat_mode = data.get("repeat_mode", s.get("repeat_mode", "daily"))
hour = data.get("hour", s.get("hour", 8))
minute = data.get("minute", s.get("minute", 0))
weekdays = data.get("weekdays", s.get("weekdays", []))
if "repeat_mode" in data or "hour" in data or "minute" in data or "weekdays" in data:
# 校验
if repeat_mode not in ("daily", "weekday", "weekly"):
raise ValueError("重复周期仅支持:每天、工作日、每周")
try:
from croniter import croniter
croniter(cron_expr)
except Exception as e:
raise ValueError(f"Cron 表达式无效: {e}")
hour = int(hour)
minute = int(minute)
if not (0 <= hour <= 23 and 0 <= minute <= 59):
raise ValueError
except (ValueError, TypeError):
raise ValueError("时间格式无效")
if repeat_mode == "weekly":
weekdays = [int(d) for d in weekdays if 1 <= int(d) <= 7]
if not weekdays:
raise ValueError("每周模式请选择至少一个星期")
cron_expr = _build_cron(repeat_mode, hour, minute, weekdays)
schedules[i]["repeat_mode"] = repeat_mode
schedules[i]["hour"] = hour
schedules[i]["minute"] = minute
schedules[i]["weekdays"] = weekdays
schedules[i]["cron"] = cron_expr
schedules[i]["next_run_at"] = _calc_next_run(cron_expr)
# 起止日期
if "start_date" in data:
schedules[i]["start_date"] = data["start_date"] or None
if "end_date" in data:
schedules[i]["end_date"] = data["end_date"] or None
# 校验起止日期
sd = schedules[i].get("start_date")
ed = schedules[i].get("end_date")
if ed and sd and ed < sd:
raise ValueError("失效日期必须晚于生效日期")
if "suite" in data:
schedules[i]["suite"] = data["suite"]
......@@ -210,12 +364,26 @@ def update_run_status(schedule_id: str, status: str, report_id: str, run_at: str
# ============================================================
def init_scheduler(app) -> None:
"""初始化 APScheduler,在 Flask app 创建后调用。"""
"""初始化 APScheduler,在 Flask app 创建后调用。
Werkzeug reloader 防护:
Flask debug 模式下,reloader 会 fork 子进程。父进程(监控进程)中
WERKZEUG_RUN_MAIN 未设置,子进程(实际服务进程)中为 'true'。
我们只在子进程中初始化 APScheduler,避免在即将被丢弃的父进程中启动。
非 debug 模式下没有 reloader,此判断不影响。
"""
global _scheduler
with _scheduler_lock:
if _scheduler is not None:
return
import os
is_reloader_parent = os.environ.get('WERKZEUG_RUN_MAIN') != 'true'
is_debug = os.environ.get('FLASK_DEBUG', '1') == '1'
if is_debug and is_reloader_parent:
logger.info("APScheduler 跳过 reloader 父进程初始化(将在子进程中初始化)")
return
try:
from apscheduler.schedulers.background import BackgroundScheduler
_scheduler = BackgroundScheduler()
......@@ -251,6 +419,20 @@ def _add_job(sched: dict) -> None:
job_id = f"schedule_{sched['id']}"
try:
trigger = CronTrigger.from_crontab(sched["cron"])
# 支持起止日期(APScheduler 使用 UTC 时区,需要统一)
start_date = sched.get("start_date")
end_date = sched.get("end_date")
if start_date:
# 转为 UTC 时区的 datetime
from datetime import timezone
trigger.start_date = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
if end_date:
# end_date 当天也要执行,设为当天 23:59:59 UTC
from datetime import timezone
trigger.end_date = datetime.strptime(end_date, "%Y-%m-%d").replace(
hour=23, minute=59, second=59, tzinfo=timezone.utc
)
_scheduler.add_job(
func=_execute_scheduled_job,
trigger=trigger,
......@@ -285,6 +467,9 @@ def _execute_scheduled_job(schedule_id: str) -> None:
logger.warning("定时任务不存在或已禁用: %s", schedule_id)
return
# 标记执行中
_update_current_status(schedule_id, "running")
try:
from . import runner_service
result = runner_service.run_inspection_sync(
......@@ -296,6 +481,7 @@ def _execute_scheduled_job(schedule_id: str) -> None:
report_id=result.get("report_id", ""),
run_at=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
)
_update_current_status(schedule_id, "success" if result.get("success") else "failed")
logger.info("定时任务执行完成: %s, report_id=%s", schedule_id, result.get("report_id"))
except Exception as e:
logger.error("定时任务执行异常: %s - %s", schedule_id, e)
......@@ -305,6 +491,7 @@ def _execute_scheduled_job(schedule_id: str) -> None:
report_id="",
run_at=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
)
_update_current_status(schedule_id, "failed")
def reload_job(schedule_id: str) -> None:
......@@ -327,3 +514,14 @@ def get_scheduler_status() -> dict:
"running": _scheduler.running,
"job_count": len(_scheduler.get_jobs()),
}
def _update_current_status(schedule_id: str, status: str) -> None:
"""更新定时任务当前执行状态(running/success/failed)。"""
schedules = _load_schedules()
for i, s in enumerate(schedules):
if s.get("id") == schedule_id:
schedules[i]["current_status"] = status
_save_schedules(schedules)
return
raise ValueError("定时任务不存在")
......@@ -5,12 +5,13 @@
<style>
/* 表格 */
.table { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,.06); border: 1px solid var(--gray-200); overflow: hidden; }
.table-head, .table-row { display: grid; grid-template-columns: 2fr 1.2fr 1fr 1fr 1fr 1.2fr 1.5fr; gap: 12px; padding: 12px 18px; align-items: center; }
.table-head, .table-row { display: grid; grid-template-columns: 2fr 1.2fr 1fr 1.5fr 1fr 1.2fr 1.5fr; gap: 12px; padding: 12px 18px; align-items: center; }
.table-head { background: var(--gray-50); font-size: 13px; color: var(--gray-500); font-weight: 600; border-bottom: 1px solid var(--gray-200); }
.table-row { border-bottom: 1px solid var(--gray-100); font-size: 14px; }
.table-row:last-child { border-bottom: none; }
.name { font-weight: 600; color: var(--gray-900); }
.cron { font-family: monospace; font-size: 13px; color: var(--gray-700); }
.schedule-desc { color: var(--gray-700); font-size: 13px; }
.schedule-desc .date-range { color: var(--gray-500); font-size: 12px; }
.status-badge { font-size: 12px; padding: 3px 10px; border-radius: 6px; font-weight: 600; }
.badge-enabled { background: #dcfce7; color: #15803d; }
.badge-disabled { background: var(--gray-100); color: var(--gray-500); }
......@@ -23,16 +24,38 @@
/* 弹窗 */
.modal-mask { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 200; align-items: center; justify-content: center; padding: 20px; }
.modal-mask.show { display: flex; }
.modal { background: #fff; border-radius: 14px; padding: 28px; width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto; }
.modal { background: #fff; border-radius: 14px; padding: 28px; width: 100%; max-width: 520px; max-height: 90vh; overflow-y: auto; }
.modal h3 { font-size: 18px; color: var(--gray-900); margin-bottom: 18px; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 13px; color: var(--gray-700); margin-bottom: 5px; font-weight: 600; }
.form-group input, .form-group select { width: 100%; padding: 9px 12px; border: 1px solid var(--gray-200); border-radius: 8px; font-size: 14px; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary); }
.form-hint { font-size: 12px; color: var(--gray-500); margin-top: 4px; }
.cron-presets { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.cron-preset { background: var(--gray-100); border: 1px solid var(--gray-200); padding: 5px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; color: var(--gray-700); }
.cron-preset:hover { background: var(--gray-200); }
/* 重复周期单选组 */
.repeat-group { display: flex; gap: 8px; }
.repeat-btn { flex: 1; padding: 10px 0; text-align: center; border: 2px solid var(--gray-200); border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; color: var(--gray-600); background: #fff; transition: all .15s; }
.repeat-btn:hover { border-color: var(--primary); color: var(--primary); }
.repeat-btn.active { border-color: var(--primary); background: var(--primary); color: #fff; }
/* 时间选择器 */
.time-picker { display: flex; align-items: center; gap: 6px; }
.time-picker select { width: 80px; padding: 9px 8px; border: 1px solid var(--gray-200); border-radius: 8px; font-size: 14px; text-align: center; }
.time-picker select:focus { outline: none; border-color: var(--primary); }
.time-sep { font-size: 18px; font-weight: 700; color: var(--gray-400); }
/* 星期多选按钮 */
.weekday-group { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.weekday-btn { width: 44px; height: 36px; display: flex; align-items: center; justify-content: center; border: 2px solid var(--gray-200); border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--gray-600); background: #fff; transition: all .15s; }
.weekday-btn:hover { border-color: var(--primary); color: var(--primary); }
.weekday-btn.active { border-color: var(--primary); background: var(--primary); color: #fff; }
/* 日期行 */
.date-row { display: flex; gap: 12px; }
.date-row .form-group { flex: 1; margin-bottom: 0; }
.date-section { margin-bottom: 14px; }
.date-section > label { display: block; font-size: 13px; color: var(--gray-700); margin-bottom: 5px; font-weight: 600; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
.btn-cancel { background: var(--gray-100); color: var(--gray-700); border: none; padding: 9px 18px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; }
.btn-save { background: var(--primary); color: #fff; border: none; padding: 9px 18px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; }
......@@ -45,6 +68,11 @@
@media screen and (max-width: 768px) {
.table-head { display: none; }
.table-row { grid-template-columns: 1fr 1fr; gap: 6px; padding: 14px; }
.modal { padding: 20px; max-width: 100%; }
.repeat-group { flex-direction: column; }
.date-row { flex-direction: column; gap: 0; }
.date-row .form-group { margin-bottom: 14px; }
}
}
</style>
{% endblock %}
......@@ -63,17 +91,17 @@
<span>任务名称</span>
<span>目标</span>
<span>套件</span>
<span>Cron</span>
<span>调度周期</span>
<span>状态</span>
<span>下次执行</span>
<span>操作</span>
</div>
{% for s in schedules %}
<div class="table-row">
<div class="table-row" data-id="{{ s.id }}" data-schedule='{{ s | tojson | safe }}'>
<span class="name">{{ s.name }}</span>
<span>{{ s.target_name or s.target_id }}</span>
<span>{{ '快速' if s.suite == 'quick' else '全量' }}</span>
<span class="cron">{{ s.cron }}</span>
<span class="schedule-desc">{{ s.description or s.cron }}</span>
<span><span class="status-badge {{ 'badge-enabled' if s.enabled else 'badge-disabled' }}">{{ '启用' if s.enabled else '禁用' }}</span></span>
<span>{{ s.next_run_at or '-' }}</span>
<div class="actions">
......@@ -96,10 +124,12 @@
<div class="modal-mask" id="modal">
<div class="modal">
<h3 id="modal-title">新增定时任务</h3>
<div class="form-group">
<label>任务名称</label>
<input type="text" id="f-name" placeholder="如:每日全量巡检">
</div>
<div class="form-group">
<label>监测目标</label>
<select id="f-target">
......@@ -108,6 +138,7 @@
{% endfor %}
</select>
</div>
<div class="form-group">
<label>巡检套件</label>
<select id="f-suite">
......@@ -115,17 +146,52 @@
<option value="full">全量巡检</option>
</select>
</div>
<div class="form-group">
<label>重复周期</label>
<div class="repeat-group">
<div class="repeat-btn active" data-mode="daily" onclick="setRepeatMode('daily')">每天</div>
<div class="repeat-btn" data-mode="weekday" onclick="setRepeatMode('weekday')">工作日</div>
<div class="repeat-btn" data-mode="weekly" onclick="setRepeatMode('weekly')">每周</div>
</div>
</div>
<div class="form-group">
<label>Cron 表达式</label>
<input type="text" id="f-cron" placeholder="0 8 * * *">
<div class="form-hint">格式:分 时 日 月 周(如 0 8 * * * 表示每天 8:00)</div>
<div class="cron-presets">
<span class="cron-preset" onclick="setCron('0 8 * * *')">每天 08:00</span>
<span class="cron-preset" onclick="setCron('0 */6 * * *')">每 6 小时</span>
<span class="cron-preset" onclick="setCron('0 8 * * 1')">每周一 08:00</span>
<span class="cron-preset" onclick="setCron('0 12,18 * * *')">每天 12:00、18:00</span>
<label>执行时间</label>
<div class="time-picker">
<select id="f-hour"></select>
<span class="time-sep">:</span>
<select id="f-minute"></select>
</div>
</div>
<div class="form-group" id="weekday-section" style="display:none;">
<label>星期几</label>
<div class="weekday-group">
<div class="weekday-btn" data-day="1" onclick="toggleWeekday(this)"></div>
<div class="weekday-btn" data-day="2" onclick="toggleWeekday(this)"></div>
<div class="weekday-btn" data-day="3" onclick="toggleWeekday(this)"></div>
<div class="weekday-btn" data-day="4" onclick="toggleWeekday(this)"></div>
<div class="weekday-btn" data-day="5" onclick="toggleWeekday(this)"></div>
<div class="weekday-btn" data-day="6" onclick="toggleWeekday(this)"></div>
<div class="weekday-btn" data-day="7" onclick="toggleWeekday(this)"></div>
</div>
<div class="form-hint" id="weekday-hint" style="display:none; color: #dc2626;">请选择至少一个星期</div>
</div>
<div class="date-section">
<label>有效期(可选)</label>
<div class="date-row">
<div class="form-group">
<input type="date" id="f-start-date" title="生效日期">
</div>
<div class="form-group">
<input type="date" id="f-end-date" title="失效日期">
</div>
</div>
<div class="form-hint">不设置则永久有效</div>
</div>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeModal()">取消</button>
<button class="btn-save" id="btn-save" onclick="saveSchedule()">保存</button>
......@@ -136,8 +202,56 @@
{% block extra_js %}
<script>
const TARGETS = {{ targets | tojson | safe }};
const SCHEDULES_DATA = {{ schedules | tojson | safe }};
let editingId = null;
let currentMode = 'daily';
let selectedWeekdays = new Set();
// 初始化时间选择器
(function initTimePicker() {
const hourSel = document.getElementById('f-hour');
const minSel = document.getElementById('f-minute');
for (let h = 0; h < 24; h++) {
const opt = document.createElement('option');
opt.value = h;
opt.textContent = String(h).padStart(2, '0');
hourSel.appendChild(opt);
}
// 分钟:整刻度 + 每5分钟
for (let m = 0; m < 60; m += 5) {
const opt = document.createElement('option');
opt.value = m;
opt.textContent = String(m).padStart(2, '0');
minSel.appendChild(opt);
}
// 默认 08:00
hourSel.value = 8;
minSel.value = 0;
})();
function setRepeatMode(mode) {
currentMode = mode;
document.querySelectorAll('.repeat-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
// 星期区域显隐
document.getElementById('weekday-section').style.display = mode === 'weekly' ? '' : 'none';
// 清除星期校验提示
document.getElementById('weekday-hint').style.display = 'none';
}
function toggleWeekday(el) {
const day = parseInt(el.dataset.day);
if (selectedWeekdays.has(day)) {
selectedWeekdays.delete(day);
el.classList.remove('active');
} else {
selectedWeekdays.add(day);
el.classList.add('active');
}
// 清除提示
document.getElementById('weekday-hint').style.display = 'none';
}
function openCreate() {
editingId = null;
......@@ -145,17 +259,56 @@ function openCreate() {
document.getElementById('f-name').value = '';
document.getElementById('f-target').selectedIndex = 0;
document.getElementById('f-suite').value = 'quick';
document.getElementById('f-cron').value = '0 8 * * *';
document.getElementById('f-hour').value = 8;
document.getElementById('f-minute').value = 0;
document.getElementById('f-start-date').value = '';
document.getElementById('f-end-date').value = '';
setRepeatMode('daily');
selectedWeekdays.clear();
document.querySelectorAll('.weekday-btn').forEach(b => b.classList.remove('active'));
document.getElementById('modal').classList.add('show');
}
function openEdit(id) {
editingId = id;
document.getElementById('modal-title').textContent = '编辑定时任务';
// TODO: 填充现有数据
const row = event.target.closest('.table-row');
const name = row.querySelector('.name').textContent;
document.getElementById('f-name').value = name;
// 从行数据中获取完整 schedule 对象
const row = document.querySelector(`.table-row[data-id="${id}"]`);
const sched = JSON.parse(row.dataset.schedule);
document.getElementById('f-name').value = sched.name || '';
// 设置目标
const targetSel = document.getElementById('f-target');
for (let i = 0; i < targetSel.options.length; i++) {
if (targetSel.options[i].value === sched.target_id) {
targetSel.selectedIndex = i;
break;
}
}
document.getElementById('f-suite').value = sched.suite || 'quick';
// 调度字段回填
const mode = sched.repeat_mode || 'daily';
setRepeatMode(mode);
document.getElementById('f-hour').value = sched.hour != null ? sched.hour : 8;
document.getElementById('f-minute').value = sched.minute != null ? sched.minute : 0;
// 星期回填
selectedWeekdays.clear();
document.querySelectorAll('.weekday-btn').forEach(b => b.classList.remove('active'));
if (sched.weekdays && sched.weekdays.length) {
sched.weekdays.forEach(d => {
selectedWeekdays.add(d);
const btn = document.querySelector(`.weekday-btn[data-day="${d}"]`);
if (btn) btn.classList.add('active');
});
}
// 日期回填
document.getElementById('f-start-date').value = sched.start_date || '';
document.getElementById('f-end-date').value = sched.end_date || '';
document.getElementById('modal').classList.add('show');
}
......@@ -163,27 +316,41 @@ function closeModal() {
document.getElementById('modal').classList.remove('show');
}
function setCron(expr) {
document.getElementById('f-cron').value = expr;
}
async function saveSchedule() {
const data = {
name: document.getElementById('f-name').value.trim(),
target_id: document.getElementById('f-target').value,
suite: document.getElementById('f-suite').value,
cron: document.getElementById('f-cron').value.trim(),
};
const name = document.getElementById('f-name').value.trim();
const target_id = document.getElementById('f-target').value;
const suite = document.getElementById('f-suite').value;
const hour = parseInt(document.getElementById('f-hour').value);
const minute = parseInt(document.getElementById('f-minute').value);
const start_date = document.getElementById('f-start-date').value || null;
const end_date = document.getElementById('f-end-date').value || null;
if (!name) { alert('请输入任务名称'); return; }
if (!data.name) {
alert('请输入任务名称');
// 星期校验
if (currentMode === 'weekly' && selectedWeekdays.size === 0) {
document.getElementById('weekday-hint').style.display = '';
return;
}
if (!data.cron) {
alert('请输入 Cron 表达式');
// 日期校验
if (end_date && start_date && end_date < start_date) {
alert('失效日期必须晚于生效日期');
return;
}
const data = {
name,
target_id,
suite,
repeat_mode: currentMode,
hour,
minute,
weekdays: currentMode === 'weekly' ? Array.from(selectedWeekdays) : [],
start_date,
end_date,
};
const url = editingId
? '/api/service-monitor/schedules/' + editingId
: '/api/service-monitor/schedules';
......@@ -199,6 +366,7 @@ async function saveSchedule() {
const d = await r.json();
if (d.success) {
closeModal();
alert(editingId ? '保存成功' : '创建成功');
location.reload();
} else {
alert(d.error?.message || '保存失败');
......@@ -218,7 +386,7 @@ async function toggleSchedule(id) {
if (d.success) {
location.reload();
} else {
alert('操作失败');
alert(d.error?.message || '操作失败');
}
} catch (e) {
alert('请求失败');
......@@ -234,9 +402,10 @@ async function deleteSchedule(id) {
});
const d = await r.json();
if (d.success) {
alert('删除成功');
location.reload();
} else {
alert('删除失败');
alert(d.error?.message || '删除失败');
}
} catch (e) {
alert('请求失败');
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论