对接文档
本组件库面向 Vue 3,提供两个页面级组件(设计器页、预览页)和一组打印 API。对接方式为「预览页 + 设计器页」两段式。
两段式使用模式
宿主系统(勾选数据 → 打印)
│
▼
PrintPreview(预览页) ──配置模板──▶ PrintDesigner(设计器页)
· 切换模板 · 拖拽/缩放/改属性编排
· 内嵌预览(模板 + 真实数据) · 保存为不同模板
· 打印| 组件 | 定位 | 使用方 |
|---|---|---|
PrintPreview | 预览页:切换模板 + 内嵌预览 + 打印 | 业务用户(操作员/文员) |
PrintDesigner | 设计器页:可视化编排模板 | 模板管理员 |
组件用法
预览页
<template>
<PrintPreview
:template="currentTemplate"
:templates="templateList" <!-- 该单据类型下可切换的模板 -->
:data="recordData" <!-- 勾选单据的真实数据 -->
@template-change="t => currentTemplate = t"
@configure="openDesigner" <!-- 跳到设计器页 -->
@back="goBack"
@print="onPrint"
/>
</template>设计器页
<template>
<PrintDesigner
v-model="currentTemplate"
:data="recordData"
:data-fields="fields"
@save="saveTemplate"
@print="print"
/>
</template>Props / Events
PrintDesigner
| Props | 类型 | 说明 |
|---|---|---|
modelValue | Template | 模板对象(v-model 双向绑定) |
data | Record<string, unknown> | 单据数据,用于 {字段} 占位与数据表格 |
dataFields | DataField[] | 可绑定字段清单(属性面板「插入字段」下拉) |
readonly | boolean | 只读模式(隐藏编辑面板,仅预览) |
| Events | 说明 |
|---|---|
update:modelValue | 模板变更 |
print | 触发打印 |
save | 点击「保存模板」,回调携带模板 JSON |
PrintPreview
| Props | 类型 | 说明 |
|---|---|---|
template | Template | 当前模板(单张模式) |
templates | Template[] | 可切换的模板列表 |
data | Record<string, unknown> | 单据真实数据(单张模式) |
dataFields | DataField[] | 可绑定字段清单(可选) |
items | BatchItem[] | 批量模式:多张单据 [{ template, data }] |
| Events | 说明 |
|---|---|
template-change | 切换模板,回调新模板 |
configure | 点击「配置模板」(宿主跳转到设计器页) |
back | 点击「返回」 |
print | 点击「打印」(组件内部已调打印 API) |
打印 API
import { printTemplate, printBatch, buildPrintHtml, buildBatchPrintHtml } from 'm7_print_designer'
// 单张打印:生成 HTML 并唤起浏览器打印
await printTemplate(template, data)
// 批量打印:多张单据合并为一次打印任务(每张从新页开始,要求同纸张尺寸)
await printBatch([
{ template, data: data1 },
{ template, data: data2 },
])
// 只生成打印 HTML(不唤起打印,适合自定义打印逻辑 / 导出 PDF)
const { html, pages } = await buildPrintHtml(template, data)授权与遥测
import { setupLicense } from 'm7_print_designer'
// 生产环境初始化时调用一次(开发/测试可完全不调用)
setupLicense({ code: 'M7PD1.xxxxxxxx.yyyyyyyy' })| 配置项 | 说明 |
|---|---|
code | 授权码。不配 = 未授权(打印附加水印),功能不受任何限制 |
report | 是否上报使用情况,默认 true |
endpoint | 上报地址,一般无需修改 |
watermark | 未授权时的水印文案 |
env | 显式声明环境;不传则按域名自动判断(localhost / *.local / *.test 视为非生产) |
- 授权校验完全离线(内置公钥本地验签),不发网络请求 —— 断网、内网、无法连接公网时照常工作
- 未授权只在打印与预览输出铺倾斜重复水印,不做功能限制;开发/测试环境(
localhost、*.local、*.test)不加水印,不干扰集成调试 - 默认开启使用情况上报:仅生产环境、每浏览器每月最多一次、未配置授权码也会上报;上报内容不含用户信息与业务数据
- 关闭遥测:
setupLicense({ report: false })(不影响授权校验与打印)
详见 授权与遥测。
数据绑定
模板只存「绑定关系」({字段} 占位 + 数据表格 dataSource),不存数据本身。同一套模板 + 不同 data 即可打印不同单据。
核心概念(三个对象)
① 字段字典 DataField[] —— 描述「有哪些字段可用」(来自后端表结构)
② 数据实例 DataRecord —— 一张单据的字段值(键值对 + 列表)
③ 列表字段 (值为数组) —— data 中值为数组的字段,供数据表格引用宿主只需提供 ① + ②,可打包为一个 DataSource 对象。
如何传入数据
<PrintDesigner v-model="template" :data="data" :data-fields="fields" />data 对应 DataRecord,data-fields 对应 DataField[]。推荐后端统一封装为 DataSource:
import type { DataSource } from 'm7_print_designer'
const ds: DataSource = await api.getPrintData('workOrder', id)
// → { fields: [...], data: {...} }
<PrintDesigner v-model="template" :data="ds.data" :data-fields="ds.fields" />传入数据的规范
字段字典 DataField
interface DataField {
key: string // 必填,唯一标识,点路径支持嵌套,如 'supplier.name'
label: string // 必填,显示名(字段选择器、表格列名默认值)
type?: FieldType // string | number | date | datetime | boolean | amount | percent | list
format?: string // 格式化串,如 date:'YYYY-MM-DD'、amount:'¥#,##0.00'
group?: string // 分组名(属性面板按组折叠展示)
desc?: string // 字段说明
}字段字典应由后端从真实数据表动态生成,前端只消费、不硬编码。推荐后端提供统一接口:
GET /api/print/fields?docType=workOrder → { fields: [ { key:'workOrderNo', label:'工单号', type:'string', group:'基础信息' }, ... ] }
key取表字段名(camelCase),label取字段中文注释。key是稳定契约,对外后勿随意改动。
数据实例 DataRecord
type DataRecord = Record<string, unknown>约束:
- 扁平键值为主,嵌套用点路径:
{ supplier: { name, contact } }对应{supplier.name}。 - 列表字段的值是数组:
items: [...]、processes: [...],供数据表格dataSource引用。 - 值为
null/undefined时渲染为空串。
命名约定
| 约定 | 示例 |
|---|---|
| 字段 key 用 camelCase | workOrderNo、supplier、deliveryDate |
| 列表字段用复数 | items、processes |
| 金额 / 合计 | amount、total、totalAmount |
| 日期 / 时间 | xxxDate、xxxTime |
| 编号 / 编码 | xxxNo、xxxCode |
类型与格式化
推荐策略:宿主预格式化——数据实例中直接存放「显示字符串」,设计器原样输出:
| 类型 | 约定 | 示例(宿主产出) |
|---|---|---|
amount 金额 | 货币符号 + 千分位 + 两位小数 | ¥ 86,540.00 |
number 数量 | 按业务精度(常 0 或 2 位) | 500 / 0.02 |
date 日期 | YYYY-MM-DD | 2026-08-29 |
datetime 时间 | YYYY-MM-DD HH:mm | 2026-08-29 08:00 |
percent 百分比 | xx% | 98.5% |
boolean 布尔 | 是 / 否 | 是 |
口径责任在供数侧,保证「同一字段、不同单据」展示一致。
字段如何对齐(绑定语法)
- 文本 / 二维码 / 条码内容用
{字段}占位,如工单号:{workOrderNo}。 - 支持点路径:
{supplier.name}。 - 一个内容可含多个占位:
{startDate} ~ {endDate}。 - 属性面板「插入字段」下拉由
dataFields字典驱动,选中即插入对应{字段}。 - 图片
src若整串是单个{字段},取原始值:值为数组时渲染多图并排({imgs}→ N 张等宽并排)。
数据表格对齐
dataTable 元素通过 dataSource 指向 data 中的数组字段:
// 元素配置
{ type: 'dataTable', dataSource: 'processes', columns: [
{ field: 'name', title: '工序', width: 60, align: 'left' },
{ field: 'hours', title: '工时', width: 40, align: 'center' },
]}
// 对应数据
{ processes: [ { name: '铣面', hours: 2 }, { name: '钻孔', hours: 3 } ] }- 行 = 数组元素(扁平对象),列
field用点路径取行内值。 - 列
width为相对比例:按列宽总和归一化为百分比,各列始终填满表格宽度。 - 按
rowHeight计算每页行数并自动分页(单主表分页)。
表头/表尾附加行
数据表格可配置自定义附加行,用于「表格标题」「合计行」「签字/备注行」等:
headerRows(表头附加行):仅首页显示,不随分页重复。footerRows(表尾附加行):仅末页显示,不随分页重复。- 每行的单元格复用
TableCell:支持合并单元格(rowSpan/colSpan)、{字段}绑定、对齐、加粗、背景色。
{
type: 'dataTable',
dataSource: 'items',
columns: [
{ field: 'name', title: '名称', width: 50, align: 'left' },
{ field: 'amount', title: '金额', width: 50, align: 'right' },
],
// 表头附加行(仅首页):标题横跨两列
headerRows: [{ cells: [
{ text: '采购明细表', colSpan: 2, align: 'center', bold: true },
{ text: '', colSpan: 0 }, // 被合并覆盖的单元格 colSpan=0
]}],
// 表尾附加行(仅末页):合计横跨两列,绑定 {total}
footerRows: [{ cells: [
{ text: '合计:{total}', colSpan: 2, align: 'right', bold: true },
{ text: '', colSpan: 0 },
]}],
}属性面板数据表格区块提供「表头附加行 / 表尾附加行」编辑:
+ 添加行、删除行、单元格文本(支持{字段})、合并右侧列数、对齐、加粗。
本页合计 / 总计合计
数据表格支持对数值列自动求和,分两种合计行:
showPageTotal(本页合计):每页底部显示一行,对本页数据求和。showGrandTotal(总计合计):末页底部显示一行,对全部数据求和。- 列
sum: true标记该列参与求和;合计行中「本页合计 / 总计」标签显示在第一列,sum列显示求和值,其余列留空。
{
type: 'dataTable',
dataSource: 'items',
showPageTotal: true, // 每页底部合计
showGrandTotal: true, // 末页底部总计
columns: [
{ field: 'name', title: '名称', width: 60, align: 'left' },
{ field: 'qty', title: '数量', width: 20, align: 'right', sum: true },
{ field: 'amount', title: '金额', width: 20, align: 'right', sum: true },
],
}求和列应存放数值(或可转数值的字符串);属性面板在列编辑区提供「计」复选框,数据表格区块提供「本页合计 / 总计」开关。
多级嵌套数据
后端数据常有层级结构(如工单详情内含 报工记录、程序单 等多个数组)。组件通过点路径支持任意层级:
- 嵌套对象:
{supplier.name}取data.supplier.name。 - 嵌套数组:
dataSource: 'mold.programs'取data.mold.programs(对象内的数组)。 - 设计器选层:属性面板「数据源」下拉列出字段字典中
type: 'list'的字段,选中即绑定对应层(如「报工记录」「程序单」),无需手写路径。
字段字典中,数组字段应标记 type: 'list',并用 children 描述其元素字段(供数据表格「列字段」下拉);嵌套数组用点路径 key:
const fields: DataField[] = [
{ key: 'workOrderNo', label: '工单号' },
{ key: 'reports', label: '报工记录', type: 'list', children: [
{ key: 'reportTime', label: '报工时间' },
{ key: 'worker', label: '报工人' },
{ key: 'quantity', label: '数量' },
]},
{ key: 'mold.programs', label: '程序单', type: 'list', children: [
{ key: 'programNo', label: '程序号' },
{ key: 'step', label: '工序' },
]},
]选层 → 选列,全程免手写 key:选中数据源后,数据表格的「列字段」下拉由该列表字段的
children驱动;选中列字段后若「列名」为空会自动带出字段 label。
系统占位符(保留字)
| 占位符 | 含义 | 说明 |
|---|---|---|
{pageNo} | 当前页码 | 仅打印时注入,画布预览为空 |
{pageCount} | 总页数 | 同上 |
pageNo/pageCount为保留字,业务字段避免使用同名 key(打印时会被页码覆盖)。
分页范围 pageScope
分页时元素默认每页重复;设置 pageScope: 'last'(属性面板「仅末页」)的元素只在最后一页出现(合计行、签字/盖章区常用)。
完整示例:加工单
// 字段字典
const fields: DataField[] = [
{ key: 'workOrderNo', label: '工单号', group: '基础信息' },
{ key: 'partName', label: '零件名称', group: '基础信息' },
{ key: 'quantity', label: '数量', type: 'number' },
{ key: 'startDate', label: '开工日期', type: 'date' },
{ key: 'totalPrice', label: '加工总价', type: 'amount' },
{ key: 'processes', label: '工序明细', type: 'list' },
]
// 数据实例
const data = {
workOrderNo: 'GJ-20260829-001',
partName: '汽车支架左件',
quantity: '500',
startDate: '2026-08-29',
totalPrice: '¥ 12,600.00', // 宿主预格式化
processes: [
{ name: '铣面', content: '精铣定位面', hours: '2' },
{ name: '钻孔', content: '钻 M6 沉孔 ×4', hours: '3' },
],
}
// 模板绑定
// 标题文本 content = "工单号:{workOrderNo}"
// 二维码 value = "{workOrderNo}"
// 数据表格 dataSource = "processes",列 name / content / hours模板管理
模板是纯 JSON,@save 回调把模板 JSON 交给宿主,由宿主负责持久化。一个单据类型可以有多个模板(例如加工单配了「标准版」「精简版」两个样式),宿主系统需要能知道并管理这些模板。
模板结构
interface Template {
id: string // 模板唯一标识
name: string // 模板名(如「加工单·标准版」)
version: string // 版本号
paper: PaperConfig // 纸张尺寸 / 方向 / 页边距
elements: PrintElement[] // 元素列表(mm 坐标 + 各自属性)
dataFields?: DataField[] // 该模板绑定的字段字典快照
}一个单据类型 → 多个模板
宿主的「模板库」按单据类型(docType)分组管理:
interface TemplateMeta {
template: Template
docType: string // 单据类型:workOrder / purchaseOrder / ...
isDefault: boolean // 是否默认模板
createdAt: string
}模板库
├── workOrder(加工单)
│ ├── 加工单·标准版(默认)
│ └── 加工单·精简版
├── purchaseOrder(采购订单)
│ ├── 采购订单·标准版(默认)
│ └── 采购订单·精简版
└── ...后端接口(建议)
GET /api/print/templates?docType=workOrder → TemplateMeta[] // 查某类型的模板列表
POST /api/print/templates → TemplateMeta // 新增模板
PUT /api/print/templates/:id → TemplateMeta // 更新模板
DEL /api/print/templates/:id → void // 删除模板前端对接流程
import { PrintPreview, PrintDesigner, type Template } from 'm7_print_designer'
// ① 加载某单据类型的模板列表(宿主知道加工单有 2 个模板)
const templateList = await api.listTemplates('workOrder')
// ② 打印时选默认模板渲染预览
const currentTemplate = templateList.find(t => t.isDefault)?.template ?? templateList[0].template
// ③ 预览页「切换模板」:在 2 个模板之间切换
function onTemplateChange(t: Template) {
currentTemplate.value = t
}
// ④ 预览页「配置模板」→ 设计器页编辑 → @save 保存
async function onSave(template: Template) {
const saved = await api.saveTemplate({ template, docType: 'workOrder' })
templateList.push(saved) // 现在加工单有 3 个模板
currentTemplate.value = saved.template
}保存为新模板 vs 更新现有模板
| 场景 | 做法 |
|---|---|
| 保存为新模板 | 生成新 id,POST 新增(保留原模板,形成多样式) |
| 更新现有模板 | 沿用原 id,PUT 覆盖(影响所有引用该模板的单据) |
| 设默认模板 | 更新 isDefault 标记,同类型仅一个默认 |
建议:设计器「保存模板」默认走「保存为新模板」(生成新
id),另提供「覆盖保存」入口更新原模板,避免误改历史模板。
字段契约与版本演进
- 模板保存时记录
dataFields快照,字段key是稳定契约。 - 新增字段:向前兼容,旧模板不受影响。
- 重命名字段:需迁移已存模板中的
{旧字段}占位与列field。 - 删除字段:绑定该字段的元素渲染为空,建议后端保留字段或提供别名。
真实对接案例:MES 加工单
以某 MES 系统的加工任务详情接口为例(moldCode / partCode / basicAffairName / assignedEquipmentName / processTaskStatus / isOutSource 等扁平 camelCase 字段),说明如何适配本组件。
结论:完全可适配。组件绑定「扁平 key + {字段} 占位」,MES 的 camelCase 字段可直接绑定;宿主只需加一层「适配器」完成字段字典 + 预格式化。
字段分类
| 类别 | MES 字段 | 处理方式 |
|---|---|---|
| 直接透传 | moldCode moldName partCode partName basicAffairName source reason remarks | 原样传给 data,{字段} 直接绑定 |
| 枚举映射 | processTaskStatus(0 / 1 / 3) | 宿主映射为中文状态(待加工 / 加工中 / 已完成) |
| 布尔映射 | isOutSource isMaterialArrived | 映射为「外协 / 厂内」「已到料 / 未到料」 |
| 时间格式化 | arrangeStartTime startTime materialArrivedTime(ISO 8601) | 格式化为 YYYY-MM-DD HH:mm |
| 数组处理 | tags(字符串数组)images(图片数组) | tags join 为字符串;images 用图片元素 {images} 多图并排 |
| 忽略 | id moldId partId 等 UUID、concurrencyStamp | 打印用 Name/Code 字段,ID 不打印 |
适配器示例
import type { DataField } from 'm7_print_designer'
const STATUS_MAP: Record<number, string> = { 0: '待加工', 1: '加工中', 2: '暂停', 3: '已完成' } // 以 MES 实际枚举为准
function fmtDateTime(iso: string | null) {
return iso ? iso.replace('T', ' ').slice(0, 16) : ''
}
function adaptWorkOrder(detail: any) {
return {
// 直接透传
moldCode: detail.moldCode,
moldName: detail.moldName,
partCode: detail.partCode,
partName: detail.partName,
basicAffairName: detail.basicAffairName,
assignedEquipmentName: detail.assignedEquipmentName || detail.assignedEquipmentCode,
supplierName: detail.supplierName || '',
source: detail.source || '',
reason: detail.reason || '',
remarks: detail.remarks || '',
// 枚举 / 布尔映射
processTaskStatus: STATUS_MAP[detail.processTaskStatus] ?? '',
isOutSource: detail.isOutSource ? '外协' : '厂内',
isMaterialArrived: detail.isMaterialArrived ? '已到料' : '未到料',
// 时间格式化
arrangeStartTime: fmtDateTime(detail.arrangeStartTime),
arrangeEndTime: fmtDateTime(detail.arrangeEndTime),
startTime: fmtDateTime(detail.startTime),
endTime: fmtDateTime(detail.endTime),
materialArrivedTime: fmtDateTime(detail.materialArrivedTime),
// 数量
estimatedQuantity: detail.estimatedQuantity ?? '',
actualQuantity: detail.actualQuantity ?? '',
// 数组
tags: (detail.tags || []).join('、'),
images: detail.images || [],
}
}
// 字段字典(后端从表结构 + 中文注释生成,见「传入数据的规范」)
const fields: DataField[] = [
{ key: 'moldCode', label: '模具编号', group: '模具信息' },
{ key: 'moldName', label: '模具名称', group: '模具信息' },
{ key: 'partCode', label: '零件编号', group: '零件信息' },
{ key: 'partName', label: '零件名称', group: '零件信息' },
{ key: 'basicAffairName', label: '工序', group: '加工信息' },
{ key: 'assignedEquipmentName', label: '设备', group: '加工信息' },
{ key: 'arrangeStartTime', label: '计划开始', type: 'datetime' },
{ key: 'arrangeEndTime', label: '计划结束', type: 'datetime' },
{ key: 'estimatedQuantity', label: '计划数量', type: 'number' },
{ key: 'actualQuantity', label: '实际数量', type: 'number' },
{ key: 'processTaskStatus', label: '状态' },
{ key: 'isOutSource', label: '是否外协' },
{ key: 'supplierName', label: '供应商' },
{ key: 'isMaterialArrived', label: '是否到料' },
{ key: 'materialArrivedTime', label: '到料时间', type: 'datetime' },
]
// 用法
const detail = await api.getWorkOrderDetail(id)
const data = adaptWorkOrder(detail)
// <PrintDesigner v-model="template" :data="data" :data-fields="fields" />两点注意
- 明细表数据来源:上述接口是「工序级任务」的扁平结构,没有嵌套的工序明细数组。若打印「工序流转卡」需要列出一张模具某零件的全部工序,宿主需按
moldId + partId聚合多个任务组装成数组(数据表格dataSource指向该数组),这是数据组装问题、非组件限制。 - 枚举口径以 MES 为准:
processTaskStatus的取值映射、isOutSource的语义请在宿主侧确认,避免状态/外协口径错误。
完整可运行示例见仓库 src/App.vue(单据列表 → 预览页 → 设计器页 → 保存模板 → 批量打印),或直接体验本页 Demo 示例。