v1.0.1: 弹窗防误关确认/布局修复/定时任务首次执行时间/日间夜间双主题/总体统计区

This commit is contained in:
2026-08-11 13:02:34 +08:00
parent dede5003a4
commit 29176fc66b
5 changed files with 218 additions and 40 deletions
+69 -17
View File
@@ -38,6 +38,33 @@ def now_str():
return store.now_str()
def _parse_first_run(s):
"""解析表单提交的首次执行时间 (datetime-local 格式), 非法返回 None"""
if not s:
return None
try:
return datetime.strptime(str(s), "%Y-%m-%dT%H:%M")
except Exception:
return None
def _schedule_next_run(sch):
"""根据调度配置 + 首次执行时间计算 next_run (str)"""
first_run = _parse_first_run(sch.get("first_run"))
base = first_run if (first_run and first_run > datetime.now()) else None
if sch.get("type") == "cron":
expr = sch.get("cron") or "0 * * * *"
nn = cron_next(expr, base or datetime.now())
if not nn:
raise ValueError("cron 表达式在未来一年内无匹配时间")
return nn.strftime("%Y-%m-%d %H:%M:%S")
sch.setdefault("interval_unit", "hours")
sch.setdefault("interval_value", 24)
if base:
return base.strftime("%Y-%m-%d %H:%M:%S")
return (datetime.now() + interval_delta(sch)).strftime("%Y-%m-%d %H:%M:%S")
def resolve_out_dir(task):
cfg = task.get("config", {}) or {}
if cfg.get("out_dir", "").strip():
@@ -161,16 +188,7 @@ def api_create_task():
sch.setdefault("enabled", True)
sch.setdefault("type", "interval")
try:
if sch.get("type") == "cron":
expr = sch.get("cron") or "0 * * * *"
nn = cron_next(expr)
if not nn:
raise ValueError("cron 表达式在未来一年内无匹配时间")
sch["next_run"] = nn.strftime("%Y-%m-%d %H:%M:%S")
else:
sch.setdefault("interval_unit", "hours")
sch.setdefault("interval_value", 24)
sch["next_run"] = (datetime.now() + interval_delta(sch)).strftime("%Y-%m-%d %H:%M:%S")
sch["next_run"] = _schedule_next_run(sch)
except ValueError as e:
return jsonify({"error": f"调度配置错误: {e}"}), 400
sch.setdefault("last_run", "")
@@ -221,13 +239,7 @@ def api_update_task(tid):
if "schedule" in body and task.get("mode") == "scheduled":
sch = {**task.get("schedule", {}), **body["schedule"]}
try:
if sch.get("type") == "cron":
nn = cron_next(sch.get("cron") or "0 * * * *")
if not nn:
raise ValueError("cron 表达式在未来一年内无匹配时间")
sch["next_run"] = nn.strftime("%Y-%m-%d %H:%M:%S")
else:
sch["next_run"] = (datetime.now() + interval_delta(sch)).strftime("%Y-%m-%d %H:%M:%S")
sch["next_run"] = _schedule_next_run(sch)
except ValueError as e:
return jsonify({"error": f"调度配置错误: {e}"}), 400
task["schedule"] = sch
@@ -285,6 +297,46 @@ def api_resume(tid):
return jsonify({"error": "任务未在运行"}), 409
# ---------------- API: 统计 ----------------
@app.route("/api/stats")
def api_stats():
tasks = store.load_tasks()
total_runs = ok = fail = imgs = 0
for t in tasks:
for r in store.get_runs(t["id"]):
total_runs += 1
st = r.get("stats") or {}
ok += st.get("ok", 0)
fail += st.get("fail", 0)
imgs += st.get("images", 0)
with JOBS_LOCK:
running = sum(1 for j in JOBS.values() if j.is_running())
# 统计各任务输出目录的磁盘占用
size = 0
seen = set()
for t in tasks:
d = os.path.realpath(resolve_out_dir(t))
if d in seen or not os.path.isdir(d):
continue
seen.add(d)
for root, _dirs, files in os.walk(d):
for f in files:
try:
size += os.path.getsize(os.path.join(root, f))
except OSError:
pass
return jsonify({
"tasks": len(tasks),
"running": running,
"runs": total_runs,
"ok": ok,
"fail": fail,
"images": imgs,
"disk_mb": round(size / 1048576, 1),
})
# ---------------- API: 运行记录与文件 ----------------
@app.route("/api/runs/<rid>")