冰蓝科技
|
028-81705109
|
|
微信扫一扫
|

Spire.Cloud 纯前端文档控件

在 Excel 里维护商品清单、订单明细这类表格时,一个单元格常常需要承载不止一种格式:说明文字要分成几行,促销语里只有几个字要加粗,库存提示则要用红色标出来。手工做法是双击单元格、选中片段、逐个调整字体,条目一多就很难批量完成。HTML 字符串恰好适合描述这类「一段文字里各部分样式不同」的内容,Spire.XLS for JavaScript 提供了 HtmlString 属性,把一段 HTML 直接写进单元格,换行、加粗、倾斜、下划线、颜色和字号都按标签渲染,批量处理只是循环里的一行赋值。它基于 WebAssembly 在浏览器端直接完成,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。


在单元格中写入带换行的 HTML 文本

在单元格里换行,通常要在编辑状态下按 Alt+Enter 手动插入。数据来自数据库或表单时,这类手工操作无法批量完成,而 HTML 里的 <br> 标签表达的正是「在此处断行」。HtmlString 会把 <br>、<div>、<p> 都解析成单元格内的换行符,一个字符串写进去就是一格多行。具体操作步骤如下:

  1. 将字体载入 VFS。
  2. 新建工作簿并取第一个工作表,把 A 列拉宽一些。
  3. 逐条拼出用 <br> 分段的 HTML 字符串。
  4. 用 HtmlString 把它们逐个写进 A 列的单元格。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中向 Excel 单元格写入带换行的 HTML 文本:

function App() {
  const setMultilineText = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将中文字体载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 新建工作簿并取第一个工作表,拉宽 A 列免得一段文字被折成好几段
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);
    sheet.Range.get('A1:A5').ColumnWidth = 30;

    // 每行的说明由三段文字组成,用 <br> 在同一个单元格里换行
    const notes = [
      '蓝牙 5.3 双设备连接<br>续航 30 小时<br>Type-C 快充',
      '静音微动按键<br>内置 800 mAh 电池<br>支持 2.4G 无线',
      '双麦克风降噪<br>单次续航 6 小时<br>磁吸充电盒',
      '1080P 高清分辨率<br>重量 780 克<br>Type-C 一线连',
      '木质箱体<br>蓝牙 5.0 连接<br>附赠遥控器',
    ];

    // 把 HTML 字符串逐格写进 A 列,一个单元格里显示三行
    notes.forEach((html, index) => {
      sheet.Range.get(`A${index + 1}`).HtmlString = html;
    });

    // 保存工作簿
    const outputFileName = 'MultilineHtmlText.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Excel HTML 富文本</h1>
      <button onClick={setMultilineText}>多行 HTML 文本</button>
    </div>
  );
}

export default App;

运行后,在单元格中写入带换行的 HTML 文本的效果:

在单元格中写入带换行的 HTML 文本


为单元格中的部分文字设置格式

同一个单元格里的文字不必格式一致。同一段内容中,往往只有其中一部分需要突出显示,其余部分保持普通字重即可。HtmlString 支持 <b>、<i>、<u> 等标签,把颜色和字号写在 style 里交给 <span> 承载,被标签包住的片段会各自成为独立的一段格式,互不影响,标签本身不会出现在单元格里。具体操作步骤如下:

  1. 与上一步相同,先把中文字体载入 VFS,再新建工作簿、取第一个工作表并把 A 列拉宽。
  2. 在 HTML 字符串中用 <b>、<i>、<u> 标出需要强调的文字,用 <span style="color:#C00000;font-size:14pt"> 包住需要突出的文字,颜色写十六进制色值,字号按磅值给出。
  3. 把字符串逐格写进 A 列,同一格里剩下的文字保持原有格式。
  4. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为 Excel 单元格的部分文字设置格式:

function App() {
  const setPartialFormatting = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将中文字体载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 新建工作簿并取第一个工作表,拉宽 A 列免得一段文字被折成好几段
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);
    sheet.Range.get('A1:A5').ColumnWidth = 30;

    // 一段促销语里的各段内容分别套上 <b>、<i>、<u> 和带样式的 <span>,
    // 加粗、倾斜、下划线、颜色和字号都只作用于被标签包住的那段文字
    const promotions = [
      '<b>满 300 减 50</b>,<i>限时三天</i>,<u>每人限购两件</u>,<span style="color:#C00000;font-size:14pt">仅剩 3 件</span>',
      '<b>第二件半价</b>,<i>仅限本周</i>,<u>不与其他优惠叠加</u>',
      '<b>下单立减 100</b>,<i>今日 24 点结束</i>,<u>赠收纳包</u>,<span style="color:#C00000;font-size:14pt">库存紧张</span>',
      '<b>以旧换新补贴 200</b>,<i>活动至月底</i>,<u>需出示旧机</u>',
      '<b>买一赠一</b>,<i>限量 500 套</i>,<u>赠品随机发放</u>,<span style="color:#E36C0A;font-size:14pt">补货中</span>',
    ];

    // 把 HTML 字符串逐格写进 A 列
    promotions.forEach((html, index) => {
      sheet.Range.get(`A${index + 1}`).HtmlString = html;
    });

    // 保存工作簿
    const outputFileName = 'PartialFormattingHtml.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Excel HTML 富文本</h1>
      <button onClick={setPartialFormatting}>部分文字格式</button>
    </div>
  );
}

export default App;

运行后,为单元格中的部分文字设置加粗、倾斜、下划线、颜色与字号的效果:

为单元格中的部分文字设置格式


常见问题

HTML 里的连续空格会被吞掉吗

原因:浏览器解析 HTML 时会把连续空格合并成一个,容易让人以为 Spire 也这么做,于是改用 &nbsp; 逐个拼空格。

解决:不会被吞。A B 写进单元格后读回来仍是 5 个空格,行首的多个空格也原样保留;&nbsp; 同样有效,但一般的对齐场景不需要替换。

写入富文本后,单元格原来的对齐方式还在吗

原因:HtmlString 会改动单元格的字体属性,于是担心它把先前设好的对齐方式一并重置掉。

解决:对齐方式不受影响。先设置 Style.HorizontalAlignment 再写 HtmlString,读回来仍是原来的对齐值,先设格式再填内容可以放心。

const cell = sheet.Range.get('A1');
cell.Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Center;
cell.HtmlString = '<b>居中</b>';

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

一张图表能让人看清一组数据的走势,却很难让人同时看清几十组数据的走势。迷你图(Sparkline)正是为此而生:它把一条趋势线压缩进单个单元格,没有坐标轴也没有图例,却能让整列趋势在扫视中一览无余。把各地区、各季度的销售数字逐行配上迷你图,哪一行的曲线在爬升、哪一行在中途掉头,往往比读一列数字快得多。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成此操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍三个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。


添加折线迷你图

