跳转至

打印设计器设计方案

产品定位与核心场景

适用打印类型 - 小票 / 票据打印:收银小票、快递面单、发货单、标签(不干胶) - 证件 / 单据:合同、收据、证书、工作证、报销单 - 名片 / 宣传单 / 海报:可视化拖拽排版,A4/A3 自定义尺寸 - 物流标签、条码、二维码打印(高频刚需) - 套打:已有纸质模板,精准定位文字填充(财务凭证、发票)

关键组成

  • 打印组件:文本、HTML、图片、页码、表格、条形码、二维码、直线、矩形、圆形
  • 页面布局:组件面板、设计画布、属性面板
  • 功能:预览、导出pdf、导出图片、打印、HTML

页面设置

纸张类型:A3/A4/A5/LETTER/CUSTOM 等;page.width / page.height 为页面宽高(mm)。

打印组件说明

通用组件

属性

位置 & 尺寸 - X - Y - 宽度 - 高度(单位为mm)

层级 - 层级数

数据&行为 - 每页重复 - 自适应高度 - 是否打印

样式

外观 - 背景颜色 - 文本颜色 - 字体 - 字号mm - 字重 - 左右对齐方式 - 垂直对齐方式

边框 - 边框样式(无/虚线/实线/点线) - 边框宽度mm - 边框颜色

文本组件

属性

内容 - 文本内容(支持 {{变量名}} 占位符,或勾选 是否 JS 后填写 JS 片段) - 属性面板:「内容」「是否 JS」 复选框同一行、两端分布;下方为多行文本框 - 勾选 是否 JS 时:隐藏「变量」按钮,显示说明「JS 片段,通过 data 获取打印数据,如 data.orderNo」 - 未勾选时:内容输入框下方 「变量」按钮 + 占位符说明(同一行);点击按钮弹出「选择变量」对话框,列出主数据源 main.variables,选中后在内容末尾追加 {{变量名}}(组件 PrintVariableSelect) - JSON 字段:props.content(内容字符串)、props.contentIsJsboolean,可选,默认 false

设计态画布:勾选 JS 时不执行脚本,原样显示源码;单元格前显示橙色 js 角标,超出宽度以 省略。

预览 / 打印 / 导出:由 resolveContentValueexecuteContentScript 执行 JS,入参 data 为当前 printData 单项(主数据源标量 + 子数据源数组键,不含当前表格行)。

JS 写法示例

1. 表达式(单行,无需 return):

示例 说明
data.orderNo 取主数据源订单号
data.orderNo + '-' + data.trackingNo 字符串拼接
data.qty > 10 ? '大单' : '普通' 条件表达式

2. 闭包 / IIFE(多行逻辑,IIFE 内须 return 结果值):

(function(){
  var no = data.orderNo || '未知';
  return '单号:' + no;
})()
(function(){
  if (!data.trackingNo) return '';
  return data.trackingNo.slice(-6);
})()

复杂分支、临时变量、多步计算等场景推荐用 IIFE;运行时通过 eval 执行整段脚本,IIFE 的返回值即为渲染文本。

HTML 组件

用于在打印页中插入 HTML 片段(非整页 HTML),支持占位符与 JS 动态生成 HTML 字符串。与文本组件不同,内容按 HTML 解析渲染innerHTML / v-html),可包含 <a><span><br> 等标签。

属性

内容

  • HTML 片段(多行文本),支持 {{变量名}} 占位符,或勾选 是否 JS 后填写 JS 片段(规则同文本组件)
  • 新建默认内容:<span>HTML 内容</span>
  • 属性面板 UI 与文本组件一致:「内容」「是否 JS」 同一行;下方为 5 行 多行文本框(spellcheck="false"
  • 未勾选 JS 时:内容区上方显示说明「支持 HTML 片段,如 <a>{{orderNo}}</a>」;下方 「变量」按钮 + 占位符说明(同文本组件,PrintVariableSelect
  • JSON 字段:props.content(HTML 字符串)、props.contentIsJsboolean,可选,默认 false

设计态画布

  • 未勾选 JS:用 v-html 原样渲染 HTML 模板(**不**替换 {{占位符}}、**不**执行 JS),便于预览标签结构
  • 勾选 JS:不执行脚本,原样显示源码;单元格前显示橙色 js 角标

预览 / 打印 / 导出

  • 占位符模式:resolveContentValue 先替换 {{变量}},再 innerHTML 写入 DOM
  • JS 模式:executeContentScript 求值得到 HTML 字符串,再 innerHTML 写入 DOM
  • 入参 data 为当前 printData 单项(同文本组件)

占位符示例

模板内容 渲染结果(orderNo = SO-001
<span>HTML 内容</span> 静态 HTML
<a href="#">{{orderNo}}</a> <a href="#">SO-001</a>
<span>{{orderNo}}</span>-<span>{{trackingNo}}</span> 多占位符组合

JS 写法示例(须 return HTML 字符串):

(function(){
  var no = data.orderNo || '';
  return '<a href="#">' + no + '</a>';
})()

样式

HTML 组件 不提供 文本颜色、字体、字号等样式项(由片段内 HTML/CSS 自行控制)。属性面板「样式」段仅包含:

  • 背景颜色
  • 边框样式(无/虚线/实线/点线)
  • 边框宽度 mm
  • 边框颜色

图片组件

属性

图片来源 - 图片内容(base64)

样式

填充类型 - 充满 - 适应 - 拉伸 透明的 - 透明的(数字)

表格组件

属性

数据&行为 - 子数据源:下拉选择,选项来自模板 dataSource.subs;持久化为 props.dataSource: "{{子数据源名称}}"(如 {{明细数据源}},键名与子数据源 name 一致) - 自动分页 - 显示表头 - 固定行数(JSON:fixedRowCount):设计态可见行数,用于计算行高与自动分页每页行数 - 显示表脚

表脚内容

属性面板顺序:表脚内容(含 是否 JS / 是否 HTML)→ 列定义

属性 JSON 字段 说明
表脚内容 footerContent 表脚文案:占位符模板、HTML 模板或 JS 片段(见下)
是否 JS footerIsJs boolean,可选,默认 false;为 truefooterContent 按 JS 片段执行;与 footerIsHtml 互斥
是否 HTML footerIsHtml boolean,可选,默认 false;为 truefooterContent 为 HTML 模板 + 占位符;与 footerIsJs 互斥

表脚内容三种模式(由 footerIsJs / footerIsHtml 决定,二者互斥):

模式 条件 运行时解析 单元格渲染
占位符(默认) 二者均未勾选 resolveTableFooterContentresolvePlaceholder textContent
HTML 模板 footerIsHtml: true footerContent{{var}} 占位符替换 innerHTML
JS 片段 footerIsJs: true executeContentScript(footerContent, context) innerHTML(返回值按 HTML 字符串写入)

表脚内容区域 UI:「表脚内容」 为多行 textarea「是否 JS」「是否 HTML」 同一行两端分布,勾选其一会自动取消另一项。勾选 JS 时下方显示 JS 说明(含 HTML 输出示例);勾选 HTML 时显示 HTML 占位符说明;均未勾选时显示「变量」按钮与占位符说明。

系统变量(以 $ 开头,运行时由 mergeSystemVariablesIntoData 注入;设计器数据源面板「变量」分组内与数据源变量一并展示:$page / $pageTotal 仅在主数据源列出,$count 仅在子数据源列出):

名称 占位符 说明
$page {{$page}} 当前页码(从 1 起)
$pageTotal {{$pageTotal}} 总页数
$count {{$count}} 当前表格子数据源总行数(分页时为全表行数)

可用于文本/HTML/条码/二维码/表格列/表脚等内容与 JS 片段(data.$page 等)。

表脚内容占位符(默认模式;运行时由 resolveTableFooterContent 解析,上下文为**主数据源标量** + 上述系统变量):

写法 示例 说明
页码 {{$page}} / {{$pageTotal}} 当前页 / 总页数
行数 {{$count}} 当前表格数据总行数(分页时为全表行数,非仅当前页)
主数据源标量 {{orderNo}} printData[n] 顶层字段
组合模板 共 {{$count}} 条 · {{orderNo}} 行数与主数据源混合

表脚内容 HTML 模板footerIsHtml === true):

示例 说明
共 <b>{{$count}}</b> 条 加粗行数
订单 {{orderNo}},合计 <span style="color:red">{{$count}}</span> 行 内联样式 + 多占位符

表脚内容 JS 片段footerIsJs === true):

  • 执行:resolveTableFooterContentexecuteContentScript;入参 data 含主数据源字段与系统变量($page$pageTotal$count 等)。
  • 返回值按 HTML 写入表脚innerHTML),可返回标签字符串。
示例 说明
'共 ' + data.$count + ' 条' 纯文本拼接
'共 <b>' + data.$count + '</b> 条' 返回 HTML 字符串
data.orderNo ? ('单号 ' + data.orderNo + ' · 共 ' + data.$count + ' 条') : ('共 ' + data.$count + ' 条') 条件表达式

设计态画布(表脚,三种模式统一):不**替换占位符、**不**执行 JS、**不**渲染 HTML DOM;原样显示 footerContentfooterIsJs: true → 橙色 **js 角标;footerIsHtml: true → 蓝色 html 角标。

表脚 JSON 示例

"footerContent": "共 {{$count}} 条"
"footerContent": "共 <b>{{$count}}</b> 条",
"footerIsHtml": true
"footerContent": "'共 <b>' + data.$count + '</b> 条'",
"footerIsJs": true

列定义

每列一条配置,支持增删。属性面板顺序:标题列内容(含 是否 JS / 是否 HTML)→ 宽度对齐

属性 JSON 字段 说明
标题 columns[].title 表头显示文字
列内容 columns[].field 单元格内容:占位符模板、HTML 模板或 JS 片段(见下)
是否 JS columns[].fieldIsJs boolean,可选,默认 false;为 truefield 按 JS 片段执行;与 fieldIsHtml 互斥
是否 HTML columns[].fieldIsHtml boolean,可选,默认 false;为 truefield 为 HTML 模板 + 占位符;与 fieldIsJs 互斥
宽度 columns[].width 列宽(mm)
对齐 columns[].align left / center / right

列内容三种模式(由 fieldIsJs / fieldIsHtml 决定,二者互斥):

模式 条件 运行时解析 单元格渲染
占位符(默认) 二者均未勾选 resolveTableColumnContentnormalizeTableColumnTemplate + resolvePlaceholder textContent
HTML 模板 fieldIsHtml: true field{{var}} 占位符替换(**不**自动把纯字段名包成 {{}} innerHTML
JS 片段 fieldIsJs: true executeTableColumnScript(field, row, context) innerHTML(返回值按 HTML 字符串写入)

列内容区域 UI:「列内容」 为多行 textarea「是否 JS」「是否 HTML」 同一行两端分布,勾选其一会自动取消另一项。勾选 JS 时下方显示 JS 说明(含 HTML 输出示例);勾选 HTML 时显示 HTML 占位符说明;均未勾选时显示「变量」按钮与占位符说明。

列内容占位符(默认模式,fieldIsJsfieldIsHtml 均未勾选;运行时由 resolveTableColumnContent 解析,合并**主数据源标量**与**当前行**字段):

写法 示例 数据来源
子数据源行字段 {{sku}}{{name}} 当前行对象(printData[n].items[]
主数据源标量 {{orderNo}} printData[n] 顶层标量(n 为当前渲染的记录下标)
组合模板 {{qty}}{{unit}} 同行 + 主数据源变量混合替换
纯字段名(兼容) sku 等价于取当前行 row.sku

设计器列内容输入框下方(未勾选 JS / HTML 时):「变量」按钮**与说明「支持占位符,如 {{sku}}、{{orderNo}}」同一行展示;点击按钮弹出「选择变量」对话框,变量按**主数据源 / **子数据源**分组,选中后在列内容末尾追加 {{变量名}}(组件 PrintVariableSelect)。

列内容 HTML 模板fieldIsHtml === true 时,运行时由 resolveTableColumnContent 做占位符替换后 innerHTML 写入):

写法 示例 说明
标签 + 占位符 <a href="#">{{sku}}</a> 运行时替换 {{sku}} 后渲染链接
内联样式 <span style="color:red">{{qty}}</span> 片段内 CSS 控制样式
多占位符 <span>{{orderNo}}</span>-<span>{{sku}}</span> 主数据源 + 当前行混合

注意:HTML 模式**不做** normalizeTableColumnTemplate,纯字段名 sku 不会自动变成 {{sku}},须在模板中手写 {{}}

列内容 JS 片段fieldIsJs === true 时,运行时由 resolveTableColumnContentexecuteTableColumnScript 执行):

上下文变量 说明
data 主数据源标量 + 当前行字段合并对象(另含 rowmain 属性)
row 当前表格行对象(子数据源一行,如 { sku, name, qty, unit }
main 主数据源标量(等同 printData[n] 顶层,不含子数组)

写法与文本组件一致。表达式**无需 return;**闭包 / IIFE 内须 return 结果值。返回值按 HTML 写入单元格innerHTML),可返回标签字符串。

示例 说明
data.sku / row.sku / sku 当前行 SKU(with(data) 下可直接写字段名)
data.sku + 999 字符串拼接或数值运算
data.qty + ' ' + data.unit 数量与单位组合
'<a>' + data.sku + '</a>' 返回 HTML 链接字符串
'<span>' + row.price * row.qty + '</span>' 返回带计算的 HTML

