表格合并小技巧

235 阅读3分钟

问题描述

在管理端系统中表格是最为常用组件之一,有些表格项会涉及到单元格合并的情况,比如有以下数据

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>

渲染后是这样子的

企业微信截图_16847486119057.png

第一列有些标题是相同的,现在需要合并第一列中标题相同的项,让这个表格看起来更有品位

借助于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,变成 2 1,然后第四行重置为0 0
  3. 然后遍历到第二行 判断条件满足要合并,则直接在第三行的基础上行+1,变成 3 1,然后第三行重置为0 0
  4. 以此类推..
  5. 总结起来就是倒序遍历,如果命中合并条件,下一个遍历直接在上一个遍历基础上加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];
}

渲染结果

image.png

完整代码

<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>