折线迷你图把一行数值按顺序连成一条细线,走势的升降一目了然。它不占单元格的位置,也不会遮住单元格里的数字,一小列宽度就能排下几十行的走势,哪一行在爬升、哪一行中途掉头,扫一眼便知。迷你图不能脱离迷你图组单独存在,同一批迷你图的类型、颜色和标记样式由组统一决定。具体操作步骤如下:

  1. 将字体与测试数据文件载入 VFS。
  2. 加载工作簿并取得第一个工作表。
  3. 用 SparklineGroups.AddGroup() 新建折线迷你图组,设置线条粗细与数据点标记颜色。
  4. 用 Add() 取到组内的迷你图集合,为每一行数据在 F 列生成一个迷你图。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为 Excel 数据添加折线迷你图:

function App() {
  const addLineSparkline = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和测试数据文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'SparklineData.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 在工作表上新建一个折线迷你图组
    const sparklineGroup = sheet.SparklineGroups.AddGroup({ sparklineType: xlsModule.SparklineType.Line });

    // 设置折线样式:加粗线条并显示数据点标记
    sparklineGroup.LineWeight = 1.5;
    sparklineGroup.ShowMarkers = true;
    sparklineGroup.MarkersColor = xlsModule.Color.get_Red();

    // 取到组内的迷你图集合
    const sparklines = sparklineGroup.Add();

    // 为每一行数据在 F 列生成一个迷你图
    for (let row = 2; row <= 9; row++) {
      sparklines.Add({
        dataRange: sheet.Range.get(`B${row}:E${row}`),
        referenceRange: sheet.Range.get(`F${row}`),
      });
    }

    // 保存工作簿
    const outputFileName = 'AddLineSparkline.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>折线迷你图</h1>
      <button onClick={addLineSparkline}>Start</button>
    </div>
  );
}

export default App;

运行后,添加折线迷你图的效果:

添加折线迷你图


添加柱形迷你图并突出最高点与最低点

柱形迷你图用一排细柱表示数值大小,比较同一行里几个数的高低时比折线更直观。它的用法与折线迷你图一致,区别在于颜色落在每根柱子本身,而不是一条连线上。

真正让柱形迷你图好用的是高低点标记:一行里最高的那一季和最矮的那一季会被自动找出来单独着色,峰值和低谷不必再对着数字比较。具体操作步骤如下:

  1. 将字体与测试数据文件载入 VFS。
  2. 加载工作簿并取得第一个工作表。
  3. 用 SparklineGroups.AddGroup() 新建柱形迷你图组(类型传 SparklineType.Column),并用 SparklineColor 设置系列颜色。
  4. 打开 ShowHighPoint 与 ShowLowPoint,分别用 HighPointColor 与 LowPointColor 指定颜色。
  5. 用 Add() 取到组内的迷你图集合,为每一行数据在 G 列生成一个迷你图并保存工作簿。

以下为完整的代码示例,演示如何在 React 中为 Excel 数据添加柱形迷你图并突出最高点与最低点:

function App() {
  const addColumnSparkline = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和测试数据文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'SparklineData.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 新建一个柱形迷你图组
    const sparklineGroup = sheet.SparklineGroups.AddGroup({ sparklineType: xlsModule.SparklineType.Column });

    // 设置迷你图本身的颜色
    sparklineGroup.SparklineColor = xlsModule.Color.get_CadetBlue();

    // 打开最高点与最低点的标记,并分别指定颜色
    sparklineGroup.ShowHighPoint = true;
    sparklineGroup.HighPointColor = xlsModule.Color.get_Red();
    sparklineGroup.ShowLowPoint = true;
    sparklineGroup.LowPointColor = xlsModule.Color.get_Purple();

    const sparklines = sparklineGroup.Add();

    // 为每一行数据在 G 列生成一个迷你图
    for (let row = 2; row <= 9; row++) {
      sparklines.Add({
        dataRange: sheet.Range.get(`B${row}:E${row}`),
        referenceRange: sheet.Range.get(`G${row}`),
      });
    }

    // 保存工作簿
    const outputFileName = 'AddColumnSparkline.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>柱形迷你图</h1>
      <button onClick={addColumnSparkline}>Start</button>
    </div>
  );
}

export default App;

运行后,添加柱形迷你图并突出最高点与最低点的效果:

添加柱形迷你图并突出最高点与最低点


清除工作表上的迷你图

报表改版或数据口径变化时,原本标在旁边的迷你图可能不再适用,需要整批清掉重画。迷你图组由工作表持有,清掉之后单元格回到普通状态,其中的数据和其他格式都不受影响。具体操作步骤如下:

  1. 将字体与测试数据文件载入 VFS。
  2. 加载工作簿并取得第二个工作表,其上已带有一组折线迷你图。
  3. 用 SparklineGroups.Clear() 清除该工作表中的全部迷你图组。
  4. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中清除工作表上的迷你图:

function App() {
  const clearSparklines = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和测试数据文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'SparklineData.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第二个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(1);

    // 清除该工作表中的全部迷你图组
    sheet.SparklineGroups.Clear();

    // 保存工作簿
    const outputFileName = 'ClearSparklines.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>清除迷你图</h1>
      <button onClick={clearSparklines}>Start</button>
    </div>
  );
}

export default App;

运行后,清除工作表上的迷你图的效果:

清除工作表上的迷你图


常见问题

遍历单元格读不到已添加的迷你图

原因:迷你图不是单元格内容。它归工作表上的迷你图组所有,画在落点单元格之上,自身的数据和设置则写在工作表的扩展区里,因此逐个单元格读取时落点列始终是空值,单元格上也没有接口可以问出「这里有没有迷你图」。

解决:改从工作表级的 SparklineGroups 读取。get_Item(0) 取到第一组,组上的 SparklineType 即可判断这一批是折线还是柱形,颜色等设置也在同一处读取:

const group = sheet.SparklineGroups.get_Item(0);
console.log(group.SparklineType);   // SparklineType.Line 或 SparklineType.Column

柱形迷你图上设置线条粗细和标记点没有效果

原因:LineWeight 与 ShowMarkers 只对折线迷你图生效。柱形迷你图用柱子表示数值,既没有连线也没有数据点标记,即使赋值成功也不会写入结果文件。

解决:需要线条和标记时,把迷你图组建成折线类型再设置:

const sparklineGroup = sheet.SparklineGroups.AddGroup({ sparklineType: xlsModule.SparklineType.Line });
sparklineGroup.LineWeight = 1.5;
sparklineGroup.ShowMarkers = true;
sparklineGroup.MarkersColor = xlsModule.Color.get_Red();

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

打印、拼版、加水印之前,先得知道每一页是横版还是竖版:横竖混排的文档直接打印会留白边,拼版也会错位。页面还可能被整体旋转过,这时候记录的页面尺寸和肉眼看到的方向就对不上了。批量处理前想把这类页面挑出来,用桌面软件只能一页页翻。

本文介绍用 Spire.PDF for JavaScript 检测 PDF 页面的旋转角度与显示方向。它基于 WebAssembly 在浏览器端直接加载与解析 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。


检测页面的旋转角度

PdfPageBase.Rotation 用来读取页面被旋转的角度,取值落在 PdfPageRotateAngle 枚举的 0°、90°、180°、270° 之内。

