打印模板 HTML 结合 APL 实现自定义数据打印
通过使用 APL 语言查询和计算数据,配合 HTML 模板引擎,在 PDF 打印模板中实现跨级对象打印、异形表格合并及数据计算等高级场景。
概述
💡 在标准打印模板无法满足复杂排版需求(如跨多个级别获取关联属性、合并单元格展示审批流、行列倒置等)时,您可以通过在 PDF 模板中嵌入 HTML 代码并调用 APL(Active PaaS Language)函数,实现百分之百自定义的打印输出。
开始之前
IMPORTANT
- 权限与灰度要求:必须联系客户成功经理开通 打印模板支持插入 APL 函数 灰度项。
- 适用模板类型:本文档介绍的方案仅适用于 PDF 模板(在线编辑器模板)。
核心操作流
配置自定义打印数据流包含以下两个核心步骤:
- 在 APL 中编写查询与计算逻辑:返回组织好的 Map 数据。
- 在 PDF 编辑器中编写 HTML 代码:通过模板语法渲染 APL 返回的变量。


典型场景案例
场景一:打印多级关联对象的字段
对于订单或合同等对象,明细列表中往往需要展示关联产品的更深层属性(如明细 -> SKU -> SPU 的属性)。由于标准明细表格仅支持展示直接关联对象的字段,您可以通过 APL 的 findByIds 查询能力,实现多级关联数据的获取与渲染。
APL 代码
groovy
/**
* @author admin01
* @codeName 打印合同产品明细
* @description 打印合同产品明细
* @createTime 2024-09-04
*/
List details = context.details.SaleContractLineObj as List
List skuIds = []
details.each { entry ->
Map detail = entry as Map
String skuId = detail.get("product_id")
skuIds.add(skuId)
}
def fqa = FQLAttribute.builder().columns(["_id", "name", "price" , "is_giveaway", "picture_path", "spu_id"]).build()
def sa = SelectAttribute.builder().build()
List prodctList = Fx.object.findByIds("ProductObj", skuIds, fqa, sa).result() as List
Map skuId2SpuIdMap = prodctList.collectEntries { [(((Map) it)._id): ((Map) it).spu_id] }
Map skuId2Map = prodctList.collectEntries { [(((Map) it)._id): ((Map) it)] }
log.info(skuId2Map)
def spuIds = skuId2SpuIdMap.values() as List
def fqa2 = FQLAttribute.builder().columns(["_id", "name", "is_spec"]).build()
def sa2 = SelectAttribute.builder().build()
List spuList = Fx.object.findByIds("SPUObj", spuIds, fqa2, sa2).result() as List
Map spuMap = spuList.collectEntries { [(((Map) it)._id): ((Map) it)] }
log.info(spuMap)
details.each { entry ->
Map detail = entry as Map
String skuId = detail.get("product_id")
Map skuData = skuId2Map.get(skuId)
String isGiveaway = skuData.get("is_giveaway")
String isGiveawayLabel = ""
if ("1" == isGiveaway) {
isGiveawayLabel = "是"
} else if ("0" == isGiveaway) {
isGiveawayLabel = "否"
} else {
isGiveawayLabel = "未知"
}
skuData.put("isGiveawayLabel", isGiveawayLabel)
detail.put("SKUObj", skuData)
String spuId = skuId2SpuIdMap.get(skuId) as String
Map spu = spuMap.get(spuId) as Map
boolean isSpec = spu.get("is_spec") as Boolean
Map spuData = [:]
if(isSpec){
spuData.put("is_spec", "多规格")
} else {
spuData.put("is_spec", "单规格")
}
detail.put("SPUObj", spuData)
}
Map map = [:]
map.put("MySaleContractLineObj", details)
return mapHTML 模板代码
html
<p>
<!--
#set(MyList = pts_ZhFaS__c.MySaleContractLineObj)
-->
</p>
<table>
<thead>
<tr class="firstRow">
<th><span class="variable-label">销售合同明细编号</span></th>
<th><span class="variable-label">是否多规格</span></th>
<th><span class="variable-label">产品名称</span></th>
<th><span class="variable-label">是否赠品</span></th>
<th><span class="variable-label">单价</span></th>
</tr>
</thead>
<tbody>
<!--#for(item : MyList)-->
<tr>
<td>${item.name}</td>
<td>${item.SPUObj.is_spec}</td>
<td>${item.SKUObj.name}</td>
<td>${item.SKUObj.isGiveawayLabel}</td>
<td>${item.SKUObj.price}</td>
</tr>
<!--#end-->
</tbody>
<tfoot>
<tr>
<td colspan="5">表格行数会根据实际数据量自动加载</td>
</tr>
</tfoot>
</table>
<p><br /></p>场景二:打印异形表格形式的审批数据
在输出审批明细时,常常希望将相同审批节点的审批记录进行单元格合并(异形表格)。

前置知识与语法
- HTML 模板引擎语法:关于模板语句控制,请参阅 乐享 HTML 模板引擎语法说明。
- HTML 合并单元格:学习表格合并原理,请参阅 GeeksforGeeks 关于 HTML 表格跨行与跨列的教程 以及 MDN 关于 td 元素的详细文档。


设计思路
- APL 接收页面传入的
instanceId(流程实例唯一编号)。 - 在 APL 中通过
Fx.approval.findTasks查询到对应的审批任务列表。 - 按任务节点名称进行分组,计算出每个单元格的
rowspan和colspan。若单元格被合并,其rowspan或colspan设为0。 - 在 HTML 中通过判断
rowspan != 0来决定是否输出该<td>,并动态绑定其rowspan属性。

1. APL 计算单元格合并逻辑
下面是 APL 对相同任务名称进行跨行合并(rowspan)计算的核心代码:
groovy
/**
* @author admin01
* @codeName 打印审批流
* @description 获取并处理审批流合并行数据
* @createTime 2024-07-08
*/
Map map = [:]
def (Boolean err, List instanceList, String errMsg) = Fx.approval.findTasks(instanceId)
List printList = []
instanceList.each { inst ->
List opinions = ((Map) inst).opinions as List
opinions.eachWithIndex { opin, i ->
Map newOpin = [:]
newOpin.task_name = ((Map) inst).task_name
String replyUser = ((List) ((Map) opin).reply_user)[0]
def (Boolean err1, Map userInfo, String errMsg1) = Fx.org.findUserById(replyUser)
if(!err1){
newOpin.reply_user = userInfo.full_name
} else {
log.info(errMsg1)
}
String type = ((Map) opin).action_type
if(type == 'agree'){
newOpin.action_type = '通过'
} else if(type == 'reject'){
newOpin.action_type = '驳回'
} else if(type == 'cancel'){
newOpin.action_type = '取消'
}
newOpin.opinion = ((Map) opin).opinion
Map spanTaskName = [task_name: [colspan: 1, rowspan: 1]]
newOpin.span = spanTaskName
printList.add(newOpin)
}
}
// 按照任务节点名称进行分组,计算 rowspan
Map groupedByTaskName = printList.groupBy { ((Map) it).task_name }.collectEntries { [(it.key): (it.value)] }
groupedByTaskName.each { entry ->
def taskList = entry.value as List<Map>
if (taskList.size() <= 1) return
taskList.eachWithIndex { task, i ->
Map span = ((Map) task).span as Map
Map taskNameSpan = span.task_name as Map
if (i == 0) {
taskNameSpan.rowspan = taskList.size()
} else {
taskNameSpan.rowspan = 0
}
}
}
map.put('instanceList', printList)
return map2. HTML 绑定与渲染
在 HTML 中结合 #if 语句,对 rowspan 为 0 的 <td> 进行隐藏,仅在 rowspan != 0 时渲染该单元格:
html
<p><br /></p>
<table width="100%">
<thead>
<tr class="firstRow">
<th>任务节点名称</th>
<th>处理人</th>
<th>处理结果</th>
<th>审批意见</th>
</tr>
</thead>
<tbody>
<!--#for(entry:pts_sNsPA__c.instanceList)-->
<tr>
<!-- #if (entry.span.task_name.rowspan != 0) -->
<td rowspan="${entry.span.task_name.rowspan}">${entry.task_name}</td>
<!-- #end -->
<td>${entry.reply_user}</td>
<td>${entry.action_type}</td>
<td>${entry.opinion}</td>
</tr>
<!--#end-->
</tbody>
</table>
<p><br /></p>实践:实现行列倒置(转置表格)
在某些统计分析场景中,您需要将查询结果的行与列进行对调。
原始数据排版(销售区域在列,季度在行):

期望实现排版(季度在列,销售区域在行):

数据 JSON 结构示例
标题头
json
[
"Sales by Region",
"Europe",
"Asia",
"North America"
]原始明细数据
json
[
{
"quarter": "Qt 1",
"eu": "21704714",
"as": "8774099",
"na": "12094215"
},
{
"quarter": "Qt 2",
"eu": "17987034",
"as": "12214447",
"na": "10873099"
},
{
"quarter": "Qt 3",
"eu": "19485029",
"as": "14536879",
"na": "15689543"
},
{
"quarter": "Qt 4",
"eu": "22567894",
"as": "15763492",
"na": "17456723"
}
]通过 APL 循环将数据重组,在 HTML 模板中使用 <!--#for--> 嵌套语句进行纵向与横向字段对齐渲染,即可实现行列倒置输出。
总结
利用打印模板结合 APL 语言,您可以灵活地:
- 按照记录中的不同业务类型或选项值将明细数据分组渲染到多个表格中。
- 对明细字段进行前置聚合(求和、求平均),并根据运算结果隐藏或高亮某些数据行。
- 沿对象引用关系链进行多层深度查询,实现极其复杂的业务单据打印。

