☰
Naive UI表格rowSpan合并实践:从基础到动态多列合并
2026/10/20 7:33:36 网站建设 项目流程

1. 项目概述与核心需求拆解

1.1 为什么Naive UI的rowSpan让人又爱又恨

先抛个场景。你接手了一个后台管理系统,列表页展示订单数据,每个订单有多个商品明细。需求方要求把相同订单号的单元格合并起来,一排订单只有一个订单号单元格,看起来干净利落。你打开Naive UI的文档,翻到Data Table组件,看到了colSpan和rowSpan的说明,然后你发现——文档里只有一段话和一个单列合并的示例,真正拿到多行多列合并的需求时,还是无从下手。

我最初也被这个坑绊过好一阵子。Naive UI的表格组件底层用的是虚拟渲染和原生表格结构,正常情况下rowSpan的写法是直接写在td标签上的。但在Naive UI的列配置体系里,我们并不会直接写HTML,而是通过columns数组里的render函数来生成单元格内容。这里的核心矛盾在于:rowSpan不是单纯的数据属性,它需要作用于当前列的td标签本身,而不是td内部的内容。

换句话说,如果只是用render函数返回一个文本节点,rowSpan根本不会生效。你必须让render函数返回一个完整的td标签,并把rowSpan作为td的属性传进去。这是整个实现过程中最关键的一个认知转变。

1.2 这篇文章能解决什么问题

我先说结论:这篇文章会从零开始,把Naive UI表格rowSpan的完整用法讲明白,包括单列合并、多列合并、动态计算合并范围、合并后内容居中、排序筛选场景下的边界问题,以及和展开行、固定列同时使用时的冲突处理。

适合的读者有两类。一类是刚开始用Naive UI,表格业务需求已经做到“合并单元格”这一步的开发者,你照着文中的代码模式改改数据就能用。另一类是已经能应付基础表格,但一遇到复杂合并需求就头疼的人,文中的踩坑记录和问题排查表能帮你省下不少跟浏览器控制台较劲的时间。

这篇文章的代码示例基于Vue 3 + Naive UI 2.x版本,TypeScript风格,但讲原理的部分不区分框架版本,只要你用Naive UI,核心思路都通用。

2. 整体设计思路:rowSpan的实现模型

2.1 先理解表格渲染的底层逻辑

要玩转rowSpan,得先搞清楚Naive UI表格渲染时我们在“干预”什么。

Naive UI的Data Table组件接收一个columns数组和data数组。columns数组中每个对象代表一列,包含key、title、render等字段。渲染时,组件内部会遍历data生成每一行,再遍历columns生成每一行的单元格。render函数的作用是决定这个单元格里放什么内容,比如返回h('span', '订单号A001')。

但rowSpan是td标签的属性,不是内部span的属性。你没法通过控制单元格内容来影响td本身,所以必须越权去控制td。

怎么越权?render函数的返回类型其实足够宽泛,除了返回普通VNode,你完全可以直接返回一个h('td', ...)的VNode。Naive UI发现render返回的VNode标签名是td时,会直接使用这个VNode作为单元格的DOM结构,而不是再包裹一层td。

这就是整个实现模型的支点:

当render返回h('td')时,Naive UI不再自动生成td外壳,而是将你返回的td直接挂载到行中。

2.2 两种合并且模型必须分清

我在实际开发中总结出两种合并模式,搞混了会写出逻辑混乱的代码。

模式一:合并“列内容”

这种模式是提前在数据里把合并信息计算好,比如每行带一个span值,渲染时直接读取。优点是逻辑直观,缺点是你得在数据处理阶段就把每行每列“是否需要合并、向下了跨几行”算清楚,数据一变就要重新计算。

模式二:动态渲染时计算

render函数在执行时,根据当前行的数据、前后行的数据关系,实时判断是否该合并。这种模式适合“合并连续相同值”的场景——订单号相同的行合并成一个单元格。数据处理时不用侵入原始数据,但render函数里要写判断逻辑。

实际项目中第二种模式用得最多。因为需求通常是“相同订单号合并”,而不是预先告诉你每行跨几行。所以核心是先建立一套“合并映射表”,让render函数快速查到该行该列要合并成什么样。

2.3 为什么需要构建合并映射表

想象你在render函数里判断两个订单号是否相同,你大概率会这样写:

if (row.orderNo === data[rowIndex - 1]?.orderNo) { return null }

