336 lines
16 KiB
Markdown
336 lines
16 KiB
Markdown
# 单据设计与打印调用帮助文档
|
||
|
||
> 适用系统: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/<模块>/<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` 为例:
|
||
|
||
```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` | 建议 | 后端 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=<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 打印页 → 预览正常 → 打印
|