ElementUI实现el-table组件的合并行功能

前言

有时遇到一些需求,需要实现ElementUI中,el-tabled组件合并单元格的功能,稍微了解一下它的数据格式,不难可以写出比合并方法。但是在鼠标经过单元行时,会出现高亮的行与鼠标经过的行不一致的BUG。因此还需要实现@cell-mouse-enter和@cell-mouse-leave这两个方法,才可解决此问题。

一、多列合并

1.示例代码

复制代码
<template>
  <div class="merge-cell">
    <div class="merge-cell-navbar">记录一下 el-table 合并行小技巧</div>

    <div class="merge-cell-content">
      <div class="merge-cell-content-container">
        <el-table
          :data="tableData"
          border
          height="100%"
          :header-cell-style="
            {
              padding: '4px', // 设置Table表头单元内边距
              backgroundColor: '#e7f0ff', // 设置Table表头背景颜色
              borderColor: '#b6d1ff', // 设置Table表头边框颜色
              color: '#000', // 设置Table表头文字颜色
              fontSize: '13px', // 设置Table表头文字大小
              fontWeight: 'normal', // 设置Table表头文字粗细
            }
          "
          :span-method="handleSpanMethod"
          :row-class-name="handleRowClassName"
          @cell-mouse-enter="handleCellMouseEnter"
          @cell-mouse-leave="handleCellMouseLeave"
        >
          <el-table-column prop="zone" label="GameZone / 服务器区域 / 游戏区域" align="center" />
          <el-table-column prop="career" label="职业" width="180" align="center" />
          <el-table-column label="英雄" align="center">
            <el-table-column prop="hero" label="英雄姓名" width="280" align="center" />
            <el-table-column prop="firstSkill" label="一技能" width="220" align="center" />
            <el-table-column prop="secondSkill" label="二技能" width="220" align="center" />
            <el-table-column prop="thirdSkill" label="三技能" width="220" align="center" />
            <el-table-column label="操作" width="180" align="center">
              <template #default="scope">
                <el-tooltip effect="dark" content="收藏" placement="top" :enterable="false" :hide-after="0" @click="handleEditClick(scope.$index, scope.row)">
                  <el-button size="small" circle>
                    <el-icon size="18"><StarFilled /></el-icon>
                  </el-button>
                </el-tooltip>
              </template>
            </el-table-column>
          </el-table-column>
        </el-table>
      </div>
    </div>
  </div>
</template>

