210 lines
9.9 KiB
JavaScript
210 lines
9.9 KiB
JavaScript
// 后台交互脚本(原生 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 上并不在
|
||
// <form id="batch"> 内部(包在 .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/<kind>/... 形式的 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();
|
||
});
|
||
})();
|