function App() {
  const detectPageRotation = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待检测的 PDF 文件载入 VFS
    const inputFileName = '页面方向样例.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    const doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 枚举的数值是序号而不是角度,先建立序号到角度的映射
    const DEGREES = {
      [pdfModule.PdfPageRotateAngle.RotateAngle0.value]: 0,
      [pdfModule.PdfPageRotateAngle.RotateAngle90.value]: 90,
      [pdfModule.PdfPageRotateAngle.RotateAngle180.value]: 180,
      [pdfModule.PdfPageRotateAngle.RotateAngle270.value]: 270,
    };

    // 逐页读取旋转角度
    const lines = [];
    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);
      lines.push(`第 ${i + 1} 页:旋转角度 ${DEGREES[page.Rotation.value]}°`);
    }

    // 检测结果写入 VFS
    const outputFileName = '旋转角度检测结果.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join('\r\n'));
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>检测页面旋转角度</h1>
      <button onClick={detectPageRotation}>
        开始检测
      </button>
    </div>
  );
}

export default App;

逐页记录旋转角度的检测结果:

逐页记录旋转角度的检测结果


检测页面的显示方向

判断页面是横是竖不能只看尺寸:PdfPageBase.Size 给出的是可见页面框的宽高,不含旋转;页面被旋转 90° 或 270° 时,显示出来的宽高要对调,才是页面实际的方向。

function App() {
  const detectPageOrientation = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待检测的 PDF 文件载入 VFS
    const inputFileName = '页面方向样例.pdf';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    const doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    const { RotateAngle90, RotateAngle270 } = pdfModule.PdfPageRotateAngle;

    // 逐页判断显示方向
    const lines = [];
    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);
      const pageSize = page.Size;

      // Size 不含旋转:旋转 90° 或 270° 时宽高对调,才是实际显示尺寸
      const quarterTurn = page.Rotation === RotateAngle90 || page.Rotation === RotateAngle270;
      const width = quarterTurn ? pageSize.Height : pageSize.Width;
      const height = quarterTurn ? pageSize.Width : pageSize.Height;
      const orientation = width >= height ? '横向' : '纵向';

      lines.push(`第 ${i + 1} 页:${orientation}(${width.toFixed(0)} × ${height.toFixed(0)} 磅)`);
    }

    // 检测结果写入 VFS
    const outputFileName = '页面方向检测结果.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join('\r\n'));
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>检测页面显示方向</h1>
      <button onClick={detectPageOrientation}>
        开始检测
      </button>
    </div>
  );
}

export default App;

按显示方向逐页判断后的检测结果:

按显示方向逐页判断后的检测结果


常见问题

page.Rotation.value 读到的是 0、1、2、3,而不是 0、90、180、270

原因:PdfPageRotateAngle 的四个成员 RotateAngle0、RotateAngle90、RotateAngle180、RotateAngle270 在 WebAssembly 绑定里的数值依次是 0、1、2、3,.value 取到的是这个序号,而不是角度值。写成 page.Rotation.value === 90 恒为 false。

解决:建立序号到角度的映射再使用。要修正旋转时也传序号——page.Rotation 收的是序号,传 90 会被当成序号 2,实际写进文档的是 180°:

// 序号 → 角度
const DEGREES = {
  [pdfModule.PdfPageRotateAngle.RotateAngle0.value]: 0,
  [pdfModule.PdfPageRotateAngle.RotateAngle90.value]: 90,
  [pdfModule.PdfPageRotateAngle.RotateAngle180.value]: 180,
  [pdfModule.PdfPageRotateAngle.RotateAngle270.value]: 270,
};

// 把当前页修正为 90°,.value 就是序号
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle90.value;

页面明明是横版,page.Size 返回的却是竖版尺寸

原因:PdfPageBase.Size 与 ActualSize 返回的是页面可见框的宽高——设过 CropBox 就是 CropBox,否则是 MediaBox——这个值不随 /Rotate 变化。示例文档第 3 页的页面框是 595 × 842 磅,/Rotate 为 90°,渲染出来是 842 × 595 的横版,Size 依旧返回 595 × 842。

解决:读到 Rotation 之后自己把宽高对调:

const { RotateAngle90, RotateAngle270 } = pdfModule.PdfPageRotateAngle;
const quarterTurn = page.Rotation === RotateAngle90 || page.Rotation === RotateAngle270;

const width = quarterTurn ? page.Size.Height : page.Size.Width;
const height = quarterTurn ? page.Size.Width : page.Size.Height;

载入的 PDF 里 doc.Sections 是空的,PageSettings.Orientation 也读不到

原因:Sections 是页面设置的容器,LoadFromFile 载入已有 PDF 时不会为现成页面重建节,doc.Sections.Count 为 0,接着调 doc.Sections.get_Item(0) 会抛 Arg_IndexOutOfRangeException。PageSettings.Orientation 描述的是新建页面时的排版意图,也不是文件里记录的方向。

解决:检测已有页面只走页级属性,doc.Pages.get_Item(i) 之后读 Rotation 与 Size;PageSettings.Orientation 留给 Sections.Add() 新建页面时使用:

// 检测已有页面
const page = doc.Pages.get_Item(0);
const angle = DEGREES[page.Rotation.value];

// 新建页面时才用得到 Orientation
const section = doc.Sections.Add();
section.PageSettings.Orientation = pdfModule.PdfPageOrientation.Landscape;

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

我们很高兴地宣布 Spire.Doc 14.9.11 版本正式发布。本次更新新增 ImportOptions、ExportOptions 类,精细化管控文档加载与导出流程;支持 HTML 转 Word 时渲染 HSL 色彩;同时修复大量格式转换、文档编辑场景下的已知缺陷,详情如下。

调整:

新功能:

问题修复:


获取 Spire.Doc 14.9.11 请点击:

https://www.e-iceblue.cn/Downloads/Spire-Doc-NET.html

扫描件、图纸、电子发票这类 PDF 往往带着大片白边,页面尺寸却仍是原来的规格;反过来,也有人只想要页面上的一小块内容,其余部分都不必出现。要把多余的部分去掉,过去要么在桌面软件里一页页手动框选,要么把文件传到服务端处理——前者很难嵌进 Web 流程,后者意味着文档离开了用户的设备。

本文介绍用 Spire.PDF for JavaScript 裁切 PDF 页面。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。本文以一个两页的示例文档演示这一过程。

有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。


裁切 PDF 页面

裁切页面靠 page.CropBox 完成,它是一个 RectangleF:x、y 从页面的左上角量起,width 与 height 决定保留多大一块,框外的内容不再显示。整份文档按同一套边距裁,就是遍历 doc.Pages,在每页 MediaBox 的基础上让出四边的距离。

