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>