Files
backend_v2/internal/handler/assets/admin.js
toom1996 42f6316125 update
2026-09-25 11:31:52 +08:00

210 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 后台交互脚本(原生 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();
});
})();