集成NopReport动态生成复杂Word表格

讲解视频: www.bilibili.com/video/BV1jx...

一. 背景与需求

如何用800行代码实现类似poi-tl的可视化Word模板一文中,我们介绍了通过Word超链接嵌入扩展信息实现报表模板的方案。其核心原理是:

  1. 利用Nop平台内置的XML格式XPL模板语言
  2. 将Word模板中的超链接替换为XPL模板标签

比如上面的Word模板将会被翻译为如下XPL实现

xml 复制代码
<orm-docx:for-each-table xpl:slotScope="table">
  <w:tbl>
    ...
    <c:for var="col" items="${table.columns.subList(0,3)}">
      <w:tr w:rsidR="00AD00A2" w14:paraId="2EAAC2A3" w14:textId="77777777" w:rsidTr="00AD00A2">
        ...
      </w:tr>
    </c:for>
  </w:tbl>
</orm-docx:for-each-table>

以上设计的关键要点是,我们完全不需要了解Office XML的样式细节,只需要在合适的地方插入循环标签、表达式标签就可以实现动态Word生成。因此实现起来非常简单,只需要不到1000行代码。

但是,它的缺点是功能有限,难以满足复杂表格的需求。另外一方面,Nop平台内置了一个非常强大的中国式报表引擎NopReport,可以直接使用Excel模板来定义报表,具体参见采用Excel作为设计器的开源中国式报表引擎:NopReport 一文的介绍。

那么,能不能将NopReport的能力引入Word模板,使用NopReport来动态生成Word表格呢?答案是,可以,而且实现起来非常简单。

nop-report-docx模块提供了基于NopReport中国式报表展开算法的解决方案,能够实现:

  • 动态列生成
  • 自动单元格合并
  • 复杂表格布局
  • 数据分组展示

二. 表格模板配置

2.1 启用NopReport表格展开

在表格的第一个单元格插入以下标注:

xml 复制代码
xpt:table=true

2.2 配置展开表达式

支持多种表达式配置方式:

标注属性配置

  • expandType: 定义展开类型
  • expandExpr: 设置展开表达式

单元格内表达式

  • ${expr}: 标准表达式语法
  • *=name: 字段声明语法

2.3 调用示例

使用XptWordTemplateParser解析得到WordTemplate,然后调用WordTemplate的generateToFile函数。

java 复制代码
public class TestXptWordTemplate extends JunitBaseTestCase {
    @Test
    public void testXptTable() {
        IResource resource = getResource("/test/test-word-report.docx");
        WordTemplate tpl = new XptWordTemplateParser().parseFromResource(resource);
        IEvalScope scope = XLang.newEvalScope();
        scope.setLocalValue("data", Arrays.asList(Map.of("name", "a", "amount", 100),
                Map.of("name", "b", "amount", 200)));
        tpl.generateToFile(getTargetFile("test-result.docx"), scope);
    }
}

三. 技术实现

graph LR A[Word模板] --> B(XptWordTemplateParser) B --> C[宏标签转换] C --> D[编译期处理] D --> E[XptWordTableRenderer] E --> F[最终文档] subgraph 编译期 C -->|生成| D end subgraph 运行期 E --> F end

3.1 表格结构转换

XptWordTemplateParser识别标记表格并转换为以下结构:

xml 复制代码
<docx-gen:GenXptTable
  xpl:lib="/nop/report/xlib/docx-gen.xlib"
  dump="true">
  <w:tbl>
    <!-- 原始表格内容 -->
  </w:tbl>
</docx-gen:GenXptTable>

3.2 宏标签处理

<doc-gen:GenXptTable>宏标签在编译的时候将Word表格解析为ExcelTable并生成XptWordTableRenderer:

xml 复制代码
<GenXptTable macro="true">
  <attr name="dump" stdDomain="boolean"/>
  <slot name="default" slotType="node"/>

  <source>
    <c:script><![CDATA[
        import io.nop.report.docx.parse.XptWordTableParser;

        // 编译期解析表格
        const output = XptWordTableParser.fromCompileScope($scope)
                         .compileTable(
                           slot_default.child(0),
                           get('ofcPkg'),
                           dump
                         );

        // 生成AST并传递编译结果
        let ast = xpl `<c:ast>${output.generateToWriter($out.writer,$scope)}</c:ast>`;
        return ast.replaceIdentifier('output', output);
     ]]></c:script>
  </source>
</GenXptTable>

关键流程

  1. 编译期调用compileTable生成模板对象
  2. 通过AST语法树变换将编译结果传递到运行期
  3. 运行期直接使用预编译的XptWordTableRenderer进行渲染

3.3 报表展开和渲染

