Ant Design Vue 表格复杂数据合并单元格
官方合并效果

官方示例
表头只支持列合并,使用 column 里的 colSpan 进行设置。 表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。
vue
<template>
<a-table :columns="columns" :data-source="data" bordered>
<template slot="name" slot-scope="text">
<a>{{ text }}</a>
</template>
</a-table>
</template>
<script>
// In the fifth row, other columns are merged into first column
// by setting it's colSpan to be 0
const renderContent = (value, row, index) => {
const obj = {
children: value,
attrs: {},
};
if (index === 4) {
obj.attrs.colSpan = 0;
}
return obj;
};
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
tel: '0571-22098909',
phone: 18889898989,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
tel: '0571-22098333',
phone: 18889898888,
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
tel: '0575-22098909',
phone: 18900010002,
address: 'Sidney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 18,
tel: '0575-22098909',
phone: 18900010002,
address: 'London No. 2 Lake Park',
},
{
key: '5',
name: 'Jake White',
age: 18,
tel: '0575-22098909',
phone: 18900010002,
address: 'Dublin No. 2 Lake Park',
},
];
export default {
data() {
const columns = [
{
title: 'Name',
dataIndex: 'name',
customRender: (text, row, index) => {
if (index < 4) {
return <a href="javascript:;">{text}</a>;
}
return {
children: <a href="javascript:;">{text}</a>,
attrs: {
colSpan: 5,
},
};
},
},
{
title: 'Age',
dataIndex: 'age',
customRender: renderContent,
},
{
title: 'Home phone',
colSpan: 2,
dataIndex: 'tel',
customRender: (value, row, index) => {
const obj = {
children: value,
attrs: {},
};
if (index === 2) {
obj.attrs.rowSpan = 2;
}
// These two are merged into above cell
if (index === 3) {
obj.attrs.rowSpan = 0;
}
if (index === 4) {
obj.attrs.colSpan = 0;
}
return obj;
},
},
{
title: 'Phone',
colSpan: 0,
dataIndex: 'phone',
customRender: renderContent,
},
{
title: 'Address',
dataIndex: 'address',
customRender: renderContent,
},
];
return {
data,
columns,
};
},
};
</script>
实际项目中实现效果