function App() {
  const cropPdfPage = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待裁切的 PDF 文件载入 VFS
    const inputFileName = '待裁切文档.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 四边统一裁掉 60 磅
    const margin = 60;

    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);

      // MediaBox 给出该页的完整范围,据此算出裁切框(x、y 自页面左上角量起)
      const width = page.MediaBox.Width;
      const height = page.MediaBox.Height;

      page.CropBox = new pdfModule.RectangleF({
        x: margin,
        y: margin,
        width: width - margin * 2,
        height: height - margin * 2,
      });
    }

    const outputFileName = '按边距裁切.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>裁切 PDF 页面</h1>
      <button onClick={cropPdfPage}>
        开始裁切
      </button>
    </div>
  );
}

export default App;

两页都按 60 磅边距裁切,页面外侧的白边与边框一并去掉:

两页都按 60 磅边距裁切,页面外侧的白边与边框一并去掉


常见问题

只能裁整份文档吗,单个页面怎么裁

原因:CropBox 是页面级属性,没有“整份文档”这一层接口;上面的循环只是为了让每页套用同一套边距。

解决:只裁某一页时,把循环去掉、直接对目标页赋值即可,x、y 同样自页面左上角量起:

// 只裁第 1 页:保留自左上角 (80, 80) 起 400 × 500 磅的一块
const page = doc.Pages.get_Item(0);
page.CropBox = new pdfModule.RectangleF({ x: 80, y: 80, width: 400, height: 500 });

裁切之后想还原怎么办

原因:CropBox 是就地改页面框,文档里没有另外记下“原来的框”。直觉上会把 page.MediaBox 赋回去,但这一步不会生效——赋值时的坐标是在当前可见区原点上叠加的,只换尺寸不改原点,可见区就停在裁切后的位置。裁掉 60 磅之后再把 MediaBox 赋回去,可见区仍然从 (60, 60) 起。

解决:裁切前留有原始文档时,重新载入它最省事。只能就着已经裁过的文件处理时,用负偏移把原点抵回页面左上角,再给回整页尺寸:

// 裁切偏移为 (offsetX, offsetY) 时的还原写法
page.CropBox = new pdfModule.RectangleF({
  x: -offsetX,
  y: -offsetY,
  width: page.MediaBox.Width,
  height: page.MediaBox.Height,
});

裁切后文件体积没变小,被裁掉的内容还能搜到

原因:CropBox 是软裁切,只改页面的可见框,框外的内容依旧留在文件里,文本搜索和复制仍能取到。

解决:如果目的是把内容真正从页面上去掉,就不能只设 CropBox,而要重建页面——新建一份同尺寸的文档,用 page.CreateTemplate() 取出内容后绘制到新页上,再另存为新文件。


获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

把产品手册的封面补到项目说明前面、把报价单的第 2 到第 3 页并进合同、把几份报告合成一份汇总,说到底都是把页面从一份 PDF 搬到另一份 PDF 里。桌面软件的做法是开两个窗口来回拖拽,拖错一次就得重来;页数一多,搬过去的顺序也容易乱。还有一类麻烦是尺寸对不上:封面是 A5、目标文档是 A4,整页搬过去会露出一圈空白。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,复制过程全部在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

本文介绍四个核心功能点,前三个搬的是整页,源页的尺寸、旋转与页边距都原样带过来;第四个只取页面内容,画到多大的页面上由你决定。

整页搬运

内容复制(模板)

有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。


复制单页并插入到指定位置

Spire.PDF for JavaScript 提供 PdfDocument.InsertPage 方法,用于把另一份文档里的某一页复制到当前文档,并指定它落在当前文档的第几位,不传落点下标时,追加到末尾。

function App() {
  const copyPageAtPosition = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 把源文档与目标文档都载入 VFS
    const sourceFileName = '源文档.pdf';
    const targetFileName = '目标文档.pdf';
    await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 分别载入两份文档
    const sourceDoc = new pdfModule.PdfDocument();
    sourceDoc.LoadFromFile(sourceFileName);
    const targetDoc = new pdfModule.PdfDocument();
    targetDoc.LoadFromFile(targetFileName);

    // 把源文档的第 1 页复制到目标文档的最前面
    // pageIndex 取自源文档,resultPageIndex 是复制结果在目标文档中的落点
    targetDoc.InsertPage({ ldDoc: sourceDoc, pageIndex: 0, resultPageIndex: 0 });

    // 保存结果文档
    const outputFileName = '复制指定页面.pdf';
    targetDoc.SaveToFile(outputFileName);
    sourceDoc.Close();
    targetDoc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>复制指定页面</h1>
      <button onClick={copyPageAtPosition}>
        开始复制
      </button>
    </div>
  );
}

export default App;

resultPageIndex 是四个功能点里唯一能控制落点的地方:传 0 插到首位、传 1 插到第 2 位,传当前页数则等于追加到末尾。

源文档的第 1 页排在目标文档最前面,文档由 2 页变为 3 页:

源文档的第 1 页排在目标文档最前面,文档由 2 页变为 3 页


一次复制多页到文档末尾

Spire.PDF for JavaScript 还提供 PdfDocument.InsertPageRange 方法,用于把源文档里一段连续的页复制过来。它接收源文档和起点、终点两个下标,没有落点参数,复制结果一律追加到当前文档末尾。

function App() {
  const appendPageRange = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 把源文档与目标文档都载入 VFS
    const sourceFileName = '源文档.pdf';
    const targetFileName = '目标文档.pdf';
    await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 分别载入两份文档
    const sourceDoc = new pdfModule.PdfDocument();
    sourceDoc.LoadFromFile(sourceFileName);
    const targetDoc = new pdfModule.PdfDocument();
    targetDoc.LoadFromFile(targetFileName);

    // 把源文档的第 2 页到第 3 页追加到目标文档末尾
    // 注意:这里是位置参数,不是对象;endIndex 含端点
    targetDoc.InsertPageRange(sourceDoc, 1, 2);

    // 保存结果文档
    const outputFileName = '复制页面范围.pdf';
    targetDoc.SaveToFile(outputFileName);
    sourceDoc.Close();
    targetDoc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>复制页面范围</h1>
      <button onClick={appendPageRange}>
        复制第 2-3 页
      </button>
    </div>
  );
}

export default App;

源文档的第 2、3 页追加后文档为 4 页:

源文档的第 2、3 页追加后文档为 4 页


复制整份文档的全部页面

整份文档都要搬时不必先算下标,PdfDocument.AppendPage 接收源文档对象,按原顺序把它的全部页追加到当前文档末尾。合并几份文档、给报告加附件这类场景,把手上的文档依次传进去就行。

function App() {
  const appendWholeDocument = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 把源文档与目标文档都载入 VFS
    const sourceFileName = '源文档.pdf';
    const targetFileName = '目标文档.pdf';
    await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 分别载入两份文档
    const sourceDoc = new pdfModule.PdfDocument();
    sourceDoc.LoadFromFile(sourceFileName);
    const targetDoc = new pdfModule.PdfDocument();
    targetDoc.LoadFromFile(targetFileName);

    // 整份文档都要复制时用 AppendPage,按原顺序追加全部页
    targetDoc.AppendPage({ doc: sourceDoc });

    // 保存结果文档
    const outputFileName = '复制全部页面.pdf';
    targetDoc.SaveToFile(outputFileName);
    sourceDoc.Close();
    targetDoc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>复制整份文档</h1>
      <button onClick={appendWholeDocument}>
        开始复制
      </button>
    </div>
  );
}

