16 KiB
单据设计与打印调用帮助文档
适用系统:F9 智慧缫丝系统(MES) 本文档说明两类能力:
- 单据页(表单录入界面):全屏单据风格的新增/编辑界面,如何设计与接入
- 打印(单据套打):打印模板如何设计、前端如何调用打印
一、总体架构
web(Vue3 MES 前端,端口 5173)
├─ 列表页 CrudPage.vue(通用列表 + 操作列按钮)
│ ├─ "新增/编辑" → BILL_MAP 路由 → 全屏单据页(BillPage.vue 或专用 xxx-bill.vue)
│ ├─ "打印" → PRINT_MAP 模板名 → 后端查模板 id → window.open(openprint 打印页)
│ └─ "审批" → WORKFLOW_MAP 发起流程
│
├─ 单据页
│ ├─ web/src/components/BillPage.vue (配置驱动通用单据页,路由 /bill/:table/:id?)
│ └─ web/src/views/<模块>/<xxx>-bill.vue (专用全屏单据页,原料 4 表为代表)
│
server(.NET F9MES 后端,端口 5136)
├─ /api/print/templates 模板 CRUD(存 Common_PrintTemplate / RepCenter_ReportDesign)
├─ /api/print/data/{表}/{id} 打印数据(关联名称 + 枚举文本自动补全)
└─ 通用 CRUD /api/xxx 单据页表单数据来源(meta 驱动)
openprint(打印渲染服务,端口 5227)
├─ 打印设计器(图形化设计模板,可导 JSON)
├─ 打印页 PrintPage.vue:解析 URL 参数 → 拉模板+数据 → 渲染 → 调系统打印对话框
└─ 静默打印 createHeadless.ts(iframe + srcdoc + window.print())
二、设计单据页(表单录入界面)
系统提供两种单据页方式,推荐优先用「配置驱动通用单据页」,只有字段交互非常特殊的表才写专用 vue。
方式 A:配置驱动通用单据页(推荐,全系统 42 张表已接入)
只需改两个配置文件,无需新建任何 vue 文件。
第 1 步:web/src/config/bill-configs.js 添加布局配置
以 Process_Trial 为例:
export const BILL_CONFIGS = {
Process_Trial: {
title: '样茧试缫单', // 页面标题(单据名称)
codeField: 'billNo', // 单号字段(自动生成编号、顶部展示)
statusField: 'status', // 状态字段(可选,自动在顶部显示状态标签)
groups: [
{ title: '试缫信息', fields: ['billNo', 'trialType', 'zhuangkouId', 'processZhuangkouId', 'trialDate', 'trialMan'] },
{ title: '试缫数据', fields: [{ name: 'dataJson', span: 16 }] } // 长文本字段占 2/3 行
]
},
// ... 其他表
}
字段 span 规则(每行 12 栅格,即 24 宽):
| span | 占宽 | 适用 |
|---|---|---|
| 8(默认) | 1/3 行(每行 3 列) | 常规字段 |
| 16 | 2/3 行 | 较长的文本字段 |
| 24 | 整行 | 备注、JSON 明细等长内容 |
字段名统一用 camelCase(与后端 meta/data 接口一致)。
第 2 步:web/src/config/table-map.js 的 BILL_MAP 加一行
export const BILL_MAP = {
'RawMaterial/outstock': '/rawmaterial/outstock-bill', // 专用单据页(方式 B)
'Process/trial': '/bill/Process_Trial', // 通用单据页:/bill/<表名>
// ...
}
路由 /bill/:table/:id? 已在 web/src/router/index.js 注册,无需再配。
接入后自动获得的能力:
- 列表页「新增」→ 打开
/bill/<表名>(无 id),「编辑」→ 打开/bill/<表名>/<id> - 表单字段控件类型由后端 meta 自动判断(输入框/数字/日期/下拉/多行文本)
- 关联字段(
*Id)自动拉取 refs 数据源生成下拉框 statusField自动在顶部显示状态标签;codeField新增时自动gencode- 保存逻辑通用:剔除系统字段 + 空值;新增成功后自动
router.replace到带 id 路径
方式 B:专用全屏单据页(原料 4 表做法)
适用:字段有特殊联动、复杂校验、作废/审批等定制按钮的核心业务单据。
参考文件:web/src/views/rawmaterial/outstock-bill.vue(最早先例)、zhuangkou-bill.vue、instock-bill.vue、inspect-bill.vue。
接入步骤:
- 新建
web/src/views/<模块>/<xxx>-bill.vue,按下方布局规范编写 table-map.js的BILL_MAP注册:'RawMaterial/outstock': '/rawmaterial/outstock-bill'router/index.js注册静态路由(meta: { hidden: true }不进菜单):{ path: '/rawmaterial/outstock-bill/:id?', // :id 可选:无 id 新增,带 id 编辑 name: 'RawMaterialOutstockBill', component: () => import('@/views/rawmaterial/outstock-bill.vue'), meta: { title: '原料出库单', hidden: true } }- 页面内处理:无
id为新增(可用gencode生成单号),带id时加载数据走crudUpdate
单据页布局规范(A、B 通用,全系统统一)
- 全页
max-width: 1360px; padding: 18px 24px,横向充分扩展 - 深色顶部工具条:返回按钮 + 页面标题 + 单据编号 + 状态标签(右上可有打印/作废等操作按钮)
- 按语义分组为多张卡片,每行 3 列(
el-col :span="8"),栅格gutter: 36 - 卡片 body
padding: 30px 36px;表单项margin-bottom: 28px;输入控件内边距加大 - 底部居中操作栏(取消 / 保存)
- 基础/简单表(warehouse/stock/team/machine 等台账类)不接单据页,用通用宽弹窗(
CrudPage.vue已按字段数自适应:≤6 字段 720px / ≤12 字段 1000px / >12 字段 1240px,全局类.crud-dialog)
三、设计打印模板
3.1 模板库与接口
模板存储在后端,表 Common_PrintTemplate(展示实体 RepCenter_ReportDesign),接口:
| 接口 | 说明 |
|---|---|
GET /api/print/templates |
列表({ items, total },不含 content) |
GET /api/print/templates/{id} |
详情(含 content,即模板 JSON 字符串) |
POST /api/print/templates |
创建({ name, content }) |
PUT /api/print/templates/{id} |
全量更新 |
DELETE /api/print/templates/{id} |
删除 |
鉴权复用 F9 JWT(Authorization: Bearer <token>)。注意:这些接口返回裸 JSON(非 ApiResult 信封)。
3.2 模板 JSON 结构
content 是一个 JSON 字符串,完整示例见 server/scripts/print-template-outstock.json(三等分原料出库单,纸张 210×99mm):
{
"name": "三等分原料出库单",
"content": {
"version": "1.0",
"document": {
"type": "report",
"page": {
"width": 210, "height": 99, "unit": "mm",
"orientation": "portrait",
"margin": { "top": 8, "bottom": 8, "left": 8, "right": 8 }
},
"sections": [
{
"type": "body",
"components": [
{
"id": "txt-title",
"type": "text",
"left": 0, "top": 0, "width": 194, "height": 10,
"value": "原料出库单",
"style": { "fontSize": 15, "fontWeight": "bold", "textAlign": "center" }
}
]
}
]
}
}
}
3.3 组件(components)类型
| type | 说明 | 关键属性 |
|---|---|---|
text |
文本/字段值 | value(固定文本)或 binding(绑定字段);style(fontSize/fontWeight/textAlign);format(格式化) |
line |
横线(分隔线/签字线) | left/top/width/height;stroke(颜色,如 #333333) |
所有组件用毫米定位:left / top / width / height。
3.4 字段绑定与格式化
binding格式:表名.字段名(PascalCase),例如:"RawMaterial_OutStock.BillNo"—— 出库单号"RawMaterial_OutStock.OutWeight"—— 出库重量"RawMaterial_OutStock.OutDate"—— 出库日期
- 小数格式化:
"format": { "kind": "decimal", "digits": 2 } - 日期格式化:
"format": { "kind": "date", "pattern": "YYYY-MM-DD" } - 模板绑定字段与数据的大小写兼容:数据接口返回 camelCase + PascalCase 双别名,模板统一用 PascalCase 即可
- 关联名称 / 枚举文本由后端自动补全(无需在模板里写关联逻辑):
*Id字段 → 自动注入{Xxx}Code/{Xxx}Name,如ZhuangkouId→ZhuangkouCode、OutOrgId→OutOrgName- 枚举字段 → 自动注入
{Prop}Text,如OutType→OutTypeText(领料/退货/报损) - 支持的关系见
PrintApiController.cs中的RefMap与EnumMaps,需要新关系时在此扩展
3.5 创建模板的两种方式
方式 1:打印设计器(图形化,推荐日常使用)
打开 openprint 应用 http://localhost:5227,进入设计器拖拽组件、绑定字段,设计完成后保存到模板库(走 /api/print/templates)。
方式 2:JSON 手写 + 种子脚本(适合快速造模板)
cd server/scripts
node seed-print-template.mjs # 读取 print-template-xxx.json,登录→查重→POST 创建
脚本内容:登录获取 token → GET /templates 查同名是否已存在 → 不存在则 POST /templates 创建。用 node 执行可避免 PowerShell 中文编码问题。
四、如何调用打印
4.1 列表页「打印」按钮(PRINT_MAP 配置)
通用列表页(CrudPage.vue)自动按配置显示「打印」按钮,只需在 web/src/config/table-map.js 加配置:
export const PRINT_MAP = {
'RawMaterial/outstock': { templateName: '三等分原料出库单', title: '原料出库单' }
}
templateName 必须与模板库里模板的 name 完全一致。配置后列表操作列自动出现「打印」按钮。
点击后的执行流程(CrudPage.vue 的 onPrint):
GET /print/templates拉模板列表,按templateName找到模板 id- 找不到 → 提示「未找到打印模板,请先在打印设计器创建」
- 找到 →
window.open(buildPrintUrl({ template: id, table: 表名, row: 行id, token }))打开 openprint 打印页
4.2 专用单据页内「打印」按钮
在专用单据页(如 outstock-bill.vue)里自行实现 onPrint(参考 305-324 行):
import { buildPrintUrl } from '@/config/print'
import { useUserStore } from '@/stores/user'
const PRINT_TEMPLATE = '三等分原料出库单' // 模板名
async function onPrint() {
const res = await request.get('/print/templates')
const items = res.items || []
const tpl = items.find((t) => t.name === PRINT_TEMPLATE)
if (!tpl) return ElMessage.warning(`未找到打印模板「${PRINT_TEMPLATE}」`)
const url = buildPrintUrl({
template: tpl.id,
table: TABLE, // 如 'RawMaterial_OutStock'
row: billId.value, // 当前单据 id
token: useUserStore().token
})
window.open(url, '_blank')
}
4.3 buildPrintUrl 参数(web/src/config/print.js)
| 参数 | 必填 | 说明 |
|---|---|---|
template |
是 | 模板 id(数字) |
table |
条件必填 | 数据表名(如 RawMaterial_OutStock);改用 data 直传时可不填 |
row |
条件必填 | 数据行 id;改用 data 直传时可不填 |
token |
建议 | 后端 JWT(openprint 打印页取模板/数据时用 Bearer 鉴权) |
api |
否 | 后端地址,默认 PRINT_API_BASE(env VITE_PRINT_API_BASE,默认 http://localhost:5136) |
data |
否 | URL-safe Base64 的 JSON 数据(直传数据,跳过接口拉取) |
4.4 openprint 打印页执行流程(openprint/src/print/PrintPage.vue)
- 解析 URL 参数:
template(或tpl)、table、row(或id)、token、api、data - 取数据:有
data参数则 base64 解码直用;否则GET {api}/api/print/data/{table}/{row}(带 Bearer) - 数据归一化:顶层
{ 表名: 行 }结构,行内补 PascalCase 别名 createHeadless({ repository })→buildRequest(templateId, data)→headless.print():- 渲染出 HTML(页面尺寸、字体就绪)
- 创建瞬态 iframe(
op-silent-print-frame类:屏幕下隐藏、@media print显示在左上角) - 调系统打印对话框,完成/取消后移除 iframe
- 页面提供「重新打印」「关闭」按钮
打印页可直接用浏览器访问调试:
http://localhost:5227/?print=1&template=2&table=RawMaterial_OutStock&row=2&token=<token>&api=http://localhost:5136
4.5 打印数据接口
GET /api/print/data/{table}/{id}(见 PrintApiController.cs):
- 返回
{ 表名: { ...行数据 } }结构(裸 JSON) - 自动注入关联
{Xxx}Code / {Xxx}Name与枚举{Xxx}Text - 未配置新表也能直接打:模板绑定的是「表名.字段名」,数据接口按表通用返回
五、常见问题排查
| 现象 | 排查方向 |
|---|---|
| 列表页没有「打印」按钮 | PRINT_MAP 是否配置了该菜单 component |
| 点击打印提示「未找到打印模板」 | 模板库里是否已创建同名模板(GET /api/print/templates 确认 name 完全一致) |
| 打印预览空白 | ① openprint 服务(5227)是否在线;② 浏览器打印对话框对屏幕外 iframe 兼容性问题——已通过 @media print + top:0 !important 修复,需刷新页面/强刷缓存;③ 模板 content 是否为空 |
| 打印出来是 A4 而不是三等分 | 模板 page.width/height 是否 210×99mm;打印对话框纸张方向/纸型是否选对 |
| 打印字段为空白 | ① binding 的 表名.字段名 是否 PascalCase 正确;② 数据接口 /api/print/data/{表}/{id} 是否返回该字段;③ 关联/枚举字段确认注入名(XxxName / XxxText) |
| 单据页保存后跳转不对 | 确认 BILL_MAP value 是 /bill/<表名>(通用)还是专用路由;专用路由须在 router/index.js 注册 |
六、关键文件清单
| 文件 | 作用 |
|---|---|
web/src/config/bill-configs.js |
通用单据页布局配置(title/codeField/statusField/groups) |
web/src/config/table-map.js |
BILL_MAP(单据页接入)、PRINT_MAP(打印按钮)、WORKFLOW_MAP(审批按钮) |
web/src/components/BillPage.vue |
配置驱动通用单据页(路由 /bill/:table/:id?) |
web/src/views/rawmaterial/outstock-bill.vue |
专用全屏单据页参考实现 |
web/src/config/print.js |
buildPrintUrl() 构造 openprint 打印页 URL |
web/src/components/CrudPage.vue |
通用列表页(操作列 新增/编辑/打印/审批 逻辑) |
web/src/router/index.js |
静态路由(专用单据页、/bill/:table/:id?) |
server/src/F9MES.Api/Controllers/PrintApiController.cs |
模板/数据接口 + 关联/枚举补全映射 |
server/src/F9MES.Application/Print/PrintService.cs |
打印服务(模板存取) |
server/scripts/print-template-outstock.json |
模板 JSON 参考示例 |
server/scripts/seed-print-template.mjs |
模板种子脚本(登录→查重→创建) |
server/scripts/verify-print-visual.mjs |
打印页视觉回归验证脚本(Playwright 截图) |
openprint/src/print/PrintPage.vue |
openprint 打印页(URL 参数 → 模板+数据 → 打印) |
openprint/src/core/headless/createHeadless.ts |
静默打印引擎(iframe 渲染 + 系统打印对话框) |
七、新增一个"可打印单据"的最小步骤清单
- 后端实体 + 通用 CRUD(已有则跳过)
- 表单单据页:
bill-configs.js加配置 +BILL_MAP加一行(两步走) - 打印模板:设计器(或 JSON 种子脚本)创建模板,记录模板名
- 打印按钮:
PRINT_MAP加一行'模块/页面': { templateName: '<模板名>', title: '<单据标题>' } - 验证:列表点「打印」→ 弹出 openprint 打印页 → 预览正常 → 打印