《Ant Design Table 拖拽排序失效?99% 的人都忽略了这个小细节!》
一、现象描述“我参照 Ant Design 官方示例实现了表格行拖拽排序。但拖拽时发现顺序乱跳甚至完全不生效而且没有任何报错。”为什么明明代码和官方示例一样却不行二、排查过程第一反应以为是DraggableBodyRow写错了检查useDrag/useDrop是否配对检查onRow是否正确传递了index和moveRow检查components是否注册发现关键线索年龄列有默认排序{ title: 年龄, sorter: (a, b) a.age - b.age, defaultSortOrder: descend, // 就是它 }推测拖拽修改了dataSource但排序逻辑立即介入把数据重新排列覆盖了拖拽结果验证注释掉defaultSortOrder拖拽恢复正常 ✅三、原理分析Ant Design Table 的排序机制排序状态下表格显示的是排序后的数据而不是dataSource的原始顺序拖拽修改的是dataSource但排序逻辑比拖拽逻辑后执行覆盖了拖拽的结果本质上是两种“排序”逻辑的冲突一种是用户点击列头触发的排序一种是拖拽触发的排序四、解决方案方案做法适用场景方案一移除defaultSortOrder让用户手动点击排序优先保证拖拽功能方案二拖拽开始时清除排序状态两者共存但拖拽时禁用列排序方案三在拖拽结束后重新应用排序保留排序状态拖拽后按当前排序重新排列五、总结与建议教训在使用 Ant Design 高级功能时各个配置项之间可能存在冲突排查时要全面建议如果拖拽和排序都要支持优先考虑用户体验——拖拽时自动清除排序状态或者用弹窗提示“排序状态下无法拖拽”Debug 方法论遇到问题先最小化复现移除其他配置再逐步添加import update from immutability-helper; import React, { useCallback, useRef, useState } from react; import { DndProvider, useDrag, useDrop } from react-dnd; import { HTML5Backend } from react-dnd-html5-backend; import { Button, Form, Input, Table } from antd; import { LockOutlined, UserOutlined } from ant-design/icons; import ./index.scope.css const type DraggableBodyRow; const DraggableBodyRow ({ index, moveRow, className, style, ...restProps }) { const ref useRef(null); const [{ isOver, dropClassName }, drop] useDrop({ accept: type, collect: (monitor) { const { index: dragIndex } monitor.getItem() || {} if (dragIndex index) { return {} } return { isOver: monitor.isOver(), dropClassName: dragIndex index ? drop-over-downward : drop-over-upward } }, drop: (item) { moveRow(item.index, index) } }) const [, drag] useDrag({ type, item: { index, }, collect: (monitor) ({ isDragging: monitor.isDragging(), }), }) drop(drag(ref)) return ( tr ref{ref} className{${className}${isOver ? dropClassName : }} style{{ cursor: move, ...style }} {...restProps} {restProps.children} /tr ) } const DragTable () { const [form] Form.useForm(); const [selectedRowKeys, setSelectedRowKeys] useState([]); const [sortOrder, setSortOrder] useState(null); const getSortTooltip () { if (sortOrder ascend) return 点击降序; if (sortOrder descend) return 取消排序; return 点击升序; }; const [dataSource, setDataSource] useState([ { key: 1, name: 胡胡, age: 32, address: 西湖区湖底公园1号, hobby: 唱歌,跳舞,弹钢琴,画画,写诗,写小说,写剧本,写歌词,写散文,写随笔,写日记,写博客,写微博,写朋友圈,写抖音,写快手,写B站,写知乎,写豆瓣,写小红书,写公众号,写头条号,写百家号,写企鹅号,写新浪微博,写网易号,写搜狐号,写凤凰号,写今日头条号, job: 歌手, salary: 100万, isMarried: 否, hometown: 四川, certification: 无, isRenter: 否, familySize: 3, retirementDate: 2050-01-01, children: [ { key: 1-1, name: 喵喵, age: 42, address: 西湖区湖底公园1号, } ] }, { key: 2, name: 胡彦祖, age: 42, address: 西湖区湖底公园1号, }, { key: 3, name: 胡巴, age: 32, address: 西湖区湖底公园1号, hobby: 唱歌,跳舞,弹钢琴,画画,写诗,写小说,写剧本,写歌词,写散文,写随笔,写日记,写博客,写微博,写朋友圈,写抖音,写快手,写B站,写知乎,写豆瓣,写小红书,写公众号,写头条号,写百家号,写企鹅号,写新浪微博,写网易号,写搜狐号,写凤凰号,写今日头条号, job: 歌手, salary: 100万, isMarried: 否, hometown: 四川, certification: 无, isRenter: 否, familySize: 3, retirementDate: 2050-01-01, }, ]) const defaultColumns [ { title: 姓名, dataIndex: name, key: name, width: 150, filters: [ { text: 胡胡, value: 胡胡, children: [ { text: 胡胡子 }] }, { text: 胡彦祖, value: 胡彦祖, }, ], filterMode: tree, filterSearch: true, onFilter: (value, record) record.name.indexOf(value) 0, }, { title: 年龄, dataIndex: age, key: age, width: 100, sorter: (a, b) a.age - b.age, defaultSortOrder: descend, showSorterTooltip: { title: getSortTooltip() }, editable: true, }, { title: 住址, dataIndex: address, key: address, width: 100, sorter: (a, b) a.name.length - b.name.length, sortDirections: [descend], showSorterTooltip: { title: getSortTooltip() }, }, { title: 爱好, dataIndex: hobby, key: hobby, width: 300, editable: true, }, { title: 工作, dataIndex: job, key: job, width: 150, editable: true, }, { title: 薪资, dataIndex: salary, key: salary, }, { title: 是否成家-老家, dataIndex: isMarried, key: isMarried, width: 200, }, { title: 老家, dataIndex: hometown, key: hometown, width: 100, }, { title: 技能证书, dataIndex: certification, key: certification, width: 150, }, { title: 是否租房, dataIndex: isRenter, key: isRenter, width: 100, }, { title: 几口人, dataIndex: familySize, key: familySize, width: 100, }, { title: 退休时间, dataIndex: retirementDate, key: retirementDate, width: 150, fixed: right, }, ]; const components { body: { row: DraggableBodyRow, }, }; const moveRow useCallback( (dragIndex, hoverIndex) { const dragRow dataSource[dragIndex]; setDataSource( update(dataSource, { $splice: [ [dragIndex, 1], [hoverIndex, 0, dragRow], ], }), ); }, [dataSource], ); const onFinish (values) { console.log(Received values of form: , values); setSelectedRowKeys([]) }; const handleTableChange (pagination, filters, sorter) { console.log(Table change:, { pagination, filters, sorter }); if (sorter.field) { setSortOrder(sorter.order); } }; return div Form form{form} namehorizontal_login layoutinline onFinish{onFinish} Form.Item nameusername rules{[{ required: true, message: Please input your username! }]} Input prefix{UserOutlined classNamesite-form-item-icon /} placeholderUsername / /Form.Item Form.Item namepassword rules{[{ required: true, message: Please input your password! }]} Input prefix{LockOutlined classNamesite-form-item-icon /} typepassword placeholderPassword / /Form.Item Form.Item shouldUpdate {() ( Button typeprimary htmlTypesubmit disabled{ !form.isFieldsTouched(true) || !!form.getFieldsError().filter(({ errors }) errors.length).length } 查询 /Button )} /Form.Item /Form div classNametable-container DndProvider backend{HTML5Backend} Table key{dataSource.map(item item.key).join(,)} components{components} bordered dataSource{dataSource} columns{defaultColumns} onChange{handleTableChange} onRow{(_, index) { const attr { index, moveRow } return attr }} scroll{{ x: max-content }} rowSelection{{ type: checkbox, selectedRowKeys: selectedRowKeys, onChange: (selectedRowKeys, selectedRows) { setSelectedRowKeys(selectedRowKeys); console.log(selectedRowKeys: ${selectedRowKeys}, selectedRows: , selectedRows); }, }} /; /DndProvider /div /div } export default DragTable