闭包 / IIFE 示例(表格列,可用 row / data / main):

(function(){
  var q = row.qty;
  return q > 1 ? row.sku + ' ×' + q : row.sku;
})()
(function(){
  return data.orderNo + ' / ' + row.sku;
})()
(function(){
  if (!row.qty) return '';
  return String(row.qty) + (row.unit || '');
})()

设计态画布(列内容,三种模式统一):列内容均**不**替换占位符、不**执行 JS、**不**渲染 HTML DOM;原样显示 field 字符串。fieldIsJs: true 时单元格前显示橙色 **js 角标;fieldIsHtml: true 时显示蓝色 html 角标;超出列宽以 省略。

设计态示例数据:画布表格**不再**用 mock 数据替换列/表脚占位符(与文本 JS 一致,设计态只看模板原文);demo **预览 / 打印**仍使用 FakePrintDataService 提供的 mock printData 执行占位符、HTML 与 JS。

列定义 JSON 示例

"columns": [
  { "field": "{{sku}}", "title": "SKU", "width": 40 },
  { "field": "{{name}}", "title": "品名", "width": 80 },
  { "field": "{{qty}}", "title": "数量", "width": 30, "align": "right" },
  { "field": "<a href=\"#\">{{sku}}</a>", "title": "链接 SKU", "width": 50, "fieldIsHtml": true },
  { "field": "'<a>' + data.sku + '</a>'", "title": "JS 链接", "width": 50, "fieldIsJs": true }
]

新增列默认列内容为 {{colN}}(N 为序号)。

样式

表头样式 - 背景 - 文本颜色 - 字号 - 对齐方式 表脚样式 - 背景 - 文本颜色 - 字号 - 对齐方式

条形码组件

属性

内容 - 文本内容(支持 {{变量名}} 占位符,或 props.contentIsJs: true 时使用 JS 片段,规则同文本组件)

样式

条形码设置 - 格式(CODE128/EAN-3/UPC/CODE39/ITF-14/MSI/Pharmacode)

显示文本 - 显示文本(是/否)

边距 - 边距mm

二维码组件

属性

内容 - 文本内容(支持占位符或 props.contentIsJs JS 片段,规则同文本组件) 二维码设置 - 纠错等级(低7%/中15%/中高25%/高30%)

页码组件

属性

标签内容 - 标签文本(例如:第) - 页码格式(数字1|第1页|1/X)

直线组件

样式

线条样式 - 颜色 - 粗细mm - 边框样式(实线/虚线/点线)

矩形组件

样式

圆角半径 - 圆角半径mm

圆形组件

数据源

数据源定义

  • 数据源是获取Print所需的数据,分为:主数据源子数据源
  • 数据源**可选**;需要动态填充占位符时再配置**主数据源**,可选**子数据源**。
  • 存储在 JSON 模板 dataSource 段。
  • SQL 命名参数(如 :orderNo)须在模板 params 段声明参数名;运行时 HTTP POST JSON body 传入实际值(printParams),执行 SQL 时与主数据源标量一并作为入参。

在设计器中显示

  • 显示在组件面板下方,**数据源**段
  • 每个数据源一个分组,显示为:数据源名称(分组名)、类型(SQL/视图/表单)、操作按钮(编辑/删除)
  • 分组内展示**变量**标题(位于变量列表上方)及变量名列表
  • **视图**类型数据源另展示**查询条件**列表(格式:条件 — 变量;无配置时不显示该块)
  • 主数据源显示在最上方;子数据源列表下方提供「添加子数据源」按钮
  • 提供添加/编辑/删除数据源(弹窗配置 SQL、视图或表单,解析变量)
  • 数据源段下方为**参数**段(见「参数」);提供「添加参数」按钮

SQL数据源

  • 通过 执行 在JSON模板中存储的SQL 获取数据。
  • 在设计阶段,通过 接口方法:hostRequestBridge.getSqlSchema(sql) 获取 sql 对应的数据列变量名。
  • 在设计阶段,通过 接口方法:hostRequestBridge.getAppDataSources(appId) 获取 应用对应的数据源列表(SQL 编辑器下拉选用)。

视图数据源

  • 此处视图非数据库 sql 视图。
  • 在设计阶段,通过接口方法:hostRequestBridge.getViewSchema(modualId, viewId) 获取对应的数据列变量名。
  • modualId 通过下拉框选择,数据来源:hostRequestBridge.getModules()
  • viewId 通过下拉框选择,数据来源:hostRequestBridge.getModuleViews(modualId)
  • hostRequestBridge 由宿主注入;测试环境使用 FakeHostApi(见下文样例)。

查询条件(queryConditions

视图数据源(主/子均可,type: "view")支持配置**查询条件**,用于运行时按条件过滤视图文档行。

说明
存储位置 dataSource.main.queryConditionsdataSource.subs[].queryConditions
设计器 UI PrintDataSourceDialog 中,选定模块/视图后展示「查询条件」分组;每行格式为 条件 — 变量,支持多条,可添加/删除
condition 视图查询条件字段名(如 parentId
conditionLabel 可选显示名(如 父文档ID);面板展示为 parentId (父文档ID)
variable 绑定变量名:取自**主数据源** variables[].name 或模板 params[].name(下拉选择)
持久化规则 保存时过滤 condition 为空的行;无有效条件时不写入 queryConditions 字段
默认值 **不**自动预填任何条件;子视图(明细)关联父文档时,设计器提示可手动添加 parentId 并绑定主数据源字段或参数

典型场景:子数据源为视图明细表时,添加条件 parentId 绑定主数据源文档 ID 字段(或 $docId / 参数),运行时按当前主记录过滤子视图行。

JSON 片段示例(子数据源为视图 + 查询条件):

{
  "id": "ds-items",
  "name": "明细数据源",
  "type": "view",
  "moduleId": "mod-shipping",
  "viewId": "view-order-list",
  "queryConditions": [
    {
      "condition": "parentId",
      "conditionLabel": "父文档ID",
      "variable": "$docId"
    }
  ],
  "variables": [
    { "name": "sku", "label": "SKU" },
    { "name": "name", "label": "品名" },
    { "name": "qty", "label": "数量" }
  ]
}

表单数据源

  • 此处表单指 myApps 模块下的**普通表单**(FORM_TYPE_NORMAL),非 SQL 视图,亦非数据库表。
  • 适用于从**表单文档**取主数据:运行时由调用方传入文档 id,按 id 加载表单字段值并填充占位符。
  • 主数据源**常用类型;**子数据源**可为 SQL(如明细子表、关联查询)或**视图(通过 queryConditions 与主记录关联,见「视图数据源 · 查询条件」)。
  • 存储在 JSON 模板 dataSource.main(或 dataSource.subs[])中,type: "form",并持久化 moduleIdformId
  • 在设计阶段:
  • 通过接口方法 hostRequestBridge.getFormSchema(modualId, formId) 获取表单可绑定字段,写入 dataSource.*.variables
  • modualId 通过下拉框选择,数据来源:hostRequestBridge.getModules()(与视图数据源共用模块列表)。
  • formId 通过下拉框选择,数据来源:hostRequestBridge.getModuleForms(modualId)
  • 变量 name 与表单字段名一致(如 ITEM_单行文本框1),用于 {{占位符}} 绑定;label 为字段显示名;另含系统字段 $docId(文档 ID,占位符 {{$docId}})。
  • 解析后可在变量选择器中勾选需要的字段(与 SQL/视图解析流程相同)。
  • 在运行时阶段:
  • printParams 通过 HTTP POST 请求体(JSON) 传入,须包含 docId(单条,字符串)或 docIds(批量,字符串数组);二者至少提供其一(推荐在模板 params 中声明 docIds,主数据源为表单时设计器自动添加)。
  • 调用 hostRequestBridge.getPrintDatas(printTemplateId, printParams) 时,宿主按模板 dataSource 定义,对每个 docId 加载对应表单文档。
  • 每个文档映射为 data 数组的一项(标量字段来自主数据源 variables[].name);批量传入 docIds 时返回多项,供宿主逐条渲染或合并 PDF。
  • 若同时配置了 SQL 子数据源,仍以当前主记录字段 + printParams 作为 SQL 命名参数入参(与视图主数据源行为一致)。
  • hostRequestBridge 由宿主注入;测试环境使用 FakeHostApi(见下文样例)。

JSON 片段示例(主数据源为表单):

{
  "main": {
    "id": "ds-main",
    "name": "主数据源",
    "type": "form",
    "moduleId": "mod-shipping",
    "formId": "form-shipping-order",
    "variables": [
      { "name": "$docId", "label": "文档ID", "type": "string" },
      { "name": "ITEM_单行文本框1", "label": "单行文本框1", "type": "string" },
      { "name": "ITEM_订单号", "label": "订单号", "type": "string" }
    ]
  }
}

运行时 printParams 示例(前端对象;HTTP 调用时作为 POST JSON body 提交):

// 单条打印
const printParams = { printTemplateId: 'tpl-form-001', docId: 'DOC-20260404-001' };

// 批量打印(docIds 优先;若同时传入 docId 与 docIds,以 docIds 为准)
const printParamsBatch = {
  printTemplateId: 'tpl-form-001',
  docIds: ['DOC-20260404-001', 'DOC-20260404-002'],
};

HTTP 请求示例(runtime,contextPath/obpm):

POST /obpm/api/runtime/{applicationId}/print/{templateId}/data
Content-Type: application/json

{
  "printTemplateId": "__yXlMfQhWiHwn2pOekhE",
  "docIds": ["DOC-20260404-001"]
}

设计器预览(contextPath/designer)路径相同:POST /designer/api/runtime/{applicationId}/print/{templateId}/data,请求体格式一致。

参数

参数定义

  • 模板级**运行时入参声明**,持久化在 JSON 模板 params[] 段(与已废弃的 dataSource.*.params 不同)。
  • 用途:
  • 声明 SQL 命名参数名(如 orderNo,对应 SQL 中 :orderNo);
  • 声明表单打印入参(如 docIds);
  • 供组件 占位符{{paramName}})与 JS 片段data.paramName)绑定。
  • 可选;无参数时可省略 params 段。

在设计器中显示

  • 显示在左侧组件面板,**数据源分组下方**的「参数」分组。
  • 每条参数展示:参数名、显示名、占位符形式(如 {{docIds}})、JS 引用(如 data.docIds)、默认值。
  • 提供「添加参数」按钮;每条支持编辑/删除(弹窗:参数名、显示名、默认值)。

与运行时 printParams 的关系

概念 说明
params[].name 对应 HTTP POST JSON body 键名(如 docIdsorderNo
params[].defaultValue **设计器预览/调试**用默认值;正式运行时由宿主 HTTP 传入实际值覆盖
渲染上下文 渲染前通过 buildTemplateParamValues 将参数值合并进每项 printData,供 resolvePropsDeep / executeContentScriptdata 访问
同名冲突 参数名与 dataSource 查询结果字段同名时,查询结果优先

表单主数据源默认参数

  • 主数据源 type=form 时,保存或加载模板若缺少 docIds 参数,设计器**自动添加**:
{
  "id": "param-xxx",
  "name": "docIds",
  "label": "文档ID列表",
  "defaultValue": "DOC-20260404-001"
}
  • docIdsdefaultValue 支持**逗号分隔**(DOC-001,DOC-002)或 JSON 数组字符串["DOC-001","DOC-002"]);预览时解析为 string[]

占位符与 JS 片段

  • 占位符模式(默认 / HTML):{{docIds}}{{orderNo}};属性面板「变量」选择器含 参数 分组。
  • JS 模式contentIsJs / fieldIsJs / footerIsJs):通过 data.paramName 访问;属性面板提供「插入 data」按钮。
  • 非法 JS 标识符(如 ITEM_单行文本框1)自动使用 bracket 形式:data["ITEM_单行文本框1"]formatJsDataRef)。
  • 适用组件:文本 / HTML / 条形码 / 二维码 content,表格列 field,表格表脚 footerContent

params 段结构

interface PrintTemplateParam {
  id: string;
  name: string;
  label?: string;
  defaultValue?: string;
}

JSON 示例

"params": [
  {
    "id": "param-docids",
    "name": "docIds",
    "label": "文档ID列表",
    "defaultValue": "DOC-20260404-001"
  },
  {
    "id": "param-order",
    "name": "orderNo",
    "label": "订单号",
    "defaultValue": "SO-20260404-001"
  }
]

设计器预览流程

  1. buildPreviewPrintParams(template):合并 params 默认值与 printTemplateId;表单主数据源且无 docIds/docId 时补默认 docIds
  2. hostRequestBridge.getPrintDatas(templateId, printParams) 获取 mock / 真实数据。
  3. applyTemplateParamsToPrintDataList(list, template, printParams) 将参数并入每项 printData
  4. PrintRenderService.renderHtmlList(template, printDataList) 渲染。

hostRequestBridge 接口

设计器 不直接访问后端,所有模块/视图/表单/SQL 元数据与运行时打印数据均通过宿主桥接。组件库 Props 或 provide/inject 注入 hostRequestBridge,类型如下:

