poi tl 导出word

1. 核心概念与模板语法

在使用 poi-tl 进行 Word 模板渲染时,针对循环列表生成表格行的场景,有以下特殊语法规则:
  • 循环开始标识:使用 {{#listName}} 标记循环的开始(通常位于表格的第一行或表头下方)。
  • 属性引用:在循环体内部,引用集合中对象的属性时,必须使用方括号 [propertyName],而不是普通的双花括号 {{propertyName}}
  • 循环结束不需要显式的结束标签,引擎会自动根据表格结构判断循环范围。

2. 后端实现 (Java)
2..1 依赖准备
确保项目中引入了 poi-tl 依赖:

<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.10.4</version> <!-- 请使用最新稳定版 -->
</dependency>

2.2 业务调用层
构建数据模型,区分普通文本、静态表格数据和动态循环数据。

public void exportWord(DealAfterReport dealAfterReport, HttpServletResponse response) throws Exception {
    Map<String, Object> dataMap = new HashMap<>();

    // 1. 基础对象数据 (对应模板 {{dealBase.dealName}})
    DealBase dealBase = new DealBase();
    dealBase.setDealName("dealName*************");
    dataMap.put("dealBase", dealBase);

    // 2. 普通文本 (对应模板 {{title}})
    dataMap.put("title", "年度财务报告");

    // 3. 静态表格数据 (对应模板 {{tableTest}})
    // 这种表格行数固定,直接渲染
    RowRenderData row0 = Rows.of("姓名", "学历").textColor("FFFFFF").bgColor("4472C4").center().create();
    RowRenderData row1 = Rows.create("李四", "博士");
    dataMap.put("tableTest", Tables.create(row0, row1));

    // 4. 动态循环数据 (对应模板 {{#aaa}})
    // 这里的 Key "aaa" 必须与模板中的 {{#aaa}} 一致
    List<Map<String, Object>> risks = new ArrayList<>();
    risks.add(Map.of("name", "技术风险", "level", "高"));
    risks.add(Map.of("name", "市场风险", "level", "中"));
    dataMap.put("aaa", risks);

    // 5. 定义需要循环渲染的标签名列表
    List<String> loopTags = Arrays.asList("aaa");

    // 6. 调用通用工具
    exportWordUtils(dataMap, response, "季度投后管理报告模板.docx", "年度财务报告.docx", loopTags);
}

2.3 通用导出工具类
封装了配置构建、流处理和 HTTP 响应头设置。

/**
 * 通用 Word 导出工具方法
 */
public void exportWordUtils(Map<String, Object> dataMap, HttpServletResponse response,
                            String templateName, String outFileName,
                            List<String> loopTags) throws Exception {
    // 1. 设置 HTTP 响应头 (处理中文文件名乱码)
    String encodedFileName = URLEncoder.encode(outFileName, StandardCharsets.UTF_8.name())
            .replaceAll("\\+", "%20");
    response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
    response.setHeader("Content-Disposition",
            "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);

    // 2. 读取模板文件
    String templatePath = "/template/" + templateName;
    try (InputStream templateIs = this.getClass().getResourceAsStream(templatePath)) {
        if (templateIs == null) {
            throw new RuntimeException("模板文件未找到:" + templatePath);
        }

        // 3. 动态构建 Configure 配置
        // HackLoopTableRenderPolicy 是处理复杂循环表格的关键策略
        ConfigureBuilder builder = Configure.builder();
        HackLoopTableRenderPolicy policy = new HackLoopTableRenderPolicy();

        if (loopTags != null && !loopTags.isEmpty()) {
            for (String tag : loopTags) {
                // 将标签名绑定到策略上
                builder.bind(tag, policy);
            }
        }
        Configure config = builder.build();

        // 4. 编译、渲染并写出
        XWPFTemplate template = XWPFTemplate.compile(templateIs, config).render(dataMap);
        template.write(response.getOutputStream());
        template.close(); // 务必关闭流
    }
}

3. 前端实现 (Vue.js)

handleExport(row) {
  const fileName = this.reportQuarterFormat(row) + '投后管理报告';
  const baseURL = process.env.VUE_APP_BASE_API || '';

  axios({
    method: 'get',
    url: baseURL + '/afterReport/dealAfterReport/exportWord',
    params: { id: row.id },
    responseType: 'blob' // 必须指定返回类型为 blob
  })
  .then((response) => {
    // 【重要修正】
    // 原代码 type: 'text/html;charset=UTF-8' 是错误的!
    // 必须使用 application/vnd.openxmlformats-officedocument.wordprocessingml.document
    // 或者简写为 application/octet-stream
    const blob = new Blob([response.data], {
      type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    });

    const link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    link.download = fileName + '.docx'; // 建议后缀使用 .docx 以匹配真实格式

    document.body.appendChild(link);
    link.click();

    // 清理 DOM 和内存
    document.body.removeChild(link);
    window.URL.revokeObjectURL(link.href);
  })
  .catch((error) => {
    console.error('导出失败:', error);
    this.$message.error('导出失败');
  });
}

 

posted @ 2026-07-17 16:48  翘中之楚  阅读(19)  评论(0)    收藏  举报