export default App;

源文档全部 4 页追加后文档为 6 页:

源文档全部 4 页追加后文档为 6 页


用页面模板复制页面内容

Spire.PDF for JavaScript 还提供 PdfPageBase.CreateTemplate 方法,用于把一页的内容取成一个 PdfTemplate,再由 Canvas.DrawTemplate 画到新建的页面上。前面三个方法搬的是整页,新页尺寸跟着源页走;模板取的是内容,画到多大的页面、画在什么位置、画几次都由你定,同一份模板可以反复使用。

function App() {
  const copyPageWithTemplate = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 把待处理的 PDF 文件载入 VFS
    const inputFileName = '源文档.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 载入文档
    const doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 取出要复用的那一页,转成模板:内容取一次,后面可以画到多页上
    const sourcePage = doc.Pages.get_Item(0);
    const template = sourcePage.CreateTemplate();

    // 第 1 处:在第 2 位插入一个 A4 新页,尺寸与源页不同,内容缩放到 297.6 x 421.6 画在 (80, 80)
    const page1 = doc.Pages.Insert(1, new pdfModule.SizeF(595.0, 842.0), new pdfModule.PdfMargins({ margin: 0.0 }));
    page1.Canvas.DrawTemplate(template, new pdfModule.PointF(80.0, 80.0), new pdfModule.SizeF(297.6, 421.6));

    // 第 2 处:再插入一个 A4 新页,同一份模板缩得更小,画在右下角
    const page2 = doc.Pages.Insert(2, new pdfModule.SizeF(595.0, 842.0), new pdfModule.PdfMargins({ margin: 0.0 }));
    page2.Canvas.DrawTemplate(template, new pdfModule.PointF(320.0, 460.0), new pdfModule.SizeF(200.0, 283.3));

    // 保存结果文档
    const outputFileName = '模板复制页面.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>模板复制页面</h1>
      <button onClick={copyPageWithTemplate}>
        开始复制
      </button>
    </div>
  );
}

export default App;

DrawTemplate 的第三个参数不传时,模板按原坐标绘制,不缩放——新页比源页大,内容就只占页面一角。新页的尺寸与页边距都由 Pages.Insert 决定,示例里四边页边距取 0,模板的绘制起点才是页面的左上角。

源文档第 1 页的内容被缩放到两个 A4 新页上,文档由 4 页变为 6 页:

源文档第 1 页的内容被缩放到两个 A4 新页上,文档由 4 页变为 6 页


常见问题

用 new PdfMargins(0.0) 建页面时报 Arg_NullReferenceException

原因:PdfMargins 的构造函数把单个数字参数当成内部句柄处理,new pdfModule.PdfMargins(0.0) 拿到的不是一个页边距对象——读它的 Left、Top 会抛 Arg_NullReferenceException,拿它去建页面也不会得到期望的页边距。

解决:用对象形式传页边距,四边都是 0 时写 { margin: 0.0 }:

// 四边页边距都为 0
const margins = new pdfModule.PdfMargins({ margin: 0.0 });

// 也可以四边分别指定
const custom = new pdfModule.PdfMargins({ left: 20.0, top: 20.0, right: 20.0, bottom: 20.0 });

复制页面时报下标越界或区间逆序

原因:页面下标从 0 开始,endIndex 含端点,所以合法区间是 0 到 Pages.Count - 1。超出这个范围会抛 Index out of range,startIndex 大于 endIndex 则抛 The start index is greater then the end index.。

解决:用 Pages.Count 兜住上界再传进去:

// 想复制第 2 到第 4 页:start = 1、end = 3,用页数兜住上界
const start = 1;
const end = Math.min(3, sourceDoc.Pages.Count - 1);
targetDoc.InsertPageRange(sourceDoc, start, end);

复制后旋转过的页面方向不对

原因:CreateTemplate() 取的是页面内容,页面的旋转角(/Rotate)不在模板里。源页带旋转角时,模板的坐标系与目标页对不上——直接 DrawTemplate 画到新页上,内容会落到可见区域之外,副本的 Rotation 也是 0。

解决:源页带旋转角时改用整页复制,内容和旋转角会一起带过去:

// 整页复制:旋转角随页面一起过来
targetDoc.InsertPage({ ldDoc: sourceDoc, pageIndex: 0, resultPageIndex: 1 });

如果必须用模板法,就先把源页的旋转角临时归零再取模板,复制完把角度补回源页和副本:

const rotation = sourcePage.Rotation.value;

// 临时归零,模板才会按页面的真实坐标导出
sourcePage.Rotation = 0;
const newPage = doc.Pages.Insert(1, sourcePage.Size, new pdfModule.PdfMargins({ margin: 0.0 }));
newPage.Canvas.DrawTemplate(sourcePage.CreateTemplate(), new pdfModule.PointF(0.0, 0.0));

// 源页还原,副本补上同样的角度
sourcePage.Rotation = rotation;
newPage.Rotation = rotation;

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

为 Word 文档设置页面背景,是合同、公文与品牌宣传类文档排版中最常用的美化手段之一:一层柔和的底色,或一张与企业视觉一致的背景图,就能为整份文档建立统一的视觉基调。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成此设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍三个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。


设置纯色背景

纯色背景的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文件载入 WASM 虚拟文件系统;然后实例化 Document 加载文件,将 Background.Type 指定为 BackgroundType.Color,并通过 Background.Color 赋值一个内置色值;最后调用 SaveToFile 将文档保存到 VFS,从 VFS 读取生成的 docx 文件,封装为 Blob 后生成下载链接。

function App() {
  const SetSolidColorBackground = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }
    // 将示例文件加载到虚拟文件系统(VFS)中
    let inputFileName = "ScienceTemplate.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // 创建 Word 文档
    let doc = new docModule.Document();

    // 加载文件
    doc.LoadFromFile(inputFileName);

    // 设置背景类型为颜色
    doc.Background.Type = docModule.BackgroundType.Color;

    // 设置背景颜色
    doc.Background.Color = docModule.Color.get_LightYellow();

    // 定义输出文件名
    const outputFileName = "SetSolidColorBackground_out.docx";

    // 将文档保存到指定路径
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>为Word文档设置纯色背景</h1>
      <button onClick={SetSolidColorBackground}>开始</button>
    </div>
  );
}
export default App;

通过 Background.Color 设置纯色背景后生成的 Word 文档

通过 Background.Color 设置纯色背景后生成的 Word 文档


设置渐变背景

渐变背景的核心流程与纯色背景一致,区别在于中间阶段:将 Background.Type 指定为 BackgroundType.Gradient 后,通过 Background.Gradient 获取背景渐变对象,分别设置起点色 Color1 与终点色 Color2,再通过 ShadingStyle 与 ShadingVariant 控制渐变的方向与过渡方式。

