diff --git a/单据设计与打印调用帮助文档.md b/单据设计与打印调用帮助文档.md new file mode 100644 index 0000000..04760ff --- /dev/null +++ b/单据设计与打印调用帮助文档.md @@ -0,0 +1,335 @@ +# 单据设计与打印调用帮助文档 + +> 适用系统: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 打印页 → 预览正常 → 打印