(function () { "use strict"; async function api(url, body, method) { var res = await fetch(url, { method: method || "POST", headers: { "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); var json = null; try { json = await res.json(); } catch (e) {} if (!res.ok || (json && json.ok === false)) { if (res.status === 401) { location.href = "/login"; return new Promise(function () {}); } throw new Error((json && json.error && json.error.message) || "请求失败(" + res.status + ")"); } return json; } function toast(msg) { var el = document.querySelector(".toast"); if (!el) return; el.textContent = msg; el.classList.add("show"); clearTimeout(el._t); el._t = setTimeout(function () { el.classList.remove("show"); }, 2600); } function refresh() { location.reload(); } function esc(v) { return String(v == null ? "" : v).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } document.querySelectorAll(".admin-nav button[data-section]").forEach(function (btn) { btn.addEventListener("click", function () { document.querySelectorAll(".admin-nav button").forEach(function (b) { b.setAttribute("aria-current", "false"); }); btn.setAttribute("aria-current", "true"); var id = btn.getAttribute("data-section"); document.querySelectorAll(".admin-main > section").forEach(function (s) { s.classList.toggle("active", s.id === id); }); }); }); /* ============ 标签树:客户端渲染 + 拖拽调整层级 ============ */ var tagRoot = document.getElementById("admin-tag-tree"); var tagDraggingId = null; function tagRowHtml(n) { var kids = n.children && n.children.length; var label = kids ? '" : '' + esc(n.name) + ""; return ( "
  • " + '
    ' + label + '' + n.count + " · #" + n.id + "" + '' + '' + '' + '' + '' + "" + "
    " + (kids ? '" : "") + "
  • " ); } function renderTagAdminTree(tree) { if (!tagRoot) return; tagRoot.innerHTML = tree && tree.length ? '" : '

    还没有标签。

    '; } async function refreshTags() { if (!tagRoot) return; var json = await api("/api/tags/tree", undefined, "GET"); renderTagAdminTree(json.data); } function clearTagDnd() { tagDraggingId = null; if (!tagRoot) return; tagRoot.classList.remove("drag-over"); tagRoot.querySelectorAll(".drag-src,.drag-over").forEach(function (el) { el.classList.remove("drag-src", "drag-over"); }); } if (tagRoot) { if (Array.isArray(window.__ADMIN_TAG_TREE__)) renderTagAdminTree(window.__ADMIN_TAG_TREE__); else refreshTags(); tagRoot.addEventListener("click", function (e) { var tog = e.target.closest(".toggler"); if (!tog) return; var li = tog.closest("li"); var ul = null; if (li) { for (var i = 0; i < li.children.length; i++) { if (li.children[i].tagName === "UL") ul = li.children[i]; } } if (!ul) return; var open = ul.classList.toggle("open"); tog.setAttribute("aria-expanded", open ? "true" : "false"); }); tagRoot.addEventListener("dragstart", function (e) { if (e.target.closest("button")) return; var row = e.target.closest(".tag-node"); if (!row) return; tagDraggingId = row.getAttribute("data-id"); e.dataTransfer.setData("text/plain", tagDraggingId); e.dataTransfer.effectAllowed = "move"; row.classList.add("drag-src"); }); tagRoot.addEventListener("dragend", clearTagDnd); tagRoot.addEventListener("dragover", function (e) { if (!tagDraggingId) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; var row = e.target.closest(".tag-node[data-drop-id]"); tagRoot.querySelectorAll(".drag-over").forEach(function (el) { el.classList.remove("drag-over"); }); if (row) row.classList.add("drag-over"); else tagRoot.classList.add("drag-over"); }); tagRoot.addEventListener("dragleave", function (e) { if (e.target === tagRoot && !e.relatedTarget) tagRoot.classList.remove("drag-over"); }); tagRoot.addEventListener("drop", async function (e) { var id = tagDraggingId; var row = e.target.closest(".tag-node[data-drop-id]"); var dropTarget = row ? row.getAttribute("data-drop-id") : null; clearTagDnd(); if (!id || dropTarget === id) return; try { await api("/api/tags/" + id, { parentId: dropTarget ? Number(dropTarget) : null }, "PATCH"); toast(dropTarget ? "已移动到目标标签下" : "已移回顶层"); await refreshTags(); } catch (err) { toast(err.message); } }); } /* ============ 批量选择 / 批量状态 ============ */ function selectedEntryIds() { var ids = []; document.querySelectorAll(".entry-check:checked").forEach(function (c) { ids.push(Number(c.getAttribute("data-id"))); }); return ids; } function updateBatchCount() { var el = document.getElementById("batch-count"); if (el) el.textContent = "已选 " + selectedEntryIds().length + " 项"; } document.addEventListener("change", function (e) { var ck = e.target; if (ck.classList && ck.classList.contains("entry-check-all")) { document.querySelectorAll(".entry-check").forEach(function (c) { c.checked = ck.checked; }); updateBatchCount(); } else if (ck.classList && ck.classList.contains("entry-check")) { updateBatchCount(); } }); document.addEventListener("click", async function (e) { var btn = e.target.closest("[data-action]"); if (!btn) return; var action = btn.getAttribute("data-action"); try { if (action === "tag-add") { var parentId = btn.getAttribute("data-parent-id"); var name = prompt(parentId ? "新增子标签名称:" : "新增顶层标签组名称:"); if (!name) return; await api("/api/tags", { name: name, parentId: parentId ? Number(parentId) : null }); await refreshTags(); toast("标签已创建"); } if (action === "tag-rename") { var name2 = prompt("重命名为:", btn.getAttribute("data-name")); if (!name2 || name2 === btn.getAttribute("data-name")) return; await api("/api/tags/" + btn.getAttribute("data-id"), { name: name2 }, "PATCH"); await refreshTags(); toast("已重命名"); } if (action === "tag-move") { var target3 = prompt("移动到哪个标签之下?填其 ID(留空移到顶层):"); if (target3 === null) return; await api("/api/tags/" + btn.getAttribute("data-id"), { parentId: target3.trim() ? Number(target3.trim()) : null }, "PATCH"); await refreshTags(); toast("已移动"); } if (action === "tag-delete") { if (!confirm("删除该标签?其子标签将上移一级,关联关系解除。")) return; await api("/api/tags/" + btn.getAttribute("data-id"), undefined, "DELETE"); await refreshTags(); toast("已删除"); } if (action === "entry-status") { await api("/api/entries/" + btn.getAttribute("data-id") + "/status", { status: btn.getAttribute("data-status") }, "PATCH"); toast("状态已更新"); refresh(); } if (action === "entry-delete") { if (!confirm("物理删除该条目?此操作不可恢复。")) return; await api("/api/entries/" + btn.getAttribute("data-id"), undefined, "DELETE"); toast("已删除"); refresh(); } if (action === "entry-select-all") { var checks = Array.prototype.slice.call(document.querySelectorAll(".entry-check")); var allOn = checks.length > 0 && checks.every(function (c) { return c.checked; }); checks.forEach(function (c) { c.checked = !allOn; }); var allEl = document.querySelector(".entry-check-all"); if (allEl) allEl.checked = !allOn; updateBatchCount(); } if (action === "entry-batch") { var ids = selectedEntryIds(); if (!ids.length) { toast("请先勾选条目"); return; } var status = btn.getAttribute("data-status"); await api("/api/admin/entries/status", { ids: ids, status: status }, "POST"); toast(status === "active" ? "已上架 " + ids.length + " 条" : "已灰掉 " + ids.length + " 条"); refresh(); } if (action === "comment-delete") { if (!confirm("删除该评论?")) return; await api("/api/comments/" + btn.getAttribute("data-id"), undefined, "DELETE"); toast("已删除"); refresh(); } if (action === "user-disable") { var status = btn.getAttribute("data-status") === "active" ? "disabled" : "active"; await api("/api/admin/users/" + btn.getAttribute("data-id") + "/status", { status: status }, "PATCH"); toast("账号状态已更新"); refresh(); } } catch (err) { toast(err.message); } }); var userAddForm = document.getElementById("user-add-form"); if (userAddForm) { userAddForm.addEventListener("submit", function (e) { e.preventDefault(); api("/api/admin/users", { username: userAddForm.username.value.trim(), password: userAddForm.password.value, role: userAddForm.role.value, }) .then(function () { toast("账号已创建"); refresh(); }) .catch(function (err) { toast(err.message); }); }); } var aiKeyForm = document.getElementById("ai-key-form"); if (aiKeyForm) { aiKeyForm.addEventListener("submit", function (e) { e.preventDefault(); api("/api/admin/ai-key", { key: aiKeyForm.key.value.trim() }, "PUT") .then(function () { toast("AI 录入密钥已更新"); refresh(); }) .catch(function (err) { toast(err.message); }); }); } document.querySelectorAll("form.source-config").forEach(function (form) { form.addEventListener("submit", function (e) { e.preventDefault(); var id = form.getAttribute("data-id"); api("/api/admin/sources/" + id, { name: form.name.value, url: form.url.value, adapter: form.adapter.value, cron_expr: form.cron_expr.value, enabled: form.enabled.checked, }, "PATCH") .then(function () { toast("内容源已保存"); refresh(); }) .catch(function (err) { toast(err.message); }); }); }); document.querySelectorAll("[data-source-run]").forEach(function (btn) { btn.addEventListener("click", async function () { var id = btn.getAttribute("data-source-run"); btn.disabled = true; try { var json = await api("/api/admin/sources/" + id + "/run", {}); var s = json.data; toast(s.error ? "拉取失败:" + s.error : "完成:发现 " + s.found + " · 新增 " + s.created + " · 跳过 " + s.skipped); if (!s.error) refresh(); } catch (err) { toast(err.message); } finally { btn.disabled = false; } }); }); document.querySelectorAll("[data-source-runs]").forEach(function (btn) { btn.addEventListener("click", async function () { var box = document.getElementById("runs-" + btn.getAttribute("data-source-runs")); if (!box) return; if (!box.hidden) { box.hidden = true; return; } try { var json = await api("/api/admin/sources/" + btn.getAttribute("data-source-runs") + "/runs", undefined, "GET"); box.textContent = JSON.stringify(json.data, null, 2).slice(0, 3000); box.hidden = false; } catch (err) { toast(err.message); } }); }); var sourceAddForm = document.getElementById("source-add-form"); if (sourceAddForm) { sourceAddForm.addEventListener("submit", function (e) { e.preventDefault(); api("/api/admin/sources", { name: sourceAddForm.name.value.trim(), url: sourceAddForm.url.value.trim(), adapter: sourceAddForm.adapter.value, cron_expr: sourceAddForm.cron_expr.value.trim(), }) .then(function () { toast("拉取源已创建"); refresh(); }) .catch(function (err) { toast(err.message); }); }); } })();