这在简单场景下能用,但一旦涉及多列合并,比如订单号相同、同时客户名也相同,需要两列各自合并,这种“边渲染边比较”的方式就会显得凌乱。而且有两个绕不开的问题:

第一,跳行判断。比如订单A有3行,订单B有2行,你已经把A的第1行用rowSpan=3合并掉了,A的第2、3行render函数要返回什么?按上面代码逻辑,第2行对比的是第1行,订单号相同,返回null就行。但第3行对比的是第2行,也相同,没问题。可如果A的第2行刚好是某列的“列内合并起始行”,逻辑就更复杂了。

第二,筛选和排序之后,行序会变,原来的合并关系会错乱。这时候只靠“上一行比较”完全不可靠。

所以更稳妥的方案是:预处理阶段生成一个二维映射表,以行索引和列key为坐标,记录每个单元格的合并信息。render函数里只查表,不推算。

映射表的结构类似这样:

// mergeMap: { // 0: { orderNo: { start: true, span: 3 } }, // 1: { orderNo: { start: false, span: 0 } }, // 2: { orderNo: { start: false, span: 0 } } // }

第0行是合并起始行,span=3代表向下合并2行。第1行、第2行标记为非起始行,对应位置在渲染时直接返回空。

构建映射表的算法我放在下一节详细拆解,但先记住这个结论:把“如何合并”的数据结构和“渲染”解耦,是避免后续维护噩梦的关键。

3. 核心实现:构建合并映射表与渲染函数

3.1 合并映射表的算法设计

假设我们有这样一组数据:

const data = [ { id: 1, orderNo: 'A001', customer: '客户X', product: '苹果' }, { id: 2, orderNo: 'A001', customer: '客户X', product: '香蕉' }, { id: 3, orderNo: 'A001', customer: '客户X', product: '橘子' }, { id: 4, orderNo: 'A002', customer: '客户Y', product: '西瓜' }, { id: 5, orderNo: 'A002', customer: '客户Y', product: '桃子' }, { id: 6, orderNo: 'A003', customer: '客户Z', product: '葡萄' } ]

如果我们要让orderNo列和customer列都按连续相同值合并,算法如下。

先明确访问方式:对于第i行、列key为orderNo的单元格,判断它是不是合并行为的起始格。如果是,标记span为连续相同值的个数。如果不是,标记为非起始格,渲染时返回null。

