// 后台交互脚本(原生 JS,无构建步骤)。data-admin-js // 两个入口页面: // - 文章管理混合列表(/admin/articles):批量通过/驳回 // - 街拍编辑页(/admin/street-snaps/:id/edit):主副图合并/拆出(街拍分组) (function () { // 表单动作里的 /admin/articles/ 换成 /admin/api/articles/,得到对应的 JSON 接口地址。 function apiOf(action) { return action.replace('/admin/articles/', '/admin/api/articles/'); } // ── 看大图弹层(所有页面通用)──────────────────────────────────────────── function initZoom() { var modal = document.getElementById('zoom-modal'); if (!modal) return; var img = document.getElementById('zoom-img'); document.querySelectorAll('.thumb[data-zoom]').forEach(function (btn) { btn.addEventListener('click', function () { img.src = btn.querySelector('img').src; modal.showModal(); }); }); modal.addEventListener('click', function (e) { if (e.target === modal) modal.close(); }); } // ── 文章管理列表:批量通过 / 驳回 ────────────────────────────────────── function initArticleList() { var selAll = document.getElementById('sel-all'); var picks = function () { return Array.prototype.slice.call(document.querySelectorAll('.row-pick')); }; var bar = document.getElementById('batchbar'); var count = document.getElementById('sel-count'); function refresh() { var n = picks().filter(function (c) { return c.checked; }).length; if (bar) bar.hidden = n === 0; if (count) count.textContent = '已选 ' + n + ' 条'; } if (selAll) selAll.addEventListener('change', function () { picks().forEach(function (c) { c.checked = selAll.checked; }); refresh(); }); picks().forEach(function (c) { c.addEventListener('change', refresh); }); var clear = document.getElementById('batch-clear'); if (clear) clear.addEventListener('click', function () { picks().forEach(function (c) { c.checked = false; }); if (selAll) selAll.checked = false; refresh(); }); function postBatch(action) { var items = picks().filter(function (c) { return c.checked; }).map(function (c) { var tr = c.closest('tr'); return { kind: tr.getAttribute('data-kind'), id: parseInt(tr.getAttribute('data-id'), 10) }; }); if (items.length === 0) { alert('请先勾选要操作的记录'); return; } if (!confirm('确认' + (action === 'approve' ? '通过' : '驳回') + '选中的 ' + items.length + ' 条?')) return; fetch('/admin/api/articles/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action, items: items }) }).then(function (r) { return r.json(); }).then(function (d) { if (d.failed && d.failed.length) { alert('成功 ' + d.ok + ' 条,失败 ' + d.failed.length + ' 条:' + d.failed[0].error); } location.reload(); }).catch(function (e) { alert('请求失败:' + e); }); } var approve = document.getElementById('batch-approve'); var reject = document.getElementById('batch-reject'); if (approve) approve.addEventListener('click', function () { postBatch('approve'); }); if (reject) reject.addEventListener('click', function () { postBatch('reject'); }); } // ── 街拍编辑页:主副图分组(合并 / 拆出)────────────────────────────── function initArticleDetail() { var batchForm = document.getElementById('batch'); var stat = document.getElementById('img-stat'); var hint = document.getElementById('hint-dismiss'); if (hint) hint.addEventListener('click', function () { var h = hint.closest('.hint'); if (h) h.style.display = 'none'; }); // 每个主图各自的「显示副图」开关:点一下只展开/收起该主图紧邻的副图(不会一次全开)。 var grid = document.querySelector('.grid'); if (grid) { grid.addEventListener('click', function (e) { var btn = e.target.closest('.toggle-det'); if (!btn) return; var pid = btn.getAttribute('data-parent'); var dets = grid.querySelectorAll('.cell.det[data-parent="' + pid + '"]'); var expand = false; dets.forEach(function (d) { if (!d.classList.contains('shown')) expand = true; }); dets.forEach(function (d) { d.classList.toggle('shown', expand); }); btn.textContent = expand ? ('隐藏副图(' + dets.length + ' 张)') : ('显示副图(' + dets.length + ' 张)'); }); } // 合并选中为一组(AJAX,成功后就地更新标签) if (batchForm) batchForm.addEventListener('submit', function (e) { e.preventDefault(); // 注意:主图 radio / 选入 checkbox 用 form="batch" 关联到批量表单,但 DOM 上并不在 //
内部(包在 .cell 里)。它们虽能随表单正确提交,却不在 form 子树中, // 故必须用 document 级查询(而非 batchForm.querySelector)才能取到已勾选的项。 var main = document.querySelector('input[name="main"]:checked'); var picks = document.querySelectorAll('input[name="img"]:checked'); if (!main) { alert('请先在其中一张主图上点「主图」'); return; } if (!picks.length) { alert('请勾选要合并的图片'); return; } // 接口 ArticleAttachImagesAPI 收 JSON(main + imgs),不能发 FormData: // multipart 会让 ShouldBindJSON 失败、返回「请求格式错误」。 var data = { main: parseInt(main.value, 10), imgs: Array.prototype.slice.call(picks).map(function (c) { return parseInt(c.value, 10); }) }; fetch(apiOf(batchForm.getAttribute('action')), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(function (r) { return r.json(); }) .then(function (d) { applyImages(d.images, kindOf(batchForm.getAttribute('action')), main.value); if (stat) stat.textContent = d.count + ' 张 · ' + d.groups + ' 组'; }) .catch(function (err) { alert('合并失败:' + err); }); }); // 单张拆出(AJAX) document.querySelectorAll('form[data-only="detail"]').forEach(function (f) { f.addEventListener('submit', function (e) { e.preventDefault(); fetch(apiOf(f.getAttribute('action')), { method: 'POST', body: new FormData(f) }) .then(function (r) { return r.json(); }) .then(function (d) { applyImages(d.images, kindOf(f.getAttribute('action'))); if (stat) stat.textContent = d.count + ' 张 · ' + d.groups + ' 组'; }) .catch(function (err) { alert('拆出失败:' + err); }); }); }); } // 从 /admin/articles//... 形式的 action 取出实体类型(street / runway)。 function kindOf(action) { var m = /^\/admin\/articles\/(\w+)\//.exec(action || ''); return m ? m[1] : 'street'; } // 更新某个主图的「显示副图」开关:按最新副图数创建/更新/移除按钮,文案随展开态变化。 function updateToggle(mainId, detailCount) { var cell = document.querySelector('.cell[data-img-id="' + mainId + '"]'); var btn = document.querySelector('.toggle-det[data-parent="' + mainId + '"]'); if (detailCount > 0) { if (!btn && cell) { btn = document.createElement('button'); btn.type = 'button'; btn.className = 'toggle-det'; btn.setAttribute('data-parent', mainId); var acts = cell.querySelector('.acts'); if (acts) acts.appendChild(btn); } var dets = document.querySelectorAll('.cell.det[data-parent="' + mainId + '"]'); var expanded = dets.length > 0 && dets[0].classList.contains('shown'); if (btn) btn.textContent = (expanded ? '隐藏副图(' : '显示副图(') + detailCount + ' 张)'; } else if (btn) { btn.remove(); } } // 展开/收起某主图的全部副图,并同步按钮文案。 function setExpanded(mainId, expanded) { var dets = document.querySelectorAll('.cell.det[data-parent="' + mainId + '"]'); dets.forEach(function (d) { d.classList.toggle('shown', expanded); }); var btn = document.querySelector('.toggle-det[data-parent="' + mainId + '"]'); if (btn) btn.textContent = (expanded ? '隐藏副图(' : '显示副图(') + dets.length + ' 张)'; } // 用服务端返回的最新图片投影,重写网格里的角色标签、副图开关与统计。 // kind 决定细节图文案;focusMain 为合并操作的目标主图 id,合并后自动展开其副图。 function applyImages(images, kind, focusMain) { if (!images) return; images.forEach(function (im) { var cell = document.querySelector('.cell[data-img-id="' + im.id + '"]'); if (!cell) return; cell.classList.toggle('det', im.isDetail === 1); if (im.isDetail === 1) { cell.setAttribute('data-parent', im.parentId); } else { cell.removeAttribute('data-parent'); updateToggle(im.id, im.detailCount); } var role = cell.querySelector('.role'); if (role) { if (im.isDetail === 1) { role.textContent = (kind === 'runway' ? '细节图' : '副图') + ' → #' + im.parentId; } else { role.textContent = '主图' + (im.detailCount ? (' · ' + im.detailCount + ' 副图') : ''); } } }); if (focusMain) setExpanded(focusMain, true); } document.addEventListener('DOMContentLoaded', function () { initZoom(); if (document.getElementById('sel-all') || document.getElementById('batchbar')) initArticleList(); if (document.getElementById('batch')) initArticleDetail(); }); })();