news 2026/9/11 4:19:51

React 拖拽排序实战:react-sortablejs 从表格行 handle 到跨列迁移的 4 个场景配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React 拖拽排序实战:react-sortablejs 从表格行 handle 到跨列迁移的 4 个场景配置

React 拖拽排序实战:react-sortablejs 从表格行 handle 到跨列迁移的 4 个场景配置

【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject

后台管理界面里"让用户自定义排列模块面板和表格行"的拖拽排序需求,用 react-sortablejs 半天就能落地。本文按表格行排序、审批流跨列迁移、大列表调优、触摸参数四个场景,把每个场景该配哪些参数、会踩哪些坑讲清楚。

为什么选它

OpenProject 官方前端走的是 dragula 与 @atlaskit/pragmatic-drag-and-drop 路线(见 frontend/package.json),并在 frontend/src/common/drag-and-drop/sortable-lists-engine.ts 里包了一层自研引擎。但在 React 子应用里实现同类交互:react-beautiful-dnd 已停止维护且只适合简单单列;@dnd-kit/sortable 跨列迁移要自己拼列表测量逻辑。react-sortablejs 最省事——传一个 React 数组,它直接替你重排;代价是 API 偏老、维护节奏慢。

30 秒跑通:最小可运行示例

最小配置只需要数据、setList 和一个 handle,下面 17 行可以直接放进任何 Vite/CRA 工程:

import { useState } from 'react'; import { ReactSortable } from 'react-sortablejs'; const MODULES = ['Backlog', 'Sprint', 'This Week', 'Done', 'Blocked']; export function ModuleReorder() { const [modules, setModules] = useState(MODULES); return ( <ReactSortable list={modules} setList={setModules} handle=".grip" animation={150}> {modules.map((name) => ( <div className="module-row" key={name}> <span className="grip" aria-label="拖拽手柄">⋮⋮</span> {name} </div> ))} </ReactSortable> ); }

关键 props 各一句话:

  • list/setList:数据源和 setter,拖拽结束后库回调 setList,你不需要读 DOM。
  • handle:只有匹配选择器的元素能发起拖拽,其余区域保持点击语义。
  • animation:占位块过渡的毫秒数,0–200 之间通常够用。
  • ghostClass:原位占位块的 class,默认值sortable-ghost没有样式,必须自己定义。

🖐 场景 A:带 handle 的行级局部排序

工作包列表动辄几十行,用户想把高优先级条目顶到最前;但整行可拖会和"点击选中/展开"冲突,所以必须提供独立的拖拽手柄。OpenProject 自身的工作包模块就是这个结构(见 frontend/src/app/features/work-packages/)。

下面这段把行内的"编辑"按钮区从拖拽范围里排除:

function WorkPackageTable({ rows, setRows }) { return ( <ReactSortable list={rows} setList={setRows} handle=".row-grip" // 只有左侧手柄能发起拖拽 filter=".row-actions" // 行内操作按钮区不参与 preventOnFilter > {rows.map((row) => ( <div className="wp-row" key={row.id}> <span className="row-grip">⠿</span> <span className="wp-id">WP-{row.id}</span> <span className="wp-subject">{row.subject}</span> <div className="row-actions"><button>编辑</button></div> </div> ))} </ReactSortable> ); }

关键参数:

  • handle=".row-grip":拖拽的唯一入口,行内其他点击不受影响。
  • filter=".row-actions":命中选择器的元素不触发拖拽。
  • preventOnFilter:命中 filter 时阻止默认行为,避免误触浏览器缩放/滚动。
  • 容器默认是div:别把tr直接塞给 ReactSortable,用 div 行模拟表格或改成卡片列表。

高频坑点:

  • 拖拽时条目"消失"→ 默认 ghostClass 无样式,占位块高度塌成 0 → 自定义 ghostClass 并保持占位块原高度。
  • handle 不生效→ 手柄元素必须位于拖拽项(容器直接子元素)内部,写在容器外层或祖先上无效。

📥 场景 B:审批流节点跨列迁移

审批流配置是典型的跨列拖拽:节点要在"部门负责人审核 / 财务审核 / 归档"之间移动。先说清机制:内部 onSort 只在 oldIndex 和 newIndex 都落在当前列表范围内才调 setList,跨列拖拽必然越界,状态不会自动同步,必须在 onEnd 里手动合并。

下面这段统一管理三列状态,跨列时在 onEnd 里搬移:

function ApprovalFlowBoard() { const [nodes, setNodes] = useState({ dept: [{ id: 'n-11', title: '金额超五万需双人复核' }], finance: [{ id: 'n-23', title: '付款方式须银行转账' }], archived: [], }); const onEnd = (evt) => { if (evt.from === evt.to) return; // 同列重排,setList 已处理 const fromKey = evt.from.dataset.stage; const toKey = evt.to.dataset.stage; const moved = [...nodes[fromKey]]; const [node] = moved.splice(evt.oldIndex, 1); const target = [...nodes[toKey]]; target.splice(evt.newIndex, 0, node); setNodes({ ...nodes, [fromKey]: moved, [toKey]: target }); }; return ['dept', 'finance', 'archived'].map((key) => ( <section key={key}> <ReactSortable list={nodes[key]} setList={(list) => setNodes((prev) => ({ ...prev, [key]: list }))} group={{ name: 'approval-chain', pull: true, put: true }} />

const RowItem = React.memo(function RowItem({ row }) { return <div className="wp-row">{row.subject}</div>; }); function LargeList({ rows, setRows }) { const onSort = useCallback((next) => setRows(next), [setRows]); // 稳定引用 return ( <ReactSortable list={rows} setList={onSort} forceFallback // CSS transform 克隆,避开原生 DnD 的布局抖动 fallbackOnBody animation={0} // 大列表关掉占位动画 > {rows.map((row) => <RowItem key={row.id} row={row} />)} </ReactSortable> ); }

关键参数:

  • forceFallback:用 transform 克隆替代原生 HTML5 DnD,条目插入/移除时浏览器不做整块布局重排。
  • fallbackOnBody:克隆节点挂到 body,规避祖先 transform/filter 形成包含块导致克隆偏移。
  • animation={0}:大列表关掉占位过渡,省一帧过渡计算。
  • React.memo+useCallback:setList 每次渲染都是新函数,引用一变库内部会重建 Sortable 实例,先稳定引用再谈性能。

高频坑点:

  • 怎么优化都是全行重渲染→ 行组件没 memo 或 setList 引用不稳定,整个列表跟着重建 → 先查回调引用是否稳定,再 memo 行组件。

📱 场景 D:触摸误触与灵敏度调优

手机和平板上有两个问题:滚动列表误触发拖拽;拖拽中手指滑出可视边缘后不会继续滚动跟随。

<ReactSortable list={cards} setList={setCards} forceFallback // 触摸不支持 HTML5 DnD,必须走 fallback delay={150} // 触摸后等 150ms 再判定拖拽 delayOnTouchOnly // delay 只对触摸生效,鼠标不受影响 scroll scrollSensitivity={40} scrollSpeed={15} > {cards.map((card) => ( <div key={card.id} className="sp-card">{card.name}</div> ))} </ReactSortable>

关键参数:

  • forceFallback:触摸设备不支持 HTML5 原生拖拽,不开 fallback 幽灵块根本不跟手。
  • delay+delayOnTouchOnly:触摸后延迟判定拖拽,滚动时不会误触发;配合delayOnTouchOnly保证鼠标体验不变慢。
  • scrollSensitivity/scrollSpeed:距边缘多少像素触发自动滚动、滚多快,必须真机微调。
  • touchStartThreshold:拖拽启动前的最小移动像素数,默认 5;调大降低误触,调小提升跟手性。

高频坑点:

  • delay 调大后鼠标也变"迟钝"→ delay 默认对鼠标和触摸都生效 → 打开delayOnTouchOnly,把延迟限制在触摸端。

配置速查表

配置项类型默认值一句话用途
listT[]数据源,要渲染的 React 数组
`set

【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 4:19:35

树莓派Pico USB原生设备模式深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 4:19:18

AI Agent用户记忆系统:跨会话身份锚定与分层架构实践

1. 项目概述&#xff1a;为什么“让 Agent 记住你”不是功能升级&#xff0c;而是范式切换我第一次在真实业务场景里部署 AI Agent 时&#xff0c;客户提了个看似简单的需求&#xff1a;“它上次跟我说过我孩子叫小满&#xff0c;这次怎么又问&#xff1f;”——当时我下意识回…

作者头像 李华
网站建设 2026/9/11 4:17:53

MuJoCo逆运动学实操:从末端目标到关节力矩

MuJoCo逆运动学实操&#xff1a;从末端目标到关节力矩 【免费下载链接】mujoco Multi-Joint dynamics with Contact. A general purpose physics simulator. 项目地址: https://gitcode.com/GitHub_Trending/mu/mujoco 写机械臂的关节控制时&#xff0c;末端要落在毫米级…

作者头像 李华
网站建设 2026/9/11 4:15:32

copyparty 主题定制速成指南:3 步上手

copyparty 主题定制速成指南&#xff1a;3 步上手 【免费下载链接】copyparty Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails all in one file 项目地址: https://gitcode.com/GitHub_Tre…

作者头像 李华
网站建设 2026/9/11 4:15:28

Ice:macOS 菜单栏管理的 4 步安装与排序教程

Ice&#xff1a;macOS 菜单栏管理的 4 步安装与排序教程 【免费下载链接】Ice Powerful menu bar manager for macOS 项目地址: https://gitcode.com/GitHub_Trending/ice/Ice Ice 是一款免费的 macOS 菜单栏管理工具&#xff0c;把屏幕右上角堆成山的图标收进隐藏区&am…

作者头像 李华
网站建设 2026/9/11 4:13:12

200个Agent并行实操复盘:任务拆解与上下文管理

一篇值得认真看的Agent并行实操复盘。先说结论&#xff1a;200多个Agent并行跑起来不难&#xff0c;难的是让它们并行干活之后还能把结果拼成一件完整、能用的东西。标题里那句“SpaceX工程师的玩法”&#xff0c;其实不是炫技&#xff0c;而是一套非常朴素的工程化思路&#x…

作者头像 李华