v1.0.4: 基本搜索功能 - 按网址/标题/任务名子串匹配查找已爬内容, 支持跳转预览
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
### 1. 前端管理界面
|
||||
- **总体统计区**:任务总数 / 运行中 / 累计运行次数 / 成功失败页面 / 图片数 / 磁盘占用
|
||||
- **🔍 基本搜索**:顶部搜索框,按网址 / 标题 / 任务名即时查找已爬内容,可直接预览文件、查看元数据、跳转任务详情
|
||||
- **🗑️ 回收站**:删除的任务统一进回收站(可恢复);只有手动在回收站中才能彻底删除或清空,防止误删
|
||||
- **日间/夜间双主题**:右上角按钮一键切换,自动记忆选择
|
||||
- 任务卡片总览:状态、进度、统计、下次调度时间一目了然
|
||||
@@ -78,6 +79,7 @@
|
||||
| POST | `/api/tasks/<id>/resume` | 恢复 |
|
||||
| POST | `/api/tasks/<id>/stop` | 终止 |
|
||||
| GET | `/api/trash` | 回收站列表 |
|
||||
| GET | `/api/search?q=` | 基本搜索(网址/标题/任务名子串匹配,轻量) |
|
||||
| POST | `/api/trash/<id>/restore` | 从回收站恢复任务 |
|
||||
| DELETE | `/api/trash/<id>` | 彻底删除单个(默认输出目录一并清理) |
|
||||
| DELETE | `/api/trash` | 清空回收站 |
|
||||
|
||||
@@ -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/<rid>")
|
||||
|
||||
+74
-1
@@ -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 = `<div class="empty" style="padding:40px 0"><div class="empty-icon">🔍</div><p>没有找到与「${esc(d.q)}」相关的内容</p></div>`;
|
||||
return;
|
||||
}
|
||||
const items = d.results.map((r) => {
|
||||
const badge = `<span class="badge ${esc(r.mode)}">${MODE_LABEL[r.mode] || esc(r.mode)}</span>`;
|
||||
if (r.type === "task") {
|
||||
return `
|
||||
<div class="search-item">
|
||||
<div class="row">
|
||||
${badge}
|
||||
<span class="title">📋 ${esc(r.task_name)}</span>
|
||||
<span class="badge">任务</span>
|
||||
</div>
|
||||
<div class="url">${r.seed_url ? "起始: " + esc(r.seed_url) : "网址 " + (r.urls_count || 0) + " 个"} · 创建于 ${esc(r.created_at || "")}</div>
|
||||
<div class="row">
|
||||
<button class="btn sm" onclick="searchOpenDetail('${r.task_id}')">📋 查看任务</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
const stCls = r.status === "OK" ? "t-ok" : "t-fail";
|
||||
const html = r.html_file ? `<span class="file-link" onclick="searchPreview('${r.task_id}','${esc(r.html_file)}','HTML: ${esc(r.url)}')">HTML</span>` : "";
|
||||
const txt = r.txt_file ? `<span class="file-link" onclick="searchPreview('${r.task_id}','${esc(r.txt_file)}','文本: ${esc(r.url)}')">TXT</span>` : "";
|
||||
const meta = r.meta_file ? `<span class="file-link" onclick="searchMeta('${r.task_id}','${esc(r.meta_file)}')">📋 元数据</span>` : "";
|
||||
return `
|
||||
<div class="search-item">
|
||||
<div class="row">
|
||||
${badge}
|
||||
<span class="title" title="${esc(r.title)}">${esc(r.title) || "(无标题)"}</span>
|
||||
<span class="badge ${stCls}">${r.status}</span>
|
||||
<span class="badge">${esc(r.run_status)}</span>
|
||||
<span class="card-line">${esc(r.crawl_time || "")}</span>
|
||||
</div>
|
||||
<div class="url">🌐 ${esc(r.url)}${r.images ? ` · 🖼️ ${r.images}` : ""}</div>
|
||||
<div class="row">
|
||||
${html}${txt}${meta}
|
||||
<button class="btn sm" onclick="searchOpenDetail('${r.task_id}')">📋 任务详情</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
body.innerHTML = `<div class="probe-summary"><span>关键词: <b>${esc(d.q)}</b></span><span>共 <b>${d.count}</b> 条结果</span></div>${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"));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
<header>
|
||||
<div class="logo">🕷️ 通用爬虫系统 <span id="version" class="version"></span></div>
|
||||
<div class="header-right">
|
||||
<div class="search-box">
|
||||
<input id="searchInput" placeholder="搜索网址 / 标题 / 任务名..." maxlength="100">
|
||||
<button id="btnSearch" class="btn sm primary" title="搜索">🔍</button>
|
||||
</div>
|
||||
<span class="status-pill" id="statusPill">运行中任务: 0</span>
|
||||
<button id="btnTheme" class="btn ghost" title="切换日间/夜间主题">☀️</button>
|
||||
<button id="btnTrash" class="btn ghost" title="回收站">🗑️ 回收站 <span id="trashCount" class="trash-count">0</span></button>
|
||||
@@ -144,6 +148,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果弹窗 -->
|
||||
<div id="searchModal" class="modal-overlay hidden">
|
||||
<div class="modal wide">
|
||||
<div class="modal-head">
|
||||
<span>🔍 搜索结果</span>
|
||||
<button class="btn ghost sm" data-close-search>✕</button>
|
||||
</div>
|
||||
<div id="searchBody" class="detail-wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回收站弹窗 -->
|
||||
<div id="trashModal" class="modal-overlay hidden">
|
||||
<div class="modal wide">
|
||||
|
||||
+10
-1
@@ -215,7 +215,16 @@ td.title-cell { max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
|
||||
/* ---------- preview ---------- */
|
||||
#previewFrame { flex: 1; border: none; background: var(--preview-bg); border-radius: 0 0 14px 14px; }
|
||||
|
||||
/* ---------- 回收站 ---------- */
|
||||
/* ---------------- 回收站 ---------------- */
|
||||
.search-box { display: flex; gap: 6px; align-items: center; }
|
||||
.search-box input { width: 250px; }
|
||||
.search-item {
|
||||
border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px;
|
||||
display: flex; flex-direction: column; gap: 5px; background: var(--panel2);
|
||||
}
|
||||
.search-item .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.search-item .title { font-weight: 600; max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.search-item .url { color: var(--muted); word-break: break-all; font-size: 12px; }
|
||||
.trash-count {
|
||||
display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 9px;
|
||||
background: var(--red); color: #fff; font-size: 11px; line-height: 18px;
|
||||
|
||||
Reference in New Issue
Block a user