<script>
export default {
    
    
  data: () => ({
    
    
    tableData: [
      {
    
    
        zone: "王者一区",
        career: "坦克",
        hero: "亚瑟",
        firstSkill: "誓约之盾",
        secondSkill: "回旋打击",
        thirdSkill: "圣剑裁决",
      },
      {
    
    
        zone: "王者一区",
        career: "坦克",
        hero: "吕布",
        firstSkill: "方天画斩",
        secondSkill: "贪狼之握",
        thirdSkill: "魔神降世",
      },
      {
    
    
        zone: "王者一区",
        career: "坦克",
        hero: "项羽",
        firstSkill: "无畏冲锋",
        secondSkill: "破釜沉舟",
        thirdSkill: "霸王斩杀",
      },
      {
    
    
        zone: "王者一区",
        career: "战士",
        hero: "云缨",
        firstSkill: "断月",
        secondSkill: "追云",
        thirdSkill: "逐星",
      },
      {
    
    
        zone: "王者一区",
        career: "战士",
        hero: "赵怀真",
        firstSkill: "拨云见明",
        secondSkill: "气定神凝",
        thirdSkill: "阴阳逆转",
      },
      {
    
    
        zone: "王者二区",
        career: "刺客",
        hero: "镜",
        firstSkill: "开锋",
        secondSkill: "裂空",
        thirdSkill: "见影",
      },
      {
    
    
        zone: "王者二区",
        career: "刺客",
        hero: "澜",
        firstSkill: "破浪",
        secondSkill: "断空",
        thirdSkill: "处决",
      },
      {
    
    
        zone: "王者二区",
        career: "刺客",
        hero: "李白",
        firstSkill: "将进酒",
        secondSkill: "神来之笔",
        thirdSkill: "青莲剑歌",
      },
      {
    
    
        zone: "王者三区",
        career: "法师",
        hero: "妲己",
        firstSkill: "灵魂冲击",
        secondSkill: "偶像魅力",
        thirdSkill: "女王崇拜",
      },
      {
    
    
        zone: "王者三区",
        career: "射手",
        hero: "后羿",
        firstSkill: "多重箭矢",
        secondSkill: "落日余晖",
        thirdSkill: "灼日之矢",
      },
      {
    
    
        zone: "王者三区",
        career: "射手",
        hero: "鲁班7号",
        firstSkill: "河豚手雷",
        secondSkill: "无敌鲨嘴炮",
        thirdSkill: "空中支援",
      },
      {
    
    
        zone: "王者三区",
        career: "辅助",
        hero: "孙膑",
        firstSkill: "时空爆弹",
        secondSkill: "时之波动",
        thirdSkill: "时光流逝",
      },
      {
    
    
        zone: "王者三区",
        career: "辅助",
        hero: "庄周",
        firstSkill: "化蝶",
        secondSkill: "蝴蝶效应",
        thirdSkill: "天人合一",
      },
    ],

    first_row: {
    
    },
    second_row: {
    
    },
  }),
  methods: {
    
    
    /**
     * 合并单元格句柄方法
     */
    handleSpanMethod({
    
    
      row, // 行
      column, // 列
      rowIndex, // 行索引
      columnIndex, // 列索引
    }) {
    
    
      if (columnIndex === 0 || columnIndex === 1) {
    
    
        // 获取当前单元格的值
        const currentValue = row[column.property];

        // 获取上一行相同列的值
        const preRow = this.tableData[rowIndex - 1];
        const preValue = preRow ? preRow[column.property] : null;

        // 如果当前值和上一行的值相同,则将当前单元格隐藏
        if (currentValue === preValue) {
    
    
          return {
    
     rowspan: 0, colspan: 0 };
        } else {
    
    
          // 否则计算当前单元格应该跨越多少行
          let rowspan = 1;
          for (let i = rowIndex + 1; i < this.tableData.length; i++) {
    
    
            const nextRow = this.tableData[i];
            const nextValue = nextRow[column.property];
            if (nextValue === currentValue) {
    
    
              rowspan++;
            } else {
    
    
              break;
            }
          }
          return {
    
     rowspan, colspan: 1 };
        }
      }
    },

    /**
     * 鼠标移入表格事件句柄方法
     */
    handleCellMouseEnter(row, column, cell, event) {
    
    
      this.second_row = this.tableData.filter((item) => {
    
    
        return this.filterSameKeys(item, row, ["zone", "career"]);
      })[0];

      this.first_row = this.tableData.filter((item) => {
    
    
        return this.filterSameKeys(item, row, ["zone"]);
      })[0];
    },

    /**
     * 鼠标移出表格事件句柄方法
     */
    handleCellMouseLeave() {
    
    
      this.currentIndex = "";
      this.currentColumnIndex = "";
      this.first_row = {
    
    };
      this.second_row = {
    
    };
    },

    /**
     * 根据 keys 数组所有字段去做合并
     */
    filterSameKeys(item, row, keys) {
    
    
      let flag = true;
      keys.forEach((key) => {
    
    
        flag = flag && item[key] === row[key];
      });
      return flag;
    },

    /**
     * 给表格行添加自定义类名
     */
    handleRowClassName({
    
     row }) {
    
    
      let flag1 = this.first_row == row ? "first_row" : "";
      let flag2 = this.second_row == row ? "second_row" : "";
      return `${
      
      flag1} ${
      
      flag2}`;
    },
  },
};
</script>

<style lang="less" scoped>
  .merge-cell {
    
    
    display: flex;
    flex-direction: column;
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;

    .merge-cell-navbar {
    
    
      position: relative;
      width: 100%;
      height: 100px;
      background-color: #686868;
      text-align: center;
      line-height: 100px;
      color: #fff;
      font-size: 25px;
    }

    .merge-cell-content {
    
    
      position: relative;
      flex: 1;
      padding: 100px;
      overflow: hidden;

      .merge-cell-content-container {
    
    
        position: relative;
        width: 100%;
        height: 100%;
        overflow: auto;
      }
    }

    :deep(.el-table) {
    
    

      td .cell {
    
    
        padding: 2.5px 0;
        color: #686868;
        font-size: 13px;
      }

      .first_row td:nth-child(1),
      .second_row td:nth-child(1),
      .first_row.second_row td:nth-child(2) {
    
    
          background: #f5f7fa !important;
      }
    }

    /* ^ 设置Table表格边框颜色 */
    :deep(.el-table--border) {
    
    
      &::before {
    
    
        background-color: #b6d1ff;
      }
      &::after {
    
    
        background-color: #b6d1ff;
      }
      .el-table__cell {
    
    
        border-color: #b6d1ff;
      }
      .el-table__inner-wrapper {
    
    
        &::before {
    
    
          background-color: #b6d1ff;
        }
        &::after {
    
    
          background-color: #b6d1ff;
        }
        .el-table__border-left-patch {
    
    
          background-color: #b6d1ff;
        }
      }
    }
    /* / 设置Table表格边框颜色 */
  }
