change(md): 完善Word.md参考文档

- 支持Word模板放置于resources根目录,使用classpath根路径加载
- 优化文档说明,强调模板路径及配置规范
- 新增DynamicTableRenderPolicy用于动态插入表格行,支持格式控制和数据绑定
- 修改示例代码展示如何绑定并使用动态表格渲染策略
- 数据层由List<Map<String,Object>>改为List<RowRenderData>以适应动态表格
- 更新WordTemplateUtil工具,增加带Configure参数的重载方法支持自定义策略
- 提供详细故障排查建议,确保动态表格渲染正常工作
- 其他相关示例和注释同步更新,统一模板使用规范和最佳实践
dev
zangch@mesnac.com 6 days ago
parent 2309ba3197
commit 0815706c2c

@ -24,7 +24,7 @@ import java.util.Objects;
* *
* 1) * 1)
* 2) resources templates classpath * 2) resources templates classpath
* {@code templates/wms_shipping_bill.docx} * {@code templates/wms_shipping_bill.docx} resources {@code .docx}
* 3) Map * 3) Map
* - data.put("shippingCode", "X123"); 使 {{shippingCode}} * - data.put("shippingCode", "X123"); 使 {{shippingCode}}
* - data.put("details", detailList); 使 {{#details}}...{{/details}} * - data.put("details", detailList); 使 {{#details}}...{{/details}}
@ -36,11 +36,14 @@ import java.util.Objects;
* Map<String, Object> data = new HashMap<>(); * Map<String, Object> data = new HashMap<>();
* data.put("shippingCode", "X123"); * data.put("shippingCode", "X123");
* data.put("details", detailList); // 列表占位符示例 * data.put("details", detailList); // 列表占位符示例
* WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", "发货单_X123", data, response); * WordTemplateUtil.renderToResponse("发货单模板.docx", "发货单_X123", data, response);
* </pre> * </pre>
* *
* *
* hwbm-cloud/ruoyi-common/ruoyi-common-word/word.md * <p>
* {@code hwbm-cloud/ruoyi-common/ruoyi-common-word/word.md}WmsShippingBill
* <p>
* Why//
*/ */
@Slf4j @Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -57,6 +60,8 @@ public final class WordTemplateUtil {
* *
* 1) response response * 1) response response
* 2) templatePath 使 classpath resources * 2) templatePath 使 classpath resources
* 3) 使 {@link Configure} RenderPolicy
* ruoyi-common-word/word.md WmsShippingBill
*/ */
public static void renderToResponse(String templatePath, String fileName, Map<String, Object> data, HttpServletResponse response) { public static void renderToResponse(String templatePath, String fileName, Map<String, Object> data, HttpServletResponse response) {
Objects.requireNonNull(response, "HttpServletResponse must not be null"); Objects.requireNonNull(response, "HttpServletResponse must not be null");

@ -35,13 +35,16 @@ poi-tl 是构建在 Apache POI 之上的 **Word 模板引擎**,通过简单的
- 建议所有业务模块将 Word 模板统一放在各自模块的 `resources/templates` 目录下,例如: - 建议所有业务模块将 Word 模板统一放在各自模块的 `resources/templates` 目录下,例如:
- `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/templates/wms_shipping_bill.docx` - `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/templates/wms_shipping_bill.docx`
- 其他模块可类比:`templates/erp_contract.docx`、`templates/crm_quote.docx` 等。 - 其他模块可类比:`templates/erp_contract.docx`、`templates/crm_quote.docx` 等。
- 也支持直接放在 `resources` 根目录classpath 根路径),例如:
- `hwbm-cloud/ruoyi-modules/ruoyi-wms/src/main/resources/发货单模板.docx`
- `WordTemplateUtil` 接收的是 **classpath 相对路径**,例如: - `WordTemplateUtil` 接收的是 **classpath 相对路径**,例如:
```java ```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}} data.put("details", details); // 对应模板 {{#details}}...{{/details}}
``` ```
- **动态表格字段(渲染策略 RenderPolicy推荐明细表格场景**
适用场景:
- Word 模板中需要用“表格”展示明细,且需要**动态插入 N 行**(常见于发货单/报价/合同明细);
- 需要在渲染时做一些格式控制(比如边框、合并单元格、空数据时删除模板行等)。
约定:
- 模板中使用一个占位符(例如 `{{detailsTable}}`),由 `DynamicTableRenderPolicy` 在渲染时接管;
- 数据侧使用 `List<RowRenderData>`(而不是 `List<Map<String,Object>>`)。
**模板制作规范(非常关键)**
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<RowRenderData> 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<RowRenderData>`,因此 `detailsTable` 必须放 `List<RowRenderData>`
不要误传 `List<Map<String,Object>>`
- **表头在,明细行全没了**
- 当前策略在 rows 为空时会删除模板行并保留表头,这是预期行为;
请检查 `buildWordExportData` 是否确实组装了 rows。
- **合计字段(可选)** - **合计字段(可选)**
```java ```java
@ -104,8 +161,13 @@ public void exportWord(@PathVariable Long shippingBillId, HttpServletResponse re
String fileName = "发货单_" + (vo != null && vo.getShippingCode() != null String fileName = "发货单_" + (vo != null && vo.getShippingCode() != null
? vo.getShippingCode() : shippingBillId); ? vo.getShippingCode() : shippingBillId);
// 3调用通用工具类完成渲染与输出 // 3配置表格动态渲染策略将模板中的 {{detailsTable}} 占位符绑定到自定义 Policy
WordTemplateUtil.renderToResponse("templates/wms_shipping_bill.docx", fileName, data, response); 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 1. 查询发货单主表 + 明细列表 VO
2. 组装主表字段(顶部信息、落款信息); 2. 组装主表字段(顶部信息、落款信息);
3. 组装明细列表 `details`(循环表格数据源)。 3. 组装明细表格 `detailsTable`(动态表格数据源)。
部分代码示意: 部分代码示意:
@ -144,22 +206,23 @@ public Map<String, Object> buildWordExportData(Long shippingBillId) {
data.put("shipper", StringUtils.blankToDefault(vo.getContactUser(), "")); // {{shipper}} data.put("shipper", StringUtils.blankToDefault(vo.getContactUser(), "")); // {{shipper}}
data.put("contactNumber", StringUtils.blankToDefault(vo.getContactNumber(), "")); // {{contactNumber}} data.put("contactNumber", StringUtils.blankToDefault(vo.getContactNumber(), "")); // {{contactNumber}}
// === 明细列表,对应 {{#details}}...{{/details}} === // === 明细表格DynamicTableRenderPolicy 动态表格渲染)===
List<Map<String, Object>> details = new ArrayList<>(); // 对应 Word 模板中的 {{detailsTable}} 占位符,由 WmsShippingDetailTablePolicy 策略处理
List<RowRenderData> detailRows = new ArrayList<>();
if (CollUtil.isNotEmpty(vo.getItemsVo())) { if (CollUtil.isNotEmpty(vo.getItemsVo())) {
int seq = 1; int seq = 1;
for (WmsShippingDetailsVo item : vo.getItemsVo()) { for (WmsShippingDetailsVo item : vo.getItemsVo()) {
Map<String, Object> row = new HashMap<>(); RowRenderData row = Rows.of(
row.put("seq", String.valueOf(seq++)); // {{seq}} String.valueOf(seq++),
row.put("materialName", StringUtils.blankToDefault(item.getMaterialName(), "")); // {{materialName}} StringUtils.blankToDefault(item.getMaterialName(), ""),
row.put("quantity", item.getShippingStockAmount() != null item.getShippingStockAmount() != null ? item.getShippingStockAmount().toPlainString() : "",
? item.getShippingStockAmount().toPlainString() : ""); // {{quantity}} StringUtils.blankToDefault(item.getUnitName(), ""),
row.put("unit", StringUtils.blankToDefault(item.getUnitName(), "")); // {{unit}} StringUtils.blankToDefault(item.getRemark(), "")
row.put("remark", StringUtils.blankToDefault(item.getRemark(), "")); // {{remark}} ).create();
details.add(row); detailRows.add(row);
} }
} }
data.put("details", details); data.put("detailsTable", detailRows);
return data; return data;
} }
@ -174,9 +237,7 @@ public Map<String, Object> buildWordExportData(Long shippingBillId) {
联系人:{{receiverName}} 联系电话:{{receiverPhone}} 联系人:{{receiverName}} 联系电话:{{receiverPhone}}
发货日期:{{shippingDate}} 合同编号:{{contractCode}} 发货日期:{{shippingDate}} 合同编号:{{contractCode}}
{{#details}} 明细表格占位符:{{detailsTable}}
序号:{{seq}} 物料:{{materialName}} 数量:{{quantity}} 单位:{{unit}} 备注:{{remark}}
{{/details}}
收货日期:{{receivedDate}} 收货日期:{{receivedDate}}
@ -184,9 +245,15 @@ public Map<String, Object> buildWordExportData(Long shippingBillId) {
发货人:{{shipper}} 联系电话:{{contactNumber}} 发货人:{{shipper}} 联系电话:{{contactNumber}}
``` ```
补充说明:
- **主表字段**使用 `{{fieldName}}` 直接渲染;
- **明细表格**使用 `DynamicTableRenderPolicy`:模板里只需在“明细表格的模板行”放置 `{{detailsTable}}`,
渲染时由 `WmsShippingDetailTablePolicy` 删除模板行并插入真实的 `List<RowRenderData>`
> 其它单据(如报价、合同、项目收货/验收)可以直接复用上述模式: > 其它单据(如报价、合同、项目收货/验收)可以直接复用上述模式:
> - 主表字段:顶部信息 + 落款信息; > - 主表字段:顶部信息 + 落款信息;
> - 明细字段:`details` 循环表格; > - 明细字段:优先采用 `DynamicTableRenderPolicy`(如 `detailsTable`);简单场景也可使用 `{{#list}}...{{/list}}` 循环;
> - 可扩展合计字段:`totalQuantity`、`totalAmount` 等。 > - 可扩展合计字段:`totalQuantity`、`totalAmount` 等。
--- ---

Loading…
Cancel
Save