function App() {
  const SetGradientBackground = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }
    // 将示例文件加载到虚拟文件系统(VFS)中
    let inputFileName = "ScienceTemplate.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // 创建Word文档
    let doc = new docModule.Document();

    // 加载文件
    doc.LoadFromFile(inputFileName);

    // 设置背景类型为渐变
    doc.Background.Type = docModule.BackgroundType.Gradient;
    let gradient = doc.Background.Gradient;

    // 设置渐变的起始颜色和结束颜色
    gradient.Color1 = docModule.Color.get_White();
    gradient.Color2 = docModule.Color.get_LightBlue();

    // 设置渐变的着色样式和方向
    gradient.ShadingVariant = docModule.GradientShadingVariant.ShadingDown;
    gradient.ShadingStyle = docModule.GradientShadingStyle.Horizontal;

    // 定义输出文件名
    const outputFileName = "SetGradientBackground_out.docx";

    // 将文档保存到指定路径
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>为Word文档设置渐变背景</h1>
      <button onClick={SetGradientBackground}>开始</button>
    </div>
  );
}
export default App;

通过 Background.Gradient 设置渐变背景后生成的 Word 文档

通过 Background.Gradient 设置渐变背景后生成的 Word 文档


设置图片背景

图片背景的核心流程与前面两种背景类似,区别在于资源准备与赋值方式:载入阶段除字体文件和目标 Word 文件外,还需将背景图片一并载入 VFS;然后将 Background.Type 指定为 BackgroundType.Picture,再调用 Background.SetPicture 传入 VFS 中的图片路径,即可将图片平铺为整页背景。

function App() {
  const SetImageBackground = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }
    // 将示例文件加载到虚拟文件系统(VFS)中
    let inputFileName1 = "ScienceTemplate.docx";
    await window.spire.FetchFileToVFS(inputFileName1, "", `${process.env.PUBLIC_URL}static/data/`);

    // 将背景图片加载到虚拟文件系统(VFS)中
    let inputFileName2 = "Background.png";
    await window.spire.FetchFileToVFS(inputFileName2, "", `${process.env.PUBLIC_URL}static/data/`);

    // 加载一个Word文档
    let doc = new docModule.Document();
    doc.LoadFromFile(inputFileName1);

    // 将背景类型设置为图片
    doc.Background.Type = docModule.BackgroundType.Picture;

    // 设置背景图片
    doc.Background.SetPicture(inputFileName2);

    // 定义输出文件名
    const outputFileName = "SetImageBackground_out.docx";

    // 将文档保存到指定路径
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>在 Word 文档中设置图片背景</h1>
      <button onClick={SetImageBackground}>开始</button>
    </div>
  );
}
export default App;

通过 Background.SetPicture 设置图片背景后生成的 Word 文档

通过 Background.SetPicture 设置图片背景后生成的 Word 文档


常见问题

打印时背景不显示

原因:Word 默认不打印页面背景色和背景图片,这是 Word 客户端的打印设置,并非文档中的背景设置丢失。打开文档时背景可以正常显示,只有打印输出时会被忽略。

解决:如果需要在打印稿中保留背景,可在 Word 中通过「文件 > 选项 > 显示」勾选「打印背景色和图像」后再打印。如果要求背景在任何环境下都必须输出,建议改用页眉中的整页形状或水印来模拟。

图片背景未生效

原因:调用 SetPicture 之前未将 Background.Type 设为 BackgroundType.Picture,或背景图片没有通过 FetchFileToVFS 载入 VFS,导致 SetPicture 找不到图片文件。

解决:先指定背景类型,再传入已载入 VFS 的图片文件名:

document.Background.Type = wasmModule.BackgroundType.Picture;
document.Background.SetPicture("Background.png");

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

在日常办公和业务开发中,Excel 文件是最常见的数据交换格式之一。随着 XML 格式的广泛应用,越来越多的系统需要将 Excel 数据导出为 OpenXML(.xml)格式以便在其他平台或系统中读取和解析。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成 Excel 与 OpenXML 之间的双向转换,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。


将 Excel 转换为 OpenXML

将 Excel 文件转换为 OpenXML 格式是一个将 .xlsx 文件保存为 .xml 文件的过程。以下是关键步骤:

  • 使用新的 xlsModule.Workbook() 方法创建一个工作簿对象。
  • 使用 LoadFromFile() 方法加载 Excel 文件。
  • 使用 SaveAsXml() 方法将工作簿保存为 OpenXML 文件。
function App() {
  const excelToOpenXML = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.XLS 尚未就绪');
      return;
    }

    // 加载字体
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将 Excel 输入文件载入 VFS
    const inputFileName = 'Sample.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 创建工作簿并加载 Excel 文件
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });

    // 转换为 OpenXML 格式并保存
    const outputFileName = 'ExcelToOpenXML.xml';
    workbook.SaveAsXml({ fileName: outputFileName });

    // 释放工作簿对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取转换后的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/xml' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Excel 转换为 OpenXML</h1>
      <button onClick={excelToOpenXML}>
        开始转换
      </button>
    </div>
  );
}

export default App;

运行后,excel文件转为 OpenXML文件的效果:

Excel 转换为 OpenXML


将 OpenXML 转换为 Excel

将 OpenXML 文件转换回 Excel 格式是双向转换的另一方向。以下是关键步骤:

  • 使用新的 xlsModule.Workbook() 方法创建一个工作簿对象。
  • 使用 xlsModule.Stream(inputFileName) 方法将 OpenXML 文件读取到流中。
  • 使用 Workbook.LoadFromXml() 方法从流中加载 OpenXML 文件。
  • 使用 Workbook.SaveToFile() 方法将 OpenXML 文件保存为 Excel 文件。
function App() {
  const openXMLToExcel = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.XLS 尚未就绪');
      return;
    }

    // 加载字体
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将 OpenXML 输入文件载入 VFS
    const inputFileName = 'OfficeOpenXMLToExcel.xml';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 创建工作簿
    const workbook = new xlsModule.Workbook();

    // 使用 Stream 读取 OpenXML 文件并加载到工作簿中
    const fileStream = new xlsModule.Stream(inputFileName);
    workbook.LoadFromXml({ stream: fileStream });

    // 保存为 Excel 格式(Excel 2013 版本)
    const outputFileName = 'OpenXMLToExcel.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2013 });

    // 释放工作簿对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取转换后的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>OpenXML 转换为 Excel</h1>
      <button onClick={openXMLToExcel}>
        开始转换
      </button>
    </div>
  );
}

export default App;

运行后, OpenXML文件转为Excel文件的效果:

OpenXML 转换为 Excel


常见问题

中文字符在 OpenXML 里变成问号或方框

原因:字体没有正确加载到 VFS。教程里加载了 simsun.ttc,但如果你的文档用了其他中文字体(比如微软雅黑、宋体之外的字体),或者字体文件路径不对,渲染和导出时就会缺字形。

解决:确认 FetchFileToVFS 的字体路径和 PUBLIC_URL 拼接正确,并且字体文件确实在 public/font/ 下。如果文档用了多种字体,需要把对应字体文件都加载进去,不能只加载一个 simsun.ttc。

SaveAsXml 和 SaveToFile 保存出来的文件,下载后打不开或提示损坏

