# 方格设计器(JPrint 打印模板设计器) **Repository Path**: nzdm/jprint_td ## Basic Information - **Project Name**: 方格设计器(JPrint 打印模板设计器) - **Description**: 为设计器起了一个名字,叫【方格】。打印模板设计器,JPrint 控件扩展功能 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 8 - **Created**: 2026-09-27 - **Last Updated**: 2026-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # print-designer JPrint 打印控件的前置设计层:**模板 JSON → 版面 → 自动分页 → PDF / PNG / 打印**。 在原有打印控件之上补上"版面从哪来"这一层。控件本身几乎不用改。 ![](img_1.png) ![](img_2.png) ## 模块结构 | 模块 | 技术 | 职责 | 关键约束 | |---|---|---|---| | `render-core` | 纯 Java(jackson + pdfbox + zxing) | 模板 + 数据 → 布局 → 分页 → PDF/PNG | **禁止依赖 Spring**,须能同时被服务端与 JavaFX 客户端引用 | | `designer-server` | Spring Boot 2.7 | 模板 CRUD + 渲染 API + 打印调度 + 托管设计器 | 只做装配,不写布局绘制代码 | | `designer-ui` | Vue3 + TS + Vite + Naive UI | 可视化拖拽设计器,产出静态产物 | 无 Java 依赖;与 `render-core` 的 Schema 必须严格对齐 | | `samples` | Java | 示例模板与渲染验证入口 | — | ## 构建与运行 ```bash # 构建(本机 mvn 脚本损坏,用封装脚本) powershell -ExecutionPolicy Bypass -File mvn-helper.ps1 install -DskipTests # 启动 java -jar designer-server/target/designer-server.jar --server.port=2025 ``` > **端口注意**:本机沙箱/部分环境会注入 `SERVER_PORT` 环境变量,覆盖 `application.properties` 里的 > `server.port`。若发现服务监听的端口与配置不符,用命令行参数 `--server.port=2025` 显式指定(优先级最高)。 启动后: - **设计器**: - 接口根路径:`/api/print/**` 设计器的产物是构建后拷进 `designer-server/src/main/resources/static/designer/` 的, 所以只要起 `designer-server` 一个服务,设计器与打印渲染就一起有了——这正是"一体化交付"的形态。 ### 设计器开发 ```bash cd designer-ui npm install npm run dev # 开发模式,:5173,/api 自动代理到 127.0.0.1:2025 npm run typecheck # vue-tsc 全量类型检查 npm run deploy # 构建并拷贝到 designer-server 的静态目录 ``` 改完设计器后需要重新 `npm run deploy` 并重建 `designer-server`,静态资源才会进 jar。 > **环境提示**:部分沙箱环境里 Node 的 `fs.cpSync` 会被中断,导致 `npm run deploy` 的拷贝步骤静默失败 > (脚本显示成功但目标目录为空)。遇到这种情况用 `cp -r dist/. <目标目录>/` 手动拷贝即可。 模板存储默认落在 `./data/templates`,可用 `-Ddesigner.templates.root=<绝对路径>` 覆盖。 库为空时会自动导入内置示例模板 `LAB_REPORT`。 ## 接口 统一 JSON 响应体:`{ "code": 0, "message": "ok", "data": ... }`,`code != 0` 即失败。 二进制接口(PDF/PNG)直接返回字节流,不套这一层。 ### 模板管理 `/api/print/templates` | 方法 | 路径 | 说明 | |---|---|---| | GET | `/` | 列表,支持 `?category=`、`?keyword=` | | GET | `/{code}` | 详情:`{ meta, content }`,`content` 为模板原始 JSON 文本 | | POST | `/` | 新建 | | PUT | `/{code}` | 保存(产生新版本) | | DELETE | `/{code}` | 删除(软删进 `_trash`,可恢复) | | PUT | `/{code}/status` | 切换草稿/发布,不产生新版本 | | GET | `/{code}/versions` | 历史版本号列表 | | GET | `/{code}/versions/{version}` | 查看指定历史版本 | | POST | `/{code}/rollback?version=n` | 回滚:旧内容作为新版本保存,回滚本身可回滚 | 保存请求体: ```json { "template": { "schemaVersion": "1.0", "meta": { "code": "LAB_REPORT", "name": "检测报告" }, "page": {}, "bands": [] }, "status": "published", "expectedVersion": 3 } ``` `expectedVersion` 是**乐观锁**:与服务端当前版本不一致时返回 **409**。设计器必须把读到的版本号回传, 否则两人同时编辑会互相静默覆盖。 ### 渲染 `/api/print/render` | 方法 | 路径 | 说明 | |---|---|---| | POST | `/pdf` | 渲染 PDF,直接下载 | | POST | `/png` | 渲染指定页 PNG(`pageIndex` 从 0 起) | | POST | `/pagecount` | 只算页数,开销远小于出图 | | POST | `/preview` | 多页 PNG,返回 dataURI 数组(设计器预览走这个) | 请求体(`templateCode` 与 `template` 二选一): ```json { "templateCode": "LAB_REPORT", "data": { "reportNo": "WT2026-0012", "items": [] }, "dpi": 110, "maxPages": 8, "fileName": "检测报告", "inlineDownload": false } ``` `inlineDownload=true` 时 `Content-Disposition: inline`,浏览器内直接预览而不下载。 ### 本地打印 `/api/print/local` | 方法 | 路径 | 说明 | |---|---|---| | GET | `/status` | 探测控件是否在线,返回打印机列表 | | GET | `/printers` | 打印机列表;控件不在线返回 **503**(不返回空列表,避免误判为"没有打印机") | | POST | `/print` | 模板 + 数据 → PDF → 提交给 JPrint 静默打印 | ```json { "templateCode": "LAB_REPORT", "data": {}, "printer": "HP LaserJet Tank 1020w", "copies": 1, "paperSize": "A4", "direction": "portrait", "sessionId": "用于 WebSocket 状态回传" } ``` ⚠️ 三个必须知道的点: 1. **`/print` 是真的打印**,调用即提交到打印机,没有二次确认。前端必须做确认弹窗。 2. 返回成功仅代表**已提交**给打印机。真实打印阶段由 JPrint 通过 WebSocket 回传,需要 `sessionId` 才能收到。 3. 控件离线时 `/print` 返回 **503**(不是 500),前端据此降级到浏览器打印。 ### 示例数据 `/api/print/samples` | 方法 | 路径 | 说明 | |---|---|---| | GET | `/` | 内置示例数据名字列表 | | GET | `/data?name=` | 取示例数据,供设计器"试数据预览" | ### 模板包 `/api/print/packs` 把"手动拷 JSON 文件同步模板"产品化。导出得到一个自描述 zip,收到包的人用解压软件就能看清内容: ``` manifest.json 包清单:schemaVersion / exportedAt / templates[] 摘要 templates/{CODE}/template.json 模板正文(与库内字节一致,可 diff) templates/{CODE}/meta.json 元信息(版本/状态/时间) assets/... 模板引用的静态资源(预留;v1 图片建议 dataURI/URL) ``` | 方法 | 路径 | 说明 | |---|---|---| | POST | `/export` | 导出。body `{ "codes": [...] }`,空数组 = 全部;返回 zip 二进制 | | POST | `/precheck` | 导入预检(multipart `file`)。只解析比对不落库,逐模板给出 `new / same / packNewer / repoNewer / invalid` | | POST | `/import` | 正式导入(multipart `file` + `strategy`)。返回与预检同构的报告,附每个模板实际执行的动作 | 导入策略(对"库内已存在同名模板"生效,新模板无论哪种策略都会导入): | 策略 | 行为 | |---|---| | `overwrite`(默认) | 覆盖并**版本递增**,历史版本照常保留,状态不变 | | `rename` | 库内不动,包内模板换新编码(`{CODE}_imp`、`_imp2`…)导入 | | `skip` | 跳过已有,只导新模板 | 新导入的模板一律 **draft**(导入不该触发"静默发布");覆盖导入走 `repository.save`, 编码白名单、原子写、版本递增等存储层护栏自动生效。 ## 设计器 三栏:**左(元素库 + 文档结构)→ 中(画布)→ 右(属性面板)**。 ### 核心概念:带(Band) 模板不是"一张画布",而是若干**带**的顺序堆叠。这和渲染内核的页面分区模型一一对应, 使用者看到的排布顺序就是文档流顺序: | 带 | 重复语义 | 典型内容 | |---|---|---| | 页眉带 | 每页重复 | 机构名、logo、验证二维码 | | 标题带 | 仅首页 | 文档标题、单据编号 | | 列头带 | 每页重复 | 表格表头 | | 明细带 | 按数据循环 | 表格行(**唯一会触发分页的带**) | | 汇总带 | 仅末尾一次 | 合计、结论、签章 | | 页脚带 | 每页重复 | 页码、打印时间 | ### 表格元素(table) 「列头带 + 明细带」手工拼装能做出表格的样子,但列宽、跨页、小计要自己操心。 `table` 是一个专用元素,放在明细带里,数据源是元素自己的 `dataSet`: | 能力 | 说明 | |---|---| | 列宽混合求解 | 固定毫米 / `auto` 均分剩余 / `"16%"` 百分比三种形态统一求解,列与列不会错位 | | 表头逐页重复 | `headerRepeat`;孤表头保护:表头后放不下 `minRowsAfterHeader` 行时整体换页 | | 行高 | 固定毫米或 `auto`(按单元格文字换行,`minRowHeight` 兜底) | | 整行不拆 | 默认整行跨页时整体挪到下一页;`allowRowSplit=true` 时超高行按文字行切开续排(拆分段不闭合下边框) | | 分组小计 | `groupBy`(数据需已按它排序)+ 分组标题行 + 每组小计;聚合列支持 `sum/count/avg/max/min` 与格式化 | | 合计 | `grandTotal` 表尾总计行;`labelColumn` 指定"小计/合计"标签落在第几列(序号列太窄放不下标签时用) | | 页尾收尾保护 | `keepClosingRows`:小计/合计会被挤到下页时,让它前面至少 `minRowsBeforeClosing` 行数据一起走——避免"最后一页只剩一行合计" | | 斑马纹 / 边框 | `zebra` 底色;外框/内线/表头底线可独立开关 | 设计器侧:元素库拖入、列编辑器(增删列、排序、宽度三态、聚合、对齐、字段插入)、 分组/小计/边框属性面板;画布上渲染表头 + 前几行示例数据用于摆位, **真实分页与小计仍以预览为准**。示例见 `samples`(TABLE_SUMMARY,46 行 4 组)。 ### 选择与拖动 | 操作 | 行为 | |---|---| | 带内空白处按下拖动 | 框选,矩形碰到即选中(实时反馈) | | `Shift` / `Ctrl` + 点击元素 | 加选 / 减选。**多选只在同一个带内成立**——元素坐标各自相对带顶,跨带没有公共坐标系 | | 拖动任意一个选中元素 | 整组一起走。吸附作用在整组包围盒的左/右/中三条边上,组内相对间距不变 | | 拖到**另一个带**上 | 目标带绿色虚线高亮;松手整组搬过去,y 取鼠标落点 | | 拖到**页签**上 | 页签绿色虚线高亮;松手整组搬到该页签的**同类带**里,目标页签没有同类带会自动补一个 | | 拖动过程 | 深色 HUD 显示 `x,y` / `宽×高` / 已选个数 / 吸附到哪条线 / 跨带落点 | | 右侧结构树里拖动元素条目 | 调整层序(顺序 = 绘制顺序,靠后在上层)。条目可单击选中,解决"画布上点不中下层元素" | 拖动期间不会改动带的归属,**松手才落位**;误拖到别的带或页签上,松手后 `Ctrl+Z` 即可整组撤回 (一次拖动只产生一个撤销点,不会把中间过程全塞进历史栈)。 ### 编辑态 vs 预览:为什么要分开 画布上只有**一页**,明细带只显示**一行**。这是刻意的: - 编辑态用 DOM 渲染,字体度量来自系统字体;渲染内核用嵌入的 TTF 度量。 两者对同一段中文的宽度判断会有毫米级出入,**前端"假装分页"只会骗人**。 - 所以真实分页一律点「预览」,由后端渲染内核出图。预览用的渲染路径与实际打印、 导出 PDF 完全相同,**预览看到的就是打印出来的**。 ### 表达式 | 语法 | 说明 | |---|---| | `${field}` | 取字段值 | | `${a.b.c}` `${items[0].name}` | 逐级取值 / 数组下标 | | `${field\|yyyy-MM-dd}` | 日期格式化 | | `${field\|#,##0.00}` | 数字格式化 | | `${field?:默认值}` | 值为空时的兜底 | | `${#index}` `${#rowCount}` `${#isLast}` | 行序号(从 1 起)/ 总行数 / 是否末行 | `#index` 这类**保留变量**由分页器在循环时注入,业务数据不需要自己准备序号列。 ### 数据源不写进模板 设计器顶部的「试数据」只用于**设计期看效果**,不会被固化进模板 JSON。 生产渲染时数据由调用方传入: ``` POST /api/print/render { "templateCode": "LAB_REPORT", "data": { ...业务数据... } } ``` 这样模板才能跨环境复用——模板里硬编码 `http://192.168.1.10:8080/...` 一换环境就废。 ### 快捷键 | 按键 | 作用 | |---|---| | `Delete` / `Backspace` | 删除选中元素(多选时整批删) | | 方向键 | 微调位置 0.5mm(多选时整组一起走) | | `Shift` + 方向键 | 微调位置 1mm | | `Ctrl/Cmd + A` | 全选当前带的元素 | | `Esc` | 取消选择 | | `Shift` / `Ctrl` + 点击元素 | 加选 / 减选 | | `Ctrl+Z` / `Ctrl+Y` | 撤销 / 重做(一次拖动 = 一个撤销点) | | `Ctrl+S` | 保存 | | 拖拽时按 `Alt` | 关闭吸附(自由定位) | | 双击元素 | 画布内直接改文本内容 | ### 协作保护 - 保存带**版本乐观锁**(`expectedVersion`)。两人同编时后保存者会收到 **409**, 设计器弹出「重新打开 / 继续编辑」的选择,而不是静默覆盖。 - 「保存」产生新版本;「发布 / 转草稿」不产生版本。草稿不会被业务系统拉到。 - 删除是**软删**(移入服务端 `_trash/`),可手动恢复。 ## 存储结构 一模板一目录(因为要放版本快照与引用资源): ``` data/templates/ ├── LAB_REPORT/ │ ├── template.json 模板正文(唯一必需文件) │ ├── meta.json 名称/分类/版本/状态/时间 │ └── versions/v1.json 历史版本快照 ├── _trash/ 软删,可恢复 └── assets/ 模板引用的图片等资源 ``` 三道护栏: - **编码白名单** `^[A-Za-z0-9_-]{1,64}$` —— 编码会拼进路径,不校验等于送人一个目录穿越漏洞; - **原子写** —— 先写 `.tmp` 再 `ATOMIC_MOVE`,避免写一半崩溃留下坏文件,也让人工同步文件时不易互相踩; - **乐观锁** —— 见上文 `expectedVersion`。 想把模板从一台机器同步到另一台,直接拷目录即可;更推荐用「模板包导出/导入」 (设计器模板库弹窗里有按钮,接口见上文 `/api/print/packs`)——带预检与冲突策略,不会覆盖错。 ## 冒烟测试 ```bash powershell -ExecutionPolicy Bypass -File smoke-test.ps1 ``` 覆盖模板读取、页数统计、60 行明细自动分页、预览、PDF 导出、乐观锁 409、编码防护 400、控件探测。 报告输出到 `target/smoke-report.txt`。 > 该脚本**不会**调用 `/api/print/local/print`——那个接口会真的出纸。 ## 状态与后续 已完成: - **渲染内核**:流式分页、表头逐页重复、页码回填、表格跨页、二维码/条码/图片、中文 TTF/TTC 字体嵌入;`#index` 等保留变量 - **表格元素**:列宽三态混合求解、表头逐页重复/孤表头保护、整行不拆与超高行续排、 分组标题/小计/合计(聚合 + 格式化 + 标签列)、页尾收尾保护、斑马纹与分段边框 - **模板存储**:JSON 文件一模板一目录、原子写、版本快照、软删、乐观锁 - **模板包**:导出 zip(manifest + 正文 + 元信息)、导入预检冲突比对、覆盖/改名/跳过三策略 - **接口**:模板 CRUD、渲染(PDF / PNG / 页数 / 多页预览)、模板包、打印双通道转发 - **设计器**:三栏可视化编辑、拖拽与缩放(含**框选/多选、跨带与跨页签拖动、结构树层序拖动**、 拖动 HUD 读数)、对齐吸附、撤销重做、字段插入、多页签(一张模板打印多张内容不同的纸)、 后端真实分页预览、模板库与版本回滚、静默打印带确认、表格列编辑器、模板包导入导出 待做: - `fixed` 固定版面(标签、面单)——Schema 已留 `pageType` 字段,渲染未实现 - 富文本、区块循环(`loop`)、图片列表(`imageList`)元素 - PDF 转 PNG 底图做"预览 100% 一致"校验 - 设计器产物瘦身:当前 gzip 后约 200KB(Naive UI 占大头)