diff --git a/README.md b/README.md index f6da37f..da36b55 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ ### 1. 前端管理界面 - **总体统计区**:任务总数 / 运行中 / 累计运行次数 / 成功失败页面 / 图片数 / 磁盘占用 +- **🔍 基本搜索**:顶部搜索框,按网址 / 标题 / 任务名即时查找已爬内容,可直接预览文件、查看元数据、跳转任务详情 - **🗑️ 回收站**:删除的任务统一进回收站(可恢复);只有手动在回收站中才能彻底删除或清空,防止误删 - **日间/夜间双主题**:右上角按钮一键切换,自动记忆选择 - 任务卡片总览:状态、进度、统计、下次调度时间一目了然 @@ -78,6 +79,7 @@ | POST | `/api/tasks//resume` | 恢复 | | POST | `/api/tasks//stop` | 终止 | | GET | `/api/trash` | 回收站列表 | +| GET | `/api/search?q=` | 基本搜索(网址/标题/任务名子串匹配,轻量) | | POST | `/api/trash//restore` | 从回收站恢复任务 | | DELETE | `/api/trash/` | 彻底删除单个(默认输出目录一并清理) | | DELETE | `/api/trash` | 清空回收站 | diff --git a/app.py b/app.py index d1882f9..3b17471 100644 --- a/app.py +++ b/app.py @@ -443,6 +443,69 @@ def api_task_probe(tid): return jsonify(result) +# ---------------- API: 搜索 ---------------- + +@app.route("/api/search") +def api_search(): + """基本搜索: 对任务名/网址/标题做子串匹配 (仅遍历本地内存数据, 轻量)""" + q = (request.args.get("q") or "").strip() + if not q: + return jsonify({"q": q, "count": 0, "results": []}) + ql = q.lower() + results = [] + seen = set() + for task in store.load_tasks(): + if task.get("deleted_at"): + continue # 回收站任务不参与搜索 + auto = task.get("auto") or {} + task_hit = ( + ql in (task.get("name") or "").lower() + or any(ql in u.lower() for u in task.get("urls", [])) + or ql in ((auto.get("seed_url") or "").lower()) + ) + for run in store.get_runs(task["id"]): + for r in run.get("results", []): + url = r.get("url") or "" + title = r.get("title") or "" + if ql not in url.lower() and ql not in title.lower(): + continue + key = (task["id"], run.get("id"), url) + if key in seen: + continue + seen.add(key) + results.append({ + "type": "page", + "task_id": task["id"], + "task_name": task.get("name", ""), + "mode": task.get("mode", ""), + "run_id": run.get("id", ""), + "run_status": run.get("status", ""), + "url": url, + "title": title, + "status": r.get("status", ""), + "crawl_time": r.get("crawl_time", ""), + "html_file": r.get("html_file", ""), + "txt_file": r.get("txt_file", ""), + "meta_file": r.get("meta_file", ""), + "images": len(r.get("images", []) or []), + }) + if len(results) >= 100: + break + if len(results) >= 100: + break + if task_hit and len(results) < 100: + results.append({ + "type": "task", + "task_id": task["id"], + "task_name": task.get("name", ""), + "mode": task.get("mode", ""), + "urls_count": len(task.get("urls", [])), + "created_at": task.get("created_at", ""), + "seed_url": auto.get("seed_url", ""), + }) + return jsonify({"q": q, "count": len(results), "results": results}) + + # ---------------- API: 运行记录与文件 ---------------- @app.route("/api/runs/") diff --git a/static/app.js b/static/app.js index 31a886e..4b9e149 100644 --- a/static/app.js +++ b/static/app.js @@ -161,6 +161,76 @@ async function delTask(tid) { } catch (e) { toast(e.message, true); } } +/* ---------------- 搜索 ---------------- */ +async function doSearch() { + const q = $("searchInput").value.trim(); + if (!q) { toast("请输入搜索关键词", true); return; } + try { + const d = await api(`/api/search?q=${encodeURIComponent(q)}`); + renderSearch(d); + showModal("searchModal"); + } catch (e) { toast(e.message, true); } +} + +function renderSearch(d) { + const body = $("searchBody"); + if (!d.results.length) { + body.innerHTML = `
🔍