原因:从 VFS 读取文件时用了错误的读取方式,或者 Blob 的 MIME 类型设置不对。比如 OpenXML 应该用 application/xml,Excel 应该用 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,写错了浏览器可能按文本处理,导致二进制内容被破坏。

解决:检查确认 a.download 的文件名扩展名和实际内容格式一致,不要出现 .xml 里装的是 xlsx 二进制的情况。


获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

把数据写进 Excel 时,最直接的做法是逐个单元格赋值,写十行就要循环十次。但数据在 JavaScript 里往往本来就是成块的——接口返回的列表、页面上已有的表格、计算出的结果集——既然是数组,就没必要拆散了再一格一格塞回去。Spire.XLS for JavaScript 提供的 InsertArray 方法接受整段数组,配合起始行列与写入方向,一次调用就能把整列或整行数据落进工作表。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成上述操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。


将一维数组导入工作表

InsertArray 按数据类型分为多个重载:文本用 stringArray,整数用 intArray,小数用 doubleArray。分成三个重载并非多余的设计——Excel 会把单元格按数据类型区别对待,数值右对齐、可以直接参与求和与图表,而被写成文本的数字左对齐,公式里引用它只会得到 0。分数、金额、数量这类要参与计算的字段,就该走数值重载。

其余参数决定数据落在哪里:firstRow 与 firstColumn 指定起始位置,行列都从 1 开始计数;isVertical 决定数组的展开方向——取 false 时沿行铺开,取 true 时沿列铺开。同一个 ['一月', '二月', '三月'],换个方向就是从 A1 到 C1 的一行表头,或是从 A1 到 A3 的一列数据。具体操作步骤如下:

  1. 将字体载入 VFS。
  2. 新建 Workbook,用 Worksheets.get(0) 取得第一个工作表。
  3. 用 InsertArray 的 stringArray 重载横向写入一行表头。
  4. 用 stringArray 纵向写入姓名列,用 intArray 纵向写入分数列。
  5. 用 AllocatedRange.AutoFitColumns 调整列宽,再以 SaveToFile 保存工作簿。

以下为完整的代码示例,演示如何在 React 中把一维数组导入工作表:

function App() {
  const importArray = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 新建工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);

    // 横向写入一行表头:isVertical 为 false,数组沿行展开到 A1:B1
    sheet.InsertArray({
      stringArray: ['姓名', '分数'],
      firstRow: 1,
      firstColumn: 1,
      isVertical: false,
    });

    // 纵向写入姓名列:isVertical 为 true,数组沿列展开到 A2:A4
    sheet.InsertArray({
      stringArray: ['张三', '李四', '王五'],
      firstRow: 2,
      firstColumn: 1,
      isVertical: true,
    });

    // 纵向写入分数列:数值走 intArray 重载,单元格中保存的是数字而非文本
    sheet.InsertArray({
      intArray: [92, 85, 78],
      firstRow: 2,
      firstColumn: 2,
      isVertical: true,
    });

    // 按内容调整列宽
    sheet.AllocatedRange.AutoFitColumns();

    // 保存工作簿
    const outputFileName = 'ImportArray.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>导入一维数组</h1>
      <button onClick={importArray}>Start</button>
    </div>
  );
}

export default App;

运行后,将一维数组导入工作表的效果:

将一维数组导入工作表


将二维数据表导入工作表

数据表的自然形式是二维数组——第一行表头,其余每行一条记录,像 [['姓名', '科目', '分数'], ['张三', '数学', 92]] 这样。要把二维数据表导入 Excel,需要把二维数组按列拆开,每一列当成一个一维数组单独写入:表头横向写一行,数据部分逐列纵向写入,文本列用 stringArray,数值列用 intArray。同一列的数据类型是统一的,所以每次调用只需选一种写法。若数据是分批到达的,用 LastRow 读出当前已用到的最后一行,下一批接着它的下一行写即可。具体操作步骤如下:

  1. 将字体载入 VFS。
  2. 新建 Workbook,用 Worksheets.get(0) 取得第一个工作表。
  3. 用 InsertArray 的 stringArray 重载把表头横向写入第一行。
  4. 把数据部分按列拆开,逐列调用 stringArray 或 intArray 纵向写入。
  5. 第二批数据到达时,用 LastRow 定位起始行,同样逐列写入。
  6. 用 AllocatedRange.AutoFitColumns 调整列宽,再以 SaveToFile 保存工作簿。

以下为完整的代码示例,演示如何在 React 中把二维数据表导入工作表:

function App() {
  const importDataTable = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 待导入的二维数据,第一行为表头
    const rows = [
      ['姓名', '科目', '分数'],
      ['张三', '数学', 92],
      ['李四', '语文', 85],
    ];

    // 新建工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);

    // 按列写入:文本列走 stringArray,数值列走 intArray
    const writeColumn = (values, firstRow, firstColumn) => {
      const isNumeric = values.every((value) => typeof value === 'number');
      if (isNumeric) {
        sheet.InsertArray({ intArray: values, firstRow, firstColumn, isVertical: true });
      } else {
        sheet.InsertArray({ stringArray: values, firstRow, firstColumn, isVertical: true });
      }
    };

    // 表头横向写入第一行
    sheet.InsertArray({
      stringArray: rows[0],
      firstRow: 1,
      firstColumn: 1,
      isVertical: false,
    });

    // 数据部分按列拆开,逐列纵向写入,从第二行开始
    const body = rows.slice(1);
    for (let column = 0; column < rows[0].length; column++) {
      writeColumn(body.map((row) => row[column]), 2, column + 1);
    }

    // 第二批数据到达,用 LastRow 定位起始行后接在已有数据下方
    const nextBatch = [
      ['王五', '英语', 78],
      ['赵六', '物理', 91],
    ];
    const startRow = sheet.LastRow + 1;
    for (let column = 0; column < rows[0].length; column++) {
      writeColumn(nextBatch.map((row) => row[column]), startRow, column + 1);
    }

    // 按内容调整列宽
    sheet.AllocatedRange.AutoFitColumns();

    // 保存工作簿
    const outputFileName = 'ImportDataTable.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>导入二维数据表</h1>
      <button onClick={importDataTable}>Start</button>
    </div>
  );
}

export default App;

运行后,将二维数据表导入工作表的效果:

将二维数据表导入工作表


常见问题

日期怎么写进单元格?

原因:dateTimeArray 不能用来写 JavaScript 的 Date——传进去会得到 0001/1/1。逐个赋值可以用 Range.DateTimeValue,但它只认 Date 对象,传字符串会提示 Value is not a Date。

解决:整列导入时,把日期写成 yyyy-mm-dd 形式的字符串交给 stringArray 最为简便。保存后的单元格是真正的日期值——读回 NumberValue 得到日期序列号(如 46037),不必再设置数字格式:

sheet.InsertArray({
  stringArray: ['2026-01-15', '2026-02-20'],
  firstRow: 1,
  firstColumn: 1,
  isVertical: true,
});

