ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Element UI表格列宽拖拽全攻略:从原理到持久化

Element UI表格列宽拖拽全攻略:从原理到持久化 后台管理系统里表格组件几乎天天都离不开而table列宽能不能拖拽这个需求看起来是小事做起来却容易翻车。我最初接手项目时产品经理只丢了一句话“表格列宽让用户自己拉一拉操作列那几个按钮别被拉没了。”就这一句话我前后折腾了两个版本才把Element UI的列宽拖拽逻辑彻底弄明白。今天就把这套方案完整拆开讲先解释清楚resizable的全局和列级控制逻辑再给可以直接复制的代码最后把拖拽事件监听、列宽持久化、以及各种坑一并整理出来帮你少走弯路。先说结论Element UI的el-table组件默认就支持列宽拖拽但有一个容易被忽略的前提同时确实可以通过列上的resizable属性单独禁止某一列调整宽度。这个方案不需要引入任何第三方库纯组件自带能力就够了适合所有Vue 2 Element UI的项目也适用于大部分读过文档但没实际踩过坑的前端开发者。1. 拆解需求列宽拖拽是怎么被“一锤子”开启的1.1 默认行为到底是怎么样的很多刚接触Element UI的人会有个误解以为表格加了一个el-table就自带拖拽功能。实际上el-table的resizable属性默认确实是true但真正让你能拖出一根竖线来调整列宽的视觉反馈依赖的是border属性。也就是说如果你的表格没有加border边框鼠标移到表头列间隙时不会出现可拖拽的竖线光标提示。我实测过的组合情况是这样的只加el-table不传任何属性代码层面resizable为true但视觉上没有拖拽分隔线用户体验几乎等于不可拖拽。加el-table border表头会出现明显的列边界鼠标移到边界会变成左右拉伸的光标拖拽生效。加el-table :resizablefalse即使有border也不会出现可拖拽的光标列宽固定。所以核心结论是开启列宽拖拽最稳的做法是同时满足“border开启 全局resizable为true”。border负责给出拖拽手柄的视觉边界resizable负责真正响应鼠标拖拽事件。两者缺一不可。1.2 “全部能拖”不等于“够用”列级禁用才是关键全局能拖了第一版上线后产品很快就来反馈“表格左侧的复选框列、序号列、操作列的按钮区别让人乱拉拉完之后按钮要么被截断要么和旁边列挤在一起能不能把这几列锁死”这就是列级禁用resizable的典型场景。Element UI的el-table-column组件上也有一个resizable属性它的优先级高于表格级别的resizable。列上的resizable取值为false时这个列对应的表头单元格不会触发拖拽逻辑。这里有一个细节你需要弄清列级resizable默认是undefined此时会继承表格的全局resizable配置。一旦你显式声明了false这个列就彻底锁定。这种继承覆盖的机制和CSS的继承有点像属于“局部覆盖全局”的设计思路。1.3 值得记住的三层开关模型用一张思维导图的方式记就行表格层el-table :resizable控制全局开关列层el-table-column :resizable控制局部开关border属性控制拖拽手柄是否可见。实际开发中我一般建议全局保持默认true需要锁死的列单独写false这样既灵活又不会把代码写散。顺带说一句有人会问为什么操作列必须锁死。我的理解是操作列通常宽度固定里面放的是按钮、图标、下拉菜单这类交互元素。用户拖拽列宽容易误触按钮或者把按钮挤压得换行、变形影响操作体验。像序号列、多选列这种承载固定逻辑的列更是没有拖拽的必要锁死才符合产品预期。2. 直接可用的实现方案五分钟搞定拖拽与列级禁用2.1 基础版给整个表格开启列宽拖拽最简单的写法如下template el-table :datatableData border stylewidth: 100% el-table-column propdate label日期 width160/el-table-column el-table-column propname label姓名 width120/el-table-column el-table-column propaddress label地址 min-width200/el-table-column /el-table /template这段代码里我没有显式写resizable但因为默认值是true配合border之后三个列都能拖拽。需要注意列宽尽量用width或min-width显式指定否则表格布局会自行分配宽度拖拽时可能产生不可预期的跳动。width和min-width的区别后面第3章会专门讲这里先记住一个结论固定宽度列用width希望随容器弹性伸缩的列用min-width。2.2 进阶版锁死某一列的宽度直接在需要锁死的列上加上:resizablefalse即可el-table border !-- 多选列锁死 -- el-table-column typeselection width50 :resizablefalse/el-table-column !-- 序号列锁死 -- el-table-column typeindex label# width60 :resizablefalse/el-table-column !-- 普通数据列可拖拽 -- el-table-column propname label姓名 min-width120/el-table-column !-- 操作列锁死 -- el-table-column label操作 width180 :resizablefalse template slot-scopescope el-button typetext sizesmall编辑/el-button el-button typetext sizesmall删除/el-button /template /el-table-column /el-table实际操作时你会发现锁死的列鼠标移上去不会出现拖拽光标表头边界不会响应mousedown非常干净。优先级规则拿捏住即可列上显式写了resizable就以列为主没写就听表格的。2.3 一个完整场景后台用户管理表格再给一个更贴合真实项目的组合示例包含了锁定列、可拖拽列、以及带溢出提示的单元格template div classuser-table-wrapper el-table :datauserList border header-dragendhandleHeaderDragEnd stylewidth: 100% el-table-column typeselection width50 :resizablefalse fixedleft/el-table-column el-table-column typeindex label序号 width60 :resizablefalse fixedleft/el-table-column el-table-column propusername label用户名 min-width120 show-overflow-tooltip/el-table-column el-table-column propnickname label昵称 min-width120 show-overflow-tooltip/el-table-column el-table-column propemail label邮箱 min-width200 show-overflow-tooltip/el-table-column el-table-column propcreateTime label创建时间 width180/el-table-column el-table-column label操作 width160 :resizablefalse fixedright template slot-scopescope el-button typetext sizesmall clickhandleEdit(scope.row)编辑/el-button el-button typetext sizesmall classdanger-text clickhandleDisable(scope.row)禁用/el-button /template /el-table-column /el-table /div /template这里有个经验加了fixedleft或fixedright的列本身就不能拖拽调整宽度这是Element UI内部对固定列的特殊处理。所以即使不写:resizablefalse固定列也是锁死的但为了代码语义清晰我仍然建议显式写出来方便其他同事阅读。2.4 锁死列的宽度为什么要用width而不用min-width在锁死的列上推荐固定用width。原因很简单锁死意味着无论表格如何变化这一列的宽度都不能变。如果用了min-width一旦表格总宽度不足列宽会被压缩锁死的效果就不彻底了。而且对于操作列这种需要腾出稳定空间给按钮的场景固定width是产品体验上的硬要求。3. 深入原理Element UI内部的列宽拖拽到底做了什么3.1 源码视角列宽调整的事件链条如果只看文档resizable只是一个布尔属性但真正去看源码会发现列宽拖拽背后有一整套事件逻辑。Element UI在表头单元格上监听了mousedown事件触发时会记录起始坐标和列的原始宽度然后在document上动态挂载mousemove和mouseup监听拖拽过程中实时计算鼠标位移量最终计算出新宽度并更新到表格布局。说人话就是你按下鼠标的那一刻开始组件就在背后“跟踪”你的鼠标移动距离鼠标往右挪多少列宽就加多少鼠标往左挪多少列宽就减多少松手时停止。这就是为什么拖拽调宽度看起来特别跟手几乎没有延迟。3.2 拖拽代理线为什么拖的时候会看到一根灰色竖线Element UI在拖拽过程中会添加一个.el-table__column-resize-proxy的DOM节点也就是你看到的那根竖直灰线。它只负责显示拖拽位置并不会实时改变列宽而是在松手那一刻才把列宽真正应用到列配置上。这样做的好处是拖拽过程中的重排操作被降到最低表格不会一卡一卡的。理解了这一点就能解释一个现象拖拽过程中数据列的单元格内容不会跟着变宽只有松手后表格才“啪”一下重新布局。这是性能优化策略不是bug。3.3 width、min-width与fit属性的三角关系Element UI表格布局中fit属性默认是true意思是“列的宽度总和自动撑满整个表格”。当fit为true时如果所有列的width之和小于表格实际宽度表格会自动把多出来的空间分配到各列上。反之如果列宽之和大于表格宽度表格容器会出现横向滚动。这里产生了最常见的坑你拖拽了A列却发现B列的宽度也跟着变了。原因就是fit在做匀摊。当鼠标松手后表格重新计算各列宽度为了让总宽度适配容器会把差额分配到没有显式固定width的列上尤其是那些用了min-width的列。所以想精确控制拖拽后的布局我的习惯是数据列尽量用min-width让它有弹性适应空间。需要严格锁定的列用width固定。如果你希望拖拽后其他列完全不动可以在拖拽结束事件里配合doLayout手动校正。3.4 列级resizable在源码里如何生效在Element UI内部每个column组件会把自己的resizable配置同步到表格的store中。表格布局对象TableLayout在初始化表头时会根据列上的resizable判断是否绑定拖拽事件。如果列级resizable为false对应表头单元格的mousedown事件不会触发拖拽逻辑。这也解释了为什么列级配置优先级更高因为判断发生在列向布局注册阶段已经是“各列独立判断”的粒度了。理解这层你甚至可以在特定业务里动态修改列的resizable配置达到“某些条件下锁定、某些条件下解锁”的效果。4. 进阶玩法监听拖拽事件把用户调好的列宽“记住”4.1 header-dragend事件能给你什么业务需求往往不只是“能拖”产品还会说“用户拖完的列宽刷新页面能不能别变回去”这就需要监听列宽拖拽结束事件。Element UI的el-table提供了header-dragend事件拖拽结束后触发回调参数依次是newWidth、oldWidth、column、event。newWidth是调整后的新宽度oldWidth是调整前的宽度column是对应列配置对象event是原生事件对象。也有一个header-drag-start事件在拖拽开始时会触发可以用来做埋点或者状态标记。4.2 结合localStorage实现列宽持久化实现思路不复杂拖拽结束拿到column和newWidth后以列的prop属性作为key把新宽度存到localStorage里。下次进入页面时在数据加载前先读取localStorage把对应的width设置到列配置上。核心代码如下const TABLE_COLUMN_WIDTH_KEY user-table-column-width; // 拖拽结束回调 handleHeaderDragEnd(newWidth, oldWidth, column) { if (!column.property) return; const savedWidths this.getSavedWidths(); savedWidths[column.property] newWidth; localStorage.setItem(TABLE_COLUMN_WIDTH_KEY, JSON.stringify(savedWidths)); }, // 读取持久化的列宽 getSavedWidths() { const raw localStorage.getItem(TABLE_COLUMN_WIDTH_KEY); return raw ? JSON.parse(raw) : {}; }, // 初始化列配置时应用保存的宽度 initColumns() { const savedWidths this.getSavedWidths(); this.columns [ { prop: date, label: 日期, minWidth: 160 }, { prop: name, label: 姓名, minWidth: 120 }, { prop: address, label: 地址, minWidth: 200 }, ].map(col { if (savedWidths[col.prop]) { return { ...col, width: savedWidths[col.prop] }; } return col; }); }需要注意列配置里的字段是minWidth或width代码里要和Element UI的props名保持统一。列宽存储的key最好加上当前页面的路由或表格的唯一标识避免多个表格互相覆盖数据。4.3 列宽恢复时机的两个小坑第一个坑是如果你通过v-for动态渲染el-table-column那么列配置修改后必须重新渲染表格才能看到效果。我的做法是把columns数组放在data里修改后通过key或v-if触发重渲染。第二个坑是表格数据异步加载完成后高度和宽度计算可能不是最终形态。如果发现恢复的列宽总是差一点试试在数据渲染完成后调用this.$refs.table.doLayout()强制表格重新计算布局。这个方法在动态置灰列、显示隐藏列、容器尺寸变化等场景下特别有用。4.4 进阶拖拽列宽与列显示隐藏联动有些系统的表格设置面板允许用户控制列显隐。这时只保存列宽还不够最好连列显隐状态一起存。我会在localStorage里维护一个对象同时保存visible和width{ visible: true, width: 180 }渲染列时统一判断既能恢复列显隐又能恢复列宽用户自定义的表格视图就完整保留下来了。这个功能对后台管理系统的体验提升非常明显。5. 高频踩坑记录列宽拖拽中常见的六个问题与对策5.1 为什么拖拽手柄完全不出现如果你按照文章开头加了border还是拖不了第一步去检查el-table-column上有没有被父级样式覆盖了光标。之前有同事给全局设置了* { cursor: default }导致拖拽光标出不来。排查方法是打开浏览器开发者工具直接查看表头单元格的cursor计算样式。另外一个变量是动画如果表格外层有一个正在进行中的CSS过渡或transform动画可能阻塞mousedown事件。把外层容器的过渡动画去掉或者用will-change优化问题通常能解决。5.2 拖拽后列错位表格布局乱掉这个现象在有多级表头或列显隐切换的表格中特别常见。根本原因是拖拽结束后列宽数据更新了但表格没有重新计算所有的列宽总和。此时调用一下this.$refs.table.doLayout()基本能恢复正常。如果doLayout还解决不了还有一种兜底方案切换一下表格的key用key强制重新渲染整张表。el-table :keytableKey reftable border :datatableData header-dragendhandleHeaderDragEnd .../el-table5.3 禁掉某一列拖拽后相邻列跟着变宽这是fit属性在“作祟”。A列禁止拖拽且没有固定widthB列拖拽后总宽度变化A列为了填满容器自动被分配了新的宽度。解决办法很直接给禁止拖拽的列加上固定的width让它没有“被分配”的空间。注意有时候需要在列上显式设置fitfalse注意这是element-plus的写法element-ui版本用表格级fit控制但在element-ui里更稳的方案是给关键列固定width。5.4 表格宽度设定为100%后拖拽到极限时撑破容器当列宽之和超过表格容器时会出现横向滚动条这是正常表现。但如果不希望出现滚动条可调整方向不是改resizable而是重新设计各列的最小宽度。说白了这是列宽配比问题和拖拽机制本身没太大关系。经验上我给表格设定一个合理的min-width总和再配合fit自动填满就不会出现一侧空白一侧挤压的尴尬局面。5.5 fixed固定列不能拖拽是真的吗是真的。Element UI的固定列fixedleft或fixedright实现了独立的浮层布局固定列区域不会响应拖拽行为。如果你确实需要固定列可以拖拽那得自己实现固定列的拖拽逻辑或者放弃固定列用普通列模拟置顶效果。实际业务里固定列的宽度变化需求很少多半是产品拍脑袋想的可以和技术确认后砍掉。5.6 拖拽后宽度没有保存刷新就丢这不算bugElement UI默认不提供列宽持久化能力。没有localStorage方案加持列宽自然跟页面共存亡。应对方案见第4章值得说明的是localStorage能存的是数字注意newWidth可能是字符串存之前一转成Number类型否则之后做加法计算会拼接字符串闹出笑话。5.7 踩坑速查表现象可能原因解决方案拖拽光标不出现缺少border属性或全局cursor被覆盖加border检查样式覆盖拖拽手柄出现但拖不动表格外层动画阻塞mousedown去掉过渡动画或调doLayout其他列跟着变fit自动匀摊宽度关键列固定width布局错乱多级表头或列显隐切换调doLayout或强制重渲染固定列无法拖拽Element UI不支持使用普通列模拟或放弃该需求刷新后列宽丢失无持久化方案结合localStorage保存和恢复6. 延伸对比Element Plus、umy-ui等其他表格方案的列宽拖拽6.1 Element PlusVue 3和Element UIVue 2的差异如果你用的是Vue 3 Element Plus列宽拖拽的整体思路几乎一样全局resizable列级resizable配合border。一个细微的差异是Element Plus的表格在列宽拖拽后的布局刷新上优化得更好错位的情况明显减少。但header-dragend的回调参数和Element UI保持了一致所以第4章的localStorage方案直接改个组件名就能迁移过来。6.2 umy-uiu-table到底强在哪从这里的热搜词里留意到“umy-ui table”出现频率不低umy-ui是一个面向大数据量的Vue 2表格增强组件它的列宽拖拽功能比Element UI更彻底支持拖拽列宽的同时还支持列拖拽排序、虚拟滚动等性能确实不错。如果你的项目核心诉求是“海量数据 列宽拖拽 列操作”umy-ui值得调研。但代价是要额外引入一套组件库学习成本和样式改造的成本都要算进排期。我个人的建议是中型后台项目用Element UI自带能力足够真正遇到大数据渲染瓶颈再考虑umy-ui别为了一个列宽拖拽轻易换轮子。6.3 Ant Design Vue的table对比Ant Design Vue的表格列宽拖拽默认也是开启的但它的操作方式略有差异需要拖拽表头右侧的分隔线而且固定列同样不支持拖拽。它的优势是配置化的列定义更规整做列显隐、列固定时更顺手劣势是有些内部样式改起来比Element UI更费劲。如果你的团队已经在用Ant Design Vue没必要为了这件事再引Element UI。6.4 移动端表格到底该不该做列宽拖拽我的答案非常明确不建议做。移动端没有鼠标悬停拖拽手势本身就很别扭而且手机屏幕宽度有限表格本来就需要横向滚动列宽拖拽在这种场景下的收益非常低却要付出大量触摸事件适配成本。移动端表格的正确方向是做合理的列优先级展示默认显示关键列其它列折叠或进入“更多”弹窗。7. 两点更进一步的优化思路7.1 给拖拽列宽加一个防抖策略用户高频拖拽时列宽变更事件触发非常密集如果每条变更都要写入localStorage会有一定的性能开销也可能让表格卡顿。我习惯在header-dragend里加个简单的防抖函数等用户停止拖拽500毫秒后再写入存储实测下来流畅很多。debounceSaveWidth: _.debounce(function(columnProp, width) { const savedWidths this.getSavedWidths(); savedWidths[columnProp] width; localStorage.setItem(TABLE_COLUMN_WIDTH_KEY, JSON.stringify(savedWidths)); }, 500)7.2 配合列拖拽排序构建用户的专属表格聊到列宽拖拽很多同学自然而然会想到列排序需求。Element UI本身没有列拖拽排序的能力需要借助sortablejs等库。我实际做过一版流程是拖拽表头排序结束后把新的列顺序、列宽、列显隐一起保存到localStorage用户每次进来都能看到自己调好的表格布局。这个功能上线后业务方反馈非常好所谓“千人千面”的表格也就这样落地了。踩过几次坑之后我个人对列宽拖拽的经验可以浓缩成一句话全局默认开启关键列锁死并固定width拖拽结束做持久化表格布局异常就doLayout。这个组合几乎能覆盖所有后台管理场景下的表格列宽需求。如果你正在做类似功能我的建议是先和产品对齐“哪些列允许拖拽、哪些列必须锁死”千万别一刀切全放开。等这一层确认了再按照本文第2章的代码结构去实现基本不会走弯路。最后再分享一个小技巧多级表头场景下子级列配置resizable时需要注意如果父级列头是合并的拖拽行为只对叶子级列生效别在父级列上费力气。
返回列表