前端复杂 table 渲染及 excel.js 导出

转载请注明出处,点击此处 查看更多精彩内容

现在我们有一个如图(甚至更复杂)的表格需要展示到页面上,并提供下载为 excel 文件的功能。

前端复杂 table 渲染及 excel.js 导出_第1张图片

前端表格渲染我们一般会使用 element-ui 等组件库提供的 table 组件,这些组件一般都是以列的维度进行渲染,而我们使用的 excel 生成工具(如 exceljs)却是以行的维度进行生成,这就导致页面渲染和 excel 生成的数据结构无法匹配。

为了解决这个问题,达到使用一套代码兼容页面渲染和 excel 生成的目的,我们需要统一使以行的维度进行数据的组织,然后分别使用原生 table 元素和 exceljs 进行页面渲染和 excel 文件生成。

功能列表

  • 单元格展示文字
  • 单元格文字尺寸
  • 单元格文字是否加粗
  • 单元格文字颜色
  • 单元格水平对齐方式
  • 单元格自定义展示内容(复杂样式、图片等)
  • 单元格合并
  • 指定行高
  • 单元格背景色
  • 是否展示单元格对角线
  • 是否展示边框

定义单元格数据结构

首先我们需要定义单元格和表格行的数据结构。

/**
 * 表格单元格配置
 */
export interface TableCell {
  /** 展示文案 */
  text?: string;
  /** 文字尺寸,默认 14 */
  fontSize?: number;
  /** 文字是否加粗 */
  bold?: boolean;
  /** 文字颜色,默认 #000000 */
  color?: string;
  /** 水平对齐方式,默认 center */
  align?: "left" | "center" | "right";
  /** 所占行数,默认 1 */
  rowspan?: number;
  /** 所占列数,默认 1 */
  colspan?: number;
  /** 高度,若一行中有多个单元格设置高度,将使用其中的最大值 */
  height?: number;
  /** 背景颜色 */
  bgColor?: string;
  /** 是否绘制对角线 */
  diagonal?: boolean;
  /** 是否绘制边框,默认 true */
  border?: ("top" | "right" | "bottom" | "left")[];
  /** 动态属性 */
  [key: string]: any;
}

/**
 * 表格行。undefined 标识被合并的单元格
 */
export type TableRow = (TableCell | undefined)[];

TableCell 表示一个单元格,定义了单元格的基本配置,如展示文案、对齐方式、单元格合并、颜色、字体大小、边框等,可根据实际需求进行扩展。

TableRow 是由多个单元格组成的表格行,undefined 用于标识被合并的单元格。

表格渲染

基于如上表格单元格和行的定义,我们可以编写一个组件用于渲染表格。






该组件接收表格数据(data)、表格列宽(colWidth)、自定义指定单元格样式的回调函数(cellStyle)等参数。

该组件对外公开名为 cell 的插槽,可自定义单元格的渲染内容。

生成 excel 文件

我们通过 exceljs 完成 excel 文件的生成。

安装 exceljs

npm install exceljs

根据表格配置生成 excel 文件

import ExcelJS, { Workbook, Worksheet } from "exceljs";

/**
 * 生成 excel 文件
 */
