/* Admin 的全部 JavaScript。 * * 原生 JS,没有框架、没有构建步骤——见 admin/AGENTS.md 前端约束。 * 只做三件纯前端的事:全选、删除前二次确认、按钮禁用。 * 搜索、删除、导入本身都是普通表单 POST,服务端渲染,不需要 JS。 * * 不要往这里加业务逻辑。业务逻辑在服务端。 */ (function () { "use strict"; /* ── 全选 / 反选 ────────────────────────────── 表头的勾选框控制本表格所有行的勾选框。 */ function setupCheckAll(root) { var master = root.querySelector("[data-check-all]"); if (!master) return; var table = master.closest("table"); if (!table) return; function rowBoxes() { return table.querySelectorAll("tbody input[type=checkbox]"); } master.addEventListener("change", function () { rowBoxes().forEach(function (box) { box.checked = master.checked; }); syncButtons(); }); table.addEventListener("change", function (e) { if (e.target.type !== "checkbox" || e.target === master) return; var boxes = Array.prototype.slice.call(rowBoxes()); master.checked = boxes.length > 0 && boxes.every(function (b) { return b.checked; }); syncButtons(); }); } /* ── 选中数量 ────────────────────────────── */ function checkedCount() { return document.querySelectorAll( "tbody input[type=checkbox]:checked" ).length; } /* ── 需要勾选才能点的按钮 ────────────────── 标了 data-need-checked 的按钮,没勾选任何行时禁用。 */ function syncButtons() { var n = checkedCount(); document.querySelectorAll("[data-need-checked]").forEach(function (btn) { btn.disabled = n === 0; }); } /* ── 删除前二次确认 ──────────────────────── 标了 data-confirm-delete 的元素,提交前必须确认, 并明确告诉用户会删掉几条、能不能恢复。 标在表单上就拦 submit,标在按钮上就拦 click—— 一个表单配多个提交按钮时(PDD 页就是),只有删除那个要确认。 属性值可以写自定义文案,里面的 {n} 会替换成勾选条数; 不写就用默认那句。 */ var DEFAULT_CONFIRM = "将删除 {n} 条记录,不可恢复。确定继续?"; function setupConfirmDelete() { document.querySelectorAll("[data-confirm-delete]").forEach(function (el) { var isForm = el.tagName === "FORM"; el.addEventListener(isForm ? "submit" : "click", function (e) { var n = checkedCount(); if (n === 0) { e.preventDefault(); return; } var text = el.getAttribute("data-confirm-delete") || DEFAULT_CONFIRM; if (!window.confirm(text.replace("{n}", n) + "\n\n确定继续?")) { e.preventDefault(); } }); }); } /* ── 弹窗 ────────────────────────────────── 弹窗**内容由服务端渲染**,这里只负责显示、隐藏和把内容取回来。 不要在这里拼业务数据——价格格式、规格顺序都是业务规则, 散到前端就没人维护得住了。 */ function openModal(modal) { modal.hidden = false; var focusable = modal.querySelector("input, button"); if (focusable) focusable.focus(); } function closeModal(modal) { modal.hidden = true; } function setupModals() { /* 打开:按钮上写 data-modal-open="弹窗的 id" */ document.querySelectorAll("[data-modal-open]").forEach(function (btn) { btn.addEventListener("click", function () { var modal = document.getElementById(btn.getAttribute("data-modal-open")); if (modal) openModal(modal); }); }); document.querySelectorAll(".modal-backdrop").forEach(function (modal) { /* 关闭:叉、取消按钮,以及点弹窗外面的灰底 */ modal.addEventListener("click", function (e) { if (e.target === modal || e.target.closest("[data-modal-close]")) { closeModal(modal); } }); }); /* Esc 关掉最上面那个打开着的弹窗 */ document.addEventListener("keydown", function (e) { if (e.key !== "Escape") return; var open = document.querySelectorAll(".modal-backdrop:not([hidden])"); if (open.length > 0) closeModal(open[open.length - 1]); }); } /* ── 双击行打开详情弹窗 ──────────────────── 行上写 data-detail-id,双击时去 /<模块>/detail?id=… 取回 已经渲染好的 HTML 片段,塞进壳子里显示。 */ function setupRowDetail() { var modal = document.getElementById("detail-modal"); if (!modal) return; var slot = modal.querySelector("[data-detail-slot]"); var base = modal.getAttribute("data-detail-url") || ""; document.querySelectorAll("tr[data-detail-id]").forEach(function (row) { row.addEventListener("dblclick", function (e) { /* 双击到勾选框或链接上时不要弹窗,那是另一个动作 */ if (e.target.closest("input, a, button")) return; slot.innerHTML = ""; openModal(modal); fetch(base + "?id=" + encodeURIComponent(row.getAttribute("data-detail-id")), { headers: { "X-Requested-With": "fetch" } }) .then(function (resp) { if (!resp.ok) throw new Error("HTTP " + resp.status); return resp.text(); }) .then(function (html) { slot.innerHTML = html; }) .catch(function (err) { /* 出错也要说清楚,不能让弹窗一直停在"正在加载…" */ slot.innerHTML = "" + ""; }); }); }); } document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll("table").forEach(setupCheckAll); setupConfirmDelete(); setupModals(); setupRowDetail(); syncButtons(); }); /* TODO(骨架): 另外两个页面的弹窗,做法照 PDD 商品页抄: 蝦皮数据页 -> 编辑弹窗(填 PDD 链接) 顺运宝页 -> 规格匹配弹窗 页面里放一个 id="detail-modal" 的壳子、行上写 data-detail-id, 服务端出一个返回片段的 /<模块>/detail,这里就不用再加代码了。 */ })();