java 复制代码
public class XptWordTableRenderer implements ITextTemplateOutput {
  private final ExcelSheet xptModel;

  public XptWordTableRenderer(ExcelSheet xptModel) {
    this.xptModel = xptModel;
  }

  @Override
  public void generateToWriter(Writer out, IEvalContext context) throws IOException {
    XptRuntime xptRt = new XptRuntime(context.getEvalScope());
    ExcelWorkbook wk = new ExcelWorkbook();
    xptRt.setWorkbook(wk);

    ExpandedSheet sheet = new ExpandedSheetGenerator(wk).generateSheet(xptModel, xptRt, new HashMap<>());
    renderExpandedSheet(out, sheet, xptRt);
  }

  protected void renderExpandedSheet(Writer out, ExpandedSheet sheet, IXptRuntime xptRt) throws IOException {

    CollectXmlHandler handler = new CollectXmlHandler(out).indentRoot(false).indent(true);

    ExpandedTable table = sheet.getTable();
    handler.beginNode("w:tbl");
    XNode node = (XNode) sheet.getModel().prop_get(VAR_XPT_NODE);
    XNode tblPr = node.childByTag("w:tblPr");
    if (tblPr != null) {
      tblPr.process(handler);
    }
    handler.beginNode("w:tblGrid");
    for (int i = 0, n = table.getColCount(); i < n; i++) {
      int width = UnitsHelper.pointsToTwips(table.getColWidth(i, DEFAULT_WIDTH));
      handler.simpleNode(null, "w:gridCol", Map.of("w:w", vl(null, width)));
    }
    handler.endNode("w:tblGrid");

    for (int i = 0, n = table.getRowCount(); i < n; i++) {
      ExpandedRow row = table.getRow(i);
      XNode tr = (XNode) row.getModel().prop_get(VAR_XPT_NODE);
      XNode trPr = tr.childByTag("w:trPr");
      handler.beginNode(tr.getLocation(), tr.getTagName(), tr.attrValueLocs());
      if (trPr != null)
        trPr.process(handler);
      renderCells(row, handler, xptRt);
      handler.endNode(tr.getTagName());
    }
    handler.endNode("w:tbl");
  }
  // ...
}
  • XptWordTableRenderer的成员变量xptTable是解析Word表格得到的报表模型对象
  • ExpandedSheetGenerator执行报表展开算法,从ExcelSheet得到ExpandedSheet对象
  • ExpandedSheet/ExpandedCell等可以通过getModel来返回报表模型对象,在报表模型对象的扩展属性中保存了原始的Word表格样式( XNode形式)
  • 使用保存的Word样式对象生成最终的xml文件。基本不需要了解word样式的具体结构,只需要修改其中的w:tblW/w:gridCol等部分节点信息。

四. 核心设计要点

XptWordTemplate的设计充分利用Nop平台内置的各种可扩展能力,利用元编程将各种结构变换联系在一起。

  1. 总体实现原理并没有改变,仍然是根据Word模板中的标注信息来将WordXML转换为XPL标签。
  2. <docx-gen:GenXptTable>在编译期执行了一个局部结构变换,利用编译期变量来保存解析结果,不需要额外的中间变量存储机制
  3. 将Word表格解析为ExcelTable时只处理了单元格大小、合并关系等,并不需要解析单元格的样式配置,只是将Word单元格对应的XNode作为扩展属性保存在ExcelCell中。
  4. XptWordTableRenderer先调用ExpandedSheetGenerator执行报表展开算法,然后再结合保存的Word表格样式信息动态生成Word XML。

基于可逆计算理论设计的低代码平台NopPlatform已开源:

相关推荐
熟悉的新风景2 小时前
springboot项目或其他项目使用@Test测试项目接口配置-spring-boot-starter-test
java·spring boot·后端
晴空月明3 小时前
分布式系统高可用性设计 - 监控与日志系统
后端
songroom4 小时前
【转】Rust: PhantomData,#may_dangle和Drop Check 真真假假
开发语言·后端·rust
红尘散仙4 小时前
Rust 终端 UI 开发新玩法:用 Ratatui Kit 轻松打造高颜值 CLI
前端·后端·rust
mldong4 小时前
mldong-goframe:基于 GoFrame + Vben5 的全栈快速开发框架正式开源!
vue.js·后端·go
come112345 小时前
Go 包管理工具详解:安装与使用指南
开发语言·后端·golang
绝无仅有5 小时前
OSS文件上传解析失败,错误:文件下载失败的排查与解决
后端·面试·架构
LaoZhangAI6 小时前
Kiro vs Cursor:2025年AI编程IDE深度对比
前端·后端