export async function generateExcel(
  rowList: TableRow[],
  colWidth: number | number[] = []
): Promise {
  // 创建表
  const workbook = new ExcelJS.Workbook();
  const worksheet = workbook.addWorksheet("Sheet1");
  // 插入表头和数据
  rowList.forEach((row) =>
    worksheet.addRow(row.map((cell) => cell?.text || ""))
  );
  // 合并单元格
  rowList.forEach((rowItem, rowIndex) => {
    rowItem.forEach((cellItem, colIndex) => {
      if (!cellItem) {
        return;
      }
      const colNoStart = convertColumnNo(colIndex);
      const colNoEnd = convertColumnNo(colIndex + (cellItem.colspan || 1) - 1);
      const rowNoStart = rowIndex + 1;
      const rowNoEnd = rowNoStart + (cellItem.rowspan || 1) - 1;
      worksheet.mergeCells(`${colNoStart}${rowNoStart}:${colNoEnd}${rowNoEnd}`);
    });
  });
  // 设置列宽
  let colWidthList: number[];
  if (Array.isArray(colWidth)) {
    colWidthList = colWidth;
  } else {
    colWidthList = new Array(rowList[0].length).fill(colWidth);
  }
  colWidthList.forEach((width, index) => {
    worksheet.getColumn(index + 1).width = width / 7.8;
  });
  // 设置默认行高
  worksheet.properties.defaultRowHeight = 28;
  // 设置单元格样式
  rowList.forEach((rowItem, rowIndex) => {
    const row = worksheet.getRow(rowIndex + 1);
    let maxHeight = worksheet.properties.defaultRowHeight;
    rowItem.forEach((cellItem, colIndex) => {
      if (!cellItem) {
        return;
      }
      const cell = row.getCell(colIndex + 1);
      maxHeight = Math.max(maxHeight, cellItem.height || 0);
      // 文字样式
      cell.font = {
        name: "等线",
        size: ((cellItem.fontSize || 14) * 11) / 14, // Excel 字体大小为 11
        bold: cellItem.bold,
        color: { argb: (cellItem.color || "#000000").slice(1) },
      };
      const border = cellItem?.border || ["top", "right", "bottom", "left"];
      // 设置边框
      cell.border = {
        top: border.includes("top") ? { style: "thin" } : undefined,
        right: border.includes("right") ? { style: "thin" } : undefined,
        bottom: border.includes("bottom") ? { style: "thin" } : undefined,
        left: border.includes("left") ? { style: "thin" } : undefined,
        diagonal: { up: false, down: cellItem?.diagonal, style: "thin" },
      };
      // 设置居中&自动换行
      cell.alignment = {
        horizontal: cellItem.align || "center",
        vertical: "middle",
        wrapText: true,
      };
      // 设置背景
      if (cellItem.bgColor) {
        cell.fill = {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: cellItem.bgColor.slice(1) },
        };
      }
    });
    row.height = maxHeight;
  });
  return workbook;
}

/**
 * 转换数字列号为字母列号
 * @param num
 */
function convertColumnNo(num: number) {
  const codeA = "A".charCodeAt(0);
  const codeZ = "Z".charCodeAt(0);
  const length = codeZ - codeA + 1;
  let result = "";
  while (num >= 0) {
    result = String.fromCharCode((num % length) + codeA) + result;
    num = Math.floor(num / length) - 1;
  }
  return result;
}

调用 generateExcel 函数传入表格配置即可生成一个 excel 工作簿对象 ExcelJS.Workbook

下载 excel 文件

/**
 * 下载为 excel 文件
 * @param workbook excel 工作簿对象
 * @param fileName 文件名
 */
export async function downloadExcel(workbook: ExcelJS.Workbook, fileName: string) {
  const buffer = await workbook.xlsx.writeBuffer();
  const blob = new Blob([buffer], { type: "arraybuffer" });
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
}

调用 downloadExcel 函数传入 ExcelJS.Workbook 对象和文件名即可下载为 excel 文件。

图片等内容处理

当前 generateExcel 函数并未处理图片等复杂内容。

由于这些内容具有不确定性,因此,我们定义一个专门处理这些内容的回调函数。

函数声明

/**
 * 渲染图片等非普通文本的数据
 */
export type RenderAdditionalData = (
  /** 行号 */
  rowIndex: number,
  /** 列号 */
  colIndex: number,
  /** excel 工作簿对象 */
  workbook: ExcelJS.Workbook,
  /** excel 工作表对象 */
  worksheet: ExcelJS.Worksheet
) => Promise | void;

将图片等内容的处理插入到 generateExcel 函数:

async function generateExcel(
  rowList: TableRow[],
  colWidth: number | number[] = [],
  renderAdditionalData?: RenderAdditionalData
): Promise {
  ...
  // 合并单元格
  rowList.forEach((rowItem, rowIndex) => {
    ...
  });

  // 渲染图片等非普通文本的数据
  if(renderAdditionalData) {
    for (let rowIndex = 0; rowIndex < rowList.length; rowIndex++) {
      const rowItem = rowList[rowIndex];
      for (let colIndex = 0; colIndex < rowItem.length; colIndex++) {
        if (!rowItem[colIndex]) {
          continue;
        }
        await renderAdditionalData(rowIndex, colIndex, workbook, worksheet);
      }
    }
  }

  // 设置默认行高
  worksheet.properties.defaultRowHeight = 28;
  ...
}

exceljs 对图片的渲染请查询官方文档。

至此,即可完成复杂 excel 表格的渲染和导出。

你可能感兴趣的:(前端,excel,vue.js)