后台排序改造:按板块标签页 + 拖拽排序 + 顺序完全固定
- 后台网站列表按板块分标签页,每个板块单独调顺序;用 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:
@@ -111,6 +111,110 @@ SQL);
|
||||
if ($isEmpty) {
|
||||
seed_groups_and_sites($pdo);
|
||||
}
|
||||
|
||||
// 老库升级:把排序号**归一化成稀疏值**(第 n 条 = n*SORT_STEP),只跑一次。
|
||||
//
|
||||
// 为什么要归一化:旧库的 sort_order 多是连续的 0,1,2…(相邻只差 1,没有空隙),
|
||||
// 而「拖拽把某条插到另两条之间」需要算出**整数**中间值 —— 1 与 2 之间没有整数。
|
||||
// 归一化后每两条之间留出 SORT_STEP,拖拽时任意插入点都有余量。
|
||||
//
|
||||
// 为什么必须**只跑一次**:db_init() 是**每次请求**都会执行的。若无条件重排,
|
||||
// 用户在后台刚拖好的顺序,会在下一个请求里被按「旧顺序」覆盖回去 ——
|
||||
// 与上面「联想词按键」那次是同一个坑。用 settings 里的哨兵键把关,跑过就永不再跑。
|
||||
//
|
||||
// 为什么这次归一化**不会打乱**用户既有顺序:重排用的排序键仍是 `sort_order, id`,
|
||||
// 是**稳定排序** —— 相对次序不变,只把「差值」放大成整齐的间隙。
|
||||
$migSortDone = (int)$pdo->query("SELECT COUNT(*) FROM settings WHERE k='mig_sort_sparse'")->fetchColumn() > 0;
|
||||
if (!$migSortDone) {
|
||||
normalize_sort_orders($pdo);
|
||||
$pdo->exec("INSERT INTO settings (k,v) VALUES ('mig_sort_sparse','1')
|
||||
ON CONFLICT(k) DO UPDATE SET v='1'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把三张表的排序号重写为 (序号 × SORT_STEP),同组内按「原 sort_order, id」稳定排序。
|
||||
* 供一次性迁移调用(也是 apply_sort_order() 的整组归一化语义)。
|
||||
*/
|
||||
function normalize_sort_orders(PDO $pdo): void
|
||||
{
|
||||
// 无分组维度的两张表
|
||||
foreach (['groups', 'engines'] as $tbl) {
|
||||
$rows = $pdo->query("SELECT id FROM {$tbl} ORDER BY sort_order, id")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$upd = $pdo->prepare("UPDATE {$tbl} SET sort_order=? WHERE id=?");
|
||||
foreach ($rows as $i => $rid) {
|
||||
$upd->execute([($i + 1) * SORT_STEP, (int)$rid]);
|
||||
}
|
||||
}
|
||||
// sites 按板块分别归一化(不同板块之间互不影响)
|
||||
$gids = $pdo->query("SELECT id FROM groups ORDER BY sort_order, id")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$upd = $pdo->prepare('UPDATE sites SET sort_order=? WHERE id=?');
|
||||
$sel = $pdo->prepare('SELECT id FROM sites WHERE group_id=? ORDER BY sort_order, id');
|
||||
foreach ($gids as $gid) {
|
||||
$sel->execute([(int)$gid]);
|
||||
foreach ($sel->fetchAll(PDO::FETCH_COLUMN) as $i => $rid) {
|
||||
$upd->execute([($i + 1) * SORT_STEP, (int)$rid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取「同组最后一条的 sort_order + SORT_STEP」——**新增条目专用**。
|
||||
*
|
||||
* 这是用户明确要求的行为:新增的链接一律排在**最后**(得到一个很大的顺序号),
|
||||
* 直到用户手动把它拖到别处,它才拥有一个「正常」的顺序。
|
||||
* ⚠️ 不要改回 `MAX+1`:那只在「旧数据恰好是连续整数」时才等于「排最后」,
|
||||
* 一旦 sort_order 之间有间隙(拖拽后必然如此),MAX+1 会**撞进已有间隙**里,
|
||||
* 让新条目落到用户排好的中间 —— 这正是用户抱怨的「排序被打乱」。
|
||||
*
|
||||
* @param int|null $groupId 该表的分组值;null = 该表没有分组维度(groups / engines)
|
||||
*/
|
||||
function next_sort_order(string $table, ?int $groupId = null, string $groupCol = 'group_id'): int
|
||||
{
|
||||
$pdo = db();
|
||||
if ($groupId === null) {
|
||||
$max = (int)$pdo->query("SELECT COALESCE(MAX(sort_order),0) FROM {$table}")->fetchColumn();
|
||||
} else {
|
||||
$st = $pdo->prepare("SELECT COALESCE(MAX(sort_order),0) FROM {$table} WHERE {$groupCol}=?");
|
||||
$st->execute([$groupId]);
|
||||
$max = (int)$st->fetchColumn();
|
||||
}
|
||||
return $max + SORT_STEP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按给定 id 顺序**整组重写**排序号(第 n 条 = n*SORT_STEP),返回实际写入行数。
|
||||
*
|
||||
* 这是拖拽排序的落库方式:一次拖动后把整个组的顺序提交上来、整组归一化。
|
||||
* 好处是每次拖完间隙都恢复整齐,之后新增仍天然排在最后。
|
||||
*
|
||||
* @param int[] $ids 该组按新顺序排列的 id(未列出/不属于本组的会被忽略)
|
||||
* @param int|null $groupId 非 null 时做**归属校验**:不属于该组的 id 直接跳过
|
||||
* (防止一个被篡改的请求把别的板块的条目拖进来)
|
||||
*/
|
||||
function apply_sort_order(string $table, array $ids, ?int $groupId = null, string $groupCol = 'group_id'): int
|
||||
{
|
||||
$pdo = db();
|
||||
$upd = $pdo->prepare("UPDATE {$table} SET sort_order=? WHERE id=?");
|
||||
$ck = $groupId === null ? null : $pdo->prepare("SELECT COUNT(*) FROM {$table} WHERE id=? AND {$groupCol}=?");
|
||||
$n = 0;
|
||||
$i = 0;
|
||||
foreach ($ids as $rid) {
|
||||
$rid = (int)$rid;
|
||||
if ($rid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($ck !== null) {
|
||||
$ck->execute([$rid, $groupId]);
|
||||
if ((int)$ck->fetchColumn() === 0) {
|
||||
continue; // 不属于本组 → 忽略
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
$upd->execute([$i * SORT_STEP, $rid]);
|
||||
$n++;
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
|
||||
function seed_settings(PDO $pdo): void
|
||||
|
||||
Reference in New Issue
Block a user