/* 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]:not(:disabled)"); } 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:not(:disabled)" ).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 setupConfirmSubmit() { document.querySelectorAll("form[data-confirm-submit]").forEach(function (form) { form.addEventListener("submit", function (e) { if (!window.confirm(form.getAttribute("data-confirm-submit"))) { e.preventDefault(); } }); }); } /* 用户列表共用一个重置密码弹窗。按钮只把用户编号和显示名放进表单, 不在前端处理权限、密码或 Session。 */ function setupUserResetModal() { var modal = document.getElementById("reset-password-modal"); if (!modal) return; var idInput = modal.querySelector("[data-reset-user-id-input]"); var nameLabel = modal.querySelector("[data-reset-username-label]"); document.querySelectorAll("[data-reset-user-id]").forEach(function (button) { button.addEventListener("click", function () { idInput.value = button.getAttribute("data-reset-user-id") || ""; nameLabel.textContent = button.getAttribute("data-reset-username") || ""; }); }); } /* 客户端列表共用绑定/转交弹窗。这里只回填行数据,权限和写入由服务端处理。 */ function setupClientAssignmentModal() { var modal = document.getElementById("assign-client-modal"); if (!modal) return; var idInput = modal.querySelector("[data-assign-client-id-input]"); var nameLabel = modal.querySelector("[data-assign-client-label]"); var select = modal.querySelector("select[name=user_id]"); var title = modal.querySelector("#assign-client-title"); var note = modal.querySelector("[data-assignment-note]"); document.querySelectorAll("[data-assign-client-id]").forEach(function (button) { button.addEventListener("click", function () { var currentName = button.getAttribute("data-assign-username") || ""; idInput.value = button.getAttribute("data-assign-client-id") || ""; nameLabel.textContent = button.getAttribute("data-assign-client-name") || ""; if (select) select.value = button.getAttribute("data-assign-user-id") || ""; title.textContent = currentName ? "转交客户端" : "绑定客户端"; note.textContent = currentName ? "当前负责人是“" + currentName + "”。保存后新负责人立即生效;既有任务不变。" : "绑定只改变负责人和网页可见范围,不影响 Client 接口或既有任务。"; }); }); } /* 采购确认弹窗只负责把服务端已经渲染的本页行按勾选结果显示出来。 规格有效性、价格和客户端权限仍由服务端重新校验。 */ function setupPurchaseModal() { var openButtons = document.querySelectorAll("[data-purchase-open]"); var form = document.querySelector("[data-purchase-form]"); if (!openButtons.length || !form) return; var submitButton = form.querySelector("[data-purchase-submit]"); openButtons.forEach(function (openButton) { openButton.addEventListener("click", function () { var selected = {}; var singleID = openButton.getAttribute("data-purchase-id"); if (singleID) { selected[singleID] = true; } else { document.querySelectorAll("tbody input[name=ids]:checked").forEach(function (box) { selected[box.value] = true; }); } var shown = 0; form.querySelectorAll("[data-purchase-row]").forEach(function (row) { var enabled = !!selected[row.getAttribute("data-purchase-row")]; row.hidden = !enabled; row.querySelectorAll("input").forEach(function (input) { input.disabled = !enabled; }); if (enabled) shown += 1; }); var empty = form.querySelector("[data-purchase-empty]"); if (empty) empty.hidden = shown > 0; }); }); form.addEventListener("submit", function (event) { if (event.defaultPrevented || !submitButton) return; submitButton.disabled = true; submitButton.setAttribute("aria-busy", "true"); submitButton.textContent = "创建中…"; }); } /* PDD 批量采集弹窗只同步已选数量并提供提交反馈。 客户端权限和任务初始状态始终由服务端决定。 */ function setupCollectModals() { document.querySelectorAll("[data-collect-open]").forEach(function (openButton) { var formID = openButton.getAttribute("data-collect-form") || "pdd-form"; var form = document.getElementById(formID); var modal = document.getElementById(openButton.getAttribute("data-modal-open")); if (!form || !modal) return; var count = modal.querySelector("[data-collect-count]"); var submitButton = modal.querySelector("[data-collect-submit]"); if (!count || !submitButton) return; openButton.addEventListener("click", function () { var fixedCount = openButton.getAttribute("data-collect-count-value"); count.textContent = fixedCount === null ? checkedCount() : fixedCount; }); form.addEventListener("submit", function (event) { if (event.submitter !== submitButton) return; submitButton.disabled = true; submitButton.setAttribute("aria-busy", "true"); submitButton.textContent = "创建中…"; }); }); } /* Excel 解析可能需要几秒。浏览器校验通过并开始上传后立即锁住提交按钮, 防止采购员因为没有反馈而重复提交同一批文件。 */ function setupUploadFeedback() { document.querySelectorAll("[data-upload-form]").forEach(function (form) { var button = form.querySelector("[data-upload-submit]"); if (!button) return; form.addEventListener("submit", function () { button.disabled = true; button.setAttribute("aria-busy", "true"); button.textContent = "导入中…"; }); }); } /* ── 弹窗 ────────────────────────────────── 弹窗**内容由服务端渲染**,这里只负责显示、隐藏和把内容取回来。 不要在这里拼业务数据——价格格式、规格顺序都是业务规则, 散到前端就没人维护得住了。 */ function openModal(modal) { modal._returnFocus = document.activeElement; modal.hidden = false; var focusable = modal.querySelector("[autofocus]") || modal.querySelector("input, button"); if (focusable) focusable.focus(); } function closeModal(modal) { modal.hidden = true; if (modal._returnFocus && modal._returnFocus.focus) { modal._returnFocus.focus(); } } 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) { /* 关闭:叉、取消按钮,以及真正点弹窗外面的灰底。 不能只看 click 的 target:从输入框按下拖选到灰底松开时,浏览器会把 合成的 click 目标设成灰底。只有手势也从灰底开始,才算有意关闭。 */ var pointerStartedOnBackdrop = false; modal.addEventListener("pointerdown", function (e) { pointerStartedOnBackdrop = e.target === modal; }); modal.addEventListener("pointercancel", function () { pointerStartedOnBackdrop = false; }); modal.addEventListener("click", function (e) { var clickedCloseButton = e.target.closest("[data-modal-close]"); var clickedBackdrop = e.target === modal && pointerStartedOnBackdrop; pointerStartedOnBackdrop = false; if (clickedBackdrop || clickedCloseButton) { 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") || ""; function loadDetail(id) { if (!modal.hidden && modal.getAttribute("data-loaded-detail-id") === id) return; modal.setAttribute("data-loaded-detail-id", id); slot.innerHTML = "
打不开详情:" + err.message + "。刷新页面后重试。