Files

4.5 KiB
Raw Permalink Blame History

技术实现方案:20-策略列设置拖拽排序

总体思路

在 ColumnSettingsPopover.jsx 内复用 Tab 设置(SettingsSection.jsx TabSettings)已验证的 原生 HTML5 Drag & Drop 模式:行 draggable + onDragStart/onDragOver/onDrop + 落点高亮 + drop 重排, 零第三方依赖。持久化沿用现有 strategy-columns/update 整表覆盖 RPC,把「保存」语义从手动按钮改为 每次变更(drop / 勾选 / ↑↓)立即提交。

改动点(单文件:src/client/views/ColumnSettingsPopover.jsx

1. 状态

  • 已有 dragKey(未被使用,正好用于 DnD):当前被拖行的 column.key
  • 新增 overKey:当前悬停落点的 column.key(用于高亮);
  • saving 保持:正在写库时禁止再次拖动/勾选。

2. 持久化函数(替换原 save

  • 新增 persist(nextList, msg):提交整表 {key, visible} → strategy-columns/update 成功 Toast 提示 + onSaved() 刷新;失败 Toast 错误 + 本地回滚(还原为服务端最新列配置)。
  • 勾选显隐:切完立即 persist(不再等「保存」按钮);
  • ↑↓ 箭头:移动完立即 persist;
  • drop 重排:重排完立即 persist(对齐 Tab 设置)。

3. 拖拽事件(照搬 TabSettings 模式)

<div key={c.key} draggable={!saving}
     onDragStart={(e) => { setDragKey(c.key); e.dataTransfer.effectAllowed = 'move'; }}
     onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverKey(c.key); }}
     onDragLeave={() => { if (overKey === c.key) setOverKey(null); }}
     onDrop={(e) => { e.preventDefault(); handleDrop(c.key); }}
     style={{ cursor: saving ? 'default' : 'grab',
              background: overKey === c.key ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent' }}>
  <span></span>
  <button></button><button></button>
  <input type="checkbox" ... />
  ...
</div>

4. handleDrop(照搬 TabSettings 逻辑,key 定位)

const handleDrop = (targetKey) => {
  if (!dragKey || dragKey === targetKey) { setDragKey(null); setOverKey(null); return; }
  const from = list.findIndex((c) => c.key === dragKey);
  const to = list.findIndex((c) => c.key === targetKey);
  if (from < 0 || to < 0) { setDragKey(null); setOverKey(null); return; }
  const next = list.slice();
  const [moved] = next.splice(from, 1);
  next.splice(to, 0, moved);
  setDragKey(null); setOverKey(null);
  persist(next, `「${moved.label}」已移动`, next);
};

5. UI 文案

  • 标题下提示改为:「勾选显示列,按住 ≡ 拖动调整顺序;代码/名称/操作固定」;
  • 底部按钮区:去掉「保存」,保留「关闭」;
  • 每行最前加 ≡ 手柄(配色沿用 Tab 设置 #999/label-tertiary)。

边界与不做

  • 服务端、API、表格渲染零改动(strategy-columns/update 已是整表覆盖语义);
  • 固定列不参与排序,不进列表(现状维持);
  • 不做方案 C(表格表头直接拖列头),不引入第三方 DnD 库;
  • saving 期间禁用拖拽与勾选,防并发写。

回归

  • scripts/test-r013-columns.mjs(列配置归一化/读写的服务端逻辑,未改动应全绿);
  • npm run typecheck + buildtsdown 产物,弹层 JSX 变更需重编 web bundle);
  • 页面人工验收:拖拽重排 / 落点高亮 / drop 即存 / ↑↓ 微调 / 刷新保持。

实现纪要(2026-09-10,老师验收反馈后修订)

老师反馈:初版按原方案做的是「整行高亮」(overKey → 目标行淡蓝底),拖动时无法判断会插入目标列的前面还是后面 观感不符合习惯。定稿修订为 间隙高亮线

  • 落点判定:拖动悬停时读鼠标在目标行内的 Y 坐标getBoundingClientRect),上半 → 插入该行之前(线画行上缘), 下半 → 插入该行之后(线画行下缘);两行之间显示 3px 主色圆角高亮线(absolute 定位在行自身 padding 内,left/right 6, 无负偏移无裁剪风险),指示精确插入位置。
  • drop 判定:drop 事件内直接按坐标重算 before(不依赖 state 闭包,避免滞后);无操作原地(insertAt 等于原槽位) 跳过写库;移除后目标下标左移(to > from 时 to-1)。
  • 状态:overKey(整行)→ overPos { idx, half }(间隙线锚点行 + 上下缘);保留 onDragEnd 清理防拖出弹层残留。
  • 边界维持:固定列不参与、服务端零改动、↑↓ 兜底保留、立即持久化不变。