interface HostRequestBridge {
  /** 应用下可选 JDBC/平台数据源列表(SQL 数据源配置用) */
  getAppDataSources(appId: string): Promise<AppDataSource[]>;
  /** 解析 SQL,返回可绑定列/变量(写入 dataSource.variables) */
  getSqlSchema(sql: string): Promise<SchemaResult>;
  /** 当前应用模块列表(视图/表单数据源 module 下拉) */
  getModules(): Promise<ModuleOption[]>;
  /** 指定模块下的视图列表(视图数据源 view 下拉) */
  getModuleViews(modualId: string): Promise<ViewOption[]>;
  /** 解析模块视图列,返回可绑定变量(写入 dataSource.main.variables) */
  getViewSchema(modualId: string, viewId: string): Promise<SchemaResult>;
  /** 指定模块下的表单列表(表单数据源 form 下拉) */
  getModuleForms(modualId: string): Promise<FormOption[]>;
  /** 解析模块表单字段,返回可绑定变量(写入 dataSource.*.variables) */
  getFormSchema(modualId: string, formId: string): Promise<SchemaResult>;
  /** 运行时:按模板 dataSource 执行查询,返回 printData 列表(见「打印数据获取」) */
  getPrintDatas(printTemplateId: string, printParams: Record<string, unknown>): Promise<Record<string, unknown>[]>;
}

interface AppDataSource {
  id: string;
  name: string;
  type: 'jdbc' | 'platform';
}

interface ModuleOption {
  id: string;
  name: string;
}

interface ViewOption {
  id: string;
  name: string;
}

interface FormOption {
  id: string;
  name: string;
}

interface SchemaVariable {
  name: string;
  label: string;
  type: 'string' | 'number' | 'boolean' | 'date';
}

interface SchemaResult {
  variables: SchemaVariable[];
}

getAppDataSources(appId)

用途:添加 SQL 子数据源时,选择应用已配置的数据库连接。

请求appId = "app-shipping"

响应样例

[
  { "id": "ds-platform-order", "name": "平台订单库", "type": "platform" },
  { "id": "ds-warehouse", "name": "仓储数据库", "type": "jdbc" }
]

getSqlSchema(sql)

用途:设计器编辑 SQL 后解析列名,写入 dataSource.subs[].variables

请求

SELECT sku, name, qty, unit FROM t_order_item WHERE order_no = :orderNo

响应样例

{
  "variables": [
    { "name": "sku", "label": "SKU", "type": "string" },
    { "name": "name", "label": "品名", "type": "string" },
    { "name": "qty", "label": "数量", "type": "number" },
    { "name": "unit", "label": "单位", "type": "string" }
  ]
}

getModules()

用途:配置视图/表单主数据源时,模块下拉选项。

响应样例

[
  { "id": "mod-shipping", "name": "发货管理" },
  { "id": "mod-order", "name": "订单管理" }
]

getModuleViews(modualId)

用途:选定模块后,视图下拉选项。

请求modualId = "mod-shipping"

响应样例

[
  { "id": "view-order-header", "name": "订单头信息" },
  { "id": "view-order-list", "name": "订单列表" }
]

getViewSchema(modualId, viewId)

用途:选定视图后解析列/字段,写入 dataSource.main.variables

请求modualId = "mod-shipping"viewId = "view-order-header"

响应样例

{
  "variables": [
    { "name": "orderNo", "label": "订单号", "type": "string" },
    { "name": "trackingNo", "label": "运单号", "type": "string" },
    { "name": "orderUrl", "label": "订单链接", "type": "string" }
  ]
}

getModuleForms(modualId)

用途:选定模块后,表单下拉选项(表单主/子数据源配置用)。

请求modualId = "mod-shipping"

响应样例

[
  { "id": "form-shipping-order", "name": "发货单表单" },
  { "id": "form-shipping-return", "name": "退货申请表" }
]

getFormSchema(modualId, formId)

用途:选定表单后解析可绑定字段,写入 dataSource.*.variables。变量 name 与表单字段名一致,供 {{占位符}} 绑定。

请求modualId = "mod-shipping"formId = "form-shipping-order"

响应样例

{
  "variables": [
    { "name": "$docId", "label": "文档ID", "type": "string" },
    { "name": "ITEM_单行文本框1", "label": "单行文本框1", "type": "string" },
    { "name": "ITEM_订单号", "label": "订单号", "type": "string" },
    { "name": "ITEM_申请日期", "label": "申请日期", "type": "date" }
  ]
}

FakeHostApi(测试 / demo)

测试页与单元测试注入 FakeHostApi,返回与上文一致的固定样例,不发起真实 HTTP:

export const FakeHostApi: HostRequestBridge = {
  getAppDataSources: async () => [
    { id: 'ds-platform-order', name: '平台订单库', type: 'platform' },
    { id: 'ds-warehouse', name: '仓储数据库', type: 'jdbc' },
  ],
  getSqlSchema: async () => ({
    variables: [
      { name: 'sku', label: 'SKU', type: 'string' },
      { name: 'name', label: '品名', type: 'string' },
      { name: 'qty', label: '数量', type: 'number' },
      { name: 'unit', label: '单位', type: 'string' },
    ],
  }),
  getModules: async () => [
    { id: 'mod-shipping', name: '发货管理' },
    { id: 'mod-order', name: '订单管理' },
  ],
  getModuleViews: async (modualId) =>
    modualId === 'mod-shipping'
      ? [
          { id: 'view-order-header', name: '订单头信息' },
          { id: 'view-order-list', name: '订单列表' },
        ]
      : [],
  getViewSchema: async () => ({
    variables: [
      { name: 'orderNo', label: '订单号', type: 'string' },
      { name: 'trackingNo', label: '运单号', type: 'string' },
      { name: 'orderUrl', label: '订单链接', type: 'string' },
    ],
  }),
  getModuleForms: async (modualId) =>
    modualId === 'mod-shipping'
      ? [
          { id: 'form-shipping-order', name: '发货单表单' },
          { id: 'form-shipping-return', name: '退货申请表' },
        ]
      : [],
  getFormSchema: async () => ({
    variables: [
      { name: '$docId', label: '文档ID', type: 'string' },
      { name: 'ITEM_单行文本框1', label: '单行文本框1', type: 'string' },
      { name: 'ITEM_订单号', label: '订单号', type: 'string' },
    ],
  }),
  getPrintDatas: async () => [{
    ITEM_单行文本框1: '22',
    ITEM_订单号: 'SO-20260404-001',
    子数据源: [
      { item_三: '12', item_四: '12' },
      { item_三: 'B', item_四: 'B' },
    ],
  }],
};

宿主集成示例:

app.provide('hostRequestBridge', myAppsHostBridge);
// 或
<PrintDesigner :host-request-bridge="myAppsHostBridge" />
方法 设计态 / 运行态 样例与模板对应关系
getAppDataSources 设计态 SQL 数据源连接下拉
getSqlSchema 设计态 dataSource.subs[].variables(明细表列)
getModules / getModuleViews 设计态 → 视图主数据源 moduleId / viewId 下拉
getViewSchema 设计态 dataSource.*.variables(视图列);queryConditions 由设计器本地配置,无单独 Schema API
getModules / getModuleForms 设计态 → 表单主/子数据源 moduleId / formId 下拉
getFormSchema 设计态 dataSource.*.variables(表单字段,如 ITEM_单行文本框1
getPrintDatas 运行态 POST /api/runtime/{appId}/print/{templateId}/data(JSON body 传 printParams);表单主数据源时 docId / docIds 驱动查询

数据存储结构

模板 json 样例

以下为设计器持久化的 printTemplate JSON 样例(含 pagedataSourceparams(可选)、elements)。运行时由宿主通过 POST JSON body 传入 printParams(键名与 params[].name 对应),按 dataSource 执行查询后得到渲染数据;查询结果与参数值合并为 printData 供占位符/JS 解析。坐标与尺寸单位均为 mm;颜色为 #RRGGBB 十六进制字符串。

整体结构

字段 类型 说明
id string 模板唯一标识
name string 模板名称
version number 模板版本号
page object 页面设置
dataSource object 数据源定义(主 + 子);可选,无数据源时运行时返回空数组 []
params array 运行时入参声明(参数名 + 预览默认值);可选
elements array 打印组件列表

模板字段变更(向后兼容)

变更 说明
移除 dataSource.*.params 旧版数据源级参数映射已废弃;改由模板根级 params[] 声明参数名,SQL 入参由运行时 printParams 提供,加载旧 JSON 时忽略 dataSource.*.params,保存后去除
新增 params[] 模板级运行时入参(idnamelabeldefaultValue);供占位符/JS 绑定及预览默认值
移除 page.headerLine / page.footerLine 加载忽略,保存后去除
移除 elements[].props.repeatFooter 加载忽略;showFooter 控制分页每页表脚
defaultRowCountfixedRowCount 加载时自动迁移为固定行数

占位符替换规则:

  • elements[].props{{xxx}} 可绑定 数据源变量dataSource.*.variables[].name)或 模板参数params[].name);运行时合并进 printData 后由 resolvePropsDeep 替换。
  • props.contentIsJs === true 时,props.content 不再按占位符解析,改为 JS 片段,由 executeContentScript(content, printData) 求值;data 含主数据源标量、子数据源及 模板参数(见「文本组件 · JS 片段」;HTML 组件结果写入 innerHTML)。
  • 表格 props.dataSource: "{{明细数据源}}" 绑定子数据源 name(见 dataSource.subs[].name);设计器中为**子数据源**下拉选取。
  • 表格 columns[].field 为**列内容**,由 fieldIsJs / fieldIsHtml 决定解析方式(二者互斥):
  • 默认(均未勾选):{{变量}} 占位符;纯字段名自动补全为 {{field}}normalizeTableColumnTemplate);单元格 textContent
  • fieldIsHtml: true:HTML 模板 + {{变量}} 占位符替换;不自动补全纯字段名;单元格 innerHTML
  • fieldIsJs: true:JS 片段,由 executeTableColumnScript(field, row, printData) 按行求值(见「表格组件 · 列内容 JS 片段」);返回值 innerHTML 渲染。
  • 表格 footerContent 为**表脚内容**,由 footerIsJs / footerIsHtml 决定解析方式(二者互斥):
  • 默认(均未勾选):{{变量}} 占位符 + 内置 {{$count}}(总行数);表脚 textContent
  • footerIsHtml: true:HTML 模板 + {{变量}} 占位符替换;表脚 innerHTML
  • footerIsJs: true:JS 片段,由 executeContentScript(footerContent, buildTableFooterContext(printData[n], rowCount)) 求值;返回值 innerHTML 渲染。
  • 设计态变量列表来自各数据源的 variables 与模板 params;SQL/视图/表单类型在设计阶段分别通过 hostRequestBridge.getSqlSchema / getViewSchema / getFormSchema 解析列名或字段名后写入。属性面板「变量」选择器分组:主数据源 / 子数据源 / 参数

JS 片段执行(运行时)

实现位于 src/models/types.ts

函数 用途
buildTemplateParamValues(template, printParams?) params[] 转为键值;printParams 优先于 defaultValuedocIds 解析为数组
formatJsDataRef(name) 生成 JS 中 data.xxxdata["xxx"]
applyTemplateParamsToPrintDataList(list, template, printParams?) 预览/渲染前将参数并入每项 printData
executeContentScript(script, data) 文本/条码/二维码/HTML contentdata 为当前 printData 单项(含参数)
executeTableColumnScript(script, row, context) 表格列 fieldcontext 为主数据源标量 + 参数,row 为当前行
resolveContentValue(content, contentIsJs, data) 统一入口:JS 或占位符
resolveTableColumnContent(field, row, context, fieldIsJs?, fieldIsHtml?) 表格列统一入口:JS / HTML 模板 / 占位符
resolveTableFooterContent(content, context, footerIsJs?, footerIsHtml?) 表格表脚统一入口:JS / HTML 模板 / 占位符

执行方式:通过 new Function + eval / 表达式求值。**单行表达式**无需 return;**IIFE 闭包**须在函数体内 return 最终字符串(或可被 String() 转换的值)。失败或结果为 null/undefined 时渲染为空字符串。

写法对照

写法 何时使用 return 示例
表达式 单行取值、拼接、三元运算 不需要 data.orderNo + '-' + data.sku
IIFE 闭包 多行、分支、临时变量 IIFE 内需要 (function(){ ... return x; })()

闭包示例(文本 / 条码 / 二维码 / HTML)

入参仅为 data(当前 printData 单项,含模板参数与数据源标量):

(function(){
  var ids = Array.isArray(data.docIds) ? data.docIds.join(', ') : data.docIds;
  return ids || data.orderNo;
})()

闭包示例(表格列)

入参为 data(合并对象)、row(当前行)、main(主数据源标量 + 参数);下列示例在 executeTableColumnScript 中均可执行:

(function(){
  var prefix = main.orderNo || data.orderNo;
  return prefix + ' · ' + row.name;
})()
(function(){
  var items = data['明细数据源'];
  if (!Array.isArray(items)) return row.sku;
  return row.sku + ' (' + items.length + '行)';
})()
(function(){
  return '<a href="#">' + row.sku + '</a>';
})()

闭包示例(表格表脚)

入参为 data(主数据源标量 + 系统变量 $count);下列示例在 executeContentScript(经 resolveTableFooterContent)中均可执行:

(function(){
  return '共 <b>' + data.$count + '</b> 条';
})()
(function(){
  var label = data.orderNo ? ('单号 ' + data.orderNo + ' · ') : '';
  return label + '共 ' + data.$count + ' 条';
})()

注意

  • 未勾选「是否 JS」时,data.sku+999 会被当作占位符名 {{data.sku+999}} 解析,变量不存在则显示空白;须勾选 JS 方可按表达式求值。
  • 表格列 fieldIsJs: true 时,脚本返回值写入 innerHTML(与 fieldIsHtml 相同),应返回 HTML 字符串而非依赖 textContent
  • 表格列 fieldIsHtml: true 时,field 为静态 HTML + {{占位符}},不做 JS 执行。
  • 表格表脚 footerIsJs: true / footerIsHtml: true 规则与列内容相同;footerIsJsdata.$count 为表格总行数。

dataSource 段结构

字段 类型 说明
dataSource.main object 主数据源(有 dataSource 时必填)
dataSource.subs array 子数据源(可选),每条对应一个数组变量(如明细表)
*.type string 数据源类型:sql / view / form
*.name string 数据源名称,设计器分组标题
*.variables array 可用变量列表(设计器展示 + 占位符绑定)
subs[].name string 子数据源名称,同时作为 printData 键名与占位符 {{name}}

**SQL 数据源**额外字段:sql(存储 SQL 语句)。设计阶段调用 hostRequestBridge.getSqlSchema(sql) 解析列名。

**视图数据源**额外字段:moduleIdviewId(myApps 模块视图,非数据库视图);可选 queryConditions[](查询条件,见上文「视图数据源 · 查询条件」)。设计阶段通过 getModules() / getModuleViews() 下拉选择,调用 hostRequestBridge.getViewSchema(moduleId, viewId) 解析列名。

queryConditions[] 每项结构:

字段 类型 说明
condition string 视图查询条件字段名
variable string 绑定变量名(主数据源变量或 params[].name
conditionLabel string 可选,条件显示名

**表单数据源**额外字段:moduleIdformId(myApps 模块普通表单)。设计阶段通过 getModules() / getModuleForms() 下拉选择,调用 hostRequestBridge.getFormSchema(moduleId, formId) 解析字段名。运行时由 printParams.docIdprintParams.docIds 指定待打印文档(推荐在 params[] 声明 docIds)。

参数(params[]:与 dataSource 并列于模板根级;详见上文「参数」章节。

完整样例

{
  "id": "tpl-shipping-001",
  "name": "发货单",
  "version": 1,
  "page": {
    "paperType": "A4",
    "width": 210,
    "height": 297
  },
  "dataSource": {
      "main": {
        "id": "ds-main",
        "name": "主数据源",
        "type": "view",
        "moduleId": "mod-shipping",
        "viewId": "view-order-header",
        "variables": [
          { "name": "orderNo", "label": "订单号" },
          { "name": "trackingNo", "label": "运单号" },
          { "name": "orderUrl", "label": "订单链接" }
        ]
      },
      "subs": [
        {
          "id": "ds-items",
          "name": "明细数据源",
          "type": "sql",
          "sql": "SELECT sku, name, qty, unit FROM t_order_item WHERE order_no = :orderNo",
          "variables": [
            { "name": "sku", "label": "SKU" },
            { "name": "name", "label": "品名" },
            { "name": "qty", "label": "数量" },
            { "name": "unit", "label": "单位" }
          ]
        }
      ]
    },
  "params": [
    {
      "id": "param-order",
      "name": "orderNo",
      "label": "订单号",
      "defaultValue": "SO-20260404-001"
    }
  ],
    "elements": [
    {
      "id": "title",
      "type": "text",
      "x": 10,
      "y": 12,
      "width": 190,
      "height": 12,
      "zIndex": 1,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "props": {
        "content": "发货单"
      },
      "style": {
        "backgroundColor": "transparent",
        "textColor": "#000000",
        "fontFamily": "SimHei",
        "fontSize": 8,
        "fontWeight": "bold",
        "textAlign": "center",
        "verticalAlign": "middle",
        "borderStyle": "none",
        "borderWidth": 0,
        "borderColor": "#000000"
      }
    },
    {
      "id": "logo",
      "type": "image",
      "x": 10,
      "y": 28,
      "width": 30,
      "height": 15,
      "zIndex": 2,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "props": {
        "src": "data:image/png;base64,iVBORw0KGgo..."
      },
      "style": {
        "fillMode": "contain",
        "opacity": 1
      }
    },
    {
      "id": "orderLink",
      "type": "html",
      "x": 130,
      "y": 28,
      "width": 60,
      "height": 8,
      "zIndex": 3,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "props": {
        "content": "<a href=\"{{orderUrl}}\">{{orderNo}}</a>"
      },
      "style": {
        "backgroundColor": "transparent",
        "borderStyle": "none",
        "borderWidth": 0,
        "borderColor": "#000000"
      }
    },
    {
      "id": "orderNo",
      "type": "text",
      "x": 45,
      "y": 28,
      "width": 80,
      "height": 8,
      "zIndex": 3,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "props": {
        "content": "订单号:{{orderNo}}"
      },
      "style": {
        "textColor": "#333333",
        "fontFamily": "SimSun",
        "fontSize": 3.5,
        "fontWeight": "normal",
        "textAlign": "left",
        "verticalAlign": "middle"
      }
    },
    {
      "id": "divider",
      "type": "line",
      "x": 10,
      "y": 48,
      "width": 190,
      "height": 0,
      "zIndex": 4,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "style": {
        "color": "#999999",
        "lineWidth": 0.3,
        "lineStyle": "solid"
      }
    },
    {
      "id": "items",
      "type": "table",
      "x": 10,
      "y": 52,
      "width": 190,
      "height": 120,
      "zIndex": 5,
      "repeatOnEachPage": false,
      "autoHeight": true,
      "printable": true,
      "props": {
        "autoPaging": true,
        "showHeader": true,
        "showFooter": true,
        "fixedRowCount": 5,
        "footerContent": "共 {{$count}} 条",
        "dataSource": "{{明细数据源}}",
        "columns": [
          { "field": "{{sku}}", "title": "SKU", "width": 40 },
          { "field": "{{name}}", "title": "品名", "width": 80 },
          { "field": "{{qty}}", "title": "数量", "width": 30, "align": "right" },
          { "field": "{{unit}}", "title": "单位", "width": 40 }
        ]
      },
      "style": {
        "headerStyle": {
          "backgroundColor": "#F5F5F5",
          "textColor": "#000000",
          "fontSize": 3.5,
          "textAlign": "center"
        },
        "footerStyle": {
          "backgroundColor": "#FFFFFF",
          "textColor": "#666666",
          "fontSize": 3,
          "textAlign": "right"
        },
        "borderStyle": "solid",
        "borderWidth": 0.2,
        "borderColor": "#CCCCCC"
      }
    },
    {
      "id": "barcode",
      "type": "barcode",
      "x": 10,
      "y": 180,
      "width": 60,
      "height": 20,
      "zIndex": 6,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "props": {
        "content": "{{trackingNo}}"
      },
      "style": {
        "format": "CODE128",
        "showText": true,
        "margin": 2
      }
    },
    {
      "id": "qrcode",
      "type": "qrcode",
      "x": 160,
      "y": 175,
      "width": 25,
      "height": 25,
      "zIndex": 7,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "props": {
        "content": "{{orderUrl}}",
        "errorCorrectionLevel": "M"
      }
    },
    {
      "id": "box",
      "type": "rect",
      "x": 155,
      "y": 170,
      "width": 35,
      "height": 35,
      "zIndex": 0,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "style": {
        "borderStyle": "dashed",
        "borderWidth": 0.3,
        "borderColor": "#CCCCCC",
        "borderRadius": 2,
        "backgroundColor": "transparent"
      }
    },
    {
      "id": "stamp",
      "type": "circle",
      "x": 130,
      "y": 210,
      "width": 30,
      "height": 30,
      "zIndex": 8,
      "repeatOnEachPage": false,
      "autoHeight": false,
      "printable": true,
      "style": {
        "borderStyle": "solid",
        "borderWidth": 0.5,
        "borderColor": "#FF0000",
        "backgroundColor": "transparent"
      }
    },
    {
      "id": "pageNum",
      "type": "pageNumber",
      "x": 90,
      "y": 285,
      "width": 30,
      "height": 6,
      "zIndex": 9,
      "repeatOnEachPage": true,
      "autoHeight": false,
      "printable": true,
      "props": {
        "label": "第",
        "format": "pageOfTotal"
      },
      "style": {
        "textColor": "#666666",
        "fontSize": 3,
        "textAlign": "center",
        "verticalAlign": "middle"
      }
    }
    ]
}

占位符与 dataSource / params 对照

占位符 所属 模板字段 绑定组件
{{orderNo}} 主数据源 dataSource.main.variables.orderNo 文本、HTML、表格列等
{{trackingNo}} 主数据源 dataSource.main.variables.trackingNo 条形码
{{orderUrl}} 主数据源 dataSource.main.variables.orderUrl HTML、二维码
{{items}} / {{明细数据源}} 子数据源 dataSource.subs[].name 表格 props.dataSource
{{docIds}} 参数 params[].name(如 docIds 文本、HTML、JS 等
{{orderNo}}(SQL 入参) 参数 params[].name(与 SQL :orderNo 对应) 占位符展示;驱动 SQL 查询

运行时:宿主通过 POST JSON body 传入 printParams(键名与 params[].name 一致),按 dataSource 查询得到 printData;前端将参数值合并进每项 printDataresolveProps 替换占位符并 toHtmlDOM

字段说明

路径 类型 说明
id string 模板唯一标识
name string 模板名称
version number 模板版本号
dataSource.main object 主数据源(有 dataSource 时必填)
dataSource.subs[] array 子数据源列表(可选)
dataSource.main.type / subs[].type string sql / view / form
dataSource.*.variables[] array 变量:namelabel;设计器分组明细
dataSource.main.moduleId / viewId string 视图数据源:模块 id、视图 id
dataSource.*.queryConditions[] array 视图数据源可选;查询条件:conditionvariable、可选 conditionLabel
dataSource.main.moduleId / formId string 表单数据源:模块 id、表单 id
dataSource.subs[].name string 子数据源名称,对应 {{name}}printData 键名
dataSource.subs[].sql string SQL 数据源语句(命名参数如 :orderNo,须在 params[] 声明)
params[] array 运行时入参声明(可选)
params[].id string 参数唯一 id
params[].name string 参数名;对应 POST JSON body 键名、{{name}} 占位符、data.name JS 访问
params[].label string 显示名(设计器展示)
params[].defaultValue string 预览/调试默认值;docIds 支持逗号分隔或 JSON 数组字符串
page.paperType string 纸张类型:A3 / A4 / A5 / LETTER / CUSTOM
page.width / height number 页面宽高(mm)
elements[].props.fixedRowCount number 表格固定行数(设计态行数、分页每页行数、行高计算)
elements[].type string 组件类型:text / html / image / table / barcode / qrcode / pageNumber / line / rect / circle
elements[].repeatOnEachPage boolean 每页重复渲染
elements[].autoHeight boolean 自适应高度(表格常用)
elements[].printable boolean 是否参与打印
elements[].props.content string 文本/条码/二维码/HTML 内容;支持 {{fieldName}} 或 JS 片段(配合 contentIsJs);HTML 组件运行时写入 innerHTML
elements[].props.contentIsJs boolean truecontent 按 JS 表达式执行(executeContentScript
elements[].props.dataSource string 表格**子数据源**占位符,如 {{明细数据源}};键名与 dataSource.subs[].name 一致,设计器下拉选取
elements[].props.footerContent string 表脚文案;占位符 / HTML / JS(配合 footerIsJs / footerIsHtml,互斥)
elements[].props.footerIsJs boolean truefooterContent 按 JS 表达式执行(executeContentScript
elements[].props.footerIsHtml boolean truefooterContent 为 HTML 模板 + 占位符
elements[].props.columns[] array 表格列:field(列内容)、fieldIsJs / fieldIsHtml(互斥,见列内容三种模式)、titlewidthalign
elements[].props.fillMode string 图片填充:fill / contain / stretch / cover
elements[].props.format string 条形码格式:CODE128 / EAN13 / UPC / CODE39 / ITF14 / MSI / Pharmacode
elements[].props.errorCorrectionLevel string 二维码纠错:L(7%) / M(15%) / Q(25%) / H(30%)
elements[].props.label string 页码前缀标签,如「第」
elements[].props.format(页码) string 页码格式:number / labeled / pageOfTotal
elements[].style.lineStyle string 线条/边框样式:solid / dashed / dotted / none

前端架构设计

技术栈:Vue 3 + TypeScript + Pinia。设计态与运行时共用领域模型(PrintTemplate / PrintBaseElement),设计器额外叠加选中、拖拽、撤销重做等交互层。

类设计

继承关系

classDiagram
    class PrintTemplate {
        +string id
        +string name
        +number version
        +PrintPage page
        +PrintBaseElement[] elements
        +toJSON() object
        +fromJSON(json) PrintTemplate
        +addElement(el) void
        +removeElement(id) void
        +getElement(id) PrintBaseElement
    }

    class PrintPage {
        +string paperType
        +number width
        +number height
    }

    class PrintBaseElement {
        <<abstract>>
        +string id
        +PrintElementType type
        +number x
        +number y
        +number width
        +number height
        +number zIndex
        +boolean repeatOnEachPage
        +boolean autoHeight
        +boolean printable
        +clone() PrintBaseElement
        +toJSON() object
        +toHtmlDOM(ctx) HTMLElement
        +getBounds() Rect
        +resolveProps(data) object
        #renderInnerDOM(wrapper, props, ctx) void
    }

    class PrintTextElement
    class PrintHtmlElement
    class PrintImageElement
    class PrintTableElement
    class PrintBarcodeElement
    class PrintQrcodeElement
    class PrintPageNumberElement
    class PrintLineElement
    class PrintRectElement
    class PrintCircleElement

    PrintTemplate --> PrintPage
    PrintTemplate --> PrintBaseElement
    PrintBaseElement <|-- PrintTextElement
    PrintBaseElement <|-- PrintHtmlElement
    PrintBaseElement <|-- PrintImageElement
    PrintBaseElement <|-- PrintTableElement
    PrintBaseElement <|-- PrintBarcodeElement
    PrintBaseElement <|-- PrintQrcodeElement
    PrintBaseElement <|-- PrintPageNumberElement
    PrintBaseElement <|-- PrintLineElement
    PrintBaseElement <|-- PrintRectElement
    PrintBaseElement <|-- PrintCircleElement

PrintBaseElement(抽象基类)

所有打印组件的公共父类,字段与 JSON elements[] 顶层属性一一对应。

成员 类型 说明
id string 组件唯一标识,新建时由 createElementId() 生成
type PrintElementType 组件类型枚举
x / y number 左上角坐标(mm)
width / height number 宽高(mm);直线 height 可为 0
zIndex number 层级,数值越大越靠上
repeatOnEachPage boolean 每页重复
autoHeight boolean 自适应高度
printable boolean 是否参与打印/导出
方法 说明
clone(): PrintBaseElement 深拷贝,生成新 id
toJSON(): PrintElementJSON 序列化为模板 JSON 节点
static fromJSON(json): PrintBaseElement 由 JSON 反序列化(委托 PrintElementFactory
getBounds(): Rect 返回 { x, y, width, height } 包围盒
resolveProps(data: Record<string, unknown>): object props{{field}} 替换为运行时数据
toHtmlDOM(ctx: RenderContext): HTMLElement \| null 渲染为可打印 DOM 节点;printable=false 或分页上下文不匹配时返回 null
renderInnerDOM(wrapper, props, ctx): void protected abstract,子类实现内部 DOM

子类各自持有 props(专有属性)与 style(样式),类型通过泛型或联合类型约束:

type PrintElementType =
  | 'text' | 'html' | 'image' | 'table' | 'barcode' | 'qrcode'
  | 'pageNumber' | 'line' | 'rect' | 'circle';

abstract class PrintBaseElement {
  abstract readonly type: PrintElementType;
  abstract props: unknown;
  abstract style: unknown;

  clone(): PrintBaseElement { /* 深拷贝 + 新 id */ }
  toJSON(): PrintElementJSON { /* 合并通用字段 + props + style */ }
  getBounds(): Rect { return { x: this.x, y: this.y, width: this.width, height: this.height }; }
  resolveProps(data: Record<string, unknown>): object { /* 模板占位符替换 */ }
  toHtmlDOM(ctx: RenderContext): HTMLElement | null { /* 见下文 */ }
  protected abstract renderInnerDOM(wrapper: HTMLElement, props: object, ctx: RenderContext): void;
}

toHtmlDOM 渲染逻辑

toHtmlDOM预览 / 打印 / 导出 的统一 DOM 出口。设计器画布仍走 Vue 组件(PrintElementHost),不走此方法。

RenderContext(渲染上下文)

字段 类型 说明
data Record<string, unknown> 运行时数据,供 resolveProps 替换占位符
pageIndex number 当前页码,从 0 起
pageTotal number 总页数
dpi number? 像素换算 dpi,默认 96;PDF 导出可用 300
mode 'preview' \| 'print' \| 'export' 渲染模式
tableSlice TableSlice? 表格分页切片,仅 PrintTableElement 使用

基类模板方法流程

toHtmlDOM(ctx)
  ├─ 1. printable=false → return null
  ├─ 2. 非 repeatOnEachPage 且不在当前页 → return null
  ├─ 3. resolveProps(ctx.data) 替换 {{field}}
  ├─ 4. createWrapper():绝对定位容器(mm 单位)
  ├─ 5. renderInnerDOM(wrapper, resolved, ctx)  ← 子类实现
  ├─ 6. autoHeight → 测量 scrollHeight 回写 height
  └─ 7. return wrapper
toHtmlDOM(ctx: RenderContext): HTMLElement | null {
  if (!this.printable) return null;
  if (!this.repeatOnEachPage && ctx.pageIndex !== this._assignedPageIndex) {
    return null;
  }

  const resolved = this.resolveProps(ctx.data);
  const el = document.createElement('div');
  el.dataset.printId = this.id;
  el.dataset.printType = this.type;
  Object.assign(el.style, {
    position: 'absolute',
    left: `${this.x}mm`,
    top: `${this.y}mm`,
    width: `${this.width}mm`,
    height: this.autoHeight ? 'auto' : `${this.height}mm`,
    zIndex: String(this.zIndex),
    boxSizing: 'border-box',
    overflow: 'hidden',
  });
  this.applyCommonStyle(el);

  this.renderInnerDOM(el, resolved, ctx);

  if (this.autoHeight) {
    const h = PrintUnitConverter.pxToMm(el.scrollHeight, ctx.dpi);
    el.style.height = `${h}mm`;
    this.height = h;
  }
  return el;
}

各子类 renderInnerDOM 要点

子类 内部 DOM 关键逻辑
PrintTextElement <div> textContent = props.content;写入 fontSize、textAlign、verticalAlign 等
PrintHtmlElement <div> innerHTML = props.content(占位符或 JS 已解析);容器仅应用背景/边框
PrintImageElement <img> src = props.srcobject-fit 映射 fillMode(fill/contain/stretch/cover)
PrintLineElement <div> border-top 画线;height: 0
PrintRectElement <div> border + borderRadius + backgroundColor
PrintCircleElement <div> border-radius: 50%;宽高相等时为正圆
PrintBarcodeElement <svg> / <img> JsBarcode 按 content + format 生成
PrintQrcodeElement <canvas> / <img> QR 库按 content + errorCorrectionLevel 生成
PrintPageNumberElement <div> 按 format 输出:number1labeled第1页pageOfTotal1/3
PrintTableElement <table> tableSlice.rows 逐行渲染;每格 resolveTableColumnContent(...);表脚 resolveTableFooterContent(...)fieldIsJs \|\| fieldIsHtmlfooterIsJs \|\| footerIsHtmlinnerHTML

表格分页autoPaging=true 时,分页由 PrintRenderService.paginate 在模板层完成,toHtmlDOM 只渲染当前页切片:

tableSlice 字段 说明
rows 当前页行数据
showHeader 是否渲染表头(分页时由 PrintRenderService 按页切片传入)
showFooter 是否渲染表脚(showFooter=true 时分页**每页**均显示)

PrintTemplate.toHtmlDOM 协作

PrintTemplate.toHtmlDOM(data: Record<string, unknown>): HTMLElement {
  const pages = PrintRenderService.paginate(this, data);
  const root = document.createElement('div');
  root.className = 'print-root';

  for (const slice of pages) {
    const pageEl = document.createElement('div');
    pageEl.className = 'print-page';
    pageEl.style.cssText = `position:relative;width:${this.page.width}mm;height:${this.page.height}mm;`;

    const ctx: RenderContext = {
      data, pageIndex: slice.index, pageTotal: pages.length, mode: 'print',
    };
    for (const el of slice.elements.sort((a, b) => a.zIndex - b.zIndex)) {
      const dom = el.toHtmlDOM({ ...ctx, tableSlice: slice.tableSlices?.[el.id] });
      if (dom) pageEl.appendChild(dom);
    }
    root.appendChild(pageEl);
  }
  return root;
}

设计态 vs 运行时

场景 渲染方式
设计器画布 Vue(PrintElementHost + 各元素组件),支持选中/拖拽;**不**执行 JS、**不**替换占位符(HTML 组件设计态用 v-html 渲染标签结构,占位符原文保留);文本/表格列/表脚 JS 显示 js 角标、HTML 显示 html 角标 + 原文
预览 / 打印 / 导出 toHtmlDOM → 纯 DOM + mm CSS;占位符、HTML 模板与 JS 片段均按 printData 求值;表格列/表脚 *IsJs \|\| *IsHtmlinnerHTML

两套路径共用 PrintBaseElement 模型。设计态仅展示模板原文;预览 / 打印走 resolvePropsDeepresolveTableColumnContentresolveTableFooterContentexecuteContentScript / executeTableColumnScript

方法职责对照

方法 职责
toJSON() 持久化模板
resolveProps(data) 占位符替换;content + contentIsJs 时走 resolveContentValue
resolveTableColumnContent(...) 表格列占位符 / HTML 模板 / JS(PrintTableElement.renderInnerDOM 按行调用;fieldIsJs \|\| fieldIsHtmlinnerHTML
resolveTableFooterContent(...) 表格表脚占位符 / HTML 模板 / JS(PrintTableElement.renderInnerDOM 渲染 <tfoot>footerIsJs \|\| footerIsHtmlinnerHTML
toHtmlDOM(ctx) 单元素 → DOM 节点
PrintTemplate.toHtmlDOM(data) 整模板分页组装
PrintRenderService.renderHtml() template.toHtmlDOM(data).outerHTML + @page CSS 包裹

组件子类

类名 type props style
PrintTextElement text { content: string; contentIsJs?: boolean } PrintTextStyle(通用文本样式 + 边框)
PrintHtmlElement html { content: string; contentIsJs?: boolean } PrintTextStyle(仅 backgroundColor + 边框;默认 <span>HTML 内容</span>
PrintImageElement image { src: string } { fillMode, opacity }
PrintTableElement table { autoPaging, showHeader, showFooter, dataSource, footerContent, footerIsJs?, footerIsHtml?, fixedRowCount, columns[] }footerIsJs / footerIsHtmlcolumns[].fieldIsJs / fieldIsHtml 互斥规则相同 { headerStyle, footerStyle, borderStyle, borderWidth, borderColor, rowHeight, headerRowHeight, footerRowHeight }
PrintBarcodeElement barcode { content: string; contentIsJs?: boolean } { format, showText, margin }
PrintQrcodeElement qrcode { content, contentIsJs?, errorCorrectionLevel }
PrintPageNumberElement pageNumber { label, format } PrintTextStyle(部分字段)
PrintLineElement line { color, lineWidth, lineStyle }
PrintRectElement rect PrintShapeStyle(边框 + 圆角 + 背景)
PrintCircleElement circle PrintShapeStyle(边框 + 背景,宽高相等时为正圆)

PrintTextStyle / PrintShapeStyle 等样式接口与上文「打印组件说明」及 JSON 样例字段保持一致。

PrintTemplate 与 PrintPage

职责
PrintTemplate 模板根对象;维护 elements 有序列表;提供增删改、按 zIndex 排序、整体 toJSON / fromJSON / toHtmlDOM
PrintPage 页面设置;paperType 变更时联动 width / height(A4 = 210×297 mm)

PrintElementFactory

工厂负责 类型 → 实例JSON → 实例 的双向映射,避免设计器/运行时各处 switch(type)

class PrintElementFactory {
  static create(type: PrintElementType, defaults?: Partial<PrintElementJSON>): PrintBaseElement;
  static fromJSON(json: PrintElementJSON): PrintBaseElement;
  static getDefaultSize(type: PrintElementType): { width: number; height: number };
  static getComponentName(type: PrintElementType): string; // Vue 组件名
}

type 默认尺寸示例:文本 80×8 mm、HTML 80×12 mm、图片 30×15 mm、表格 190×60 mm、条码 60×20 mm、二维码 25×25 mm。


Vue 组件分层

层次 组件 职责
页面 PrintDesigner.vue 三栏布局:组件面板 + 画布 + 属性面板;工具栏(预览/保存/导出)
画布 PrintCanvas.vue 纸张渲染、缩放、标尺、选中虚线框、8 个缩放手柄(四角 + 四边中点)、拖拽/缩放
组件面板 PrintElementPalette.vue 可拖拽的组件列表,拖入画布时 PrintElementFactory.create
属性面板 PrintPropertyPanel.vue 根据选中元素 type 动态加载属性表单项;文本/HTML/表格支持 PrintVariableSelect(主/子数据源 + 参数;JS 模式插入 data.xxx
元素渲染 PrintElementHost.vue 统一包装层:选中态、虚线边框、8 方向缩放手柄、事件代理
通用 UI PrintVariableSelect.vue 「变量」按钮 + 弹窗选择变量,供属性面板插入 {{变量名}}
数据源 PrintDataSourcePanel.vue 主/子数据源列表、变量展示、视图查询条件展示、添加/编辑/删除
数据源弹窗 PrintDataSourceDialog.vue 编辑主/子数据源:类型、SQL/模块/视图/表单、查询条件(视图)、解析变量
参数 PrintParamsPanel.vue 模板参数列表、占位符/JS 引用展示、添加/编辑/删除;表单主数据源自动补 docIds
参数弹窗 PrintParamDialog.vue 编辑参数名、显示名、默认值
元素渲染 PrintText.vuePrintCircle.vue type 对应的展示组件(设计态 + 运行时复用);HTML 用 PrintHtml.vuev-html
预览 PrintPreview.vue 只读画布 + 分页预览
导出 调用 PrintRenderService 生成 HTML / PDF / 图片

元素 Vue 组件与模型类对应关系:

PrintText.vue      ← PrintTextElement
PrintHtml.vue      ← PrintHtmlElement
PrintImage.vue     ← PrintImageElement
PrintTable.vue     ← PrintTableElement

PrintElementHost 通过 component :is="factory.getComponentName(element.type)" 动态挂载子组件,传入 elementmode: 'design' | 'preview' | 'print'


状态与服务

Pinia Store — usePrintDesignerStore

状态 / 动作 说明
template: PrintTemplate 当前编辑中的模板
selectedIds: string[] 选中组件 id(支持多选)
zoom: number 画布缩放比例
history: HistoryStack 撤销/重做栈
addElement / updateElement / removeElement 元素 CRUD,每次变更入栈
select / clearSelection 选中管理
loadTemplate / saveTemplate 调用 API 读写持久化 JSON

PrintRenderService

方法 说明
renderHtml(template, data): string 调用 template.toHtmlDOM(data),输出完整打印 HTML(含 @page、mm 单位 CSS)
renderPdf(template, data): Blob 基于 HTML 或 canvas 导出 PDF
renderImage(template, data): Blob 按页导出 PNG
paginate(template, data): PrintPageSlice[] 表格自动分页、每页重复元素拆分

PrintUnitConverter

方法 说明
mmToPx(mm, dpi?): number 画布显示换算(默认 96 dpi)
pxToMm(px, dpi?): number 拖拽结束后回写 mm
getPaperSize(paperType): { width, height } 标准纸张尺寸表

目录结构(建议)

print-designer/
├── models/
│   ├── PrintBaseElement.ts
│   ├── PrintTextElement.ts
│   ├── PrintHtmlElement.ts
│   ├── …(各子类)
│   ├── PrintTemplate.ts
│   ├── PrintPage.ts
│   └── types.ts
├── factory/
│   └── PrintElementFactory.ts
├── store/
│   └── usePrintDesignerStore.ts
├── services/
│   ├── PrintRenderService.ts
│   └── PrintUnitConverter.ts
├── components/
│   ├── PrintDesigner.vue
│   ├── PrintCanvas.vue
│   ├── PrintElementPalette.vue
│   ├── PrintPropertyPanel.vue
│   ├── PrintDataSourcePanel.vue
│   ├── PrintParamsPanel.vue
│   ├── PrintParamDialog.vue
│   ├── PrintVariableSelect.vue
│   ├── PrintElementHost.vue
│   └── elements/
│       ├── PrintText.vue
│       ├── PrintHtml.vue
│       └── …
└── composables/
    ├── useCanvasDrag.ts
    ├── useCanvasResize.ts
    └── useHistory.ts

打包

打印设计器以 独立 npm 组件库 形式发布,供 myApps 设计器(obpm-designer-vue3)与运行时按需集成。构建产物为 ESM,Pinia 作为 peerDependency 由宿主注入,避免多实例冲突。

包结构

@myapps/print-designer/
├── src/                    # 源码(见「目录结构」)
├── demo/                   # 本地测试页
│   ├── App.vue
│   ├── main.ts
│   └── index.html
├── dist/
│   ├── print-designer.es.js    # ESM 入口
│   ├── print-designer.css      # 组件样式(若未 CSS-in-JS)
│   └── index.d.ts              # 类型声明
├── vite.config.ts          # library mode 配置
├── package.json
└── tsconfig.json

构建配置(Vite library mode)

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'PrintDesigner',
      formats: ['es'],
      fileName: () => 'print-designer.es.js',
    },
    rollupOptions: {
      external: ['vue', 'pinia', 'vue-demi'],
      output: {
        globals: { vue: 'Vue', pinia: 'Pinia' },
      },
    },
    cssCodeSplit: false,
  },
});

对外导出(src/index.ts

导出 说明
PrintDesigner 设计器主组件(三栏布局)
PrintPreview 只读预览组件
PrintTemplate / PrintBaseElement 子类 领域模型,供运行时直接 toHtmlDOM
PrintElementFactory 元素创建与反序列化
PrintRenderService HTML / PDF / 图片导出
usePrintDesignerStore Pinia store 定义(不自动注册
types PrintElementTypeRenderContext 等类型
// src/index.ts
export { default as PrintDesigner } from './components/PrintDesigner.vue';
export { default as PrintPreview } from './components/PrintPreview.vue';
export { PrintTemplate } from './models/PrintTemplate';
export { PrintElementFactory } from './factory/PrintElementFactory';
export { PrintRenderService } from './services/PrintRenderService';
export { usePrintDesignerStore } from './store/usePrintDesignerStore';
export * from './models/types';

package.json 约定

{
  "name": "@myapps/print-designer",
  "type": "module",
  "main": "./dist/print-designer.es.js",
  "module": "./dist/print-designer.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/print-designer.es.js",
      "types": "./dist/index.d.ts"
    },
    "./style.css": "./dist/print-designer.css"
  },
  "files": ["dist"],
  "peerDependencies": {
    "vue": "^3.4.0",
    "pinia": "^2.1.0"
  },
  "devDependencies": {
    "vue": "^3.4.0",
    "pinia": "^2.1.0",
    "vite": "^5.0.0",
    "@vitejs/plugin-vue": "^5.0.0"
  }
}

不打包 Pinia 的原因:宿主应用(设计器 / 运行时)通常已有全局 createPinia() 实例;若组件库内嵌 Pinia,会导致 store 隔离、DevTools 多实例、持久化插件重复注册等问题。组件库仅 导出 usePrintDesignerStore,由使用者在已有 Pinia 上下文中调用。

宿主集成方式

设计器嵌入(obpm-designer-vue3):

// main.ts(宿主已有 createPinia)
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import PrintDesignerPage from './PrintDesignerPage.vue';
import '@myapps/print-designer/style.css';

const app = createApp(PrintDesignerPage);
app.use(createPinia());   // 宿主统一注入
app.mount('#app');
<!-- PrintDesignerPage.vue -->
<script setup lang="ts">
import { PrintDesigner, usePrintDesignerStore } from '@myapps/print-designer';
import { onMounted } from 'vue';

const store = usePrintDesignerStore(); // 使用宿主 Pinia 实例

onMounted(async () => {
  await store.loadTemplate('tpl-shipping-001');
});
</script>

<template>
  <PrintDesigner @save="store.saveTemplate" />
</template>

运行时仅渲染(无需 Pinia,直接调模型):

import { PrintTemplate, PrintRenderService } from '@myapps/print-designer';

const template = PrintTemplate.fromJSON(printTemplate);
const resolvedDataList = await hostRequestBridge.getPrintDatas(templateId, printParams);
const html = resolvedDataList
  .map((resolvedData) => PrintRenderService.renderHtml(template, resolvedData))
  .join('');

测试页面(demo)

demo/ 作为 Vite 多页入口,用于本地开发与回归验证,不随 npm 包发布

demo/
├── index.html
├── main.ts
└── App.vue
// demo/main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import '../src/style/index.css';

createApp(App).use(createPinia()).mount('#app');
<!-- demo/App.vue:切换设计 / 预览 / 导出 -->
<script setup lang="ts">
import { ref } from 'vue';
import {
  PrintDesigner,
  PrintPreview,
  usePrintDesignerStore,
  PrintRenderService,
} from '../src/index';

const store = usePrintDesignerStore();
const mode = ref<'design' | 'preview'>('design');
const previewHtml = ref('');
const mockResolvedData = [{ orderNo: 'SO-001', items: [] }]; // 运行时由 dataSource 查询得到,数组每项渲染一次

function handlePreview() {
  previewHtml.value = PrintRenderService.renderHtml(store.template, mockResolvedData[0]);
  mode.value = 'preview';
}
</script>

<template>
  <div class="demo-toolbar">
    <button @click="mode = 'design'">设计</button>
    <button @click="handlePreview">预览 HTML</button>
  </div>
  <PrintDesigner v-if="mode === 'design'" />
  <PrintPreview v-else :html="previewHtml" />
</template>

本地启动:

npm run dev          # vite --config vite.config.demo.ts
npm run build        # 构建组件库 dist/
npm run build:demo   # 可选:构建 demo 静态页用于 CI 截图对比

vite.config.demo.ts(开发专用)

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  root: 'demo',
  resolve: { alias: { '@': resolve(__dirname, 'src') } },
});

构建与发布 checklist

步骤 命令 / 动作
类型检查 vue-tsc --noEmit
构建 ESM vite build → 产出 dist/print-designer.es.js
生成 d.ts vue-tsc -p tsconfig.build.json --emitDeclarationOnly
本地联调 npm run dev,demo 页验证拖拽/属性/预览/导出
宿主集成测 在 obpm-designer-vue3 中 npm link 或 workspace 引用
发布 npm publish --access restricted(私有 registry)

注意事项

  • Barcode / QR 依赖(JsBarcode、qrcode 等)可打入组件库,或同样设为 peerDependencies 由宿主按需安装;默认建议 打入,减少集成成本。
  • 样式隔离:组件根节点使用 .print-designer 前缀,避免污染宿主全局样式;宿主需显式 import '@myapps/print-designer/style.css'
  • Tree-shaking:运行时若仅需 PrintRenderService,应支持按路径导入模型与服务,避免拉入完整设计器 UI(可通过 exports 子路径 ./render 扩展)。

打印数据获取

打印数据接口

  • 在运行阶段,通过 接口方法:hostRequestBridge.getPrintDatas(printTemplateId, printParams) 获取对应的数据。
  • printTemplateId 为打印模板 id(路径 {templateId} 与 body 中 printTemplateId 通常一致,body 中字段可选)。
  • printParams 为打印参数,通过 HTTP POST 请求体 JSON 提交(Content-Type: application/json),**不再**使用 Query String 传参。
  • 其中 hostRequestBridge 通过调用宿主注入(测试环境注入 FakeHostApi)。
  • 返回值为数组dataRecord<string, unknown>[];每条记录对应一次可独立渲染的打印单元(主数据源一行 → 数组一项)。单条打印时数组长度为 1;批量打印时有多项。

打印数据样例

运行时数据由宿主按模板 dataSource 定义执行查询后返回。HTTP 响应 data 为对象数组;数组每项为扁平键值对象(Record<string, unknown>),键名与 dataSource.*.variables[].name、子数据源 name 及合并后的 params[].name 一致。主数据源字段为标量;子数据源字段为对象数组;参数值在渲染前由 applyTemplateParamsToPrintDataList 并入每项 printData

HTTP 请求与响应格式

Runtime 示例8083,context 如 /obpm):

POST /obpm/api/runtime/sOZu9kthmxyP8qQfq0e/print/__yXlMfQhWiHwn2pOekhE/data
Content-Type: application/json

{
  "printTemplateId": "__yXlMfQhWiHwn2pOekhE",
  "docIds": ["DOC-20260404-001"]
}

Designer 预览示例8082,context 如 /designer):

POST /designer/api/runtime/sOZu9kthmxyP8qQfq0e/print/__yXlMfQhWiHwn2pOekhE/data
Content-Type: application/json

{
  "printTemplateId": "__yXlMfQhWiHwn2pOekhE",
  "docIds": ["DOC-20260404-001"]
}

请求体字段(键名与模板 params[].name 一致,均可选除业务必填项外):

字段 类型 说明
printTemplateId string 模板 ID;与路径 {templateId} 一致时可省略
docIds string[] 表单主数据源:待打印文档 ID 列表
docId string 单条文档 ID(与 docIds 二选一;同时存在时以 docIds 为准)
其他 标量 / 数组 模板 params[] 声明的 SQL 入参等(如 orderNo
  • 请求体可为空 {};设计器预览时服务端自动设置 _designPreview,表单主数据源无 docIds 时查前 10 条样例文档。
  • Runtime 端对 body 中字符串值(含 docIds 元素)按当前用户 ID 尝试 DesUtil 解密。
  • 后端 PrintDataRequestUtil.buildPrintParams 将 JSON body 合并进 ParamsTable;body 字段**覆盖**同名 Query 参数(若存在)。

响应data 为数组,不再包裹为单个对象):

{
  "errcode": 0,
  "errmsg": "ok",
  "data": [
    {
      "ITEM_单行文本框1": "22",
      "子数据源": [
        { "item_三": "12", "item_四": "12" },
        { "item_三": "B", "item_四": "B" },
        { "item_三": "wfwqe", "item_四": "wqefq" },
        { "item_三": "测试3", "item_四": "测试4" },
        { "item_三": "A", "item_四": "A" }
      ]
    }
  ]
}

批量打印时 data 含多项,例如 "data": [{ ... }, { ... }]。无 dataSource 或查询无结果时返回 "data": []

调用流程

  1. 用户触发打印,HTTP POST 请求携带 JSON body(printParams,键名与模板 params[].name 一致,如 docIdsorderNo)。
  2. 宿主加载 printTemplate JSON(含 dataSourceparams 定义)。
  3. 调用 hostRequestBridge.getPrintDatas(printTemplateId, printParams),按 dataSource.main / dataSource.subs 依次查询并组装为 数组
  4. 前端 applyTemplateParamsToPrintDataList(printDataList, template, printParams) 将参数值合并进每项 printData(供占位符与 JS 的 data 访问)。
  5. 对数组每一项调用 PrintRenderService.renderHtml(template, printData) 渲染 HTML;批量打印时由宿主拼接多份 HTML 或合并为 PDF。
// printParams:POST JSON body + 模板 params 声明(示例)
const printParams = {
  printTemplateId: 'tpl-shipping-001',
  orderNo: 'SO-20260404-001',  // 对应 params[].name,驱动 SQL :orderNo
  docIds: ['DOC-20260404-001'], // 表单主数据源
};

// 宿主侧 HTTP 调用(伪代码)
const printDataList = applyTemplateParamsToPrintDataList(
  await fetch(`/obpm/api/runtime/${appId}/print/${templateId}/data`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(printParams),
  }).then((r) => r.json()).then((res) => res.data),
  printTemplate,
  printParams,
);

// 渲染(单条或批量)
const template = PrintTemplate.fromJSON(printTemplate);
const html = printDataList
  .map((printData) => PrintRenderService.renderHtml(template, printData))
  .join('');

完整样例(单条记录)

对应上文「模板 json 样例」中的 tpl-shipping-001(发货单)。docId 仅作为查询入参,不出现在 printData 中;查询结果字段与占位符一一对应。以下为 data 数组中**一项**的结构:

[
  {
    "orderNo": "SO-20260404-001",
    "trackingNo": "SF1234567890123",
    "orderUrl": "https://example.com/order/SO-20260404-001",
    "items": [
      { "sku": "SKU-001", "name": "无线鼠标", "qty": 2, "unit": "个" },
      { "sku": "SKU-002", "name": "机械键盘", "qty": 1, "unit": "个" },
      { "sku": "SKU-003", "name": "USB-C 数据线", "qty": 5, "unit": "条" }
    ]
  }
]

字段说明(数组单项)

字段 类型 来源 模板占位符 用途
orderNo string 主数据源 main.variables.orderNo 或参数 params[].name {{orderNo}} / data.orderNo 文本、SQL 入参
trackingNo string 主数据源 main.variables.trackingNo {{trackingNo}} 条形码组件 content
orderUrl string 主数据源 main.variables.orderUrl {{orderUrl}} 二维码组件 content
items / 子数据源键名 array 子数据源 subs[].name(如 明细数据源 {{明细数据源}} 表格 props.dataSource;元素为行对象
items[].sku string 子数据源列 sku 表格列内容 field: "{{sku}}"
items[].name string 子数据源列 name 表格列内容 field: "{{name}}"
items[].qty number 子数据源列 qty 表格列内容 field: "{{qty}}"
items[].unit string 子数据源列 unit 表格列内容 field: "{{unit}}"
docIds string[] 参数 params[].name(表单主数据源) {{docIds}} / data.docIds 占位符展示;驱动表单文档加载

约定与注意

  • 数组语义data 每项独立渲染一份模板;主数据源为视图时,视图多行对应多项;主数据源为表单时,每个 docId 对应一项及其关联子数据源。
  • 键名一致:单项 printData 的键须与模板 dataSource 变量名一致;params 合并后的键可用于占位符/data.xxx;未声明字段在占位符处渲染为空字符串。
  • 子数据源为数组items 等子数据源变量必须是 Record<string, unknown>[];空明细时返回 [],表格仍按 showHeader / showFooter 渲染。
  • 类型:标量字段建议 string / number / booleandocIds 可为 string[];渲染前由 resolvePropsDeep 统一转为字符串显示。
  • printParams 与 printDataprintParams 是 HTTP POST JSON body 入参,用于驱动查询(及声明参数名);printData 是查询结果 + 合并后的参数值,直接交给 PrintRenderService;占位符/data 访问的是合并后的 printData,不是原始 HTTP 参数对象。
  • 测试环境:使用上文 FakeHostApigetPrintDatas 返回与「打印数据样例」相同的 mock 数据(单元素数组),供 demo 预览与单元测试。

后端架构设计(Java)

打印设计器后端分 设计时态obpm-designer)与 运行时态obpm-runtime),核心领域逻辑放在 obpm-core。前端组件库通过 hostRequestBridge 桥接,不直接访问后端obpm-designer-vue3 实现桥接层,将 TypeScript 接口映射为下方 HTTP 端点。