function buildMergeMap(data, mergeKeys) { const map = {} for (const key of mergeKeys) { let spanCount = 1 let startIndex = 0 for (let i = 1; i < data.length; i++) { if (data[i][key] === data[startIndex][key]) { spanCount++ } else { // 结算上一段 if (spanCount > 1) { map[startIndex] = map[startIndex] || {} map[startIndex][key] = { start: true, span: spanCount } for (let j = startIndex + 1; j < startIndex + spanCount; j++) { map[j] = map[j] || {} map[j][key] = { start: false, span: 0 } } } // 开启新的段 startIndex = i spanCount = 1 } } // 处理最后一段 if (spanCount > 1) { map[startIndex] = map[startIndex] || {} map[startIndex][key] = { start: true, span: spanCount } for (let j = startIndex + 1; j < startIndex + spanCount; j++) { map[j] = map[j] || {} map[j][key] = { start: false, span: 0 } } } } return map }

这段代码只处理了spanCount大于1的情况,单个值不需要合并,省掉大量无意义的标记。对上面的示例数据,运行结果会标记第0行的orderNo和customer都是起始行、span=3,第3行是A002段起始行、span=2,第4行的orderNo和customer是非起始行。

这里有个细节:orderNo和customer的合并段完全重合时,第3行和第4行会拿到两组标记,但都在同一行上,互不冲突,因为key不同。

3.2 render函数如何与映射表协作

有了映射表,渲染就好办了。列的render定义如下:

const createMergeRender = (key) => (row, rowIndex) => { const mergeInfo = mergeMap[rowIndex]?.[key] if (!mergeInfo) { // 没有合并需求,正常渲染 return h('span', row[key]) } if (!mergeInfo.start) { // 不是起始行,让位给上方合并单元格 return null } // 是起始行,渲染带rowSpan的td return h( 'td', { rowspan: mergeInfo.span, style: 'text-align: center; vertical-align: middle;' }, row[key] ) }

等一下,这里有一个容易忽略但很重要的点。当render返回null时,Naive UI的行为是什么?它会在该位置渲染一个空的td(表格结构上不能平白少一个单元格,否则列数对不上)。空td会正常占位,高度会被上方的rowSpan覆盖掉,视觉上看起来就是合并过的效果。如果此时连td都不渲染,列数错乱,表格行结构会被撑破。

所以合并单元格的“被合并行”位置,返回值用null就行,不要试图跳过单元格。

再注意一个顺序问题:rowSpan所在的td,必须是该行这一列渲染顺序中出现在前面的。因为表格列是按columns数组顺序从左到右渲染,如果A列被合并,但B列是正常渲染,只要A列的起始行td先渲染,且带有rowspan,后续行对应位置的空td就会自动“隐藏”在该区域下方。

3.3 columns配置示例

现在把这些串起来。完整的一列配置长这样:

const columns = [ { key: 'orderNo', title: '订单号', render: createMergeRender('orderNo') }, { key: 'customer', title: '客户', render: createMergeRender('customer') }, { key: 'product', title: '商品', render: (row) => h('span', row.product) } ]

columns定义很简洁,关键逻辑都在createMergeRender里。页面上的Data Table用法跟普通表格一模一样,不需要额外的props。

<template> <n-data-table :columns="columns" :data="data" /> </template>

这套组合拳打下来,单列合并、多列合并都已经能正常显示。但这里还缺一个东西:合并单元格默认的对齐方式。td的默认对齐是居中,但Naive UI表格单元格默认内容是左对齐的,如果你合并的单元格里是文本,视觉上会偏左不居中。上面render里我已经加了text-align: center,可以根据需求调整。

3.4 合并后内容如何垂直居中

如果你合并的是顶行开始的连续单元格,内容默认贴着表格顶部。需求方通常希望合并单元格里的内容垂直居中。Naive UI对td没有默认垂直居中样式,但我们可以通过style直接干预。

最方便的做法是设置vertical-align: middle,配合text-align: center。但某些情况下,比如单元格内容只有一行文本,它会在td顶部居中,垂直方向不变。这时候可以给td设置display: table-cell,然后vertical-align: middle。td本身就是table-cell,所以直接写就行。

但如果你在render函数返回的td里放的是一个输入框、日期选择器等组件,垂直居中对齐可能会被组件自身高度影响。遇到这种情况,建议给td内部包一层div,用height + line-height或flex布局强行居中。我常用的写法:

return h('td', { rowspan: mergeInfo.span, style: 'height: 100%; vertical-align: middle; text-align: center;' }, h('div', { style: 'display: flex; align-items: center; justify-content: center;' }, row[key]) )

这种情况下td的height会被表格的auto值覆盖,所以通常还需要给td加一个足够大的height值作为兜底,比如style里写height: 80px,让flex布局有据可依。具体数值可以根据行高调整。

4. 实操过程:从简单到复杂的三个完整案例

4.1 案例一:单个字段连续行合并

先看一个最典型的需求:“订单号相同的行合并为一行”。

数据源:

const orderData = [ { id: 1, orderNo: 'D001', amount: 100 }, { id: 2, orderNo: 'D001', amount: 200 }, { id: 3, orderNo: 'D002', amount: 300 }, { id: 4, orderNo: 'D003', amount: 150 }, { id: 5, orderNo: 'D003', amount: 80 } ]

调用buildMergeMap(orderData, ['orderNo'])生成映射表,然后用createMergeRender('orderNo')定义列。订单号D001被标记为第0行起始、span=2,D002一行单独存在,不合并,D003在第3行起始、span=2。

这里有个小陷阱:如果你的数据里D001、D002、D003的顺序本来是乱的,比如D001的两行分散在不同位置,就不能合并。因为rowSpan只能向下作用于连续行,跨行合并必须行相邻。如果业务上必须合并乱序数据,先排序再合并。我通常在请求数据后、setData之前做这个预处理,而不是在模板里排序,避免映射表失效。

4.2 案例二:多列合并且各列规律不同

继续扩展。订单号和客户名都合并,但商品名称不要合并。类似第一部分示例数据。重点在于两列的合并规律可能不完全一致,比如同一个订单号下有不同的客户?正常情况下不会,但同客户名对应多个订单号在公司业务中可能出现。

举个例子:

const complexData = [ { id: 1, orderNo: 'D001', customer: '客户A', product: '苹果' }, { id: 2, orderNo: 'D001', customer: '客户A', product: '香蕉' }, { id: 3, orderNo: 'D002', customer: '客户A', product: '橘子' }, { id: 4, orderNo: 'D002', customer: '客户B', product: '西瓜' }, { id: 5, orderNo: 'D002', customer: '客户B', product: '桃子' } ]

buildMergeMap(complexData, ['orderNo', 'customer'])会分别对各列生成映射段。第0行orderNo段跨度2、customer段跨度2,第2行orderNo段跨度3、customer是单独一个值不合并,第3行orderNo段跨度2、customer段跨度2,第4行customer非起始行,product列正常显示。

运行出来的表格视觉效果是:订单号D001跨2行、D002跨3行,客户A在D001两行合并、而D002第一行客户A单独一格,同一个客户由于不连续反而分成了两段。这个结果是对的,因为rowSpan只能合并“相邻相同值”。如果你的产品预期是把“不相邻的客户A”也合并起来,那就不属于表格rowSpan能解决的问题,需要换业务方案或数据方案,比如在数据层用分组填充辅助字段。

4.3 案例三:动态查询结果排序后的合并

实际项目中数据大多来自接口,而且还有排序需求。Naive UI表格的sortable列排序后,数据顺序变化,映射表必须重新构建。所以不要把buildMergeMap的结果存成固定常量,要在数据变更后同步更新。

一个完整实现思路:

<script setup> import { computed } from 'vue' const props = defineProps({ rows: { type: Array, required: true } }) const mergeMap = computed(() => buildMergeMap(props.rows, ['orderNo', 'customer'])) const columns = computed(() => [ { key: 'orderNo', title: '订单号', sorter: (rowA, rowB) => rowA.orderNo.localeCompare(rowB.orderNo), render: (row, rowIndex) => createMergeRender('orderNo', mergeMap.value)(row, rowIndex) }, // ...其他列 ]) </script>

这里createMergeRender接收mergeMap参数,相当于做了闭包封装。Naive UI的sortable本身会改变表格内部排序,但data还是原data,渲染时row的顺序已经变了。如果你的映射表不跟着变,会出现错位合并。实测下来这是最容易出bug的地方,因为sorter触发的排序是组件内部行为,很难在外部察觉到数据顺序变化。

我的做法是:自己不依赖组件内部排序,而是用受控排序。监听update:sorter,拿到排序字段和方向后,在外部对数据排序,再传给Data Table。这样mergeMap的依赖数据永远是自己手里的排序结果,不会被组件内部状态打乱。

5. 常见问题与排查技巧实录

5.1 合并后单元格出现多余边框和间距

这是最常遇到的第一坑。渲染出的rowSpan单元格和普通单元格之间,会有轻微的边框断裂。原因是td的border-collapse和Naive UI默认的表头表体边框样式叠加。

排查思路:打开浏览器开发者工具,检查td的样式,看是border-collapse、border还是box-shadow在起作用。Naive UI表格默认用了border-collapse: collapse,理论上cell边框会合并,但rowSpan的td样式是动态生成的,开发者工具里显示的行内样式可能覆盖了默认样式。

常见处理是把mergeMap起始行的td加上style: 'border-top: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;' 用与表格默认边框一致的颜色修复。但如果主题色变了,这个值也得变。

另一个更隐蔽的原因:当表格设置了bordered,Naive UI会用类名给每个td加边框,而你动态生成的td没有继承那些类名,视觉上就缺了边框。你需要在render函数中手动给td补上Naive UI的默认类名,比如.n-data-table-td或通过父级样式隔离很难精准定位。建议参考Naive UI源码里td的class命名,保持一致。

5.2 合并列与固定列冲突

Naive UI表格列可以设置fixed: 'left'或fixed: 'right',固定列使用sticky定位实现。当固定列同时也是合并列时,会出现合并单元格的td无法吸附固定的问题。

实测经验:left固定列如果做rowSpan,基本能正常工作,因为sticky只对水平方向吸附,垂直方向上的rowSpan不影响。但right固定列和rowSpan组合容易出现错位,尤其是表格水平滚动时,右侧固定列会遮住部分合并单元格。

还有一种情况:你的合并列在中间位置,左右都有固定列,水平滚动时合并td跟随正常列滚动,不会固定在左列。这是符合预期的,因为rowSpan只做垂直合并,不涉及水平滚动吸附。需求方如果在表格滚动时希望合并列固定,那就不能用rowSpan方案,得单独用CSS实现模拟单元格。

5.3 合并与展开行、树形数据冲突

Naive UI支持expandable展开行和tree数据。一旦开启展开,表格的行数不是data.length,而是动态变化。如果合并列索引基于原始data长度计算,展开后会错位。

排查经验:展开行是插入一行额外的tr,rowSpan跨的高度会被撑开,但被合并的单元格下边的null td并不会跟着扩展,会造成视觉断裂。树形数据更麻烦,因为父行和子行的逻辑关系强,rowSpan完全不适用。我的建议是:如果表格同时需要展开行或树形数据,尽量避免使用rowSpan合并逻辑,或者只合并不参与展开动作的列。

如果一定要结合,可以考虑给展开行也用td+rowSpan的方式,展开行的单元格在原本需要被合并的列上设置合适的span来抵消高度。这种方案非常脆弱,后来我放弃在项目中使用。

5.4 合并后点击事件失效或重复触发

合并单元格的td跨了多行,但事件仍绑定在render返回的VNode上。比如点击订单号合并单元格时,你希望触发一个跳转,只触发一次,但实际点击不同区域时可能触发多次。

排查思路:要确保VNode的onClick只绑定一次。render函数里如果写了箭头函数,每次渲染都会创建新函数,但VNode事件系统在Naive UI内部是按patch机制绑定的,不会重复绑定。问题通常出在事件冒泡上,点击合并单元格,也会冒泡到父级tr或table上,如果你的外层加了点击事件,就会被触发多次。

实操建议:在onClick处理函数里加上event.stopPropagation(),避免冒泡干扰。如果合并单元格内还有子元素,比如按钮,按钮点击和单元格点击事件要分开,不要都用同一个事件委托。

5.5 表格虚拟滚动的兼容性问题

Naive UI的Data Table有一个virtual-scroll选项,开启后表格只渲染可视区域的行,滚动时动态创建行。rowSpan方案遇到virtual-scroll会完全失效,因为“可视区域外”的行根本没有渲染,合并信息也无从谈起。

如果你开启了virtual-scroll,同时又想用rowSpan,直接的结果就是滚动后合并单元格消失或错乱。这个我踩过很深的坑,最后给出的建议是:需求上明确需要合并单元格时,关闭虚拟滚动。表格行数非常多(超过1000行)并且还要合并,就得自己想办法了,比如切换到分组表格或重构成多个表格再叠加。

5.6 合并单元格的序号列序号不连续

表格左侧通常有一个序号列,显示行号。合并后,被合并的行不显示序号,那么序号列怎么处理?

如果使用Naive UI内置的序号列(通过type: 'index'),它会基于data数组的下标生成序号,不管合并,所以序号会按行数依次显示1、2、3,即使中间有行被合并掉了,看起来序号仍完整,但1、2、3所在行的垂直高度小于上方的合并单元格,形式上是两列表格,序号列是正常的。

如果想要序号列也对齐到合并单元格,需要给序号列也建立一种特殊的合并逻辑。通常不用,因为序号只是标记数据条目,不是业务字段。跳过即可。

6. 进阶:合并单元格的自定义样式与交互扩展

6.1 合并单元格内渲染表单组件

不只是显示文本,合并单元格内还可以放输入框、下拉选择等表单组件。比如合并几行“备注”列,但只允许在合并单元格里编辑一次备注。

代码如下:

const remarkColumns = { key: 'remark', title: '备注', render: (row, rowIndex) => { const mergeInfo = remarkMergeMap[rowIndex]?.remark if (mergeInfo?.start) { return h('td', { rowspan: mergeInfo.span, style: 'padding: 0;' }, h('n-input', { value: row.remark, 'onUpdate:value': (v) => { updateRemark(v) }, placeholder: '请输入备注' }) ) } return null } }

这里n-input的value绑定到合并起始行的row数据,但实际上合并区域覆盖了多行的“位置”,用户只填一次,就同时“代表”了这些行的备注值。这是业务上常见的“合并编辑器”模式。

注意点:合并单元格的高度由跨行总高度决定,但n-input默认高度只有34px左右,会导致单元格下方留白。可以给td内包裹一个div,设置min-height: 100%; display: flex; align-items: center;,把输入框垂直撑起来。

6.2 合并列的单元格点击高亮

当用户点击合并单元格时,希望整块合并区域高亮。由于只有起始行的td被渲染,给这个td加class或style就能覆盖整块区域。但如果想单独高亮某一行在被合并后的“子区域”,那在视觉上做不到,因为那一行没有属于自己的td。

可替代方案:在起始行td内部再渲染一个绝对定位的元素,覆盖整个合并区域高度,用JS计算这个区域的高度,然后手动模拟子区域高亮。这个方案实现成本高且计算依赖DOM,只建议在视觉要求极高的场景用。普通场景里,给td加背景色就能满足大部分需求。

6.3 合并映射表的性能优化

数据量较大时,buildMergeMap每次都在computed里执行,会重复遍历所有列。虽然单次遍历开销不高,但如果在大数据表格中频繁触发(比如每次点击排序),会有一些性能损耗。

优化方向:

function buildMergeMap(data, mergeKeys) { // 为每个key构建一个独立的关联段记录 const keyMaps = {} for (const key of mergeKeys) { keyMaps[key] = [] let i = 0 while (i < data.length) { let j = i + 1 while (j < data.length && data[j][key] === data[i][key]) j++ if (j - i > 1) { keyMaps[key].push({ start: i, span: j - i }) } i = j } } // 再整合到统一map中 const map = {} for (const key of mergeKeys) { for (const seg of keyMaps[key]) { for (let r = seg.start; r < seg.start + seg.span; r++) { map[r] = map[r] || {} map[r][key] = r === seg.start ? { start: true, span: seg.span } : { start: false, span: 0 } } } } return map }

这种写法把每列的段先抽离出来,性能略好一些,可读性也有所提升。如果你的合并列不超过两三个,这个优化可有可无,但用上面这种写法,后续想给单列单独配置合并规则会更方便。

7. 常见问题速查表

现象可能原因解决方案
合并单元格没有生效,仍是普通行render函数只返回内容,没返回tdrender中改为返回h('td', …)
合并后出现多余边框/间隙动态td缺少Naive UI默认类名或边框样式给td补上border和padding样式
合并后内容靠上不居中缺少vertical-align或高度支持给td加vertical-align: middle,必要时用flex包裹
合并后行高错乱,下方内容被遮挡被合并行返回了内容,而非null非起始行在render中return null
排序后合并错乱映射表基于排序前数据生成排序后再重新计算mergeMap,或使用受控排序
开启虚拟滚动后合并失效virtual-scroll只渲染可视行关闭虚拟滚动或改用其他方案
固定列合并的td脱位固定列与rowSpan冲突避免对fixed列做合并,或调整固定列布局
展开行后合并区域断裂展开行插入额外tr破坏结构避免同时使用展开行和rowSpan
合并单元格内表单组件高度塌陷td内组件默认高度不足包一层div并用flex撑满
数据中相同值不连续导致未合并rowSpan只能作用于相邻连续行先排序再合并,或业务层特殊处理

8. 我的实操体会与最终建议

Naive UI表格的rowSpan,本质上是对底层表格模型的一次“越权操作”。它能用,但绝不是官方文档里那种贴一段代码就完事的功能。你需要掌握三件事:td维度的渲染介入、合并映射表的数据结构、以及表格组件其他特性对合并行为的干扰。

我实际做过的某个后台项目中,曾经同时出现排序、固定列、展开行、虚拟滚动、合并单元格五个需求,需求方希望全部叠加。我在技术方案评审阶段就明确否掉了这个组合,最终在“保留展开行”和“保留合并单元格”之间二选一。这个取舍不是技术做不到,而是组合场景的维护成本太高,任何一个交互变化都可能让合并错位,光排错时间就够重写一个页面了。

如果你只是普通后台表格,用文中的buildMergeMap + createMergeRender组合,半个小时就能跑通。如果你要上排序,记得把排序处理成受控模式。如果要固定列,避免把固定列本身作为合并列。如果数据量巨大还要合并,先关闭虚拟滚动再说。

最后分享一个小技巧:mergeMap构建完成后,你可以把索引和key打印到控制台,核对一遍映射结果。很多看起来诡异的表格错乱,其实都是映射表里span计算失误。数据量少时人工核对一遍,比在页面上肉眼盯着找问题快得多。

rowSpan这个功能,Naive UI提供了思路,但真正做到好用,得靠我们这些使用者把细节填满。希望上面的经验能帮你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询