实现原理
分层说明
-
数据预处理
- 使用prepareData方法按markId字段分组
- 组内数据按mergeIs字段排序(值为"是"的排在前)
-
双层级合并机制
- 主合并层:相同markId的"名称"列合并
- 次级合并层:在相同markId组内,连续mergeIs === '是'的"数量"列合并
-
合并标识管理
- 通过rowSpan属性控制行合并数
- rowSpan=0表示该单元格被合并
- originalIndex记录原始位置用于合并定位
-
动态计数器机制
- primarySpan跟踪名称列合并跨度
- secondarySpan跟踪数量列合并跨度
- 遇到分组边界或状态变化时重置计数器
js
{
markId: "分组标识", // 用于主合并层级
mergeIs: "是/否", // 用于次级合并层级
name: "显示内容", // 名称列数据
num: "数值" // 数量列数据
}
数据流向示意图
graph TD
A[原始数据] --> B{预处理模块}
B -->|分组排序| C[结构优化数据]
C --> D{合并处理器}
D --> E[可合并数据集]
E --> F[表格渲染]
表格组件配置
vue
<template>
<section class="console-section-box">
<div class="con">
<a-table
:columns="columns"
:data-source="tableData"
:showHeader="true"
:loading="tableLoading"
:pagination="pagination"
:bordered="true"
:rowKey="
(record, index) => {
return index;
}
"
:scroll="{ x: true }"
>
</a-table>
</div>
<a-back-top />
</section>
</template>
合并逻辑
js
<script>
import { mockData } from '~/mock/index.js';
const productColumn = [
{
title: '名称',
dataIndex: 'name',
customRender: (value, row, index) => {
const { rowSpan, originalIndex } = row.nameCellObj || { rowSpan: 1, originalIndex: index };
const obj = {
children: value,
attrs: {}
};
if (index === originalIndex) {
obj.attrs.rowSpan = rowSpan;
obj.attrs.colSpan = 1;
}
return obj;
},
align: 'center',
width: 90
},
{
title: '类型',
dataIndex: 'type',
align: 'center',
width: 100
},
{
title: '数量',
dataIndex: 'num',
key: 'num',
customRender: (value, row, index) => {
const { rowSpan, originalIndex } = row.numCellObj || { rowSpan: 1, originalIndex: index };
const obj = {
children: value,
attrs: {}
};
if (index === originalIndex) {
obj.attrs.rowSpan = rowSpan;
obj.attrs.colSpan = 1;
}
return obj;
},
align: 'center',
width: 90
}
];
export default {
name: '',
data() {
return {
tableLoading: false,
tableData: [],
pagination: {
current: 1, // 当前页码
pageSize: 10000, // 每页显示条数
total: 0,
showTotal: total => `共有 ${total} 条数据` //分页中显示总的数据
},
columns: productColumn,
};
},
async mounted() {
await this.fetchData();
},
methods: {
async fetchData() {
this.tableLoading = true;
try {
const res = await this.XXXX();
if (res.code === 0) {
this.tableData = mockData;
this.pagination.total = res.data.length;
this.handleCellMerge(this.tableData);
}
} catch (error) {
console.error('Error fetching data:', error);
}
this.tableLoading = false;
},
// 根据数据合并单元格
handleCellMerge(arr) {
if (!arr?.length) return;
const processor = {
currentMarkId: null,
currentMergeIs: null,
primarySpan: 1,
secondarySpan: 1,
// 初始化单元格状态
initialize(row, index) {
row.nameCellObj = { rowSpan: 1, originalIndex: index };
row.numCellObj = { rowSpan: 1, originalIndex: index };
},
// 主合并逻辑
processPrimary(index, rows) {
if (rows[index].markId === this.currentMarkId) {
this.primarySpan++;
rows[index - this.primarySpan + 1].nameCellObj.rowSpan = this.primarySpan;
rows[index].nameCellObj.rowSpan = 0;
return true;
}
this.currentMarkId = rows[index].markId;
this.primarySpan = 1;
return false;
},
// 次级合并逻辑
processSecondary(index, rows) {
if (rows[index].mergeIs === this.currentMergeIs && this.currentMergeIs === '是') {
this.secondarySpan++;
rows[index - this.secondarySpan + 1].numCellObj.rowSpan = this.secondarySpan;
rows[index].numCellObj.rowSpan = 0;
return true;
}
this.currentMergeIs = rows[index].mergeIs;
this.secondarySpan = 1;
return false;
}
};
const sortedData = this.prepareData(arr);
processor.currentMarkId = sortedData[0].markId;
processor.currentMergeIs = sortedData[0].mergeIs;
// 单次遍历处理所有合并逻辑
sortedData.forEach((item, index) => {
processor.initialize(item, index);
if (index === 0) return;
if (processor.processPrimary(index, sortedData)) {
processor.processSecondary(index, sortedData);
} else {
processor.currentMergeIs = item.mergeIs;
}
});
arr.splice(0, arr.length, ...sortedData);
},
// 分组排序方法
prepareData(originData) {
// 使用Map提高分组性能
const groups = new Map();
for (const item of originData) {
const group = groups.get(item.markId) || [];
group.push(item);
groups.set(item.markId, group);
}
// 预计算排序权重避免重复计算
return Array.from(groups.values()).flatMap(group => group.sort((a, b) => (b.mergeIs === '是') - (a.mergeIs === '是')));
}
}
};
</script>
mock数据
mock/index.js
js
export const mockData = [
{
name: '数据A',
num: '9999999',
type: 'AAA',
mergeIs: '是',
markId: 'ITEM_001'
},
{
name: '数据A',
num: '9999999',
type: 'BBB',
mergeIs: '是',
markId: 'ITEM_001'
},
{
name: '数据A',
num: '9999999',
type: 'CCC',
mergeIs: '否',
markId: 'ITEM_001'
},
{
name: '数据A',
num: '9999999',
type: 'DDD',
mergeIs: '否',
markId: 'ITEM_001'
},
{
name: '数据A',
num: '9999999',
type: 'EEE',
mergeIs: '否',
markId: 'ITEM_001'
},
{
name: '数据B',
num: '600',
type: 'AAA',
mergeIs: '是',
markId: 'ITEM_002'
},
{
name: '数据B',
num: '9999999',
type: 'BBB',
mergeIs: '否',
markId: 'ITEM_002'
},
{
name: '数据B',
num: '600',
type: 'CCC',
mergeIs: '是',
markId: 'ITEM_002'
},
{
name: '数据B',
num: '9999999',
type: 'DDD',
mergeIs: '否',
markId: 'ITEM_002'
},
{
name: '数据B',
num: '9999999',
type: 'EEE',
mergeIs: '否',
markId: 'ITEM_002'
},
{
name: '数据C',
num: '9999999',
type: 'AAA',
mergeIs: '否',
markId: 'ITEM_003'
},
{
name: '数据C',
num: '9999999',
type: 'BBB',
mergeIs: '否',
markId: 'ITEM_003'
},
{
name: '数据C',
num: '9999999',
type: 'CCC',
mergeIs: '否',
markId: 'ITEM_003'
},
{
name: '数据C',
num: '9999999',
type: 'DDD',
mergeIs: '否',
markId: 'ITEM_003'
},
{
name: '数据C',
num: '9999999',
type: 'EEE',
mergeIs: '否',
markId: 'ITEM_003'
}
];
5. 样式
scss
<style lang="scss" scoped>
.con {
min-height: calc(100vh - 160px);
padding: 24px;
border-radius: 8px;
background-color: #fff;
}
.project-info-box {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding-bottom: 20px;
}
.project-info {
width: 100%;
height: 60px;
line-height: 60px;
display: flex;
justify-content: space-between;
p {
margin: 0;
}
}
</style>