Ant Design Vue 表格复杂数据合并单元格
官方合并效果
官方示例
表头只支持列合并,使用 column 里的 colSpan 进行设置。
表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。
<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 cellif (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跟踪数量列合并跨度
- 遇到分组边界或状态变化时重置计数器
{markId: "分组标识", // 用于主合并层级mergeIs: "是/否", // 用于次级合并层级name: "显示内容", // 名称列数据num: "数值" // 数量列数据
}
数据流向示意图
表格组件配置
<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>
合并逻辑
<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
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. 样式
<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>