</style>

2.运行效果

二、单列合并

1.示例代码

复制代码
<template>
  <div class="merge-cell">
    <div class="merge-cell-navbar">记录一下 el-table 合并行小技巧</div>

    <div class="merge-cell-content">
      <div class="merge-cell-content-container">
        <el-table
          :data="tableData"
          border
          height="100%"
          :header-cell-style="
            {
              padding: '4px', // 设置Table表头单元内边距
              backgroundColor: '#e7f0ff', // 设置Table表头背景颜色
              borderColor: '#b6d1ff', // 设置Table表头边框颜色
              color: '#000', // 设置Table表头文字颜色
              fontSize: '13px', // 设置Table表头文字大小
              fontWeight: 'normal', // 设置Table表头文字粗细
            }
          "
          :span-method="handleSpanMethod"
        >
          <el-table-column prop="zone" label="GameZone / 服务器区域 / 游戏区域" align="center" />
          <el-table-column prop="career" label="职业" width="180" align="center" />
          <el-table-column label="英雄" align="center">
            <el-table-column prop="hero" label="英雄姓名" width="280" align="center" />
            <el-table-column prop="firstSkill" label="一技能" width="220" align="center" />
            <el-table-column prop="secondSkill" label="二技能" width="220" align="center" />
            <el-table-column prop="thirdSkill" label="三技能" width="220" align="center" />
            <el-table-column label="操作" width="180" align="center">
              <template #default="scope">
                <el-tooltip effect="dark" content="收藏" placement="top" :enterable="false" :hide-after="0" @click="handleEditClick(scope.$index, scope.row)">
                  <el-button size="small" circle>
                    <el-icon size="18"><StarFilled /></el-icon>
                  </el-button>
                </el-tooltip>
              </template>
            </el-table-column>
          </el-table-column>
        </el-table>
      </div>
    </div>
  </div>
</template>

