# 技术实现方案: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 模式) ```jsx
{ 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' }}> ...
``` ### 4. handleDrop(照搬 TabSettings 逻辑,key 定位) ```js 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 + build(tsdown 产物,弹层 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 清理防拖出弹层残留。 - 边界维持:固定列不参与、服务端零改动、↑↓ 兜底保留、立即持久化不变。