From 0815706c2c3a234c612a89df89b3c56154d7bf29 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Fri, 12 Dec 2025 14:48:56 +0800 Subject: [PATCH] =?UTF-8?q?change(md):=20=E5=AE=8C=E5=96=84Word.md?= =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持Word模板放置于resources根目录,使用classpath根路径加载 - 优化文档说明,强调模板路径及配置规范 - 新增DynamicTableRenderPolicy用于动态插入表格行,支持格式控制和数据绑定 - 修改示例代码展示如何绑定并使用动态表格渲染策略 - 数据层由List>改为List以适应动态表格 - 更新WordTemplateUtil工具,增加带Configure参数的重载方法支持自定义策略 - 提供详细故障排查建议,确保动态表格渲染正常工作 - 其他相关示例和注释同步更新,统一模板使用规范和最佳实践 --- .../common/word/util/WordTemplateUtil.java | 13 ++- ruoyi-common/ruoyi-common-word/word.md | 107 ++++++++++++++---- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java b/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java index c04658de..435a726e 100644 --- a/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java +++ b/ruoyi-common/ruoyi-common-word/src/main/java/org/dromara/common/word/util/WordTemplateUtil.java @@ -24,7 +24,7 @@ import java.util.Objects; * 设计要点: * 1) 仅提供模板加载、渲染、响应输出的通用能力,不侵入具体业务字段。 * 2) 模板文件放在各业务模块的 resources 下(建议放 templates 目录),通过 classpath 路径传入,例如 - * {@code templates/wms_shipping_bill.docx}。 + * {@code templates/wms_shipping_bill.docx};也支持放在 resources 根目录,例如 {@code 发货单模板.docx}。 * 3) 业务方负责组装模板所需的数据 Map(包含普通字段与列表),本工具不约束字段名称。 * - 普通字段示例:data.put("shippingCode", "X123"); → 模板里使用 {{shippingCode}} * - 列表字段示例:data.put("details", detailList); → 模板里使用 {{#details}}...{{/details}} @@ -36,11 +36,14 @@ import java.util.Objects; * Map data = new HashMap<>(); * data.put("shippingCode", "X123"); * data.put("details", detailList); // 列表占位符示例 - * WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单_X123", data, response); + * WordTemplateUtil.renderToResponse("发货单模板.docx", "发货单_X123", data, response); * * - * 更多背景说明、模板字段约定及发货单完整示例可参考: - * hwbm-cloud/ruoyi-common/ruoyi-common-word/word.md + * 更多背景说明、模板字段约定及完整示例请参考: + *

+ * {@code hwbm-cloud/ruoyi-common/ruoyi-common-word/word.md}(重点查看“发货单(WmsShippingBill)标准示例”小节)。 + *

+ * Why:将“模板路径/占位符/动态表格渲染策略”等共性信息沉淀到统一文档,避免不同模块各自维护导致的口径不一致。 */ @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -57,6 +60,8 @@ public final class WordTemplateUtil { * 注意: * 1) 本方法内部会重置响应头并向 response 写入二进制流,调用前不要再向 response 写任何内容。 * 2) templatePath 使用的是 classpath 相对路径,打包后模板应放在各模块的 resources 目录中。 + * 3) 若涉及“表格动态插入行”等复杂渲染,请使用带 {@link Configure} 的重载方法,绑定自定义 RenderPolicy。 + * 参考:ruoyi-common-word/word.md 的“发货单(WmsShippingBill)标准示例”。 */ public static void renderToResponse(String templatePath, String fileName, Map data, HttpServletResponse response) { Objects.requireNonNull(response, "HttpServletResponse must not be null"); diff --git a/ruoyi-common/ruoyi-common-word/word.md b/ruoyi-common/ruoyi-common-word/word.md index aa420936..9dfde22c 100644 --- a/ruoyi-common/ruoyi-common-word/word.md +++ b/ruoyi-common/ruoyi-common-word/word.md @@ -35,13 +35,16 @@ poi-tl 是构建在 Apache POI 之上的 **Word 模板引擎**,通过简单的 - 建议所有业务模块将 Word 模板统一放在各自模块的 `resources/templates` 目录下,例如: - `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/templates/wms_shipping_bill.docx` - 其他模块可类比:`templates/erp_contract.docx`、`templates/crm_quote.docx` 等。 +- 也支持直接放在 `resources` 根目录(classpath 根路径),例如: + - `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/发货单模板.docx` - `WordTemplateUtil` 接收的是 **classpath 相对路径**,例如: ```java -WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单_X123", data, response); +WordTemplateUtil.renderToResponse("发货单模板.docx", "发货单_X123", data, response); ``` -部署后,只要模板文件在 Jar 的 `BOOT-INF/classes/templates` 下即可被正常加载。 + 部署后,只要模板文件在 Jar 的 classpath 下即可被正常加载(Spring Boot 可执行 Jar 通常位于 `BOOT-INF/classes/` 下; + 若放在 `resources/templates`,则路径通常为 `BOOT-INF/classes/templates/`;若放在 `resources` 根目录,则路径通常为 `BOOT-INF/classes/`)。 --- @@ -74,6 +77,60 @@ WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单 data.put("details", details); // 对应模板 {{#details}}...{{/details}} ``` +- **动态表格字段(渲染策略 RenderPolicy,推荐明细表格场景)**: + + 适用场景: + + - Word 模板中需要用“表格”展示明细,且需要**动态插入 N 行**(常见于发货单/报价/合同明细); + - 需要在渲染时做一些格式控制(比如边框、合并单元格、空数据时删除模板行等)。 + + 约定: + + - 模板中使用一个占位符(例如 `{{detailsTable}}`),由 `DynamicTableRenderPolicy` 在渲染时接管; + - 数据侧使用 `List`(而不是 `List>`)。 + + **模板制作规范(非常关键)**: + + 1. 在 Word 中画一个表格(例如 5 列:序号/名称/数量/单位/备注); + 2. 至少包含两行: + - 表头行(固定不动) + - 模板行(数据行模板) + 3. 在“模板行”的任意一个单元格中放置占位符 `{{detailsTable}}`。 + - 渲染时会删除该模板行,并插入真实数据行; + - 不建议在模板行做复杂的单元格合并,否则可能导致插入行结构与预期不一致。 + + **后端绑定方式(Controller 侧)**: + + ```java + // 1) 绑定占位符 detailsTable 到渲染策略 + Configure config = Configure.builder() + .bind("detailsTable", new WmsShippingDetailTablePolicy()) + .build(); + + // 2) 输出(使用带 Configure 的重载方法) + WordTemplateUtil.renderToResponse("发货单模板.docx", fileName, data, config, response); + ``` + + **后端数据结构(Service 侧)**: + + ```java + List rows = new ArrayList<>(); + rows.add(Rows.of("1", "胶辊", "10", "件", "加急").create()); + data.put("detailsTable", rows); // 对应模板 {{detailsTable}} + ``` + + **常见问题与排查**: + + - **表格没渲染/仍显示 {{detailsTable}}**: + - `Configure.bind("detailsTable", ...)` 的 key 必须与模板占位符、`data.put("detailsTable", ...)` 完全一致; + - 确认 Controller 调用的是带 `Configure` 的 `renderToResponse(...)` 重载方法。 + - **报类型转换异常(ClassCastException)**: + - 策略里会把 data 强转为 `List`,因此 `detailsTable` 必须放 `List`; + 不要误传 `List>`。 + - **表头在,明细行全没了**: + - 当前策略在 rows 为空时会删除模板行并保留表头,这是预期行为; + 请检查 `buildWordExportData` 是否确实组装了 rows。 + - **合计字段(可选)**: ```java @@ -104,8 +161,13 @@ public void exportWord(@PathVariable Long shippingBillId, HttpServletResponse re String fileName = "发货单_" + (vo != null && vo.getShippingCode() != null ? vo.getShippingCode() : shippingBillId); - // 3)调用通用工具类完成渲染与输出 - WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", fileName, data, response); + // 3)配置表格动态渲染策略:将模板中的 {{detailsTable}} 占位符绑定到自定义 Policy + Configure config = Configure.builder() + .bind("detailsTable", new WmsShippingDetailTablePolicy()) + .build(); + + // 4)调用通用工具类完成渲染与输出(使用自定义 Configure) + WordTemplateUtil.renderToResponse("发货单模板.docx", fileName, data, config, response); } ``` @@ -117,7 +179,7 @@ public void exportWord(@PathVariable Long shippingBillId, HttpServletResponse re 1. 查询发货单主表 + 明细列表 VO; 2. 组装主表字段(顶部信息、落款信息); -3. 组装明细列表 `details`(循环表格数据源)。 +3. 组装明细表格 `detailsTable`(动态表格数据源)。 部分代码示意: @@ -144,22 +206,23 @@ public Map buildWordExportData(Long shippingBillId) { data.put("shipper", StringUtils.blankToDefault(vo.getContactUser(), "")); // {{shipper}} data.put("contactNumber", StringUtils.blankToDefault(vo.getContactNumber(), "")); // {{contactNumber}} - // === 明细列表,对应 {{#details}}...{{/details}} === - List> details = new ArrayList<>(); + // === 明细表格(DynamicTableRenderPolicy 动态表格渲染)=== + // 对应 Word 模板中的 {{detailsTable}} 占位符,由 WmsShippingDetailTablePolicy 策略处理 + List detailRows = new ArrayList<>(); if (CollUtil.isNotEmpty(vo.getItemsVo())) { int seq = 1; for (WmsShippingDetailsVo item : vo.getItemsVo()) { - Map row = new HashMap<>(); - row.put("seq", String.valueOf(seq++)); // {{seq}} - row.put("materialName", StringUtils.blankToDefault(item.getMaterialName(), "")); // {{materialName}} - row.put("quantity", item.getShippingStockAmount() != null - ? item.getShippingStockAmount().toPlainString() : ""); // {{quantity}} - row.put("unit", StringUtils.blankToDefault(item.getUnitName(), "")); // {{unit}} - row.put("remark", StringUtils.blankToDefault(item.getRemark(), "")); // {{remark}} - details.add(row); + RowRenderData row = Rows.of( + String.valueOf(seq++), + StringUtils.blankToDefault(item.getMaterialName(), ""), + item.getShippingStockAmount() != null ? item.getShippingStockAmount().toPlainString() : "", + StringUtils.blankToDefault(item.getUnitName(), ""), + StringUtils.blankToDefault(item.getRemark(), "") + ).create(); + detailRows.add(row); } } - data.put("details", details); + data.put("detailsTable", detailRows); return data; } @@ -174,9 +237,7 @@ public Map buildWordExportData(Long shippingBillId) { 联系人:{{receiverName}} 联系电话:{{receiverPhone}} 发货日期:{{shippingDate}} 合同编号:{{contractCode}} -{{#details}} -序号:{{seq}} 物料:{{materialName}} 数量:{{quantity}} 单位:{{unit}} 备注:{{remark}} -{{/details}} +明细表格占位符:{{detailsTable}} 收货日期:{{receivedDate}} @@ -184,9 +245,15 @@ public Map buildWordExportData(Long shippingBillId) { 发货人:{{shipper}} 联系电话:{{contactNumber}} ``` +补充说明: + +- **主表字段**使用 `{{fieldName}}` 直接渲染; +- **明细表格**使用 `DynamicTableRenderPolicy`:模板里只需在“明细表格的模板行”放置 `{{detailsTable}}`, + 渲染时由 `WmsShippingDetailTablePolicy` 删除模板行并插入真实的 `List`。 + > 其它单据(如报价、合同、项目收货/验收)可以直接复用上述模式: > - 主表字段:顶部信息 + 落款信息; -> - 明细字段:`details` 循环表格; +> - 明细字段:优先采用 `DynamicTableRenderPolicy`(如 `detailsTable`);简单场景也可使用 `{{#list}}...{{/list}}` 循环; > - 可扩展合计字段:`totalQuantity`、`totalAmount` 等。 ---