模块职责

模块 职责
obpm-designer 设计时 REST 控制器:模板 CRUD、SQL/视图/表单 Schema 解析、模块/视图/表单/数据源元数据
obpm-core PrintDesignTimeService / PrintRuntimeService:模板持久化、数据源查询、列信息解析(复用 ReportDesignTimeService
obpm-runtime 运行时打印数据查询、HTML 渲染入口(可选 PDF/图片导出)

存储模型

复用现有 Report 实体(cn.myapps.core.common.model.report.Report),通过 isPrint = 1 标识打印模板,新增模板类型常量:

/** 新版 JSON 打印设计器模板 */
public static final String TYPE_PRINT_JSON = "PRINT_JSON";
字段 用途
id 模板 ID,对应 printTemplate.id
name 模板名称
parentId 所属模块 ID
applicationid 所属应用 ID
isPrint 固定为 1
templateType 固定为 TYPE_PRINT_JSON
printTemplate 存储完整打印设计器 JSON 字符串(page + dataSource + elements
xmlTemplate 不使用;保留给 JRXML / 脚本报表,与 printTemplate 互不混写

printTemplateReport 实体**新增独立字段**(@XmlJavaTypeAdapter(CDataAdapter.class)),与 xmlTemplateuReportTemplatescriptTemplate 并列,按 templateType 选择读写字段,避免 JSON 与旧版 XML 模板冲突。

持久化路径遵循 Report.getPath(),保存在 storage/workspace/{applicationId}/... 下,与报表资源同级。

统一约定

Base URL

服务 端口 基础路径
设计器 8082 http://{host}:8082{contextPath}/api/designtime
运行时 8083 http://{host}:8083{contextPath}/api/runtime

contextPathmyapps.context-path.designer / myapps.context-path.runtime 配置,默认可为空。

响应格式

与现有设计器 API 一致,返回 Resource 对象:

{
  "errcode": 0,
  "errmsg": "ok",
  "data": { ... }
}

Schema 类型映射

后端 QueryColumnInfo → 前端 SchemaVariable 的转换规则:

columnTypeName / columnClassName type
NUMBER / java.lang.Integer 等数值类 number
DATE / java.util.Date date
BOOLEAN / java.lang.Boolean boolean
其他 string

设计时态

状态:已实现(2026-04,含 form-schema 2026-07)
Controller:cn.myapps.designtime.print.controller.PrintDesignTimeController
Service:cn.myapps.core.designtime.print.service.PrintDesignTimeService / PrintDesignTimeServiceImpl
基础路径:/api/designtime/applications/{applicationId}/print/...(Controller 映射前缀为 /api/designtime/applications

PrintDesignTimeService 为委托 ReportDesignTimeService 的薄封装,不继承 DesignTimeService,**不注册**到 DesignTimeServiceFactory / DesignTimeServiceResolver;通过 DesignTimeServiceManager.printDesignTimeService() 直接 new PrintDesignTimeServiceImpl() 获取实例。

类结构

classDiagram
    class PrintDesignTimeController {
        +getTemplates()
        +getPrintTemplate(templateId)
        +savePrintTemplate(content)
        +getSqlSchema(body)
        +getViewSchema(moduleId, viewId)
    }
    class PrintDesignTimeService {
        +query(appId, moduleId, ...) DataPackage
        +findById(appId, templateId) Report
        +createTemplate(...) Report
        +updateTemplate(...) Report
        +deleteTemplates(...) void
        +parseSqlSchema(...) PrintSchemaResult
        +parseViewSchema(...) PrintSchemaResult
    }
    class ReportDesignTimeService {
        +getQueryColumnInfos()
        +getViewColumnsInfos()
    }
    PrintDesignTimeController --> PrintDesignTimeService
    PrintDesignTimeService --> ReportDesignTimeService

getAppDataSources / getModules / getModuleViews 不新建端点,直接复用现有 API(见「hostRequestBridge 映射」表)。

接口清单

GET /{applicationId}/print/templates

获取当前应用下的 JSON 打印模板列表(isPrint=1templateType=PRINT_JSON)。

查询参数

参数 类型 必填 说明
moduleId string 按模块过滤
searchword string 按名称模糊查询
pageNo int 页码,默认 1
linesPerPage int 每页条数,默认 10

响应 dataDataPackage<Report>(仅返回 idnameparentIdtemplateType 等摘要字段)。


GET /{applicationId}/print/templates/{templateId}

对应 hostRequestBridge 侧由宿主组装;后端提供模板 JSON 原文。

响应 data

{
  "id": "tpl-shipping-001",
  "name": "发货单",
  "moduleId": "mod-shipping",
  "templateType": "PRINT_JSON",
  "printTemplate": "{ ... 打印设计器 JSON 字符串或已解析对象 ... }"
}

实现PrintDesignTimeService.findById → 读取 Report.printTemplate 字段。


POST /{applicationId}/modules/{moduleId}/print/templates

新建打印模板。

请求体:完整 printTemplate JSON(见「模板 json 样例」)。id 为空时由后端 Sequence.getDesignTimeSequence() 生成。

响应 data{ "id": "tpl-shipping-001" }


PUT /{applicationId}/print/templates/{templateId}

保存(更新)打印模板。对应设计器 savePrintTemplate(jsonTemplate)

请求体:完整 printTemplate JSON;version 字段由前端自增,后端原样持久化。

处理逻辑

  1. 校验 id 与路径 templateId 一致;
  2. 将 JSON 序列化写入 Report.printTemplate
  3. 同步 name 等元数据(parentId 在创建时设定,更新时不改模块);
  4. PrintDesignTimeService.updateTemplateReportDesignTimeService.update 写盘。

响应 data{ "id": "tpl-shipping-001", "version": 2 }


DELETE /{applicationId}/print/templates

批量删除。请求体:String[] 模板 ID 数组(与现有报表删除风格一致)。


POST /{applicationId}/print/sql-schema

解析 SQL 列信息。对应 hostRequestBridge.getSqlSchema(sql)

请求体

{
  "sql": "SELECT sku, name, qty, unit FROM t_order_item WHERE order_no = :orderNo",
  "dataSourceName": "平台订单库"
}
字段 必填 说明
sql SQL 语句,支持命名参数(:paramName
dataSourceName 应用数据源名称,对应 getAppDataSources 返回的 name

实现:通过 DataSourceDesignTimeService 解析数据源,直接调用 OpenQueryService.getQueryColumnInfos(sql, dataSource) 获取列元数据(不构造临时 Report,避免 getParent() 为空导致 NPE)。

响应 data

{
  "variables": [
    { "name": "sku", "label": "SKU", "type": "string" },
    { "name": "name", "label": "品名", "type": "string" },
    { "name": "qty", "label": "数量", "type": "number" },
    { "name": "unit", "label": "单位", "type": "string" }
  ]
}

与现有报表列解析接口 POST /applications/{applicationId}/reports/{reportId}/columninfostype=DATASOURCE_TYPE_SQL)逻辑相同,本接口为打印设计器专用薄封装,响应结构对齐前端 SchemaResult


GET /{applicationId}/print/view-schema

解析视图列信息。对应 hostRequestBridge.getViewSchema(moduleId, viewId)

查询参数

参数 类型 必填 说明
moduleId string 模块 ID(校验视图归属,与现有视图 API 一致)
viewId string 视图 ID

实现ReportDesignTimeService.getViewColumnsInfos(applicationId, viewId),仅取 COLUMN_TYPE_FIELD 类型列;并校验 viewId 所属模块与 moduleId 一致。

列表查询说明:未传 moduleId 时,以 applicationId 作为 parentId 查询后再按 isPrint=1 + templateType=PRINT_JSON 过滤;列表项仅返回摘要字段(idnameparentIdtemplateTypeisPrint),不含 printTemplate 正文。

响应 data

{
  "variables": [
    { "name": "orderNo", "label": "订单号", "type": "string" },
    { "name": "trackingNo", "label": "运单号", "type": "string" },
    { "name": "orderUrl", "label": "订单链接", "type": "string" }
  ]
}

GET /{applicationId}/print/form-schema

解析表单字段信息。对应 hostRequestBridge.getFormSchema(moduleId, formId)

查询参数

参数 类型 必填 说明
moduleId string 模块 ID(校验表单归属)
formId string 表单 ID

实现:读取表单定义中的字段列表(Form.getAllFieldMap()),映射为 PrintSchemaResultvariables[].name ← 字段 name(如 单行文本框1),label ← 字段显示名;并固定追加系统字段 $docId(文档 ID,运行时取 Document.getId())。

响应 data

{
  "variables": [
    { "name": "$docId", "label": "文档ID", "type": "string" },
    { "name": "单行文本框1", "label": "单行文本框1", "type": "string" },
    { "name": "订单号", "label": "订单号", "type": "string" }
  ]
}

与视图列解析 GET .../print/view-schema 结构相同,响应对齐前端 SchemaResult


hostRequestBridge 映射(设计态)

前端方法 HTTP 端点 说明
getAppDataSources(appId) GET /api/designtime/applications/{appId}/datasources 复用 DataSourceController
getSqlSchema(sql) POST /api/designtime/applications/{appId}/print/sql-schema 新建,见上
getModules() GET /api/designtime/applications/{appId}/modules?parentId={appId} 复用 ModuleDesignTimeController
getModuleViews(moduleId) GET /api/designtime/applications/{appId}/modules/{moduleId}/views 复用 ViewController
getViewSchema(moduleId, viewId) GET /api/designtime/applications/{appId}/print/view-schema?moduleId=&viewId= 新建,见上
getModuleForms(moduleId) GET /api/designtime/applications/{appId}/modules/{moduleId}/forms 复用 FormDesignTimeController(待实现桥接)
getFormSchema(moduleId, formId) GET /api/designtime/applications/{appId}/print/form-schema?moduleId=&formId= 已实现,见上
保存模板 PUT /api/designtime/applications/{appId}/print/templates/{templateId} 已实现
新建模板 POST /api/designtime/applications/{appId}/modules/{moduleId}/print/templates 已实现
加载模板 GET /api/designtime/applications/{appId}/print/templates/{templateId} 已实现

obpm-designer-vue3ObpmPrintHostBridge 实现 HostRequestBridge,将上表封装为 Promise 方法供 @myapps/print-designer 注入。

hostRequestBridge 映射(运行态 / 设计器预览)

前端方法 HTTP 端点 说明
getPrintDatas POST /api/runtime/{appId}/print/{templateId}/data runtimeobpm-runtime:8083,context 如 /obpm);Content-Type: application/json,body 为 printParams
getPrintDatas(设计器内预览) POST /api/runtime/{appId}/print/{templateId}/data designerobpm-designer:8082,context 如 /designer),逻辑与 runtime 相同

示例(designer):

POST /designer/api/runtime/sOZu9kthmxyP8qQfq0e/print/__yXlMfQhWiHwn2pOekhE/data
Content-Type: application/json

{
  "printTemplateId": "__yXlMfQhWiHwn2pOekhE",
  "docIds": ["DOC-20260404-001"]
}

响应 data 为数组(见「打印数据获取 · HTTP 请求与响应格式」)。


运行时态

状态:已实现/data 端点,2026-07)
Runtime Controller:cn.myapps.runtime.print.controller.PrintRuntimeController
设计器预览 Controller:cn.myapps.designtime.print.controller.PrintDesignTimeRuntimeController(路径与 runtime 一致,复用 PrintRuntimeServiceImpl
请求体解析:cn.myapps.core.runtime.print.util.PrintDataRequestUtil
Service:cn.myapps.core.runtime.print.service.PrintRuntimeService / PrintRuntimeServiceImpl

服务 完整路径示例
runtime(8083,context /obpm POST /obpm/api/runtime/{applicationId}/print/{templateId}/data
designer(8082,context /designer POST /designer/api/runtime/{applicationId}/print/{templateId}/data

请求Content-Type: application/json;body 为 printParams(如 printTemplateIddocIdsorderNo 等,键名与模板 params[].name 一致)。设计器预览使用设计器登录态(SuperUserVO)解密路径 ID 及 body 中加密字符串,数据查询用户为 ObpmSystem.getInstance();预览模式服务端设置 _designPreview=true

PrintRuntimeService 不经 DesignTimeServiceFactory;两端 Controller 均直接 new PrintRuntimeServiceImpl()。body 解析由 PrintDataRequestUtil.buildPrintParams 完成。

类结构

classDiagram
    class PrintRuntimeController {
        +getPrintData(templateId, params)
        +renderHtml(templateId, params)
    }
    class PrintRuntimeService {
        +loadTemplate(appId, templateId) PrintTemplate
        +resolvePrintData(template, printParams) List~Map~
        +executeMainDataSource(ds, params) Map
        +executeSubDataSource(ds, params, mainData) List
    }
    PrintRuntimeController --> PrintRuntimeService

POST /{applicationId}/print/{templateId}/data

按模板 dataSource 定义执行查询,返回 printData 列表。对应 hostRequestBridge.getPrintDatas(printTemplateId, printParams)

请求

说明
方法 POST
Content-Type application/json
路径参数 applicationIdtemplateId(支持加密 ID,按登录用户解密)
请求体 printParams JSON 对象;见「打印数据获取 · HTTP 请求与响应格式」

请求体示例

{
  "printTemplateId": "__yXlMfQhWiHwn2pOekhE",
  "docIds": ["DOC-20260404-001"],
  "orderNo": "SO-20260404-001"
}

实现PrintRuntimeController / PrintDesignTimeRuntimeControllerPrintDataRequestUtil.buildPrintParamsPrintRuntimeServiceImpl.resolvePrintData — 加载 Report.printTemplate,解析 dataSource(可选),按类型执行查询后组装为 List<Map<String, Object>>dataSource 段时返回空数组 [],供纯静态排版模板使用。

SQL 数据源要求type=sql 的主/子数据源须配置 dataSourceId(应用数据源 id);SQL 支持 :paramName 命名参数,参数名须在模板 params[] 中声明,入参由运行时 HTTP POST JSON bodyprintParams)与已解析主数据源标量提供。

视图主数据源:复用 AbstractView.getViewTypeImpl().getViewDatas,取首行文档字段映射到 main.variables[].name;若配置了 main.queryConditions,查询前按 variable 绑定 printParams 过滤。

表单主数据源:按 printParams.docId(单条)或 printParams.docIds(数组)加载表单文档;每个 docId 对应 data 数组一项,字段名与 main.variables[].name 对齐(如 ITEM_单行文本框1)。若同时传入 docIddocIds,以 docIds 为准。

  1. 加载 ReportisPrint=1templateType=PRINT_JSON),解析 printTemplate 得到 dataSource
  2. 执行 主数据源
  3. type=view:按 moduleId/viewId 查询视图;若有 queryConditions[],将每条 { condition, variable }variableprintParams 解析为视图过滤入参;**每行文档**映射为数组一项的标量 Map;
  4. type=form:按 docId/docIds 加载表单文档,**每个文档**映射为数组一项的标量 Map;
  5. type=sql:绑定 printParams 与主数据源字段,执行 SQL,**每行**转为数组一项的标量 Map;
  6. 对主数据源 每一行,依次执行 子数据源 subs[]
  7. type=sql:将 printParams + 当前主行标量合并为 SQL 入参,执行 SQL,结果集转为 List<Map>,以子数据源 name 为键写入当前项;
  8. type=view:按 moduleId/viewId 查询视图;将 queryConditions[] 中每条 { condition, variable } 解析为过滤参数——variableprintParams 与当前主行标量取值,作为视图条件 condition 的入参(如 parentId 关联父文档);结果集转为 List<Map>,以子数据源 name 为键写入当前项;
  9. type=form:按条件加载关联表单文档(若配置),映射为数组写入当前项;
  10. 合并为 List<Map<String, Object>> 返回(HTTP 响应 data 即为该列表)。

响应 data(数组;以下为单条记录示例,与「打印数据样例」一致)

[
  {
    "orderNo": "SO-20260404-001",
    "trackingNo": "SF1234567890123",
    "orderUrl": "https://example.com/order/SO-20260404-001",
    "items": [
      { "sku": "SKU-001", "name": "无线鼠标", "qty": 2, "unit": "个" }
    ]
  }
]

完整 HTTP 响应:

{
  "errcode": 0,
  "errmsg": "ok",
  "data": [
    {
      "orderNo": "SO-20260404-001",
      "trackingNo": "SF1234567890123",
      "orderUrl": "https://example.com/order/SO-20260404-001",
      "items": [
        { "sku": "SKU-001", "name": "无线鼠标", "qty": 2, "unit": "个" }
      ]
    }
  ]
}

GET /{applicationId}/print/{templateId}/html

一站式渲染:内部调用 getPrintData + 服务端 PrintRenderService(与前端 npm 包同构的 Java 移植,或 Feign 调用 Node 微服务,一期可仅返回 printData 由前端渲染)。

请求体:同 /dataPOSTContent-Type: application/json,body 为 printParams)。

