Files
awesome/public/js/admin.js
T
cheney 3f7b48325f
Build and Deploy / build (push) Successful in 1m37s
Build and Deploy / deploy (push) Successful in 31s
feat: 管理台内容状态过滤+预览、批量上架/灰掉、标签拖拽调整层级
2026-09-21 11:15:26 +08:00

361 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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
? '<button type="button" class="tree-row toggler" aria-expanded="true"><span class="tw">▶</span> <span class="name">' + esc(n.name) + "</span></button>"
: '<span class="nameleaf"><span class="name">' + esc(n.name) + "</span></span>";
return (
"<li>" +
'<div class="tree-row tag-node" draggable="true" data-id="' + n.id + '" data-drop-id="' + n.id + '">' +
label +
'<span class="mono cnt">' + n.count + " · #" + n.id + "</span>" +
'<span class="row-actions" style="margin-left:8px">' +
'<button class="btn btn-sm" type="button" data-action="tag-add" data-parent-id="' + n.id + '">+子级</button>' +
'<button class="btn btn-sm" type="button" data-action="tag-rename" data-id="' + n.id + '" data-name="' + esc(n.name) + '">改名</button>' +
'<button class="btn btn-sm" type="button" data-action="tag-move" data-id="' + n.id + '">移动</button>' +
'<button class="btn btn-sm btn-danger" type="button" data-action="tag-delete" data-id="' + n.id + '">删</button>' +
"</span>" +
"</div>" +
(kids ? '<ul class="tag-children open">' + n.children.map(tagRowHtml).join("") + "</ul>" : "") +
"</li>"
);
}
function renderTagAdminTree(tree) {
if (!tagRoot) return;
tagRoot.innerHTML = tree && tree.length
? '<ul class="tag-tree">' + tree.map(tagRowHtml).join("") + "</ul>"
: '<p class="mono dim">还没有标签。</p>';
}
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); });
});
}
})();