<script>
export default {
    
    
  data: () => ({
    
    
    tableData: [
      {
    
    
        zone: "王者一区",
        career: "坦克",
        hero: "亚瑟",
        firstSkill: "誓约之盾",
        secondSkill: "回旋打击",
        thirdSkill: "圣剑裁决",
      },
      {
    
    
        zone: "王者一区",
        career: "坦克",
        hero: "吕布",
        firstSkill: "方天画斩",
        secondSkill: "贪狼之握",
        thirdSkill: "魔神降世",
      },
      {
    
    
        zone: "王者一区",
        career: "坦克",
        hero: "项羽",
        firstSkill: "无畏冲锋",
        secondSkill: "破釜沉舟",
        thirdSkill: "霸王斩杀",
      },
      {
    
    
        zone: "王者一区",
        career: "战士",
        hero: "云缨",
        firstSkill: "断月",
        secondSkill: "追云",
        thirdSkill: "逐星",
      },
      {
    
    
        zone: "王者一区",
        career: "战士",
        hero: "赵怀真",
        firstSkill: "拨云见明",
        secondSkill: "气定神凝",
        thirdSkill: "阴阳逆转",
      },
      {
    
    
        zone: "王者二区",
        career: "刺客",
        hero: "镜",
        firstSkill: "开锋",
        secondSkill: "裂空",
        thirdSkill: "见影",
      },
      {
    
    
        zone: "王者二区",
        career: "刺客",
        hero: "澜",
        firstSkill: "破浪",
        secondSkill: "断空",
        thirdSkill: "处决",
      },
      {
    
    
        zone: "王者二区",
        career: "刺客",
        hero: "李白",
        firstSkill: "将进酒",
        secondSkill: "神来之笔",
        thirdSkill: "青莲剑歌",
      },
      {
    
    
        zone: "王者三区",
        career: "法师",
        hero: "妲己",
        firstSkill: "灵魂冲击",
        secondSkill: "偶像魅力",
        thirdSkill: "女王崇拜",
      },
      {
    
    
        zone: "王者三区",
        career: "射手",
        hero: "后羿",
        firstSkill: "多重箭矢",
        secondSkill: "落日余晖",
        thirdSkill: "灼日之矢",
      },
      {
    
    
        zone: "王者三区",
        career: "射手",
        hero: "鲁班7号",
        firstSkill: "河豚手雷",
        secondSkill: "无敌鲨嘴炮",
        thirdSkill: "空中支援",
      },
      {
    
    
        zone: "王者三区",
        career: "辅助",
        hero: "孙膑",
        firstSkill: "时空爆弹",
        secondSkill: "时之波动",
        thirdSkill: "时光流逝",
      },
      {
    
    
        zone: "王者三区",
        career: "辅助",
        hero: "庄周",
        firstSkill: "化蝶",
        secondSkill: "蝴蝶效应",
        thirdSkill: "天人合一",
      },
    ],

    first_row: {
    
    },
    second_row: {
    
    },
  }),
  methods: {
    
    
    /**
     * 合并单元格句柄方法
     */
    handleSpanMethod({
    
    
      row, // 行
      column, // 列
      rowIndex, // 行索引
      columnIndex, // 列索引
    }) {
    
    
      const rowspanArr = this.formatRowspanAndColspan(this.tableData, 'zone')
      if (columnIndex === 0) {
    
    
        // console.log('row =>', row)
        // console.log('column =>', column)
        // console.log('rowIndex =>', rowIndex)
        // console.log('columnIndex =>', columnIndex)
        // console.log('\n')
        return {
    
    
          rowspan: rowspanArr[rowIndex],
          colspan: 1
        }
      }
    },

    /**
     * 合并单元格辅助 
     */
    formatRowspanAndColspan(tableList, keyName) {
    
    
      const keyNameList = []
      tableList.forEach(
        t => {
    
    keyNameList.push(t[keyName])}
      )
      // console.log('keyNameList =>', keyNameList)

      let prev // 上一个键名的索引
      let contin = 0 // 连续相同键名个数
      const computedList = [] // 计算后的键名列表
      for (let i = 0; i < keyNameList.length; i++) {
    
    
        if (computedList.length === 0) {
    
    
          computedList.push({
    
     'key': keyNameList[i], 'val': 1 })
        } else {
    
    
          if (keyNameList[prev] === keyNameList[i]) {
    
    
            contin++
            computedList.push({
    
     'key': keyNameList[i], 'val': 0 })
          } else {
    
    
            if (contin > 0) {
    
    
              const index = computedList.length - 1 - contin
              const key = computedList[index].key
              const val = computedList[index].val
              const obj = {
    
     'key': key, 'val': val + contin}
              computedList.splice(index, 1, obj)
            }
            computedList.push({
    
     'key': keyNameList[i], 'val': 1 })
            contin = 0
          }
        }

        prev = i // 将 i 赋值给 prev,以便当下一次循环时获取上一个键值的索引
      }
      if (contin > 0) {
    
    
        const index = computedList.length - 1 - contin
        const key = computedList[index].key
        const val = computedList[index].val
        const obj = {
    
     'key': key, 'val': val + contin}
        computedList.splice(index, 1, obj)
      }
      // console.log('computedList =>', computedList)

      const finalList = []
      computedList.forEach(
        t => {
    
    finalList.push(t.val)}
      )
      // console.log('finalList =>', finalList)

      return finalList
    },
  },
};
</script>

<style lang="less" scoped>
  .merge-cell {
    
    
    display: flex;
    flex-direction: column;
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;

    .merge-cell-navbar {
    
    
      position: relative;
      width: 100%;
      height: 100px;
      background-color: #686868;
      text-align: center;
      line-height: 100px;
      color: #fff;
      font-size: 25px;
    }

    .merge-cell-content {
    
    
      position: relative;
      flex: 1;
      padding: 100px;
      overflow: hidden;

      .merge-cell-content-container {
    
    
        position: relative;
        width: 100%;
        height: 100%;
        overflow: auto;
      }
    }

    :deep(.el-table) {
    
    

      td .cell {
    
    
        padding: 2.5px 0;
        color: #686868;
        font-size: 13px;
      }

      .first_row td:nth-child(1),
      .second_row td:nth-child(1),
      .first_row.second_row td:nth-child(2) {
    
    
          background: #f5f7fa !important;
      }
    }

    /* ^ 设置Table表格边框颜色 */
    :deep(.el-table--border) {
    
    
      &::before {
    
    
        background-color: #b6d1ff;
      }
      &::after {
    
    
        background-color: #b6d1ff;
      }
      .el-table__cell {
    
    
        border-color: #b6d1ff;
      }
      .el-table__inner-wrapper {
    
    
        &::before {
    
    
          background-color: #b6d1ff;
        }
        &::after {
    
    
          background-color: #b6d1ff;
        }
        .el-table__border-left-patch {
    
    
          background-color: #b6d1ff;
        }
      }
    }
    /* / 设置Table表格边框颜色 */
  }
</style>
相关推荐
却尘15 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare16 分钟前
浅浅看一下设计模式
前端
Lee川19 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust