vue3中Table导出Excel(二)--合并表头

前言:上次发布的根据tBody导出合并后的excel文件,这篇文章是后面遇到了导出合并表头excel,新封装的方法,比较通用,根据antdV的Columns合并表头

一、需求&实现效果

项目中经常有一些表格导出,然后产品又让跟页面展示的表格一样,然后又是前端导出,这时就遇到了合并表头导出的问题,于是就有了这篇文章

Table

项目中的table

导出的Excel

导出的效果

二、代码实现

思路

1.首先我项目中用的excel导出插件是xlsx,配套的xlsx-js-style(给导出的excel添加样式)
2.看了下xlsx插件导出时可以合并单元格,但是需要对应的格式

//merges
[
  {
  	//s:是开始单元格对象,r:是row坐标,c:col坐标
    s: {
      r: 0,
      c: 0,
    },
    //e:是开始单元格对象,r:是row坐标,c:col坐标
    e: {
      r: 0,
      c: 0,
    },
  },
];

3.那么我们就需要处理出合并表头变成这个merges,在看一下我的们数据源,也就是antdV的多级表头

const columns: TableColumnsType = [
  {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    width: 100,
    fixed: 'left',
    filters: [
      {
        text: 'Joe',
        value: 'Joe',
      },
      {
        text: 'John',
        value: 'John',
      },
    ],
    onFilter: (value: string, record: TableDataType) => record.name.indexOf(value) === 0,
  },
  {
    title: 'Other',
    children: [
      {
        title: 'Age',
        dataIndex: 'age',
        key: 'age',
        width: 200,
        sorter: (a: TableDataType, b: TableDataType) => a.age - b.age,
      },
      {
        title: 'Address',
        children: [
          {
            title: 'Street',
            dataIndex: 'street',
            key: 'street',
            width: 200,
          },
          {
            title: 'Block',
            children: [
              {
                title: 'Building',
                dataIndex: 'building',
                key: 'building',
                width: 100,
              },
              {
                title: 'Door No.',
                dataIndex: 'number',
                key: 'number',
                width: 100,
              },
            ],
          },
        ],
      },
    ],
  },
  {
    title: 'Company',
    children: [
      {
        title: 'Company Address',
        dataIndex: 'companyAddress',
        key: 'companyAddress',
        width: 200,
      },
      {
        title: 'Company Name',
        dataIndex: 'companyName',
        key: 'companyName',
      },
    ],
  },
  {
    title: 'Gender',
    dataIndex: 'gender',
    key: 'gender',
    width: 80,
    fixed: 'right',
  },
];

4.antdv的Column多级表头,如果出现多级嵌套的话 就会有一个children 以此类推

1.根据Column格式生成一个带层级的level对象,对象的key就是深度

... 
const level = {};//level对象
getColKeys(columns, level, 0);//调用getColkeys方法生成level
const max = Math.max(...Object.values(level).map((item: any) => item.length));//找到level最多的数据的一层
Object.keys(level).forEach((item: any) => {
  if (level[item].length < max) {//给每一层补充数据
     level[item].push(...new Array(max - level[item].length).fill(null));//因为xlsx纵向合并传row会显示文字所以替换成null
  }
});
...

/**
 * 根据Columns生成带level
 * @param target Columns
 * @param levelObj level对象
 * @param level 深度
 * @returns 偏移量
 */
export function getColKeys(target, levelObj, level) {
  let offset = 0;//偏移
  if (!levelObj[level]) {//判断当前层级有没有数据,没有的话就初始化为空数组
    levelObj[level] = [];
  }
  //如果不是第0级的数据那么就补充当前层级和上个层级的空位null
  if (level > 0) {
    levelObj[level].push(
      ...new Array(levelObj[level - 1].length - 1 - levelObj[level].length).fill(null),
    );
  }
  target.forEach((item) => {
    levelObj[level].push(item.title);
    if (item.children && item.children.length > 0) {
      //递归调用,然后获取到返回的偏移量,当前的偏移量+子元素返回的偏移量,就等于当前格子所战的格子数
      const childrenOffset = getColKeys(item.children, levelObj, level + 1);
      offset += childrenOffset;
      levelObj[level].push(...new Array(childrenOffset - 1).fill('col'));
    } else {
      offset++;
    }
  });

  return offset;
}

醉后生成的结果

2.根据生成level对象生成merges数据

