Files
F9Web/单据设计与打印调用帮助文档.md

336 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 单据设计与打印调用帮助文档
> 适用系统:F9 智慧缫丝系统(MES)
> 本文档说明两类能力:
> 1. **单据页(表单录入界面)**:全屏单据风格的新增/编辑界面,如何设计与接入
> 2. **打印(单据套打)**:打印模板如何设计、前端如何调用打印
---
## 一、总体架构
```
webVue3 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.tsiframe + 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/<表名>/<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`
**接入步骤:**
1. 新建 `web/src/views/<模块>/<xxx>-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 <token>`)。注意:这些接口返回**裸 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` | 建议 | 后端 JWTopenprint 打印页取模板/数据时用 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=<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 打印页 → 预览正常 → 打印