表格列宽拖拽调整 — 问题总结
版本
- “vue”: “2.6.11”,
- “vue-draggable-resizable”: “^2.3.0”,
- "ant-design “:”1.7.0“
问题 1:thDom为 null 导致getBoundingClientRect报错
现象:TypeError: Cannot read properties of null (reading 'getBoundingClientRect')
根因:onDrag中this.$set(draggingState, key, 0)触发 Vue 响应式重渲染。旧<th>销毁时 Vue 2 的ref回调用null清理闭包中的thDom,之后dragstop事件拿到的就是null。
解决:用非响应式对象this._dragX作为拖拽手柄x的数据源,onDrag期间不触发重渲染,仅在onDragstop时通过$set提交最终值。外加if (!thDom) return防御。
问题 2:vue-draggable-resizabletransform 不对、拖拽错列、拖拽点乱跑
现象:拖拽 handle 的transform位置计算错误,拖拽 A 列实际改变了 B 列的宽度。
根因:vue-draggable-resizable内部用transform: translateX()驱动拖拽,但 CSS 把 handle 固定在right: -5px(left: auto !important)。组件内部的x定位和 CSS 定位坐标系冲突,onDrag上报的x值不可信。
解决:移除vue-draggable-resizable依赖,改用原生mousedown/mousemove/mouseup实现。Handle 仅靠 CSS(right: -5px)定位,用鼠标位移增量delta计算新宽度,彻底消除坐标系冲突。
问题 3:拖拽一次后位置不确定刷新 / 跳动
现象:拖拽过程中 handle 位置跳动,释放后列宽不确定。
根因:同问题 1,onDrag触发重渲染导致vue-draggable-resizable的xprop 被重置。
解决:随问题 2 一并解决,原生事件方案不依赖任何响应式状态驱动拖拽位置。
问题 4:Scoped 样式不生效 — handlewidth: 0导致无法展示拖拽按钮
现象:.table-draggable-handle的width: 10px不生效,handle 宽度为 0,无法拖拽。其他样式(如position: absolute)正常。
根因:Vue 2 scoped 样式通过data-v-xxx属性匹配,但h()在 render 函数中创建的元素不在 template 里,不会自动添加 scope 属性。scoped 块(line 814)的width对 handle 不生效,实际走的是非 scoped 块(line 1040),而该块之前缺少width。
解决:在非 scoped 样式块中补充width: 10px; top: 0; z-index: 1,去掉vue-draggable-resizable时代的残留样式(height: 100% !important; left: auto !important)。
问题 5:替换 header cell 后排序失效
现象:拖拽列无法点击排序。
根因:ant-design-vue 传给components.header.cell的props是完整 VNode data 对象(含class、on、style等顶层属性),但代码把所有属性塞进了attrs({ attrs: { ...restProps } }),导致on.click(排序回调)、class(排序样式)丢失。
解决:改为正确分层:
// 之前(错误){attrs:{...restProps,width:col.width},class:'resize-table-th'}// 之后(正确){...restProps,attrs:{...restProps.attrs,width:col.width},class:['resize-table-th',restProps.class]}问题 6:拖拽结束后触发了排序
现象:拖拽过程中sorter图标变化,释放鼠标后触发了排序请求。
根因:mousedown之后会触发click事件,click冒泡到<th>被 ant-design-vue 的排序逻辑捕获。
解决:在拖拽 handle 上添加click: e => { e.stopPropagation() },阻止click事件冒泡到<th>。
问题 7:depColumns树形结构只遍历了顶层
现象:depColumns是树形结构({ title: "基本情况", children: [...] }),叶子节点才有dataIndex和width。但created初始化和resizeableTitle查找都只处理了顶层数组。
解决:两处都改为递归遍历:
created—walkColumns递归展开所有层级,只收集叶子节点(无children或children为空的列)resizeableTitle—findColumn递归搜索children找到匹配的列
问题 8:需要按列控制可拖拽 + 最大/最小宽度
需求:
- 只有标记了
isDraggable: true的列才出现拖拽手柄 - 支持
minWidth/maxWidth限制拖拽范围
解决:
- 判断条件从
!col.width改为!col.isDraggable onMouseMove中用Math.min(Math.max(newWidth, col.minWidth || 50), col.maxWidth || Infinity)限制范围,minWidth 默认 50px
最终列配置示例
{title:"业务线",dataIndex:"serviceLine",width:70,isDraggable:true,// 显式开启拖拽minWidth:100,// 最小宽度(默认 50)maxWidth:400,// 最大宽度(默认无上限)}最终列代码示例
<template><a-table bordered:columns="columns":components="tableComponents":data-source="data"></a-table></template><script>importVuefrom'vue';constcolumns=[{title:'Date',dataIndex:'date',width:200,},{title:'Amount',dataIndex:'amount',width:100,},{title:'Type',dataIndex:'type',width:100,},{title:'Note',dataIndex:'note',width:100,},{title:'Action',key:'action',isDraggable:true,// 显式开启拖拽minWidth:100,// 最小宽度(默认 50)maxWidth:400,// 最大宽度(默认无上限)},];constdata=[{key:0,date:'2018-02-11',amount:120,type:'income',note:'transfer',},{key:1,date:'2018-03-11',amount:243,type:'income',note:'transfer',},{key:2,date:'2018-04-11',amount:98,type:'income',note:'transfer',},];exportdefault{name:'App',data(){return{depColumns:data,};},computed:{tableComponents(){return{header:{cell:this.resizeableTitle,},}},},created(){constdraggingMap={}constwalkColumns=(cols)=>{cols.forEach(col=>{if(col.children&&col.children.length){walkColumns(col.children)}else{constk=col.dataIndex||col.keyif(col.width){draggingMap[k]=col.width}}})}walkColumns(this.depColumns)this.draggingState=draggingMap},methods:{resizeableTitle(h,props,children){letthDom=nullconst{key,...restProps}=propsconstfindColumn=(cols,key)=>{for(constcolofcols){if(col.children&&col.children.length){constfound=findColumn(col.children,key)if(found)returnfound}else{constk=col.dataIndex||col.keyif(k===key)returncol}}returnnull}constcol=findColumn(this.depColumns,key)if(!col||!col.isDraggable){returnh('th',{...restProps},children)}letstartX=0letstartWidth=0constonMouseDown=e=>{e.preventDefault()e.stopPropagation()startX=e.pageX startWidth=thDom?thDom.getBoundingClientRect().width:col.widthconstonMouseMove=e=>{constdelta=e.pageX-startXconstnewWidth=startWidth+deltaconstmin=col.minWidth||50constmax=col.maxWidth||Infinitycol.width=Math.min(Math.max(newWidth,min),max)}constonMouseUp=()=>{document.removeEventListener('mousemove',onMouseMove)document.removeEventListener('mouseup',onMouseUp)document.body.style.cursor=''document.body.style.userSelect=''constfinalWidth=thDom?thDom.getBoundingClientRect().width:col.widththis.$set(this.draggingState,key,finalWidth)col.width=finalWidth}document.addEventListener('mousemove',onMouseMove)document.addEventListener('mouseup',onMouseUp)document.body.style.cursor='col-resize'document.body.style.userSelect='none'}consthandleEl=h('div',{class:'table-draggable-handle',on:{mousedown:onMouseDown,click:e=>{e.stopPropagation()},},})returnh('th',{...restProps,attrs:{...restProps.attrs,width:col.width},class:['resize-table-th',restProps.class],ref:r=>{thDom=r},},[...(Array.isArray(children)?children:[children]),handleEl,])},}};</script><style lang="less">.resize-table-th{position:relative;.table-draggable-handle{position:absolute;top:0;right:-5px;bottom:0;width:10px;z-index:1;cursor:col-resize;touch-action:none;}}</style>