You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中编辑Excel如何保留样式、公式及引用?

解决Excel模板填充时保留公式与样式的替代方案

核心问题

你基于React+Kotlin开发的应用,需要从用户提供的Excel模板填充数据后返回,但使用XLSX、ExcelJS等前端库时,生成的文件会丢失公式、引用及样式(ExcelJS仅保留部分样式),且认为Aspose Cloud成本过高,寻求免费/低成本替代方案。

可行替代方案

1. 后端使用Apache POI(Kotlin兼容)

  • 优势:Apache POI是Java/Kotlin生态中最成熟的Excel处理库,对.xlsx格式的公式、样式、单元格引用支持完整,开源免费无成本。
  • 实现方式:Kotlin可直接调用POI的API,加载模板文件后仅修改数据单元格,原有公式和样式会完整保留。
  • 注意点:操作复杂样式时需复用样式对象,避免创建过多样式导致文件体积异常增大。

2. 后端使用JXLS+JXLS-Transformer

  • 原理:基于Apache POI封装,提供模板占位符语法,直接在Excel模板中标记数据位置,填充时自动保留所有原有格式和公式。
  • 优势:简化模板填充逻辑,无需手动定位单元格,适合包含大量复杂公式和样式的模板场景。
  • 使用方式:在Excel模板中用${数据字段名}标记占位,后端通过JXLS加载模板并绑定数据集合即可。

3. 前端优化ExcelJS使用(有限改善)

如果必须在前端处理,可尝试以下优化:

  • 加载模板时显式开启preserveStyles选项:await workbook.xlsx.load(arrayBuffer, { preserveStyles: true })
  • 公式需以=开头写入,ExcelJS会保留公式计算逻辑(注意不要覆盖原有公式单元格)
  • 局限性:复杂数组公式、跨表引用仍可能丢失,仅适合样式和公式相对简单的场景。

4. 自建开源Excel处理服务

部署开源的OnlyOffice Document Server,通过API调用完成模板填充,完全保留Excel的所有格式和公式,成本仅为服务器部署费用,适合多实例或分布式场景。

后端Kotlin示例(Apache POI)

import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.FileInputStream
import java.io.FileOutputStream

fun fillExcelTemplate(templatePath: String, outputPath: String, dataList: List<Map<String, Any>>) {
    // 加载模板文件
    val templateStream = FileInputStream(templatePath)
    val workbook = XSSFWorkbook(templateStream)
    val sheet = workbook.getSheetAt(0)
    
    // 从第3行开始填充数据(对应前端代码的nuPonto=3)
    var currentRow = 2
    dataList.forEach { data ->
        val row = sheet.createRow(currentRow++)
        // 填充数据,原有公式和样式自动保留
        row.createCell(4).setCellValue(data["idEquipeEps"] as Int)
        row.createCell(5).setCellValue(data["noEncarregado"] as String)
        // 其他单元格依次填充...
    }
    
    // 输出文件
    val outputStream = FileOutputStream(outputPath)
    workbook.write(outputStream)
    
    // 关闭资源
    outputStream.close()
    workbook.close()
    templateStream.close()
}

前端ExcelJS代码优化参考

import React, { useState, useEffect } from "react";
import { useMsal } from "@azure/msal-react";
import { Client } from "@microsoft/microsoft-graph-client";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import "isomorphic-fetch";
import { useGetMsalTokenQuery } from "../components/store/service/UsersData";

