使用el-table完成纵向单元和合并及动态列设置

1,224 阅读5分钟

现在的需求如下图, 完成的效果如下。 有这么几个关注点:

  • 1、第一列显示的是当前大类, 第二列显示的是当前小类,第三列之后开始就是数据, 我们正常表格都是行数据, 这里我们使用列数据,列是动态的, 数量不一, 行是固定的。
  • 2、我们可以配置每一列数据的插槽,这里面第一列的数据为基础数据,后面的数据每一列会和第一列的对比大小, 显示箭头。
  • 3、el-table 组件 鼠标悬浮的时候会自动给增加一个悬浮背景色, 当我们这里用到了单元格合并以后,这个背景色就会把左边合并了的 悬浮样式和右边第一行的一起触发,这里下面代码在 css 中单独处理了,使 el-table的背景颜色不跟着改变。

image.png 那么在做这个需求的时候, 主要有几个关注点,第一个就是列合并的思路, 这里我写到业务代码里了, 没有做动态列合并, 因为这种场景不多见, 但是这个也是能够封装的, 第二个就是列的值的显示思路, 这里我们既然使用 el-table 修改, 那么肯定数据实际上还是按照行数据来的, 那数据的读取就不能够按照正常使用的思维来, 第三个是渲染时候的判定, 我们需要针对自己需要的显示进行一些动态判定。 代码如下:

  • 代码当中关键的地方都写了注释, 这里最后一行 也就是 renderCompareValue 这里有一个渲染百分比的东西,这个是单独处理的,不需要关注。
  • 箭头的图标可以自己找 icon 就行了。

image.png

这里是传入的参数, 实际上也就是前两列的表头的,数据长的大概就是上图的样子,后端还是按照行分类给我们的, 这里没有让后端更改,所以在代码里前端进行了改造。我们最终需要使用的数据长的样子如下图,这里面使用汉字为 key 的 就是我们的列表头,他们的值是对象, value 就是我们显示的值, effect 就是是否需要显示箭头的策略, 0 不显示, 2 上箭头, 1 下箭头这样。

image.png

basisColumns: [
    { label: '类别', prop: 'compareType', width: 140 },
    { label: '对比内容', prop: 'content', width: 220 },
]

下面是完整代码

<template>
  <div>
    <el-table
      :data="tableData"
      :span-method="objectSpanMethod"
      border
      stripe
      class="compare-table"
      style="margin-top: 20px"
      :cell-style="cellStyleHandler"
      :cell-class-name="cellStyleHandler2"
      >
      <el-table-column
        v-for="item in columns"
        :key="item.label"
        :prop="item.prop"
        :width="item?.width"
        :label="item.label"
        align="center"
      >
        <template slot-scope="scope">
          <template v-if="renderOtherSlot(scope, item)">
            <div>
              {{  scope.row[item.prop].value ? '否' : '是'}}
            </div>
          </template>
          <template v-else-if="renderSlot(scope) === 'RENDER'">
            <div class="slot-class">
              <div>
                <el-tooltip effect="dark" content="1%" placement="top" :disabled="true">
                  <i class="el-icon--left" v-if="renderCompare(scope,2)" ><svg-icon icon-class="compare_big" /></i>
                  <i class="el-icon--left" v-if="renderCompare(scope,1)"><svg-icon icon-class="compare_small" /></i>
                </el-tooltip>
              </div>
              <div>{{renderCompareValue(scope) ?? '-'}}</div>
            </div>
          </template>
          <template v-else>
            {{renderSlot(scope)}}
          </template>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>

