feat: 管理台内容状态过滤+预览、批量上架/灰掉、标签拖拽调整层级
This commit is contained in:
+169
-14
@@ -26,7 +26,13 @@
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
setTimeout(function () { location.reload(); }, 500);
|
||||
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) {
|
||||
@@ -42,14 +48,141 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".tree-row[data-tree]").forEach(function (row) {
|
||||
row.addEventListener("click", function (e) {
|
||||
if (e.target.closest("button[data-tag-action]")) return;
|
||||
var target = document.getElementById(row.getAttribute("data-tree"));
|
||||
if (!target) return;
|
||||
var open = target.classList.toggle("open");
|
||||
row.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
/* ============ 标签树:客户端渲染 + 拖拽调整层级 ============ */
|
||||
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) {
|
||||
@@ -63,28 +196,32 @@
|
||||
var name = prompt(parentId ? "新增子标签名称:" : "新增顶层标签组名称:");
|
||||
if (!name) return;
|
||||
await api("/api/tags", { name: name, parentId: parentId ? Number(parentId) : null });
|
||||
toast("标签已创建"); refresh();
|
||||
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");
|
||||
toast("已重命名"); refresh();
|
||||
await refreshTags();
|
||||
toast("已重命名");
|
||||
}
|
||||
|
||||
if (action === "tag-move") {
|
||||
var target3 = prompt("移动到哪个标签之下?填其 ID(留空移到顶层):\n" + btn.getAttribute("data-ids"));
|
||||
var target3 = prompt("移动到哪个标签之下?填其 ID(留空移到顶层):");
|
||||
if (target3 === null) return;
|
||||
await api("/api/tags/" + btn.getAttribute("data-id"),
|
||||
{ parentId: target3.trim() ? Number(target3.trim()) : null }, "PATCH");
|
||||
toast("已移动"); refresh();
|
||||
await refreshTags();
|
||||
toast("已移动");
|
||||
}
|
||||
|
||||
if (action === "tag-delete") {
|
||||
if (!confirm("删除该标签?其子标签将上移一级,关联关系解除。")) return;
|
||||
await api("/api/tags/" + btn.getAttribute("data-id"), undefined, "DELETE");
|
||||
toast("已删除"); refresh();
|
||||
await refreshTags();
|
||||
toast("已删除");
|
||||
}
|
||||
|
||||
if (action === "entry-status") {
|
||||
@@ -99,6 +236,24 @@
|
||||
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");
|
||||
@@ -203,4 +358,4 @@
|
||||
.catch(function (err) { toast(err.message); });
|
||||
});
|
||||
}
|
||||
})();
|
||||
})();
|
||||
Reference in New Issue
Block a user