问题描述
在管理端系统中表格是最为常用组件之一,有些表格项会涉及到单元格合并的情况,比如有以下数据
const tableData = [
{
"title": "鸿蒙过滤",
"subTitle": "消息推送间隔",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "鸿蒙过滤",
"subTitle": "一天上限",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "鸿蒙过滤",
"subTitle": "用户单游下发频控",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "鸿蒙下发量",
"subTitle": "--",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓过滤",
"subTitle": "业务侧过滤",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓过滤",
"subTitle": "未到安卓广告生效时间",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓过滤",
"subTitle": "单用户广告位曝光周次数上限",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓下发量",
"subTitle": "--",
"uv": "2,000,000",
"percentage": "25%"
}
]
借助于element-plus 几行代码加一个表格
<template>
<el-table
border
:data="tableData"
>
<el-table-column prop="title" label="平台指标" />
<el-table-column prop="subTitle" label="过滤项" />
<el-table-column prop="uv" label="去量uv" />
<el-table-column prop="percentage" label="占比" />
</el-table>
<template>
渲染后是这样子的
第一列有些标题是相同的,现在需要合并第一列中标题相同的项,让这个表格看起来更有品位
借助于el-table的span-method 可以对单元格进行设置
<script setup>
const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
// 判断 然后设置单元格数
// ...
// 不需要判断的 默认 1 1
return { rowspan: 1, colspan: 1 };
}
</script>
<template>
<el-table
border
:data="tableData"
:span-method="spanMethod"
>
<el-table-column prop="title" label="平台指标" />
<el-table-column prop="subTitle" label="过滤项" />
<el-table-column prop="uv" label="去量uv" />
<el-table-column prop="percentage" label="占比" />
</el-table>
<template>
对于规律性很强单元格合并逻辑 比如单双行判断,那么很简单,直接一个一个if else就直接设置好了没一个格子占据的单元格数。但是对于复杂一点的,比如要实现上述的:第一列有些标题是相同的,现在需要合并第一列中标题相同的项,直接一个spanMethod方法就无能为力了,因为除了设置当前的,所有第一列的上下行格子是可能互相影响的,涉及到重新设置的问题。
遍历单元格提前设置好每个格子占据的单元格数,然后在spanMethod方法中直接获取,初步的代码涉及如下:
const spanMap = generateSpanMap(tableData.value);
const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
const cellIndex = `${rowIndex}-${columnIndex}`;
return spanMap[cellIndex];
}
接着补充核心代码,基本思路就是遍历表格数据获得一个二维数组,就是对应的每个格子,遍历二维数组设置好每个格子占据单元格数。遍历需要注意的是:因为只涉及到行的合并,行是倒序遍历的,如果列也要合并,那么列也要倒序遍历。
之所以要倒序遍历,是因为单元格合并的话,如果第二行和第三行以及第四行合并(同一列),那么要设置第二行单元格信息就是 3 1,第三行和第四行就是 0 0(实际就是不会被渲染了)。为啥不是第二行和第三行设置为0 0, 第四行设置为 3 1。答案是不能,前面格子的设置为 0 0 后,同一行其他不为0 0 的格子不就顶上来了吗。
如果正序遍历就不方便了,可以自行想象。而倒序遍历的话,那么是
- 先设置的第四行为 1 1
- 然后遍历到第三行,判断条件满足要合并,则直接在第四行的基础上行+1,变成 2 1,然后第四行重置为0 0
- 然后遍历到第二行 判断条件满足要合并,则直接在第三行的基础上行+1,变成 3 1,然后第三行重置为0 0
- 以此类推..
- 总结起来就是倒序遍历,如果命中合并条件,下一个遍历直接在上一个遍历基础上加1,上一个遍历重置为 0 0 就可以了。而正序遍历就不好处理合并了。
// 遍历表格生成每一个格子的所占的单元格数
const generateSpanMap = (tableData) => {
const arr = []
const spanMap = {}
tableData.forEach(item => {
const rowArr = [
item.title,
item.subTitle,
item.uv,
item.percentage,
];
arr.push(rowArr);
});
for (let rowIndex = arr.length - 1; rowIndex >= 0; rowIndex--) {
const rowArr = arr[rowIndex];
for (let columnIndex = 0; columnIndex < rowArr.length; columnIndex++) {
// 遍历每个格子 设置所占据的单元格数 保存到spanMap中
handleCellSpan({ arr, rowIndex, columnIndex }, spanMap中);
}
}
return spanMap;
}
/**
* 设置表格中每个格子的所占单元格数 通过行列索引定位每个格子的值
* 这里只对第一列(平台指标)进行上下行合并,列之间不合并
* @param {object.arr} 表格数据 二维数组 存储了每个格子的值
* @param {object.rowIndex} 表格行索引
* @param {object.columnIndex} 表格列索引
* @param {object} spanMap 保存单元格的单元格数的对象
*/
function handleCellSpan({ arr, rowIndex, columnIndex }, spanMap) {
// 当前单元格行列索引 用于存储
const cellIndex = `${rowIndex}-${columnIndex}`;
// 当前单元格上一行的索引 行是倒序遍历的 所以上一行索引要加1 不涉及列的合并,列索引不需要变化
const prevcellIndex = `${rowIndex + 1}-${columnIndex}`;
/**
* 1. 只对第一列(平台指标)进行上下行的合并,其余列不需要判断处理 直接设置1 1
* 2. 因为是倒序遍历 最后一行最先遍历 被遍历时不需要判断,单元格数直接设置为 1 1
*/
if (columnIndex !== 0 || rowIndex === arr.length - 1) {
return spanMap[cellIndex] = { rowspan: 1, colspan: 1 };
}
// 比较第一列 上下行之间的值
const title = arr[rowIndex][columnIndex];
const prevTitle = arr[rowIndex + 1][columnIndex];
// 上下行值不相同,不需要合并 单元格数直接设置为 1 1
if (title !== prevTitle) {
return spanMap[cellIndex] = { rowspan: 1, colspan: 1 };
}
// 上下行值相同,取上一个被遍历的单元格的单元格数 用于进行合并
const { rowspan } = spanMap[prevcellIndex];
spanMap[cellIndex] = { rowspan: rowspan + 1, colspan: 1 }; // 合并行 行加1
spanMap[prevcellIndex] = { rowspan: 0, colspan: 0 }; // 被合并了 这个单元格不存在了
return spanMap[cellIndex];
}
渲染结果
完整代码
<script setup>
import { ref } from 'vue'
const mockData = [
{
"title": "鸿蒙过滤",
"subTitle": "消息推送间隔",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "鸿蒙过滤",
"subTitle": "一天上限",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "鸿蒙过滤",
"subTitle": "用户单游下发频控",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "鸿蒙下发量",
"subTitle": "--",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓过滤",
"subTitle": "业务侧过滤",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓过滤",
"subTitle": "未到安卓广告生效时间",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓过滤",
"subTitle": "单用户广告位曝光周次数上限",
"uv": "2,000,000",
"percentage": "25%"
},
{
"title": "安卓下发量",
"subTitle": "--",
"uv": "2,000,000",
"percentage": "25%"
}
]
const tableData = ref(mockData)
/**
* 设置表格中每个格子的所占单元格数 通过行列索引定位每个格子的值
* 这里只对第一列(平台指标)进行上下行合并,列之间不合并
* @param {object.arr} 表格数据 二维数组 存储了每个格子的值
* @param {object.rowIndex} 表格行索引
* @param {object.columnIndex} 表格列索引
* @param {object} spanMap 保存单元格的单元格数的对象
*/
const handleCellSpan = ({ arr, rowIndex, columnIndex }, spanMap) => {
// 当前单元格行列索引
const cellIndex = `${rowIndex}-${columnIndex}`;
// 当前单元格上一行的索引 行是倒序遍历的 所以上一行索引要加1 不涉及列的合并,列索引不需要变化
const prevcellIndex = `${rowIndex + 1}-${columnIndex}`;
/**
* 1. 只对第一列(平台指标)进行上下行的合并,其余列不需要判断处理 直接设置1 1
* 2. 因为是倒序遍历 最后一行最先遍历 被遍历时不需要判断,单元格数直接设置为 1 1
*/
if (columnIndex !== 0 || rowIndex === arr.length - 1) {
return spanMap[cellIndex] = { rowspan: 1, colspan: 1 };
}
// 比较第一列 上下行之间的值
const title = arr[rowIndex][columnIndex];
const prevTitle = arr[rowIndex + 1][columnIndex];
// 上下行值不相同,不需要合并 单元格数直接设置为 1 1
if (title !== prevTitle) {
return spanMap[cellIndex] = { rowspan: 1, colspan: 1 };
}
// 上下行值相同,取上一个被遍历的单元格的单元格数 用于进行合并
const { rowspan } = spanMap[prevcellIndex];
spanMap[cellIndex] = { rowspan: rowspan + 1, colspan: 1 }; // 合并行 行加1
spanMap[prevcellIndex] = { rowspan: 0, colspan: 0 }; // 被合并了 这个单元格不存在了
return spanMap[cellIndex];
}
// 遍历表格生成每一个格子的所占的单元格数
const generateSpanMap = (tableData) => {
const arr = []
const spanMap = {}
tableData.forEach(item => {
const rowArr = [
item.title,
item.subTitle,
item.uv,
item.percentage,
];
arr.push(rowArr);
});
for (let rowIndex = arr.length - 1; rowIndex >= 0; rowIndex--) {
const rowArr = arr[rowIndex];
for (let columnIndex = 0; columnIndex < rowArr.length; columnIndex++) {
handleCellSpan({ arr, rowIndex, columnIndex }, spanMap);
}
}
return spanMap;
}
const spanMap = generateSpanMap(tableData.value)
const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
const cellIndex = `${rowIndex}-${columnIndex}`;
return spanMap[cellIndex];
}
</script>
<template>
<main :style="{width: '800px'}">
<el-table
border
:data="tableData"
:span-method="spanMethod"
>
<el-table-column prop="title" label="平台指标" />
<el-table-column prop="subTitle" label="过滤项" />
<el-table-column prop="uv" label="去量uv" />
<el-table-column prop="percentage" label="占比" />
</el-table>
</main>
</template>
<style>
.origin-table {
width: 600px;
border: 1px solid #ccc;
border-collapse: collapse;
}
.origin-table th {
border: 1px solid #ccc;
}
.origin-table td {
border: 1px solid #ccc;
height: 30px;
}
</style>