后台排序改造:按板块标签页 + 拖拽排序 + 顺序完全固定

- 后台网站列表按板块分标签页,每个板块单独调顺序;用 URL hash
  (#sgroup-<gid>) 定位,上移/下移提交后回跳到原来的板块
- 支持鼠标拖拽排序(原生 HTML5 拖拽,零库依赖):拖动时实时重排预览,
  松手后整组落库(Ajax 动作 site_reorder,带 CSRF 校验与板块归属校验,
  防止跨板块搬条目)
- 稀疏排序:新增链接的顺序号 = 同板块 MAX + SORT_STEP(1000),天然排最后;
  拖拽落库、历史数据迁移时整组归一化为 index * SORT_STEP
- 修复真根因:旧机制靠「交换两条 sort_order」上移/下移,一旦历史数据里
  出现重复值,交换即无效、位置退化为按 id 排序 —— 这正是「排了顺序又被
  打乱」的来源。新增一次性归一化迁移,用 settings.mig_sort_sparse 作哨兵,
  避免每次请求重复覆盖用户设置
- 新增/修改链接不再用 MAX+1(MAX+1 会落进已有间隙、插到用户排好的中间)
- 改变所属板块时,该条目自动排到新板块的最后
- 后台页面 + 拖拽手柄做了配套样式,触屏设备降级为上下移按钮
This commit is contained in:
2026-09-29 09:21:51 +08:00
parent aac6d3e0f4
commit 717a8877bb
6 changed files with 443 additions and 41 deletions
+46
View File
@@ -129,6 +129,52 @@ body.admin { background: #eef0f3; color: #333; font-size: 14px; }
/* 已隐藏的行稍作弱化 */
.tbl tr.row-off td:first-child, .tbl tr.row-off td:nth-child(2) { color: #98a2b3; }
/* ===== 网站列表:板块标签页 + 拖拽排序 ===== */
/* 板块标签条(后台自己的配色,不复用前台的 .tabs —— 前台的底色是半透明,
放在后台白卡上会显得发灰) */
.sites-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.sites-tabs .tab {
display: inline-flex; align-items: center; gap: 6px;
border: 1px solid #dfe3e8; background: #f7f9fb; color: #4a5568; cursor: pointer;
padding: 7px 14px; border-radius: 999px; font-size: 13px; transition: all .15s;
}
.sites-tabs .tab:hover { background: #eef2f7; }
.sites-tabs .tab.active { background: #2F6BF7; border-color: #2F6BF7; color: #fff; }
.sites-tabs .tab .tab-count {
display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 999px;
background: rgba(0,0,0,.08); font-size: 11px; line-height: 16px; text-align: center;
}
.sites-tabs .tab.active .tab-count { background: rgba(255,255,255,.28); }
/* 面板:一次只显示一个板块 */
.sites-panel { display: none; }
.sites-panel.active { display: block; }
/* 拖拽手柄列 */
.tbl-sortable .th-drag, .tbl-sortable .td-drag { width: 30px; padding-right: 0; }
.drag-handle {
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 26px; border-radius: 5px; color: #b6bec9; cursor: grab;
vertical-align: middle; user-select: none;
}
.drag-handle:hover { background: #eef2f7; color: #6b7684; }
tr[draggable="true"] { cursor: default; }
/* 正在被拖动的那一行 */
.tbl tr.dragging { opacity: .45; background: #eef3ff !important; }
.tbl tr.dragging td { background: #eef3ff !important; }
/* 拖拽/保存状态提示(每个板块一条) */
.sort-status { min-height: 18px; margin: 8px 0 0; }
.sort-status:empty { display: none; }
/* 触屏等不支持 HTML5 拖拽的环境:手柄给个提示色,微调按钮仍可用 */
@media (hover: none) {
.drag-handle { color: #c8cfd8; }
.sort-status { display: none; }
}
/* 登录页 */
.login-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-card { width: 340px; background: #fff; border-radius: 14px; padding: 30px 26px; box-shadow: 0 10px 30px rgba(0,0,0,.08); text-align: center; }