打印设计器设计方案
产品定位与核心场景¶
适用打印类型 - 小票 / 票据打印:收银小票、快递面单、发货单、标签(不干胶) - 证件 / 单据:合同、收据、证书、工作证、报销单 - 名片 / 宣传单 / 海报:可视化拖拽排版,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.contentIsJs(boolean,可选,默认 false)
设计态画布:勾选 JS 时不执行脚本,原样显示源码;单元格前显示橙色 js 角标,超出宽度以 … 省略。
预览 / 打印 / 导出:由 resolveContentValue → executeContentScript 执行 JS,入参 data 为当前 printData 单项(主数据源标量 + 子数据源数组键,不含当前表格行)。
JS 写法示例
1. 表达式(单行,无需 return):
| 示例 | 说明 |
|---|---|
data.orderNo |
取主数据源订单号 |
data.orderNo + '-' + data.trackingNo |
字符串拼接 |
data.qty > 10 ? '大单' : '普通' |
条件表达式 |
2. 闭包 / IIFE(多行逻辑,IIFE 内须 return 结果值):
复杂分支、临时变量、多步计算等场景推荐用 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.contentIsJs(boolean,可选,默认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 字符串):
样式¶
HTML 组件 不提供 文本颜色、字体、字号等样式项(由片段内 HTML/CSS 自行控制)。属性面板「样式」段仅包含:
- 背景颜色
- 边框样式(无/虚线/实线/点线)
- 边框宽度 mm
- 边框颜色
图片组件¶
属性¶
图片来源 - 图片内容(base64)
样式¶
填充类型 - 充满 - 适应 - 拉伸 透明的 - 透明的(数字)
表格组件¶
属性¶
数据&行为
- 子数据源:下拉选择,选项来自模板 dataSource.subs;持久化为 props.dataSource: "{{子数据源名称}}"(如 {{明细数据源}},键名与子数据源 name 一致)
- 自动分页
- 显示表头
- 固定行数(JSON:fixedRowCount):设计态可见行数,用于计算行高与自动分页每页行数
- 显示表脚
表脚内容
属性面板顺序:表脚内容(含 是否 JS / 是否 HTML)→ 列定义。
| 属性 | JSON 字段 | 说明 |
|---|---|---|
| 表脚内容 | footerContent |
表脚文案:占位符模板、HTML 模板或 JS 片段(见下) |
| 是否 JS | footerIsJs |
boolean,可选,默认 false;为 true 时 footerContent 按 JS 片段执行;与 footerIsHtml 互斥 |
| 是否 HTML | footerIsHtml |
boolean,可选,默认 false;为 true 时 footerContent 为 HTML 模板 + 占位符;与 footerIsJs 互斥 |
表脚内容三种模式(由 footerIsJs / footerIsHtml 决定,二者互斥):
| 模式 | 条件 | 运行时解析 | 单元格渲染 |
|---|---|---|---|
| 占位符(默认) | 二者均未勾选 | resolveTableFooterContent → resolvePlaceholder |
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):
- 执行:
resolveTableFooterContent→executeContentScript;入参data含主数据源字段与系统变量($page、$pageTotal、$count等)。 - 返回值按 HTML 写入表脚(
innerHTML),可返回标签字符串。
| 示例 | 说明 |
|---|---|
'共 ' + data.$count + ' 条' |
纯文本拼接 |
'共 <b>' + data.$count + '</b> 条' |
返回 HTML 字符串 |
data.orderNo ? ('单号 ' + data.orderNo + ' · 共 ' + data.$count + ' 条') : ('共 ' + data.$count + ' 条') |
条件表达式 |
设计态画布(表脚,三种模式统一):不**替换占位符、**不**执行 JS、**不**渲染 HTML DOM;原样显示 footerContent。footerIsJs: true → 橙色 **js 角标;footerIsHtml: true → 蓝色 html 角标。
表脚 JSON 示例:
列定义
每列一条配置,支持增删。属性面板顺序:标题 → 列内容(含 是否 JS / 是否 HTML)→ 宽度 → 对齐。
| 属性 | JSON 字段 | 说明 |
|---|---|---|
| 标题 | columns[].title |
表头显示文字 |
| 列内容 | columns[].field |
单元格内容:占位符模板、HTML 模板或 JS 片段(见下) |
| 是否 JS | columns[].fieldIsJs |
boolean,可选,默认 false;为 true 时 field 按 JS 片段执行;与 fieldIsHtml 互斥 |
| 是否 HTML | columns[].fieldIsHtml |
boolean,可选,默认 false;为 true 时 field 为 HTML 模板 + 占位符;与 fieldIsJs 互斥 |
| 宽度 | columns[].width |
列宽(mm) |
| 对齐 | columns[].align |
left / center / right |
列内容三种模式(由 fieldIsJs / fieldIsHtml 决定,二者互斥):
| 模式 | 条件 | 运行时解析 | 单元格渲染 |
|---|---|---|---|
| 占位符(默认) | 二者均未勾选 | resolveTableColumnContent → normalizeTableColumnTemplate + 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 占位符说明;均未勾选时显示「变量」按钮与占位符说明。
列内容占位符(默认模式,fieldIsJs 与 fieldIsHtml 均未勾选;运行时由 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 时,运行时由 resolveTableColumnContent → executeTableColumnScript 执行):
| 上下文变量 | 说明 |
|---|---|
data |
主数据源标量 + 当前行字段合并对象(另含 row、main 属性) |
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):
设计态画布(列内容,三种模式统一):列内容均**不**替换占位符、不**执行 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.queryConditions 或 dataSource.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",并持久化moduleId、formId。 - 在设计阶段:
- 通过接口方法
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 键名(如 docIds、orderNo) |
params[].defaultValue |
**设计器预览/调试**用默认值;正式运行时由宿主 HTTP 传入实际值覆盖 |
| 渲染上下文 | 渲染前通过 buildTemplateParamValues 将参数值合并进每项 printData,供 resolvePropsDeep / executeContentScript 的 data 访问 |
| 同名冲突 | 参数名与 dataSource 查询结果字段同名时,查询结果优先 |
表单主数据源默认参数¶
- 主数据源
type=form时,保存或加载模板若缺少docIds参数,设计器**自动添加**:
docIds的defaultValue支持**逗号分隔**(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 段结构¶
JSON 示例:
"params": [
{
"id": "param-docids",
"name": "docIds",
"label": "文档ID列表",
"defaultValue": "DOC-20260404-001"
},
{
"id": "param-order",
"name": "orderNo",
"label": "订单号",
"defaultValue": "SO-20260404-001"
}
]
设计器预览流程¶
buildPreviewPrintParams(template):合并params默认值与printTemplateId;表单主数据源且无docIds/docId时补默认docIds。hostRequestBridge.getPrintDatas(templateId, printParams)获取 mock / 真实数据。applyTemplateParamsToPrintDataList(list, template, printParams)将参数并入每项printData。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。
请求:
响应样例:
{
"variables": [
{ "name": "sku", "label": "SKU", "type": "string" },
{ "name": "name", "label": "品名", "type": "string" },
{ "name": "qty", "label": "数量", "type": "number" },
{ "name": "unit", "label": "单位", "type": "string" }
]
}
getModules()¶
用途:配置视图/表单主数据源时,模块下拉选项。
响应样例:
getModuleViews(modualId)¶
用途:选定模块后,视图下拉选项。
请求:modualId = "mod-shipping"
响应样例:
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 样例(含 page、dataSource、params(可选)、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[] |
模板级运行时入参(id、name、label、defaultValue);供占位符/JS 绑定及预览默认值 |
移除 page.headerLine / page.footerLine |
加载忽略,保存后去除 |
移除 elements[].props.repeatFooter |
加载忽略;showFooter 控制分页每页表脚 |
defaultRowCount → fixedRowCount |
加载时自动迁移为固定行数 |
占位符替换规则:
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 优先于 defaultValue;docIds 解析为数组 |
formatJsDataRef(name) |
生成 JS 中 data.xxx 或 data["xxx"] |
applyTemplateParamsToPrintDataList(list, template, printParams?) |
预览/渲染前将参数并入每项 printData |
executeContentScript(script, data) |
文本/条码/二维码/HTML content;data 为当前 printData 单项(含参数) |
executeTableColumnScript(script, row, context) |
表格列 field;context 为主数据源标量 + 参数,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 items = data['明细数据源'];
if (!Array.isArray(items)) return row.sku;
return row.sku + ' (' + items.length + '行)';
})()
闭包示例(表格表脚)¶
入参为 data(主数据源标量 + 系统变量 $count);下列示例在 executeContentScript(经 resolveTableFooterContent)中均可执行:
(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规则与列内容相同;footerIsJs时data.$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) 解析列名。
**视图数据源**额外字段:moduleId、viewId(myApps 模块视图,非数据库视图);可选 queryConditions[](查询条件,见上文「视图数据源 · 查询条件」)。设计阶段通过 getModules() / getModuleViews() 下拉选择,调用 hostRequestBridge.getViewSchema(moduleId, viewId) 解析列名。
queryConditions[] 每项结构:
| 字段 | 类型 | 说明 |
|---|---|---|
condition |
string |
视图查询条件字段名 |
variable |
string |
绑定变量名(主数据源变量或 params[].name) |
conditionLabel |
string |
可选,条件显示名 |
**表单数据源**额外字段:moduleId、formId(myApps 模块普通表单)。设计阶段通过 getModules() / getModuleForms() 下拉选择,调用 hostRequestBridge.getFormSchema(moduleId, formId) 解析字段名。运行时由 printParams.docId 或 printParams.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;前端将参数值合并进每项 printData 后 resolveProps 替换占位符并 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 |
变量:name、label;设计器分组明细 |
dataSource.main.moduleId / viewId |
string |
视图数据源:模块 id、视图 id |
dataSource.*.queryConditions[] |
array |
视图数据源可选;查询条件:condition、variable、可选 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 |
为 true 时 content 按 JS 表达式执行(executeContentScript) |
elements[].props.dataSource |
string |
表格**子数据源**占位符,如 {{明细数据源}};键名与 dataSource.subs[].name 一致,设计器下拉选取 |
elements[].props.footerContent |
string |
表脚文案;占位符 / HTML / JS(配合 footerIsJs / footerIsHtml,互斥) |
elements[].props.footerIsJs |
boolean |
为 true 时 footerContent 按 JS 表达式执行(executeContentScript) |
elements[].props.footerIsHtml |
boolean |
为 true 时 footerContent 为 HTML 模板 + 占位符 |
elements[].props.columns[] |
array |
表格列:field(列内容)、fieldIsJs / fieldIsHtml(互斥,见列内容三种模式)、title、width、align |
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.src;object-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 输出:number→1,labeled→第1页,pageOfTotal→1/3 |
PrintTableElement |
<table> |
按 tableSlice.rows 逐行渲染;每格 resolveTableColumnContent(...);表脚 resolveTableFooterContent(...);fieldIsJs \|\| fieldIsHtml 或 footerIsJs \|\| footerIsHtml → innerHTML |
表格分页: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 \|\| *IsHtml 时 innerHTML |
两套路径共用 PrintBaseElement 模型。设计态仅展示模板原文;预览 / 打印走 resolvePropsDeep、resolveTableColumnContent、resolveTableFooterContent、executeContentScript / executeTableColumnScript。
方法职责对照¶
| 方法 | 职责 |
|---|---|
toJSON() |
持久化模板 |
resolveProps(data) |
占位符替换;content + contentIsJs 时走 resolveContentValue |
resolveTableColumnContent(...) |
表格列占位符 / HTML 模板 / JS(PrintTableElement.renderInnerDOM 按行调用;fieldIsJs \|\| fieldIsHtml → innerHTML) |
resolveTableFooterContent(...) |
表格表脚占位符 / HTML 模板 / JS(PrintTableElement.renderInnerDOM 渲染 <tfoot>;footerIsJs \|\| footerIsHtml → innerHTML) |
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 / footerIsHtml 与 columns[].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.vue … PrintCircle.vue |
各 type 对应的展示组件(设计态 + 运行时复用);HTML 用 PrintHtml.vue(v-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)" 动态挂载子组件,传入 element 与 mode: '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 |
PrintElementType、RenderContext 等类型 |
// 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/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)。 - 返回值为数组:
data为Record<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": []。
调用流程¶
- 用户触发打印,HTTP POST 请求携带 JSON body(
printParams,键名与模板params[].name一致,如docIds、orderNo)。 - 宿主加载
printTemplateJSON(含dataSource、params定义)。 - 调用
hostRequestBridge.getPrintDatas(printTemplateId, printParams),按dataSource.main/dataSource.subs依次查询并组装为 数组。 - 前端
applyTemplateParamsToPrintDataList(printDataList, template, printParams)将参数值合并进每项printData(供占位符与 JS 的data访问)。 - 对数组每一项调用
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/boolean;docIds可为string[];渲染前由resolvePropsDeep统一转为字符串显示。 - printParams 与 printData:
printParams是 HTTP POST JSON body 入参,用于驱动查询(及声明参数名);printData是查询结果 + 合并后的参数值,直接交给PrintRenderService;占位符/data访问的是合并后的printData,不是原始 HTTP 参数对象。 - 测试环境:使用上文
FakeHostApi,getPrintDatas返回与「打印数据样例」相同的 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 标识打印模板,新增模板类型常量:
| 字段 | 用途 |
|---|---|
id |
模板 ID,对应 printTemplate.id |
name |
模板名称 |
parentId |
所属模块 ID |
applicationid |
所属应用 ID |
isPrint |
固定为 1 |
templateType |
固定为 TYPE_PRINT_JSON |
printTemplate |
存储完整打印设计器 JSON 字符串(page + dataSource + elements) |
xmlTemplate |
不使用;保留给 JRXML / 脚本报表,与 printTemplate 互不混写 |
printTemplate 为 Report 实体**新增独立字段**(@XmlJavaTypeAdapter(CDataAdapter.class)),与 xmlTemplate、uReportTemplate、scriptTemplate 并列,按 templateType 选择读写字段,避免 JSON 与旧版 XML 模板冲突。
持久化路径遵循 Report.getPath(),保存在 storage/workspace/{applicationId}/... 下,与报表资源同级。
统一约定¶
Base URL¶
| 服务 | 端口 | 基础路径 |
|---|---|---|
| 设计器 | 8082 |
http://{host}:8082{contextPath}/api/designtime |
| 运行时 | 8083 |
http://{host}:8083{contextPath}/api/runtime |
contextPath 由 myapps.context-path.designer / myapps.context-path.runtime 配置,默认可为空。
响应格式¶
与现有设计器 API 一致,返回 Resource 对象:
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-schema2026-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=1 且 templateType=PRINT_JSON)。
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
moduleId |
string |
否 | 按模块过滤 |
searchword |
string |
否 | 按名称模糊查询 |
pageNo |
int |
否 | 页码,默认 1 |
linesPerPage |
int |
否 | 每页条数,默认 10 |
响应 data:DataPackage<Report>(仅返回 id、name、parentId、templateType 等摘要字段)。
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 字段由前端自增,后端原样持久化。
处理逻辑:
- 校验
id与路径templateId一致; - 将 JSON 序列化写入
Report.printTemplate; - 同步
name等元数据(parentId在创建时设定,更新时不改模块); PrintDesignTimeService.updateTemplate→ReportDesignTimeService.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}/columninfos(type=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 过滤;列表项仅返回摘要字段(id、name、parentId、templateType、isPrint),不含 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()),映射为 PrintSchemaResult;variables[].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-vue3 中 ObpmPrintHostBridge 实现 HostRequestBridge,将上表封装为 Promise 方法供 @myapps/print-designer 注入。
hostRequestBridge 映射(运行态 / 设计器预览)¶
| 前端方法 | HTTP 端点 | 说明 |
|---|---|---|
getPrintDatas |
POST /api/runtime/{appId}/print/{templateId}/data |
runtime(obpm-runtime:8083,context 如 /obpm);Content-Type: application/json,body 为 printParams |
getPrintDatas(设计器内预览) |
POST /api/runtime/{appId}/print/{templateId}/data |
designer(obpm-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(如 printTemplateId、docIds、orderNo 等,键名与模板 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 |
| 路径参数 | applicationId、templateId(支持加密 ID,按登录用户解密) |
| 请求体 | printParams JSON 对象;见「打印数据获取 · HTTP 请求与响应格式」 |
请求体示例
{
"printTemplateId": "__yXlMfQhWiHwn2pOekhE",
"docIds": ["DOC-20260404-001"],
"orderNo": "SO-20260404-001"
}
实现:PrintRuntimeController / PrintDesignTimeRuntimeController → PrintDataRequestUtil.buildPrintParams → PrintRuntimeServiceImpl.resolvePrintData — 加载 Report.printTemplate,解析 dataSource(可选),按类型执行查询后组装为 List<Map<String, Object>>;无 dataSource 段时返回空数组 [],供纯静态排版模板使用。
SQL 数据源要求:type=sql 的主/子数据源须配置 dataSourceId(应用数据源 id);SQL 支持 :paramName 命名参数,参数名须在模板 params[] 中声明,入参由运行时 HTTP POST JSON body(printParams)与已解析主数据源标量提供。
视图主数据源:复用 AbstractView.getViewTypeImpl().getViewDatas,取首行文档字段映射到 main.variables[].name;若配置了 main.queryConditions,查询前按 variable 绑定 printParams 过滤。
表单主数据源:按 printParams.docId(单条)或 printParams.docIds(数组)加载表单文档;每个 docId 对应 data 数组一项,字段名与 main.variables[].name 对齐(如 ITEM_单行文本框1)。若同时传入 docId 与 docIds,以 docIds 为准。
- 加载
Report(isPrint=1,templateType=PRINT_JSON),解析printTemplate得到dataSource; - 执行 主数据源:
type=view:按moduleId/viewId查询视图;若有queryConditions[],将每条{ condition, variable }的variable从printParams解析为视图过滤入参;**每行文档**映射为数组一项的标量 Map;type=form:按docId/docIds加载表单文档,**每个文档**映射为数组一项的标量 Map;type=sql:绑定printParams与主数据源字段,执行 SQL,**每行**转为数组一项的标量 Map;- 对主数据源 每一行,依次执行 子数据源
subs[]: type=sql:将printParams+ 当前主行标量合并为 SQL 入参,执行 SQL,结果集转为List<Map>,以子数据源name为键写入当前项;type=view:按moduleId/viewId查询视图;将queryConditions[]中每条{ condition, variable }解析为过滤参数——variable从printParams与当前主行标量取值,作为视图条件condition的入参(如parentId关联父文档);结果集转为List<Map>,以子数据源name为键写入当前项;type=form:按条件加载关联表单文档(若配置),映射为数组写入当前项;- 合并为
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 由前端渲染)。
请求体:同 /data(POST,Content-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:内部调用ReportDesignTimeService或FormDesignTimeService,结果映射为PrintSchemaResult(variables[].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[]中variable从printParams+ 当前主行标量解析,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-core:PrintDesignTimeService / PrintDesignTimeServiceImpl |
✅ 已完成 |
| 3 | obpm-designer:PrintDesignTimeController(模板 CRUD + sql-schema + view-schema + form-schema) |
✅ 已完成 |
| 4 | obpm-runtime:PrintRuntimeController(/data 端点) |
✅ 已完成 |
| 5 | obpm-designer:PrintDesignTimeController(form-schema 端点) |
✅ 已完成 |