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

news2024/10/7 18:29:58

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

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

效果图.png

前端表格渲染我们一般会使用 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 用于标识被合并的单元格。

表格渲染

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

<template>
  <div class="custom_table">
    <table>
      <colgroup>
        <col
          v-for="(width, index) in colWidthList"
          :key="index"
          :style="{ width: `${width}px` }"
        />
      </colgroup>
      <tr
        v-for="(row, rowIndex) in data"
        :key="rowIndex"
        :style="{ height: calcRowHeight(row) }"
      >
        <td
          v-for="(cell, colIndex) in row.filter((item) => !!item)"
          :key="colIndex"
          :class="[
            'table-cell',
            ...getCellBorderClass(cell),
            { 'table-cell--diagonal': cell?.diagonal },
          ]"
          :style="{
            fontSize: `${cell?.fontSize || 14}px`,
            fontWeight: cell?.bold ? 'bold' : 'initial',
            color: cell?.color || '#000000',
            textAlign: cell?.align || 'center',
            background: cell?.bgColor || '#ffffff',
            ...cellStyle?.(cell),
          }"
          :rowspan="cell?.rowspan"
          :colspan="cell?.colspan"
        >
          <slot name="cell" :cell="cell">{{ cell?.text }}</slot>
        </td>
      </tr>
    </table>
  </div>
</template>

<script setup lang="ts">
import { computed, CSSProperties } from "vue";
import { TableCell, TableRow } from "@/utils/excel-helper";

export interface Props {
  /** 表格数据 */
  data: TableRow[];
  /** 表格列宽。number[] 精确指定每列的宽度;number 表示所有列统一使用指定宽度 */
  colWidth?: number | number[];
  /** 自定义指定单元格的样式 */
  cellStyle?: (cell?: TableCell) => CSSProperties;
}

const props = withDefaults(defineProps<Props>(), {});

export interface Slots {
  cell?: (props: { cell?: TableCell }) => void;
}

defineSlots<Slots>();

// 列宽
const colWidthList = computed(() => {
  if (!props.colWidth) {
    return [];
  }
  if (Array.isArray(props.colWidth)) {
    return props.colWidth;
  }
  return new Array(props.data[0]?.length).fill(props.colWidth);
});

// 计算行高
const calcRowHeight = (row: TableRow) => {
  const heightList = row.map((item) => item?.height || 0);
  return `${Math.max(25, ...heightList)}px`;
};

// 获取边框样式
const getCellBorderClass = (cell?: TableCell) => {
  const border = cell?.border || ["top", "right", "bottom", "left"];
  return border.map((item) => `table-cell--border-${item}`);
};
</script>

<style lang="scss" scoped>
.custom_table {
  display: flex;
  width: fit-content;
  max-width: -webkit-fill-available;
  font-size: 14px;
  overflow: auto;

  table {
    flex-shrink: 0;
    border-collapse: collapse;
  }

  td {
    height: 20px;
    line-height: 20px;
    padding: 8px 6px 6px;
    text-align: center;
    white-space: break-spaces;
    word-break: break-all;
  }

  .table-cell {
    &--border-top {
      border-top: 1px solid #606266;
    }

    &--border-right {
      border-right: 1px solid #606266;
    }

    &--border-bottom {
      border-bottom: 1px solid #606266;
    }

    &--border-left {
      border-left: 1px solid #606266;
    }

    &--diagonal {
      position: relative;

      &::before {
        content: "";
        position: absolute;
        inset: 0;
        background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxsaW5lIHgxPSIwIiB5MT0iMCIgeDI9IjEwMCUiIHkyPSIxMDAlIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjEiLz48L3N2Zz4=)
          no-repeat 100% center !important;
      }
    }
  }
}
</style>