响应 data{ "html": "<div class=\"print-page\">...</div>" }

POST /{applicationId}/print/{templateId}/pdf(可选,二期)

导出 PDF。请求体可携带 printParams;响应 application/pdf 二进制流。


核心服务实现要点

PrintDesignTimeService(obpm-core,已实现)

源码:

路径
PrintDesignTimeService obpm-core/.../designtime/print/service/PrintDesignTimeService.java
PrintDesignTimeServiceImpl obpm-core/.../designtime/print/service/PrintDesignTimeServiceImpl.java
PrintSchemaResult obpm-core/.../common/model/print/PrintSchemaResult.java
PrintSchemaVariable obpm-core/.../common/model/print/PrintSchemaVariable.java
public interface PrintDesignTimeService {
    DataPackage<Report> query(String applicationId, String moduleId,
                              String searchword, int pageNo, int linesPerPage);
    Report findById(String applicationId, String templateId);
    Report createTemplate(String applicationId, String moduleId,
                          String jsonContent, IBaseUser user);
    Report updateTemplate(String applicationId, String templateId,
                          String jsonContent, IBaseUser user);
    void deleteTemplates(IBaseUser user, String applicationId, String[] ids);
    PrintSchemaResult parseSqlSchema(String applicationId, String sql,
                                     String dataSourceName, ParamsTable params);
    PrintSchemaResult parseViewSchema(String applicationId, String moduleId,
                                      String viewId);
    PrintSchemaResult parseFormSchema(String applicationId, String moduleId,
                                      String formId);
}
  • 实例获取:DesignTimeServiceManager.printDesignTimeService()new PrintDesignTimeServiceImpl()(不经 DesignTimeServiceFactory)。
  • parseSqlSchema / parseViewSchema / parseFormSchema:内部调用 ReportDesignTimeServiceFormDesignTimeService,结果映射为 PrintSchemaResultvariables[].name ← 列名/字段名,label ← 列标签/字段显示名);重复名自动追加 1 后缀(与报表 columninfos 一致)。
  • createTemplate / updateTemplate:校验 templateType == PRINT_JSON,JSON 写入 Report.printTemplate(不触碰 xmlTemplate);同模块下名称不可重复。