...
const merges: any = []
Object.keys(level).forEach((key) => {
  merges.push(...getMerge(level[key], key, level));
});
...

/**
 * 根据level生成merges
 * @param target 目标层级
 * @param deep   深度
 * @param source level源数据
 * @returns
 */
function getMerge(target, deep, source) {
  //默认合并开始的row:rsindex=0 结束合并的reindex
  //每一列开始遍历如果下一个是col index不用管 如果不是col 那么rsindex=index reindex=index
  //根据当前index向深层遍历,找到csindex和ceindex
  //然后push到merge数组中
  const merge: any = [];
  deep = Number(deep);
  // let se = {};
  target.forEach((item, index) => {
    if (item != 'col') {
      const se: any = {
        s: { r: deep, c: index },
      };
      if (index == target.length - 1) {
        se.e = { r: getCol(se.s.c), c: target.length - 1 };
      }
      if (merge.length > 0) {
        merge[merge.length - 1].e = { r: getCol(merge[merge.length - 1].s.c), c: index - 1 };
      }
      merge.push(se);
    } else {
      if (index == target.length - 1) {
        merge[merge.length - 1].e = {
          r: getCol(merge[merge.length - 1].s.c),
          c: target.length - 1,
        };
      }
      if (index == 0) {
        const se: any = {
          s: { r: deep, c: index },
        };
        merge.push(se);
      }
      return;
    }
  });
  //处理纵向合并
  function getCol(index) {
    let col = deep;
    for (let i = deep; i < Object.keys(source).length; i++) {
      if (source[i][index] == null) {
        col = i;
      }
    }
    return col;
  }
  return merge;
}

最终生成的结果

4.全部代码

全部代码奉上,exportExcel方法是之前一篇导出excel文件的方法改装的链接在下面
链接: vue3中Table导出Excel

import * as XLSX from 'xlsx';
import XLSXJSStyle from 'xlsx-js-style';
/*
 * @param tableData 要导出的数据(必填)(isMerge为处需要处理成二维数组每个元素就是一行的数据)
 * @param columns 要导出的数据的表头(必填)
 * @param name  导出文件名称默认为test
 * @param sheetName
 * @param isMerge
 * @param merges 要合并行/列 格式为 [{s:{r:x,c:x},e:{}}]
 */
export function exportExcel(
  tableData,
  columns,
  name = 'test',
  sheetName = 'sheetName',
  isMerge: boolean = false,
  // topColorBg = 'F5F5F5',
  merges: any = [],
) {
  /* convert state to workbook */
  const data: any[] = [];
  const keyArray: any[] = columns.map((item) => item.key); //获取key
  let titleArr: any[] = columns.map((item) => item.title); //获取表头
  const level = {};
  if (isMerge) {
    getColKeys(columns, level, 0);
    const max = Math.max(...Object.values(level).map((item: any) => item.length));
    Object.keys(level).forEach((item: any) => {
      if (level[item].length < max) {
        level[item].push(...new Array(max - level[item].length).fill(null));
      }
    });
    titleArr = [...Object.values(level)];
    Object.keys(level).forEach((key) => {
      merges.push(...getMerge(level[key], key, level));
    });
    data.splice(0, 0, ...titleArr);
    data.push(...tableData);
  } else {
    data.splice(0, 0, titleArr);
    // keyArray为英文字段表头
    tableData.forEach((item) => {
      const arr: any[] = keyArray.map((key) => {
        return item[key];
      });
      data.push(arr);
    });
  }

  console.log('data', data);
  const ws: any = XLSX.utils.aoa_to_sheet(data);
  const wb = XLSX.utils.book_new();
  // 此处隐藏英文字段表头
  // const wsrows = [{ hidden: true }]
  // ws['!rows'] = wsrows // ws - worksheet
  const cols = new Array(data[0].length).fill({ wch: 20 });
  ws['!cols'] = cols; // 将cols添加到sheet中 设置列宽
  ws['!merges'] = merges;
  console.log(ws);
  for (const key in ws) {
    if (key != '!rows' && key != '!merges' && key != '!ref') {
      ws[key].s = {
        border: {
          bottom: { style: 'thin', color: { rgb: 'D0D0D0' } },
          left: { style: 'thin', color: { rgb: 'D0D0D0' } },
          top: { style: 'thin', color: { rgb: 'D0D0D0' } },
          right: { style: 'thin', color: { rgb: 'D0D0D0' } },
        },
        alignment: {
          horizontal: 'center', //水平居中
          vertical: 'center', //垂直居中
        },
      };
    }
    const max = Object.keys(level).length || 1;
    if (Number(key.replace(/[A-Z]/g, '')) <= max) {
      ws[key].s = {
        fill: { fgColor: { rgb: 'F5F5F5' } },
        border: {
          bottom: { style: 'thin', color: { rgb: 'D0D0D0' } },
          left: { style: 'thin', color: { rgb: 'D0D0D0' } },
          top: { style: 'thin', color: { rgb: 'D0D0D0' } },
          right: { style: 'thin', color: { rgb: 'D0D0D0' } },
        },
        alignment: {
          horizontal: 'center', //水平居中
          vertical: 'center', //垂直居中
        },
      };
    }
  }
  XLSX.utils.book_append_sheet(wb, ws, sheetName);
  const wbout = XLSXJSStyle.write(wb, { bookType: 'xlsx', bookSST: true, type: 'array' });
  console.log(wbout);
  console.log(wb);
  try {
    downloadByData(wbout, `${name}.xlsx`);
  } catch (error) {
    console.log(error);
  }
}
/**
 * 根据level生成merges
 * @param target 目标层级
 * @param deep   深度
 * @param source level源数据
 * @returns
 */