没有找到与「${esc(d.q)}」相关的内容

`; + return; + } + const items = d.results.map((r) => { + const badge = `${MODE_LABEL[r.mode] || esc(r.mode)}`; + if (r.type === "task") { + return ` +
+
+ ${badge} + 📋 ${esc(r.task_name)} + 任务 +
+
${r.seed_url ? "起始: " + esc(r.seed_url) : "网址 " + (r.urls_count || 0) + " 个"} · 创建于 ${esc(r.created_at || "")}
+
+ +
+
`; + } + const stCls = r.status === "OK" ? "t-ok" : "t-fail"; + const html = r.html_file ? `HTML` : ""; + const txt = r.txt_file ? `TXT` : ""; + const meta = r.meta_file ? `📋 元数据` : ""; + return ` +
+
+ ${badge} + ${esc(r.title) || "(无标题)"} + ${r.status} + ${esc(r.run_status)} + ${esc(r.crawl_time || "")} +
+
🌐 ${esc(r.url)}${r.images ? ` · 🖼️ ${r.images}` : ""}
+
+ ${html}${txt}${meta} + +
+
`; + }).join(""); + body.innerHTML = `
关键词: ${esc(d.q)}${d.count} 条结果
${items}`; +} + +/* 搜索结果的跳转动作: 先关搜索弹窗再打开目标 */ +function searchOpenDetail(tid) { + hideModal("searchModal"); + openDetail(tid); +} +function searchPreview(tid, path, title) { + hideModal("searchModal"); + previewFile(tid, path, title); +} +function searchMeta(tid, metaFile) { + hideModal("searchModal"); + showMeta(tid, metaFile); +} + /* ---------------- 回收站 ---------------- */ async function openTrash() { try { @@ -700,6 +770,8 @@ $("btnNew").onclick = openCreate; $("btnNew2").onclick = openCreate; $("btnRefresh").onclick = loadTasks; $("btnTrash").onclick = openTrash; +$("btnSearch").onclick = doSearch; +$("searchInput").addEventListener("keydown", (e) => { if (e.key === "Enter") doSearch(); }); $("btnProbe").onclick = () => { const p = collectProbeFromForm(); if (!p.seed_url) { toast("请先填写起始网址", true); return; } @@ -718,6 +790,7 @@ document.querySelectorAll("[data-close-preview]").forEach((b) => b.onclick = () document.querySelectorAll("[data-close-probe]").forEach((b) => b.onclick = () => hideModal("probeModal")); document.querySelectorAll("[data-close-meta]").forEach((b) => b.onclick = () => hideModal("metaModal")); document.querySelectorAll("[data-close-trash]").forEach((b) => b.onclick = () => hideModal("trashModal")); +document.querySelectorAll("[data-close-search]").forEach((b) => b.onclick = () => hideModal("searchModal")); /* 表单改动监听 -> 脏标记 */ $("taskForm").addEventListener("input", () => { formDirty = true; }); @@ -744,7 +817,7 @@ document.addEventListener("keydown", (e) => { } stopLogPoll(); $("previewFrame").src = "about:blank"; - ["detailModal", "probeModal", "metaModal", "trashModal", "previewModal"].forEach((id) => $(id).classList.add("hidden")); + ["detailModal", "probeModal", "metaModal", "trashModal", "searchModal", "previewModal"].forEach((id) => $(id).classList.add("hidden")); } }); diff --git a/static/index.html b/static/index.html index 63a9d54..fb4dfd7 100644 --- a/static/index.html +++ b/static/index.html @@ -10,6 +10,10 @@
+ 运行中任务: 0 @@ -144,6 +148,17 @@
+ + +