PrintRuntimeService(obpm-core,已实现)

源码:

路径
PrintRuntimeService obpm-core/.../runtime/print/service/PrintRuntimeService.java
PrintRuntimeServiceImpl obpm-core/.../runtime/print/service/PrintRuntimeServiceImpl.java
PrintDataRequestUtil obpm-core/.../runtime/print/util/PrintDataRequestUtil.java
PrintRuntimeController obpm-runtime/.../runtime/print/controller/PrintRuntimeController.java
PrintDesignTimeRuntimeController obpm-designer/.../designtime/print/controller/PrintDesignTimeRuntimeController.java
public interface PrintRuntimeService {
    List<Map<String, Object>> resolvePrintData(String applicationId, String templateId,
                                               ParamsTable printParams, IFrontEndUser user);
}
  • 视图主数据源:复用 ViewDesignTimeService + getViewDatas,**视图每一行**生成 data 数组一项,列名与 dataSource.main.variables[].name 对齐;若配置了 queryConditions,在查询前按条件过滤(绑定 printParams 与主数据源上下文);
  • 视图子数据源:按 subs[].moduleId/viewId 查询;queryConditions[]variableprintParams + 当前主行标量解析,condition 作为视图过滤字段(常见:parentId 绑定主记录文档 ID);
  • 表单主数据源:按 moduleId/formId 定位表单定义,对每个 docId 调用 DocumentProcess 加载文档,字段值映射到 dataSource.main.variables[].name
  • SQL 子数据源PreparedStatement 绑定 :paramName;命名参数从 printParams 与已解析的主数据源标量中取值;

与现有报表 API 的关系

能力 现有接口 打印设计器
SQL 列解析 POST .../reports/{reportId}/columninfos POST .../print/sql-schema
视图列解析 同上(type=DATASOURCE_TYPE_VIEW GET .../print/view-schema
表单字段解析 GET .../print/form-schema(已实现)
模板列表 GET .../reports?isPrint=1 GET .../print/templates(仅 PRINT_JSON
模板存储 Report.xmlTemplate(JRXML 等) Report.printTemplate(独立字段存 JSON)

旧版 Jasper/脚本打印模板(TYPE_JRXML / TYPE_SCRIPT)不受影响;列表接口通过 templateType 过滤,避免混排。


实现清单

步骤 内容 状态
1 Report 增加 TYPE_PRINT_JSON 常量与 printTemplate 字段 ✅ 已完成
2 obpm-corePrintDesignTimeService / PrintDesignTimeServiceImpl ✅ 已完成
3 obpm-designerPrintDesignTimeController(模板 CRUD + sql-schema + view-schema + form-schema) ✅ 已完成
4 obpm-runtimePrintRuntimeController/data 端点) ✅ 已完成
5 obpm-designerPrintDesignTimeControllerform-schema 端点) ✅ 已完成