function getMerge(target, deep, source) {
  //默认合并开始的row:rsindex=0 结束合并的reindex
  //每一列开始遍历如果下一个是col index不用管 如果不是col 那么rsindex=index reindex=index
  //根据当前index向深层遍历,找到csindex和ceindex
  //然后push到merge数组中
  const merge: any = [];
  deep = Number(deep);
  // let se = {};
  target.forEach((item, index) => {
    if (item != 'col') {
      const se: any = {
        s: { r: deep, c: index },
      };
      if (index == target.length - 1) {
        se.e = { r: getCol(se.s.c), c: target.length - 1 };
      }
      if (merge.length > 0) {
        merge[merge.length - 1].e = { r: getCol(merge[merge.length - 1].s.c), c: index - 1 };
      }
      merge.push(se);
    } else {
      if (index == target.length - 1) {
        merge[merge.length - 1].e = {
          r: getCol(merge[merge.length - 1].s.c),
          c: target.length - 1,
        };
      }
      if (index == 0) {
        const se: any = {
          s: { r: deep, c: index },
        };
        merge.push(se);
      }
      return;
    }
  });
  function getCol(index) {
    let col = deep;
    for (let i = deep; i < Object.keys(source).length; i++) {
      if (source[i][index] == null) {
        col = i;
      }
    }
    return col;
  }
  return merge;
}
/**
 * 根据Columns生成带level
 * @param target 目标层级
 * @param levelObj level对象
 * @param level 深度
 * @returns
 */
export function getColKeys(target, levelObj, level) {
  let offset = 0;
  if (!levelObj[level]) {
    levelObj[level] = [];
  }
  if (level > 0) {
    levelObj[level].push(
      ...new Array(levelObj[level - 1].length - 1 - levelObj[level].length).fill(null),
    );
  }
  target.forEach((item) => {
    levelObj[level].push(item.title);
    if (item.children && item.children.length > 0) {
      const childrenOffset = getColKeys(item.children, levelObj, level + 1);
      offset += childrenOffset;
      levelObj[level].push(...new Array(childrenOffset - 1).fill('col'));
    } else {
      offset++;
    }
  });

  return offset;
}

export function downloadByData(data: BlobPart, filename: string, mime?: string, bom?: BlobPart) {
  const blobData = typeof bom !== 'undefined' ? [bom, data] : [data];
  const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });

  const blobURL = window.URL.createObjectURL(blob);
  const tempLink = document.createElement('a');
  tempLink.style.display = 'none';
  tempLink.href = blobURL;
  tempLink.setAttribute('download', filename);
  if (typeof tempLink.download === 'undefined') {
    tempLink.setAttribute('target', '_blank');
  }
  document.body.appendChild(tempLink);
  tempLink.click();
  document.body.removeChild(tempLink);
  window.URL.revokeObjectURL(blobURL);
}

修改后的exportExcel多了isMerge参数,如果需要合并表头,传true,但是tableData特殊处理(要自己处理成一一对应的)

处理完的数据

结束

方法有点简陋,但是是可以用的,我同事在其他项目中直接引用我这个方法可以成功导出合并表头后的excel,真不错啊!!!
水一帖子!!!

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值