板块管理页支持拖拽排序(group_reorder)

- 板块列表与网站列表同款的拖拽交互:按住行首手柄拖动,实时重排预览,
  松手即保存(Ajax 动作 group_reorder,整组归一化为 index * SORT_STEP)
- 板块没有分组维度,group_reorder 不带 group_id、不做归属校验
  (apply_sort_order 不传 $groupId 时按给定 id 顺序整组重写)
- admin.js 的拖拽排序从「只认网站列表」泛化为「所有带 data-sortable 的
  tbody」:落库 action / group_id / 落库地址都改由 tbody 的 data-* 提供,
  网站列表行为不变(仍从 #sites-tabs 读地址、带 group_id)
- 新增板块仍排最后(MAX + SORT_STEP,上一轮已实现);板块上移/下移按钮
  保留作无 JS 回退;页面顶部补说明文案
- 验证:HTTP e2e 新增 S12 系列 18 条(结构 / 拖拽落库 / 前台同步 /
  新增排最后 / 上移回退),72/72;jsdom 新增板块页 19 项,26+19 全绿;
  反向验证:函数层 4 变体 + HTTP 层 7 变体(新增 G:group_reorder 失效)
  + jsdom 4 变体全部精确报红、基准全绿;全量既有回归 193/193
This commit is contained in:
2026-09-29 21:45:16 +08:00
parent 717a8877bb
commit e81e6900e2
3 changed files with 44 additions and 16 deletions
+17 -11
View File
@@ -244,24 +244,30 @@
/**
* 拖拽排序:按住某一行拖动即可改顺序,松手即保存。
* 用于「网站列表」(每板块一个 tbody)与「板块列表」(整表一个 tbody)。
*
* 实现要点:
* · 用**原生 HTML5 拖拽**(draggable=true + dragstart/dragover/drop),
* 不引任何第三方库(本项目零依赖);
* · 拖动过程中实时把被拖行**移动到光标位置**(真·所见即所得),
* 而不是只画一条插入线 —— 个人导航站的列表不长,实时重排体验更直观;
* · **只允许在本板块的 tbody 内拖动**:跨板块拖会被忽略(拖到别的板块前先改「板块」再排);
* · 松手后收集该 tbody 的 id 顺序 → POST 给 site_reorder → 服务端整组归一化。
* · 网站列表**只允许在本板块的 tbody 内拖动**:跨板块拖会被忽略(拖到别的板块前先改「板块」再排);
* · 松手后收集该 tbody 的 id 顺序 → POST 给对应 action(tbody 上的 data-sort-action)
* → 服务端整组归一化。
*/
function initDragSort() {
var bodies = $$('tbody[data-group-id]');
var bodies = $$('tbody[data-sortable]');
if (!bodies.length) return;
var wrap = $('#sites-tabs');
var url = wrap ? wrap.getAttribute('data-reorder-url') : '';
var csrf = (document.querySelector('input[name="csrf"]') || {}).value || '';
bodies.forEach(function (body) {
var dragRow = null;
var action = body.getAttribute('data-sort-action') || 'site_reorder';
var gid = body.getAttribute('data-group-id') || '';
// 落库地址:优先 tbody 自带;网站列表没有时回退到标签容器上的
var url = body.getAttribute('data-reorder-url')
|| (wrap ? wrap.getAttribute('data-reorder-url') : '');
body.addEventListener('dragstart', function (ev) {
var row = ev.target.closest ? ev.target.closest('tr') : null;
@@ -293,7 +299,7 @@
if (!dragRow) return;
dragRow.classList.remove('dragging');
dragRow = null;
saveOrder(body);
saveOrder(body, action, gid, url);
});
});
@@ -309,16 +315,16 @@
return null;
}
/** 收集顺序并提交;成功后更新状态提示 */
function saveOrder(body) {
/** 收集顺序并提交;成功后更新状态提示(action/gid/url 来自所属 tbody 的 data-*) */
function saveOrder(body, action, gid, url) {
if (!url) return;
var gid = body.getAttribute('data-group-id');
var ids = $$('tr', body).map(function (r) { return r.getAttribute('data-id'); }).filter(Boolean);
var status = document.querySelector('.sort-status[data-group-id="' + gid + '"]');
var scope = body.closest('.sites-panel') || body.closest('.card') || document;
var status = $('.sort-status', scope);
var fd = new FormData();
fd.append('action', 'site_reorder');
fd.append('group_id', gid);
fd.append('action', action);
if (gid) { fd.append('group_id', gid); }
fd.append('order', ids.join(','));
fd.append('ajax', '1');
fd.append('csrf', csrf);