const ModelInventarioCopy = () => {
  const { instance, accounts } = useMsal();
  const [fileId, setFileId] = useState("");
  const [siteId, setSiteId] = useState("");
  const [accessToken, setAccessToken] = useState("");
  const {
    data: dataToken,
    error: errorToken,
    isLoading: loadingToken,
  } = useGetMsalTokenQuery();

  useEffect(() => {
    if (dataToken) setAccessToken(dataToken);
    else if (errorToken) console.error("令牌获取失败:", errorToken);
  }, [dataToken, errorToken]);

  useEffect(() => {
    setFileId("idexample");
    setSiteId("idexample");
  }, []);

  const getAuthenticatedClient = (token) => {
    return Client.init({
      authProvider: (done) => done(null, token),
    });
  };

  const downloadAndModifyExcel = async () => {
    if (!accessToken) return;
    try {
      const client = getAuthenticatedClient(accessToken);
      const response = await client
        .api(`/sites/${siteId}/drive/items/${fileId}/content`)
        .responseType("arraybuffer")
        .get();
      await modifyExcelTemplate(response);
    } catch (error) {
      console.error("SharePoint文件下载失败:", error);
    }
  };

  const modifyExcelTemplate = async (arrayBuffer) => {
    try {
      const workbook = new ExcelJS.Workbook();
      // 开启样式保留
      await workbook.xlsx.load(arrayBuffer, { preserveStyles: true });
      const worksheet = workbook.getWorksheet(1);
      if (!worksheet) return console.error("未找到工作表");

      let startRow = 3;
      const dataList = [
        {
          idEquipeEps: 1,
          noEncarregado: "Lucas B",
          dtOperacao: "01/09/2024",
          dtSituacao: "01/09/2024",
          cdMedicao: 123,
          dsDivisaoDois: "testenoDiv2",
          noDivisaoTres: "testenoDiv3",
          nuParcela: 111,
          nuLine: 1,
          nuTree: 1,
          idCodTreeOne: 1234,
          vlCapFirstLevel: 1,
          vlHeigthFirstLevel: 1,
          dsObservation: "nothing",
          noOperacao: "testenoopp",
          vlCapRated: 1,
          vlHeigthRated: 1,
        },
        {
          idEquipeEps: 2,
          noEncarregado: "Lucas C",
          dtOperacao: "01/09/2024",
          dtSituacao: "01/09/2024",
          cdMedicao: 123,
          dsDivisaoDois: "testenoDiv2",
          noDivisaoTres: "testenoDiv3",
          nuParcela: 111,
          nuLine: 1,
          nuTree: 1,
          idCodTreeOne: 12345,
          vlCapFirstLevel: 1,
          vlHeigthFirstLevel: 1,
          dsObservation: "wtf",
          noOperacao: "testenoopp",
          vlCapRated: 2,
          vlHeigthRated: 2,
        },
      ];

      dataList.forEach((item) => {
        if (item) {
          const row = worksheet.getRow(startRow);
          row.getCell(5).value = item.idEquipeEps || "";
          row.getCell(6).value = item.noEncarregado || "";
          row.getCell(7).value = item.dtOperacao || "";
          row.getCell(8).value = item.dtSituacao || "";
          row.getCell(10).value = item.cdMedicao || "";
          row.getCell(11).value = item.dsDivisaoDois || "";
          row.getCell(12).value = item.noDivisaoTres || "";
          row.getCell(13).value = item.nuParcela || "";
          row.getCell(14).value = item.nuLine || "";
          row.getCell(15).value = item.nuTree || "";
          row.getCell(17).value = item.idCodTreeOne || "";
          row.getCell(18).value = item.vlCapFirstLevel || "";
          row.getCell(20).value = item.vlHeigthFirstLevel || "";
          row.getCell(23).value = item.dsObservation || "";
          row.getCell(24).value = item.noOperacao || "";
          row.getCell(25).value = item.vlCapRated || "";
          row.getCell(26).value = item.vlHeigthRated || "";
          row.commit();
        }
        startRow++;
      });

      const buffer = await workbook.xlsx.writeBuffer();
      const blob = new Blob([buffer], {
        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      });
      saveAs(blob, "已更新模板.xlsx");
    } catch (error) {
      console.error("Excel文件修改失败:", error);
    }
  };

  return (
    <div>
      <button onClick={downloadAndModifyExcel} disabled={!accessToken}>
        下载并修改文件
      </button>
    </div>
  );
};

export default ModelInventarioCopy;

内容的提问来源于stack exchange,提问作者Lucas Barbosa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 11:09:49