该组件接收表格数据(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<ExcelJS.Workbook> {
  // 创建表
  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> | void;

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

async function generateExcel(
  rowList: TableRow[],
  colWidth: number | number[] = [],
  renderAdditionalData?: RenderAdditionalData
): Promise<ExcelJS.Workbook> {
  ...
  // 合并单元格
  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 表格的渲染和导出。如需其他配置可自行扩展。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1434153.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C/C++ - 容器list

目录 容器特性 list 容器特性 使用场景 构造函数 默认构造函数 填充构造函数 范围构造函数 复制构造函数 大小函数 函数&#xff1a;size 函数&#xff1a;empty​ 函数&#xff1a;max_size​ 增加函数 函数&#xff1a;​push_back​ 函数&#xff1a;push_f…

Kubernetes 简介

&#x1f4ce;k8s 入门到微服务项目实战.xmindhttps://www.yuque.com/attachments/yuque/0/2024/xmind/35457682/1707117691869-1ea2805d-7218-4223-a0a9-877147ca84b2.xmind 目录 1、概念介绍 应用部署的演变 Kubernetes 架构图 分层架构 2、Kubernetes 组件 控制面板组…

基恩士 KV-8000 PLC通讯简单测试

1、KV-8000通讯协议 基恩士 KV-8000 PLC支持多种通讯方式&#xff0c;包括&#xff1a;OPC UA、Modbus、上位链路命令等。其中OPC UA需要对服务器和全局变量进行设置&#xff0c;Modbus需要调用功能块。默认支持的是上位链路命令&#xff0c;实际是一条条以回车换行结束的ASCII…

vulhub中AppWeb认证绕过漏洞(CVE-2018-8715)

AppWeb是Embedthis Software LLC公司负责开发维护的一个基于GPL开源协议的嵌入式Web Server。他使用C/C来编写&#xff0c;能够运行在几乎先进所有流行的操作系统上。当然他最主要的应用场景还是为嵌入式设备提供Web Application容器。 AppWeb可以进行认证配置&#xff0c;其认…

在bash或脚本中,如何并行执行命令或任务(命令行、parallel、make)

最近要批量解压归档文件和压缩包&#xff0c;所以就想能不能并行执行这些工作。因为tar自身不支持并行解压&#xff0c;但是像make却可以支持生成一些文件&#xff0c;所以我才有了这种想法。 方法有两种&#xff0c;第一种不用安装任何软件或工具&#xff0c;直接bash或其他 …

html2canvas 截图功能使用 VUE

html2canvas 是一个 JavaScript 库&#xff0c;可以将网页内容转换为 Canvas 元素&#xff0c;并生成图像或 PDF 文件。使用 html2canvas&#xff0c;你可以在客户端将网页的内容截图&#xff0c;并将其作为图像或 PDF 文件保存或分享。 以下是一些 html2canvas 库的特点和用途…

C语言笔试题之反转链表(头插法)

实例要求&#xff1a; 1、给定单链表的头节点 head &#xff1b;2、请反转链表&#xff1b;3、最后返回反转后的链表&#xff1b; 案例展示&#xff1a; 实例分析&#xff1a; 1、入参合理性检查&#xff0c;即head ! NULL || head->next ! NULL&#xff1b;2、while循环…

深刻理解树状数组--树状数组构造定义与动态维护区间和的合理性证明

文章目录 一.树状数组概览二.树状数组构造定义lowbit运算树状数组的结点值的定义树状数组结点层次的定义树状数组父子结点关系定义 三.关于树状数组结构的重要证明引理1引理2树状数组模板题 一.树状数组概览 树状数组的下标从1开始标识,其物理结构是线性表,逻辑结构是一颗多叉…

Vue中v-if和v-show区别

Vue中v-if和v-show是两个常用的指令&#xff0c;用于控制元素的显示和隐藏。虽然它们都能达到相同的效果&#xff0c;但在实现机制和使用场景上有一些区别。本文将详细介绍v-if和v-show的区别&#xff0c;并且通过示例代码来演示它们的使用。 首先&#xff0c;让我们来看一下v…

Debezium发布历史110

原文地址&#xff1a; https://debezium.io/blog/2021/09/22/deep-dive-into-a-debezium-community-connector-scylla-cdc-source-connector/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. Debezium 1.7.0.CR2…

世界各国都在追求“主权人工智能能力”,国家级人工智能硬件需求将剧增

NVIDIA的CEO黄仁勋最近在接受媒体采访时指出&#xff0c;世界各国都打算在本国内建立和运行自主的人工智能基础设施&#xff0c;这将全面带动NVIDIA硬件产品需求的飙升。黄仁勋表示&#xff0c;包括印度、日本、法国和加拿大在内的国家政府都在讨论投资“国家主权人工智能能力”…

关于harbor做HA

我起初是用helm在k8s上装的harbor&#xff0c;最近遇到如下故障&#xff1a;就是服务器硬件设备故障突然死机&#xff0c;恰巧是harbor容器所在的服务器&#xff0c;其他在这个服务器上运行的容器&#xff0c;由kubelet调度到其他正常的服务器上重启去了&#xff0c;但harbkor并…

2024.1.26力扣每日一题——边权重均等查询

2024.1.26 题目来源我的题解方法一 使用dfs对每一组查询都求最近公共祖先&#xff08;会超时&#xff0c;通不过&#xff09;方法二 不需要构建图&#xff0c;直接在原始数组上进行求最大公共祖先的操作。 题目来源 力扣每日一题&#xff1b;题序&#xff1a;2846 我的题解 …

[C++] opencv + qt 创建带滚动条的图像显示窗口代替imshow

在OpenCV中&#xff0c;imshow函数默认情况下是不支持滚动条的。如果想要显示滚动条&#xff0c;可以考虑使用其他库或方法来进行实现。 一种方法是使用Qt库&#xff0c;使用该库可以创建一个带有滚动条的窗口&#xff0c;并在其中显示图像。具体步骤如下&#xff1a; 1&…

《汇编语言》- 读书笔记 - 各章检测点归档

《汇编语言》- 读书笔记 - 各章检测点归档 检测点 1.1检测点 2.1检测点 2.2检测点 2.3检测点 3.1检测点 3.2检测点 6.1检测点 9.1 检测点 1.1 1个CPU 的寻址能力为8KB&#xff0c;那么它的地址总线的宽度为 13 。 解&#xff1a;8KB 8192B 213 1KB的存储器有 10…

###C语言程序设计-----C语言学习(9)#函数基础

前言&#xff1a;感谢您的关注哦&#xff0c;我会持续更新编程相关知识&#xff0c;愿您在这里有所收获。如果有任何问题&#xff0c;欢迎沟通交流&#xff01;期待与您在学习编程的道路上共同进步。 一. 基础知识的学习 1.函数的定义 函数是一个完成特定工作的独立程序模块&…

酷开系统 | 拓展内容营销边界,酷开科技大屏价值全面升维

丰富的内容是智能大屏吸引消费者的关键。随着智能大屏各类垂直应用的增多&#xff0c;和长、短视频等多元内容的加入&#xff0c;使消费者的使用需求进一步激发和释放&#xff0c;这些流量的加入&#xff0c;也使大屏成为了营销的天然宝藏。酷开科技一直致力于OTT大屏营销&…

这个人脸考勤技术,拿走直接套用!请收藏!

随着科技的不断发展&#xff0c;人脸识别技术在各个领域得到了广泛应用&#xff0c;其中之一就是人脸考勤系统。 三维人脸考勤系统作为现代智能考勤的代表&#xff0c;通过先进的人脸识别技术&#xff0c;为组织提供了一种高效、准确、安全的考勤解决方案。 客户案例 制造企业…

【XR806开发板试用】TCP通信测试 Ping 命令测试

1.工程准备 由于要使用wifi功能&#xff0c;直接从wlan_demo复制一份出来&#xff0c;然后修改。 源文件只留下 main.c 就可以了。 BUILD.gn文件 import("//device/xradio/xr806/liteos_m/config.gni")static_library("app_mying") {configs []sources…

【C语言】const修饰指针的不同作用

目录 const修饰变量 const修饰指针变量 ①不用const修饰 ②const放在*的左边 ③const放在*的右边 ④*的左右两边都有const 结论 const修饰变量 变量是可以修改的&#xff0c;如果把变量的地址交给⼀个指针变量&#xff0c;通过指针变量的也可以修改这个变量。 但…