(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]; }); } var SECTION_IDS = [ "sec-content", "sec-tags", "sec-comments", "sec-contacts", "sec-sources", "sec-accounts", ]; function activateSection(id, { syncNav } = { syncNav: true }) { if (!SECTION_IDS.includes(id)) return; document.querySelectorAll(".admin-main > section").forEach(function (s) { s.classList.toggle("active", s.id === id); }); if (syncNav) { document.querySelectorAll(".admin-nav [data-section]").forEach(function (b) { b.setAttribute("aria-current", b.getAttribute("data-section") === id ? "true" : "false"); }); } } // 菜单项是真实链接:点击即向后台发起一次新的 /admin?section=… 请求, // 服务端重新查库渲染,因此每次切分区拿到的都是最新数据(无需手动刷新)。 document.querySelectorAll(".admin-nav a[data-section]").forEach(function (link) { link.addEventListener("click", function (e) { // 普通左键点击交给浏览器完成整页跳转(= 重新拉数据); // 中键 / Ctrl / Cmd 点击保留「新标签打开」的默认行为。 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; activateSection(link.getAttribute("data-section")); document.querySelector(".admin-nav-refresh")?.setAttribute( "href", link.getAttribute("href") ); }); }); // 兼容旧链接 /admin#sec-xxx:按 hash 切分区但不重新拉取 (function restoreHashSection() { var hash = (location.hash || "").replace("#", ""); if (hash && SECTION_IDS.includes(hash)) activateSection(hash); })(); /* ============ 标签树:客户端渲染 + 拖拽调整层级 ============ */ 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 ( "
还没有标签。
'; } 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 === "contact-status") { var enabled = btn.getAttribute("data-enabled") === "1"; if (enabled && !confirm("禁用后该联系方式将无法继续评论,确认?")) return; await api("/api/admin/contacts/" + btn.getAttribute("data-id") + "/status", { enabled: !enabled }, "PATCH"); toast(enabled ? "已禁用" : "已启用"); refresh(); } if (action === "contact-rename") { var nick = prompt("修改昵称:", btn.getAttribute("data-name")); if (!nick || nick === btn.getAttribute("data-name")) return; await api("/api/admin/contacts/" + btn.getAttribute("data-id"), { nickname: nick }, "PATCH"); toast("昵称已更新"); refresh(); } if (action === "contact-delete") { if (!confirm("删除该评论人员?其历史评论会保留。")) return; await api("/api/admin/contacts/" + btn.getAttribute("data-id"), undefined, "DELETE"); toast("已删除"); refresh(); } if (action === "relation-add") { var sel = document.getElementById("f-related"); if (!sel || !sel.value) { toast("没有可关联的条目"); return; } var opt = sel.options[sel.selectedIndex]; await api("/api/admin/entries/" + window.__ADMIN_ENTRY_ID__ + "/relations", { relatedId: Number(sel.value) }, "POST"); toast("已关联:" + opt.textContent.trim().split(" · ")[0]); refresh(); } if (action === "relation-remove") { if (!confirm("移除该关联?")) return; await api("/api/admin/entries/" + window.__ADMIN_ENTRY_ID__ + "/relations/" + btn.getAttribute("data-related-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; if (s.error) { toast("拉取失败:" + s.error); } else { toast("完成:发现 " + s.found + " · 新增 " + s.created + " · 跳过 " + s.skipped); 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); }); }); } })();