当前位置: 首页 > news >正文

《Ant Design Table 拖拽排序失效?99% 的人都忽略了这个小细节!》

一、现象描述

“我参照 Ant Design 官方示例,实现了表格行拖拽排序。但拖拽时发现顺序乱跳,甚至完全不生效,而且没有任何报错。”

  • 为什么明明代码和官方示例一样,却不行?

二、排查过程

  1. 第一反应:以为是DraggableBodyRow写错了

    • 检查useDrag/useDrop是否配对

    • 检查onRow是否正确传递了indexmoveRow

    • 检查components是否注册

  2. 发现关键线索:年龄列有默认排序

    { title: '年龄', sorter: (a, b) => a.age - b.age, defaultSortOrder: 'descend', // 👈 就是它! }
  3. 推测:拖拽修改了dataSource,但排序逻辑立即介入,把数据重新排列,覆盖了拖拽结果

  4. 验证:注释掉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} name="horizontal_login" layout="inline" onFinish={onFinish}> <Form.Item name="username" rules={[{ required: true, message: 'Please input your username!' }]} > <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="Username" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: 'Please input your password!' }]} > <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="Password" /> </Form.Item> <Form.Item shouldUpdate> {() => ( <Button type="primary" htmlType="submit" disabled={ !form.isFieldsTouched(true) || !!form.getFieldsError().filter(({ errors }) => errors.length).length } > 查询 </Button> )} </Form.Item> </Form> <div className='table-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
http://www.jsqmd.com/news/1159903/

相关文章:

  • Claude Code审查配置终极校验清单:覆盖OWASP Top 10、PCI-DSS 4.2及ISO/IEC 27001:2022的9项强制合规项
  • 论文AI率30%降不下来?这几招直接把论文AI率干到5%以下
  • 2026年华为、红帽持证应届生,一线真实起薪差多少?
  • Open Design 生成可交互原型完整实操教程
  • Fable 4D飞溅效果:动态点云渲染技术解析与实践
  • Cursor AI生成HTML页面的7个致命误区:92%开发者踩坑的隐藏陷阱与绕过方案
  • 认知神经科学研究报告【20260110】
  • Zoutendijk可行方向法 MATLAB 2024b 实现:3步解析线性约束非线性规划求解
  • Godot游戏开发:Phantom Camera插件实现专业级镜头控制
  • 福建工厂采购 SolidWorks 全攻略,版本、报价、避坑一次讲清
  • 海归大厂面试被评价不接地气?这3个话术能提供履历合规修正「蒸汽求职分享」
  • code0 claude-sonnet-4-5-20250929 企业实战:跨部门文档协作的 AI 模型配置指南
  • 锂电池保护电路DW01失效分析:3种常见故障现象与CS引脚电压排查
  • Codex Plugin 教程:Marketplace、Plugin、Skill 是如何被识别和加载的?
  • Flowframes视频插帧终极指南:3步将24fps视频流畅提升至60fps
  • 有什么工具可以导出智能体对话?豆包聊天记录备份FAQ版
  • OpenCode嵌入式AI开发工具链:Docker化全闭环部署指南
  • 基础 2 —— Docker 的下载与安装
  • 帝舵中国官方售后服务中心|服务电话及全部网点地址权威信息通告(2026年7月更新) - 帝舵中国官方服务中心
  • 调查研究-224 Prefill 与 Decode 分离:高并发 LLM Serving 的下一层架构
  • 企业生成式AI落地实战:从战略规划到规模部署的完整指南
  • 米哈游 2024 年营收结构解析:二次元 ARPG 如何贡献超 300 亿流水与 60% 净利率
  • 主流单片机开发平台如何与三易串口屏通讯
  • SVPWM 空间矢量调制:从 MATLAB/Simulink 仿真到 STM32 实现,谐波降低 15%
  • 【大数据毕业设计】基于 Django 的人口普查大数据挖掘研判系统的设计与实现 基于 Django 的人口结构普查数据分析系统(源码+文档+远程调试,全bao定制等)
  • 广州吟颂调研:职称评审的“可控性”博弈:实力之外,拼的是信息与细节
  • 2026精选GEO服务商推荐榜单:聚焦GEO优化技术实力的权威解析
  • 强力解析:UABEA如何彻底改变你的Unity游戏资源编辑工作流
  • 从Google Search到生产级可信AI Agent:Agentic RAG工程化实战指南
  • QtIFW(Qt Installer Framework 4.8.1)打包Qt程序