export default {
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    basisData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
        arrSort: [],
        tableData: [
          {compareType: '第一列数据',type:'segmentNum',content:'数量1'},
          {compareType: '第一列数据',type:'segmentUseLength',content:'输量2'},
          {compareType: '第一列数据',type:'stationNum',content:'输量3' },
          {compareType: '第一列数据',type:'loadRatio',content:'数量4'},
          {compareType: '第一列数据',type:'power',content:'数量5'},
          {compareType: '第一列数据',type:'stationInvestment',content:'数量6' },
          {compareType: '第一列数据',type:'segmentInvestment',content:'数量7'},
          {compareType: '第一列数据',type:'buildCost',content:'数量8'},
          {compareType: '第一列数据',type:'operatingCost',content:'数量9'},
          {compareType: '第一列数据',type:'presentCost',content:'数量10'},
          {compareType: '第一列数据',type:'turnoverEnergyConsumption',content:'数量11'},

          {compareType: '第二列数据',type:'loadRationRelaxMax',content:'测试1'},
          {compareType: '第二列数据',type:'energyTarget',content:'测试2'},
          {compareType: '第二列数据',type:'turnoverTarget',content:'测试3'},
          {compareType: '第二列数据',type:'costTarget',content:'测试4'},

          {compareType: '第三列数据',type:'minimumTotalTurnover',content:'测试5'},
          {compareType: '第三列数据',type:'optimalLoadRatio',content:'测试6'},
          {compareType: '第三列数据',type:'minimumReceptionCost',content:'测试7'},
          {compareType: '第三列数据',type:'minimumEnergy',content:'测试8'},

          {compareType: '第四列数据',type:'maxRunTime',content:'测试9(min)'},
          {compareType: '第四列数据',type:'convergencePrecision',content:'测试10'},
          {compareType: '第四列数据',type:'isWaterConservancy',content:'测试11'},
          
          {compareType: '推荐排序',type:'sort',content:'测试12'},
        ]
    };
  },
  // 数据重置
  created() {
    this.arrSort = []
    // 改造数据源
    this.basisData.forEach((item)=>{
      const { modelName, constraints, executeConfig, optimizeTargetWeight, basIndicators} = item 
      this.arrSort.push(modelName) 
      const currentObj = { ...constraints, ...executeConfig, ...optimizeTargetWeight, ...basIndicators}
      Object.keys(currentObj).forEach((big)=>{
        // 循环,对照数据,找到 tableData 展示 数据中的对应项, 进行赋值。
        const showCurrent = this.tableData.find((small)=>small.type === big)
        if(showCurrent){
          // 响应式复制, 给当前列 复制 value 对象
          this.$set(showCurrent, modelName, { value: currentObj[big]})
        }
      })
    })
    const denominator = this.tableData.find(item=>item.type === 'presentCost')[this.arrSort[0]].value
    // 第一层循环当前数据、第二层 找到当前列对应的数据, 与第一列对应的数据进行对比, 给当前列数据显示对象 复制对比结果
    this.tableData.forEach((item)=>{
      this.arrSort.map((it,index)=>{
        const compare = this.arrSort[index]
        const compareValue = item[compare]?.value || null  // 当前列的值
        const currentValue = item[this.arrSort[0]]?.value || null  // 第一列需要对比的值
        // 如果 需要对比的值为 null  或者当前列的值为 null  则不对比
        // 找到第一列的 值, 需要用作分母的, 这里判定 如果分母是 0 , 则不渲染箭头
        if(item.type === 'sort' && denominator === 0){
          item[compare].effect = 0
        }else if(currentValue === null || compareValue === null){ item[compare].effect = 0 }  
        else{
          item[compare].effect = compareValue > currentValue ? 2 : compareValue < currentValue ? 1 : 0;
        }
      })
    })
  },
  mounted() {
  },
  methods: {
    // 渲染对比的值
    renderCompareValue(data){
      const { row, column:{ label } } = data 
      const { type } = row
      const value = row[label]?.value
      if(type === 'sort' &&  value){
        const current = this.tableData.find(item=>item.type === 'presentCost')
        // 计算百分比 使用当前列的值 / 第一列的值 * 100%   第一列的 label 为 this.arrSort[0] 当前 label 为 lable
        // 如果 分母是 0 , 则不计算
        const denominator = current[this.arrSort[0]].value   // 分母
        const molecule = current[label].value
        if(denominator === 0) return '-'
        return value + `(${(molecule / denominator * 100).toFixed(2)}%)` 
      }
      return value ?? '-'
    },
    renderCompare(data,num){
      // 渲染箭头方向
      const { row, column:{ label } } = data 
      return row[label]?.effect === num
    },
    renderSlot(scope){
      const { row, column } = scope
      const { property } = column
      // 排除第一列, 第二列之后的使用 箭头插槽渲染
      let arr = [...this.arrSort].slice(1)
      if(arr.includes(property)){
        return 'RENDER'
      }
      // 值是对象的,标识不是类别 和 对比内容, 直接返回 value 
      const value = row[property]?.value
      if(row.type === 'sort' && typeof row[property] === 'object') return value ? `${value}(100%)` : '-' // 渲染第一列, 经济评价指标的百分比
      if(typeof row[property] === 'object') return value ?? '-'
      return row[property]  // 返回前两列的固定label
    },
    // 渲染不需要对比的值
    renderOtherSlot(scope, item){
      const { row, column: { property } } = scope
      if(row.type === "isWaterConservancy" && property !== 'compareType' && property !== 'content') return true
      return false
    },
    // 设置斑马线样式
    cellStyleHandler2({row, column, rowIndex, columnIndex}){
      if(columnIndex === 0) return 'cell-reset'
    },
    cellStyleHandler({row, column, rowIndex, columnIndex}){
      // 设置第一列的样式
      if(columnIndex === 0){
        return {
          backgroundColor: "#fff"
        }
      }
    },
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      // columnIndex 为当前列的索引, 我们只合并第一列, compareType 为当前列的值 rowIndex 为当前列的行数,  rowspan 是我们要合并多少行, 这里拿第一个 switch 的条件距离,rowspan 11 表示合并11行, colspan 表示 显示1列。 那么我们在想要合并11行的话, 就在第 11 行 合并即可, 其他 10行的 行跨度 和 列跨度 都返回0 即可。
      const { compareType, content } = row
        if (columnIndex === 0) {
          switch(compareType){
            case "第一列数据":
              // console.log('data', row, column, rowIndex, rowIndex % 11, '---',columnIndex);
              if (rowIndex % 11 === 0) {
                  return {
                    rowspan: 11, // 行跨度
                    colspan: 1 // 列跨度
                  };
                } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            case "第二列数据":
              if (rowIndex % 11 === 0) {
                return {
                  rowspan: 4,
                  colspan: 1
                };
              } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            case "第三列数据": 
              if (rowIndex % 15 === 0) {
                  return {
                    rowspan: 4,
                    colspan: 1
                  };
              } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            case "第四列数据":
              // console.log('data', row, column, rowIndex, rowIndex % 20, '---',columnIndex);
              if (rowIndex % 19 === 0) {
                    return {
                      rowspan: 3,
                      colspan: 1
                    };
              } else {
                return {
                  rowspan: 0,
                  colspan: 0
                };
              }
            default: 
              return {
                rowspan: 1,
                colspan: 1
              }
          }
        }
      }
  },
};
</script>

<style lang="scss" scoped>
::v-deep
  .el-table--striped
  .el-table__body
  tr.el-table__row--striped
  td.el-table__cell {
  background-color: #f6faff;
}
::v-deep .el-table--enable-row-hover .el-table__body tr:hover > td {
  background-color: transparent !important;
}
::v-deep
  .el-table--enable-row-hover
  .el-table__body
  tr.el-table__row--striped:hover
  > td {
  background-color: #f6faff !important;
}
.slot-class {
  display: flex;
  justify-content: center;
  > div:first-child {
    display: flex;
    align-items: center;
  }
}
// 修改鼠标悬浮时候,让第一列背景不跟着改变
::v-deep .el-table--enable-row-hover .el-table__body tr:hover > td.cell-reset {
  background-color: transparent !important;
}
.el-icon--left {
  width: 16px;
  height: 16px;
}

.compare-table {
  width: 100%;
  overflow: auto;
}

</style>