Skip to content

打印模板 HTML 结合 APL 实现自定义数据打印

通过使用 APL 语言查询和计算数据,配合 HTML 模板引擎,在 PDF 打印模板中实现跨级对象打印、异形表格合并及数据计算等高级场景。

概述

💡 在标准打印模板无法满足复杂排版需求(如跨多个级别获取关联属性、合并单元格展示审批流、行列倒置等)时,您可以通过在 PDF 模板中嵌入 HTML 代码并调用 APL(Active PaaS Language)函数,实现百分之百自定义的打印输出。

开始之前

IMPORTANT

  • 权限与灰度要求:必须联系客户成功经理开通 打印模板支持插入 APL 函数 灰度项。
  • 适用模板类型:本文档介绍的方案仅适用于 PDF 模板(在线编辑器模板)。

核心操作流

配置自定义打印数据流包含以下两个核心步骤:

  1. 在 APL 中编写查询与计算逻辑:返回组织好的 Map 数据。
  2. 在 PDF 编辑器中编写 HTML 代码:通过模板语法渲染 APL 返回的变量。

APL 绑定入口

HTML 编辑界面


典型场景案例

场景一:打印多级关联对象的字段

对于订单或合同等对象,明细列表中往往需要展示关联产品的更深层属性(如明细 -> 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 map

HTML 模板代码

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>

场景二:打印异形表格形式的审批数据

在输出审批明细时,常常希望将相同审批节点的审批记录进行单元格合并(异形表格)。

标准效果对比希望实现的效果

前置知识与语法

Rowspan 说明

Colspan 说明

设计思路

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

APL 逻辑示意

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 map

2. HTML 绑定与渲染

在 HTML 中结合 #if 语句,对 rowspan0<td> 进行隐藏,仅在 rowspan != 0 时渲染该单元格:

HTML 控制配置

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 语言,您可以灵活地:

  • 按照记录中的不同业务类型或选项值将明细数据分组渲染到多个表格中。
  • 对明细字段进行前置聚合(求和、求平均),并根据运算结果隐藏或高亮某些数据行。
  • 沿对象引用关系链进行多层深度查询,实现极其复杂的业务单据打印。

相关主题