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 -5
View File
@@ -11,6 +11,7 @@ const state = {
detail: { task: null, runId: null, logOffset: 0, timer: null },
logTimer: null,
};
let formDirty = false; // 新建/编辑表单是否有未保存修改
/* ---------------- 工具 ---------------- */
function toast(msg, isErr) {
@@ -48,6 +49,21 @@ async function loadTasks() {
} catch (e) {
toast("加载任务失败: " + e.message, true);
}
loadStats();
}
async function loadStats() {
try {
const s = await api("/api/stats");
$("stTasks").textContent = s.tasks;
$("stRunning").textContent = s.running;
$("stRuns").textContent = s.runs;
$("stOk").textContent = s.ok;
$("stFail").textContent = s.fail;
$("stImgs").textContent = s.images;
$("stDisk").textContent = s.disk_mb >= 1024
? (s.disk_mb / 1024).toFixed(1) + " GB" : s.disk_mb + " MB";
} catch (e) { /* 统计失败忽略 */ }
}
function taskStatusBadge(t) {
@@ -156,8 +172,17 @@ function switchMode(mode, lock) {
$("autoBox").classList.toggle("hidden", mode !== "auto");
}
function toLocalInputVal(iso) {
if (!iso) return "";
const d = new Date(String(iso).replace(" ", "T"));
if (isNaN(d.getTime())) return "";
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
}
function openCreate() {
state.editTask = null;
formDirty = false;
$("modalTitle").textContent = "新建爬取任务";
$("taskForm").reset();
$("taskForm").elements["delay_min"].value = 2;
@@ -195,6 +220,7 @@ async function openEdit(tid) {
f.elements["interval_value"].value = t.schedule.interval_value ?? 24;
f.elements["interval_unit"].value = t.schedule.interval_unit || "hours";
f.elements["cron"].value = t.schedule.cron || "";
f.elements["first_run"].value = toLocalInputVal(t.schedule.next_run || "");
}
if (t.mode === "auto" && t.auto) {
f.elements["seed_url"].value = t.auto.seed_url || "";
@@ -209,6 +235,7 @@ async function openEdit(tid) {
$("formHint").textContent = t.running
? "⚠️ 任务运行中:参数修改将热更新(网址/规则改动下次运行生效)"
: "";
formDirty = false;
switchMode(t.mode, true);
showModal("taskModal");
}
@@ -259,6 +286,7 @@ async function submitForm(e) {
interval_unit: f.elements["interval_unit"].value,
interval_value: parseInt(f.elements["interval_value"].value) || 1,
cron: f.elements["cron"].value.trim(),
first_run: f.elements["first_run"].value || "",
};
}
try {
@@ -275,6 +303,7 @@ async function submitForm(e) {
});
toast(`任务「${t.name}」已创建`);
}
formDirty = false;
hideModal("taskModal");
loadTasks();
} catch (err) { toast(err.message, true); }
@@ -457,6 +486,29 @@ function previewFile(tid, path, title) {
function showModal(id) { $(id).classList.remove("hidden"); }
function hideModal(id) { $(id).classList.add("hidden"); }
/* 关闭任务弹窗: 有未保存修改时先确认 */
function safeCloseTaskModal() {
if (formDirty && !confirm("有未保存的修改,确定要放弃吗?")) return false;
formDirty = false;
hideModal("taskModal");
return true;
}
/* ---------------- 主题切换 ---------------- */
function applyTheme(theme) {
document.body.dataset.theme = theme;
$("btnTheme").textContent = theme === "light" ? "🌙" : "☀️";
try { localStorage.setItem("crawler_theme", theme); } catch (e) { /* ignore */ }
}
$("btnTheme").onclick = () => {
applyTheme(document.body.dataset.theme === "light" ? "dark" : "light");
};
try {
applyTheme(localStorage.getItem("crawler_theme") || "dark");
} catch (e) {
applyTheme("dark");
}
/* ---------------- 事件绑定 ---------------- */
$("btnNew").onclick = openCreate;
$("btnNew2").onclick = openCreate;
@@ -468,24 +520,36 @@ document.querySelectorAll("#modeTabs .tab").forEach((b) => {
$("taskForm").onsubmit = submitForm;
$("taskForm").elements["schedule_type"].onchange = syncScheduleUI;
document.querySelectorAll("[data-close]").forEach((b) => b.onclick = () => hideModal("taskModal"));
document.querySelectorAll("[data-close]").forEach((b) => b.onclick = safeCloseTaskModal);
document.querySelectorAll("[data-close-detail]").forEach((b) => b.onclick = () => { stopLogPoll(); hideModal("detailModal"); });
document.querySelectorAll("[data-close-preview]").forEach((b) => b.onclick = () => { $("previewFrame").src = "about:blank"; hideModal("previewModal"); });
/* 表单改动监听 -> 脏标记 */
$("taskForm").addEventListener("input", () => { formDirty = true; });
$("taskForm").addEventListener("change", () => { formDirty = true; });
document.querySelectorAll(".modal-overlay").forEach((ov) => {
ov.addEventListener("mousedown", (e) => {
if (e.target === ov) {
if (ov.id === "detailModal") stopLogPoll();
if (ov.id === "previewModal") $("previewFrame").src = "about:blank";
ov.classList.add("hidden");
if (ov.id === "taskModal") {
safeCloseTaskModal();
} else {
if (ov.id === "detailModal") stopLogPoll();
if (ov.id === "previewModal") $("previewFrame").src = "about:blank";
ov.classList.add("hidden");
}
}
});
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
if (!$("taskModal").classList.contains("hidden")) {
safeCloseTaskModal();
return;
}
stopLogPoll();
$("previewFrame").src = "about:blank";
["taskModal", "detailModal", "previewModal"].forEach((id) => $(id).classList.add("hidden"));
["detailModal", "previewModal"].forEach((id) => $(id).classList.add("hidden"));
}
});
+12
View File
@@ -11,12 +11,22 @@
<div class="logo">🕷️ 通用爬虫系统 <span id="version" class="version"></span></div>
<div class="header-right">
<span class="status-pill" id="statusPill">运行中任务: 0</span>
<button id="btnTheme" class="btn ghost" title="切换日间/夜间主题">☀️</button>
<button id="btnRefresh" class="btn ghost">⟳ 刷新</button>
<button id="btnNew" class="btn primary"> 新建任务</button>
</div>
</header>
<main>
<div class="stats-bar" id="statsBar">
<div class="stat-card"><div class="stat-num" id="stTasks">0</div><div class="stat-label">📋 任务总数</div></div>
<div class="stat-card"><div class="stat-num num-run" id="stRunning">0</div><div class="stat-label">🔄 正在运行</div></div>
<div class="stat-card"><div class="stat-num" id="stRuns">0</div><div class="stat-label">📊 累计运行次数</div></div>
<div class="stat-card"><div class="stat-num num-ok" id="stOk">0</div><div class="stat-label">✅ 成功页面</div></div>
<div class="stat-card"><div class="stat-num num-fail" id="stFail">0</div><div class="stat-label">❌ 失败页面</div></div>
<div class="stat-card"><div class="stat-num" id="stImgs">0</div><div class="stat-label">🖼️ 已爬图片</div></div>
<div class="stat-card"><div class="stat-num" id="stDisk">0</div><div class="stat-label">💾 磁盘占用</div></div>
</div>
<div id="taskList" class="task-grid"></div>
<div id="emptyState" class="empty hidden">
<div class="empty-icon">🕸️</div>
@@ -63,6 +73,8 @@
</div>
<div id="scheduleBox" class="hidden box">
<div class="field"><label>首次执行时间(可留空;留空 = 尽快执行;已过的时间将立即执行)</label>
<input name="first_run" type="datetime-local"></div>
<div class="row2">
<div class="field"><label>调度类型</label>
<select name="schedule_type">
+63 -16
View File
@@ -10,12 +10,36 @@
--red: #ff5d6c;
--yellow: #ffc857;
--purple: #a78bfa;
--overlay: rgba(5, 8, 15, .72);
--box-bg: #141b2b;
--log-bg: #0b0f1a;
--thumb: #2a3550;
--preview-bg: #fff;
}
body[data-theme="light"] {
--bg: #f2f5fa;
--panel: #ffffff;
--panel2: #eef1f7;
--border: #d8e0ee;
--text: #1b2436;
--muted: #5d6b86;
--accent: #2f6bff;
--green: #0e9f6e;
--red: #e04a5a;
--yellow: #b8860b;
--purple: #7c5cf0;
--overlay: rgba(15, 23, 42, .35);
--box-bg: #f7f9fd;
--log-bg: #f4f6fa;
--thumb: #cdd7e6;
--preview-bg: #fff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg); color: var(--text);
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
font-size: 14px; min-height: 100vh;
transition: background .25s, color .25s;
}
header {
display: flex; align-items: center; justify-content: space-between;
@@ -38,7 +62,7 @@ main { padding: 20px 24px; max-width: 1500px; margin: 0 auto; }
padding: 7px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
transition: .15s; white-space: nowrap;
}
.btn:hover { border-color: var(--accent); color: #fff; }
.btn:hover { border-color: var(--accent); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { filter: brightness(1.1); }
.btn.ghost { background: transparent; }
@@ -46,6 +70,21 @@ main { padding: 20px 24px; max-width: 1500px; margin: 0 auto; }
.btn.sm { padding: 3px 8px; font-size: 12px; border-radius: 6px; }
.btn:disabled { opacity: .4; cursor: not-allowed; }
/* ---------- 统计区 ---------- */
.stats-bar {
display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px; margin-bottom: 20px;
}
.stat-card {
background: var(--panel); border: 1px solid var(--border);
border-radius: 12px; padding: 12px 16px;
}
.stat-num { font-size: 24px; font-weight: 700; line-height: 1.2; }
.stat-num.num-ok { color: var(--green); }
.stat-num.num-fail { color: var(--red); }
.stat-num.num-run { color: var(--accent); }
.stat-label { font-size: 12px; color: var(--muted); margin-top: 3px; }
/* ---------- task grid ---------- */
.task-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; }
.card {
@@ -53,7 +92,7 @@ main { padding: 20px 24px; max-width: 1500px; margin: 0 auto; }
border-radius: 12px; padding: 16px; display: flex; flex-direction: column; gap: 10px;
transition: .15s;
}
.card:hover { border-color: #3a4a75; transform: translateY(-1px); }
.card:hover { border-color: var(--accent); transform: translateY(-1px); }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.card-name { font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
@@ -61,7 +100,7 @@ main { padding: 20px 24px; max-width: 1500px; margin: 0 auto; }
font-size: 11px; padding: 2px 8px; border-radius: 10px;
background: var(--panel2); border: 1px solid var(--border); color: var(--muted);
}
.badge.batch { color: #7cc4ff; border-color: #7cc4ff55; }
.badge.batch { color: var(--accent); border-color: var(--accent); }
.badge.scheduled { color: var(--purple); border-color: var(--purple); }
.badge.auto { color: var(--yellow); border-color: var(--yellow); }
.badge.running { color: var(--green); border-color: var(--green); animation: pulse 1.6s infinite; }
@@ -88,7 +127,7 @@ main { padding: 20px 24px; max-width: 1500px; margin: 0 auto; }
/* ---------- modal ---------- */
.modal-overlay {
position: fixed; inset: 0; background: rgba(5, 8, 15, .7);
position: fixed; inset: 0; background: var(--overlay);
display: flex; align-items: center; justify-content: center; z-index: 100;
backdrop-filter: blur(3px);
}
@@ -101,24 +140,29 @@ main { padding: 20px 24px; max-width: 1500px; margin: 0 auto; }
.modal.tall { width: 1000px; height: 88vh; }
.modal-head {
display: flex; justify-content: space-between; align-items: center;
padding: 14px 18px; border-bottom: 1px solid var(--border); font-size: 15px; font-weight: 600;
padding: 14px 18px; border-bottom: 1px solid var(--border);
font-size: 15px; font-weight: 600; flex-shrink: 0;
}
.modal-foot {
display: flex; justify-content: flex-end; gap: 10px; align-items: center;
padding: 12px 18px; border-top: 1px solid var(--border);
padding: 12px 18px; border-top: 1px solid var(--border); flex-shrink: 0;
}
.hint { color: var(--yellow); font-size: 12px; margin-right: auto; }
.tabs { display: flex; gap: 6px; padding: 12px 18px 0; }
.tabs { display: flex; gap: 6px; padding: 12px 18px 0; flex-shrink: 0; }
#taskForm { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.tab {
padding: 8px 16px; background: transparent; border: 1px solid var(--border);
border-bottom: none; border-radius: 10px 10px 0 0; color: var(--muted);
cursor: pointer; font-size: 13px;
}
.tab.active { background: var(--panel2); color: #fff; border-color: var(--accent); }
.tab.active { background: var(--panel2); color: var(--text); border-color: var(--accent); }
.tab:disabled { opacity: .4; cursor: not-allowed; }
.form-body { padding: 16px 18px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
.form-body {
padding: 16px 18px; overflow-y: auto; flex: 1 1 auto; min-height: 0;
display: flex; flex-direction: column; gap: 12px;
}
.field { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 0; }
.field label { font-size: 12px; color: var(--muted); }
.field.check { flex-direction: row; align-items: center; gap: 8px; padding-top: 22px; }
@@ -130,8 +174,12 @@ input, select, textarea {
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
textarea { resize: vertical; }
input[type="checkbox"] { width: auto; accent-color: var(--accent); }
input[type="datetime-local"] { color-scheme: light dark; }
.row2 { display: flex; gap: 12px; }
.box { border: 1px dashed var(--border); border-radius: 10px; padding: 12px; display: flex; flex-direction: column; gap: 10px; background: #141b2b; }
.box {
border: 1px dashed var(--border); border-radius: 10px; padding: 12px;
display: flex; flex-direction: column; gap: 10px; background: var(--box-bg);
}
.hidden { display: none !important; }
/* ---------- detail ---------- */
@@ -144,7 +192,7 @@ input[type="checkbox"] { width: auto; accent-color: var(--accent); }
padding: 5px 12px; border-radius: 8px; border: 1px solid var(--border);
background: var(--panel2); cursor: pointer; font-size: 12px; color: var(--muted);
}
.run-chip.active { border-color: var(--accent); color: #fff; }
.run-chip.active { border-color: var(--accent); color: var(--text); }
.run-panel { border: 1px solid var(--border); border-radius: 10px; padding: 14px; display: flex; flex-direction: column; gap: 10px; background: var(--panel2); }
.run-stats { display: flex; gap: 16px; font-size: 12px; flex-wrap: wrap; }
.table-wrap { overflow: auto; max-height: 320px; border: 1px solid var(--border); border-radius: 8px; }
@@ -155,27 +203,26 @@ td.url-cell { max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
td.title-cell { max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
.t-ok { color: var(--green); } .t-fail { color: var(--red); }
.file-link { color: var(--accent); cursor: pointer; text-decoration: underline; }
.file-link:hover { color: #9ec0ff; }
.thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
.thumbs img { width: 96px; height: 72px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); cursor: pointer; }
.logs {
background: #0b0f1a; border: 1px solid var(--border); border-radius: 8px;
background: var(--log-bg); border: 1px solid var(--border); border-radius: 8px;
padding: 10px; font-family: ui-monospace, Consolas, monospace; font-size: 12px;
max-height: 220px; overflow-y: auto; line-height: 1.7;
}
.logs .info { color: var(--text); } .logs .warn { color: var(--yellow); } .logs .error { color: var(--red); }
/* ---------- preview ---------- */
#previewFrame { flex: 1; border: none; background: #fff; border-radius: 0 0 14px 14px; }
#previewFrame { flex: 1; border: none; background: var(--preview-bg); border-radius: 0 0 14px 14px; }
/* ---------- misc ---------- */
.toast {
position: fixed; top: 70px; left: 50%; transform: translateX(-50%);
background: var(--panel2); border: 1px solid var(--accent); color: #fff;
background: var(--panel2); border: 1px solid var(--accent); color: var(--text);
padding: 10px 20px; border-radius: 10px; z-index: 300; font-size: 13px;
box-shadow: 0 8px 30px rgba(0,0,0,.4); animation: fadein .2s;
}
.toast.error { border-color: var(--red); }
@keyframes fadein { from { opacity: 0; transform: translate(-50%, -8px); } }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #2a3550; border-radius: 4px; }
::-webkit-scrollbar-thumb { background: var(--thumb); border-radius: 4px; }