# 单据设计与打印调用帮助文档 > 适用系统:F9 智慧缫丝系统(MES) > 本文档说明两类能力: > 1. **单据页(表单录入界面)**:全屏单据风格的新增/编辑界面,如何设计与接入 > 2. **打印(单据套打)**:打印模板如何设计、前端如何调用打印 --- ## 一、总体架构 ``` 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/<模块>/-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` 为例: ```js 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` 加一行** ```js 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/<表名>/` - 表单字段控件类型由后端 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`。 **接入步骤:** 1. 新建 `web/src/views/<模块>/-bill.vue`,按下方布局规范编写 2. `table-map.js` 的 `BILL_MAP` 注册:`'RawMaterial/outstock': '/rawmaterial/outstock-bill'` 3. `router/index.js` 注册静态路由(`meta: { hidden: true }` 不进菜单): ```js { path: '/rawmaterial/outstock-bill/:id?', // :id 可选:无 id 新增,带 id 编辑 name: 'RawMaterialOutstockBill', component: () => import('@/views/rawmaterial/outstock-bill.vue'), meta: { title: '原料出库单', hidden: true } } ``` 4. 页面内处理:无 `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 `)。注意:这些接口返回**裸 JSON**(非 ApiResult 信封)。 ### 3.2 模板 JSON 结构 `content` 是一个 JSON 字符串,完整示例见 `server/scripts/print-template-outstock.json`(三等分原料出库单,纸张 210×99mm): ```json { "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 手写 + 种子脚本(适合快速造模板)** ```bash 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` 加配置: ```js export const PRINT_MAP = { 'RawMaterial/outstock': { templateName: '三等分原料出库单', title: '原料出库单' } } ``` `templateName` 必须与模板库里模板的 `name` 完全一致。配置后列表操作列自动出现「打印」按钮。 **点击后的执行流程**(`CrudPage.vue` 的 `onPrint`): 1. `GET /print/templates` 拉模板列表,按 `templateName` 找到模板 id 2. 找不到 → 提示「未找到打印模板,请先在打印设计器创建」 3. 找到 → `window.open(buildPrintUrl({ template: id, table: 表名, row: 行id, token }))` 打开 openprint 打印页 ### 4.2 专用单据页内「打印」按钮 在专用单据页(如 `outstock-bill.vue`)里自行实现 `onPrint`(参考 305-324 行): ```js 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`) 1. 解析 URL 参数:`template`(或 `tpl`)、`table`、`row`(或 `id`)、`token`、`api`、`data` 2. 取数据:有 `data` 参数则 base64 解码直用;否则 `GET {api}/api/print/data/{table}/{row}`(带 Bearer) 3. 数据归一化:顶层 `{ 表名: 行 }` 结构,行内补 PascalCase 别名 4. `createHeadless({ repository })` → `buildRequest(templateId, data)` → `headless.print()`: - 渲染出 HTML(页面尺寸、字体就绪) - 创建瞬态 iframe(`op-silent-print-frame` 类:屏幕下隐藏、`@media print` 显示在左上角) - 调系统打印对话框,完成/取消后移除 iframe 5. 页面提供「重新打印」「关闭」按钮 **打印页可直接用浏览器访问调试**: ``` http://localhost:5227/?print=1&template=2&table=RawMaterial_OutStock&row=2&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 渲染 + 系统打印对话框) | --- ## 七、新增一个"可打印单据"的最小步骤清单 1. 后端实体 + 通用 CRUD(已有则跳过) 2. **表单单据页**:`bill-configs.js` 加配置 + `BILL_MAP` 加一行(两步走) 3. **打印模板**:设计器(或 JSON 种子脚本)创建模板,记录模板名 4. **打印按钮**:`PRINT_MAP` 加一行 `'模块/页面': { templateName: '<模板名>', title: '<单据标题>' }` 5. 验证:列表点「打印」→ 弹出 openprint 打印页 → 预览正常 → 打印