若只需精确控制某一格,也可以逐个赋值,注意传 Date 对象而非字符串:

sheet.Range.get('A1').DateTimeValue = new Date(Date.UTC(2026, 0, 15));

数组里的空值会打断写入吗?

原因:不会。null、undefined 与空字符串都只写成空单元格,InsertArray 按数组下标定位每一格,与值本身无关,所以后面的元素不会移位,也不会报错。

解决:无须额外处理,直接传入即可。下面这一行会写入 A1:D1 四格,丁 仍落在第四列:

sheet.InsertArray({
  stringArray: ['甲', null, '', '丁'],
  firstRow: 1,
  firstColumn: 1,
  isVertical: false,
});

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

报表里的表头和汇总行如果只有数据和边框,翻看起来并不容易一眼定位。给这些单元格加上底色是最常见的做法,但纯色填充能表达的层次有限:有时需要一道由浅到深的过渡来提示重点,有时需要一层网点或斜纹底纹来区分普通行与汇总行,同时又不至于盖住单元格里的数字。Spire.XLS for JavaScript 通过 Interior 对象同时支持渐变填充与图案填充,基于 WebAssembly 在浏览器端直接完成上述操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍两个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。


为单元格添加渐变填充

渐变填充由三步组成:先把填充类型指定为渐变,再给出两端的颜色,最后说明过渡的方向与变化方式。这三步都落在同一个 Interior 对象上——FillPattern 决定填充类型,Gradient.ForeColor 与 Gradient.BackColor 给出两端颜色,Gradient.TwoColorGradient 接收两个参数:方向取自 GradientStyleType,横向、纵向、两个斜向以及自中心、自角落向外辐散都在这一个枚举里;明暗变体取自 GradientVariantsType,取值是 ShadingVariants1 到 ShadingVariants4。同一组颜色,换个方向与变体就是完全不同的观感。具体操作步骤如下:

  1. 将字体与测试数据载入 VFS。
  2. 新建 Workbook,用 LoadFromFile 载入工作簿,再用 Worksheets.get(0) 取得第一个工作表。
  3. 用 Range.get 取到表头区域,把 Style.Interior.FillPattern 设为 ExcelPatternType.Gradient。
  4. 用 Gradient.ForeColor 与 Gradient.BackColor 指定渐变的两端颜色。
  5. 用 Gradient.TwoColorGradient 指定渐变方向与明暗变体。
  6. 以 SaveToFile 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为 Excel 单元格添加渐变填充:

function App() {
  const applyGradient = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和测试数据载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'FillSample.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 载入工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取表头区域 A1:E1
    const header = sheet.Range.get('A1:E1');

    // 将填充类型指定为渐变
    header.Style.Interior.FillPattern = xlsModule.ExcelPatternType.Gradient;

    // 指定渐变的两端颜色
    header.Style.Interior.Gradient.ForeColor = xlsModule.Color.FromArgb(255, 255, 255);
    header.Style.Interior.Gradient.BackColor = xlsModule.Color.FromArgb(79, 129, 189);

    // 双色渐变:第一个参数决定渐变方向,第二个参数决定明暗变体
    header.Style.Interior.Gradient.TwoColorGradient(
      xlsModule.GradientStyleType.Horizontal,
      xlsModule.GradientVariantsType.ShadingVariants1,
    );

    // 保存工作簿
    const outputFileName = 'GradientFill.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加渐变填充</h1>
      <button onClick={applyGradient}>Start</button>
    </div>
  );
}

export default App;

运行后,为表头添加渐变填充的效果:

为单元格添加渐变填充


为单元格添加图案填充

图案填充不是一种单一效果,而是一组内置样式的总称:网点、横纹、竖纹、斜纹、棋盘格等都归在 ExcelPatternType 之下,取值有几十种,密度从 5% 到 75% 不等。与渐变填充相比,图案填充占用的视觉重量更轻,铺在整行数据上也不会干扰阅读,因此更适合用来标记汇总行或需要区分的数据段。

设置方式与渐变同源:Interior.FillPattern 选定图案样式后,再给出两种颜色。此处有一处容易写反的地方——Interior.Color 决定的是图案线本身的颜色,Interior.PatternColor 决定的则是铺在下面的底色,属性名与实际作用正好相反。把深色交给 Interior.Color、浅色交给 Interior.PatternColor,网点才会浮在浅底之上。具体操作步骤如下:

  1. 将字体与测试数据载入 VFS。
  2. 新建 Workbook,用 LoadFromFile 载入工作簿,再用 Worksheets.get(0) 取得第一个工作表。
  3. 用 Range.get 取到汇总行区域,把 Style.Interior.FillPattern 设为所需的 ExcelPatternType 取值。
  4. 用 Interior.Color 指定图案线的颜色,用 Interior.PatternColor 指定底色。
  5. 以 SaveToFile 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为 Excel 单元格添加图案填充:

function App() {
  const applyPattern = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体和测试数据载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'FillSample.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 载入工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取汇总行区域 A5:E5
    const total = sheet.Range.get('A5:E5');

    // 选定内置图案样式,这里用 12.5% 灰度网点
    total.Style.Interior.FillPattern = xlsModule.ExcelPatternType.Percent125Gray;

    // Interior.Color 决定图案线本身的颜色,Interior.PatternColor 决定铺在下面的底色
    total.Style.Interior.Color = xlsModule.Color.FromArgb(192, 80, 77);
    total.Style.Interior.PatternColor = xlsModule.Color.FromArgb(255, 242, 204);

    // 保存工作簿
    const outputFileName = 'PatternFill.xlsx';
    workbook.SaveToFile({ fileName: outputFileName });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加图案填充</h1>
      <button onClick={applyPattern}>Start</button>
    </div>
  );
}

export default App;

运行后,为汇总行添加图案填充的效果:

为单元格添加图案填充


常见问题

填充色设好了,单元格里的字体样式为什么没跟着变?

原因:Style.Interior 只负责填充。填充、字体、边框是 Style 之下三个彼此独立的成员,改动其中一个不会波及另外两个——底色刷深了字还是原来的黑色,自然就糊成一片;反过来给 Style.Font 换了颜色,底色也不会跟着动。

解决:填充与字体分开设置,需要对比度时两处一起调:

// 填充
sheet.Range.get('A1:E1').Style.Interior.Color = xlsModule.Color.FromArgb(79, 129, 189);

// 字体
sheet.Range.get('A1:E1').Style.Font.Color = xlsModule.Color.FromArgb(255, 255, 255);
sheet.Range.get('A1:E1').Style.Font.IsBold = true;

Style.Font 下还有 FontName、Size、IsItalic、Underline 等属性,与填充互不干涉。

填充之后想把颜色去掉,怎么恢复成无填充?

原因:换一种颜色只是换了一种填充,并不会把填充去掉——填充类型仍是非 None 的取值,图案或底色依旧写在单元格的样式里。

解决:把填充类型设回 ExcelPatternType.None,原有的图案与配色一并失效:

sheet.Range.get('A5:E5').Style.Interior.FillPattern = xlsModule.ExcelPatternType.None;

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。