PrintDot Client 对接指南 展开目录 ← 返回文档列表 加载中... # PrintDot Client 使用指南与规范 ## 1. 项目简介 **PrintDot Client** 是一款面向本地打印场景设计的桌面端打印助手。它充当浏览器(或其他客户端)与操作系统打印机之间的桥梁,通过 WebSocket 协议接收打印指令,并调用系统打印机完成打印。 主要功能: - 自动获取操作系统已安装的打印机列表。 - 启动 WebSocket 服务监听打印请求(默认端口 1122)。 - 支持自定义服务端口和安全密钥(Secret Key)。 - **打印内容要求**:接收 Base64 编码的 PDF 文件内容,并调用系统打印命令进行打印。 - 支持高级打印参数:打印份数、份数间隔,以及更多打印设置。 - 提供可视化的管理界面,实时查看日志和打印机状态。 - **日志实时监控**:支持实时查看系统日志。 ## 2. 启动与配置 ### 2.1 启动应用 可以直接运行编译后的可执行文件(如 `print-dot-client.exe`)。 **注意**: 程序启动时会自动开启 WebSocket 服务(默认端口 1122)。 **Windows 打印说明**: - 请确保 Windows 打印后端可用且程序有权限访问。 - 安装包会在安装前尝试关闭正在运行的 `PrintDot.exe`,并覆盖安装目录中的核心二进制文件,避免旧版本残留。 ### 2.2 界面配置 启动后,界面提供以下配置项: - **Port**: WebSocket 服务监听端口(默认 `1122`)。 - **Secret Key**: 安全密钥(可选)。如果设置了密钥,客户端在连接或发送请求时必须通过鉴权。 - **Start/Stop Server**: 点击按钮即可启动或停止 WebSocket 服务。 - **Connection URL**: 服务启动后,界面会显示完整的连接地址(如 `wss://127.0.0.1:1122/ws?key=...`)。 **操作说明**: - **退出程序**: 使用菜单栏 `Menu` -> `Quit` (Ctrl+Q),或使用托盘菜单的 `Quit` 可完全退出程序。 - **后台运行**: 点击主窗口关闭按钮 (X) 不会退出程序,而是将程序最小化到系统托盘区。程序启动后会在系统托盘区显示图标,可用于快速唤起窗口或退出。 - **托盘菜单**: 在系统托盘图标上右键单击,可选择 `Show Main Window` 显示主窗口或 `Quit` 退出程序。 - **打开设置**: 使用菜单栏 `Menu` -> `Settings` (Ctrl+I) 可打开设置窗口。 - **查看日志**: 使用菜单栏 `Menu` -> `System Logs` (Ctrl+L) 可查看实时日志. ## 3. WebSocket 接口规范 ### 3.1 连接信息 - **协议**: Secure WebSocket (`wss://`) - **地址**: `wss://127.0.0.1:<PORT>/ws` - **鉴权**: - 如果设置了 `Secret Key`,建议在连接 URL 中携带:`wss://127.0.0.1:1122/ws?key=YOUR_PASSWORD` - 如果连接时未携带 key,也可以在发送的消息体中包含 `key` 字段(但不推荐,连接可能被拒绝)。 ### 3.2 消息类型 #### 3.2.1 连接成功响应 (Server -> Client) 连接建立后,服务端会立即发送当前的打印机列表: ```json { "type": "printer_list", "data": [ {"name": "Microsoft Print to PDF", "isDefault": true}, {"name": "ZDesigner GK888t", "isDefault": false} ] } ``` #### 3.2.2 获取打印机列表 (Client -> Server) 客户端可以随时发送以下 JSON 消息主动获取最新的打印机列表: ```json { "type": "get_printers" } ``` 服务端将回复与 **3.2.1** 相同格式的 `printer_list` 消息。 #### 3.2.3 获取打印机能力 (Client -> Server) 客户端可以请求指定打印机的可用参数: ```json { "type": "get_printer_caps", "printer": "Microsoft Print to PDF" } ``` 服务端响应示例: ```json { "type": "printer_caps", "printer": "Microsoft Print to PDF", "data": { "paperSizes": ["A4", "Letter"], "printerPaperNames": ["A4", "Letter"], "duplexSupported": false, "colorSupported": true } } ``` > 说明:不同系统返回字段会有差异,Windows 返回 Win32\_Printer/Win32\_PrinterConfiguration 信息;Linux/macOS 返回 lpoptions 解析结果。 #### 3.2.4 发送打印任务 (Client -> Server) 客户端发送的 JSON 数据包结构如下(按功能分类): ```json { "printer": "Microsoft Print to PDF", // [必填] 目标打印机名称 "content": "data:application/pdf;base64,JVBERi...", // [必填] Base64 编码的 PDF 内容 (支持带前缀或纯 Base64) "key": "123456", // [选填] 鉴权密钥 (若连接时已验证可省略) "job": { "name": "My Print Job 001", // [选填] 任务名称 (仅用于日志记录) "copies": 2, // [选填] 打印份数,默认 1 "intervalMs": 1000 // [选填] 份数间延迟(毫秒),用于手动隔张打印 }, "pages": { "range": "1-3,5", // [选填] 页码范围 (支持 N / N-M / N,M / 反向区间) "set": "odd" // [选填] odd | even }, "layout": { "scale": "fit", // [选填] noscale | shrink | fit "orientation": "portrait" // [选填] portrait | landscape }, "color": { "mode": "color" // [选填] color | monochrome }, "sides": { "mode": "duplex" // [选填] simplex | duplex | duplexshort | duplexlong }, "paper": { "size": "A4" // [选填] A4 | letter | legal | tabloid | statement | A2 | A3 | A5 | A6 }, "tray": { "bin": "2" // [选填] 纸盒编号或名称,例如 2 / Manual } } ``` > **注意**: > > 1. `content` 字段必须是 **PDF 文件的 Base64 编码字符串**。 > - 支持标准 Data URI 格式:`data:application/pdf;base64,JVBERi...` > - 也支持纯 Base64 字符串:`JVBERi...` > - 服务端会自动去除 `data:` 前缀(如果有)并校验解码后的内容是否以 `%PDF` 开头。 | 字段 | 类型 | 说明 | | :------------------- | :------ | :--------------------------------------------------------------- | | `printer` | String | 目标打印机名称。 | | `content` | String | **Base64 编码的 PDF 内容**。 | | `job.name` | String | 打印任务名称。 | | `job.copies` | Integer | **打印份数**。`intervalMs` 为 0 时由系统命令一次性处理。 | | `job.intervalMs` | Integer | **隔张间隔 (ms)**。大于 0 时服务端逐份打印。 | | `pages.range` | String | **页码范围**:`N` / `N-M` / `N,M` / 反向区间。 | | `pages.set` | String | **奇偶页**:`odd` / `even`。 | | `layout.scale` | String | **缩放**:`noscale` / `shrink` / `fit`。 | | `layout.orientation` | String | **方向**:`portrait` / `landscape`。 | | `color.mode` | String | **颜色模式**:`color` / `monochrome`。 | | `sides.mode` | String | **单双面**:`simplex` / `duplex` / `duplexshort` / `duplexlong`。 | | `paper.size` | String | **纸张**:如 `A4`、`letter`、`legal`。未提供时会尝试从 PDF 的 MediaBox 自动识别常见尺寸。 | | `tray.bin` | String | **纸盒**:编号或名称。 | ##### 3.2.4.1 平台支持说明 **Windows** - `pages.range` / `pages.set` / `layout.scale` / `layout.orientation` / `color.mode` / `sides.mode` / `paper.size` / `tray.bin` / `job.copies` 会被转换为 Windows 打印选项。 **Linux/macOS (lp/CUPS)** - `pages.range` -> `-P`。 - `pages.set` -> `-o page-set=odd|even`。 - `layout.scale` -> `fit-to-page` / `scaling=100`(`shrink` 使用默认行为)。 - `layout.orientation` -> `-o orientation-requested=3|4`(驱动可能忽略)。 - `color.mode` -> `-o ColorModel=Gray` / `-o ColorModel=RGB`(部分驱动可能忽略)。 - `sides.mode` -> `-o sides=...`。 - `paper.size` -> `-o media=...`。 - `tray.bin` -> `-o InputSlot=...`(依赖驱动支持)。 #### 3.2.5 服务端响应 (Server -> Client) 服务端响应统一采用带 `type` 的消息结构,前端应优先根据 `type` 做分发。 **打印任务响应类型:`print_result`** **成功响应:** ```json { "type": "print_result", "status": "success", "message": "Printed successfully" } ``` **失败响应:** ```json { "type": "print_result", "status": "error", "message": "Content must be a PDF file" } ``` > 说明: > > - 打印机列表消息类型为 `printer_list`。 > - 打印机能力消息类型为 `printer_caps`。 > - 与打印流程相关的成功/失败均返回 `print_result`,用于前端稳定地 resolve/reject `print()` Promise。 **Windows 队列跟踪说明:** Windows 下服务端会等待打印任务进入打印队列并完成后才返回 `success`。 若 120 秒内未入队或 5 分钟内未完成,将返回 `error` 并给出超时提示。 #### 3.2.6 发送 HTML 打印任务 (Client -> Server, type = "print\_html") PrintDot Client 集成了 vue-print-designer,因此可以直接接收任意 HTML 字符串。 HTML 会被丢到客户端 WebView 里的 vue-print-designer 渲染管线重新拍成 PDF,再走系统打印。 **请求包:** ```json { "type": "print_html", "html": "<div style=\"font-family:sans-serif;padding:24px\"><h1>订单确认</h1><p>订单号:SO-001</p></div><div style=\"page-break-after:always\"></div><div>第二页内容</div>", "printer": "Microsoft Print to PDF", // [选填] 不填走系统默认 "key": "123456", // [选填] 连接时已验证可省略 "pageSize": { "width": 210, "height": 297 }, // [选填] 自定义页面尺寸 (mm),默认 A4 "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "pages": { "range": "", "set": "" }, "layout": { "scale": "fit", "orientation": "portrait" }, "color": { "mode": "color" }, "sides": { "mode": "simplex" }, "paper": { "size": "A4" }, "tray": { "bin": "" } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :---------------------------------------------------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------- | | `type` | String | 固定为 `print_html`。 | | `html` | String | **必填**。原始 HTML 字符串。每个顶层块级子元素(`<div>`/`<section>`/`<table>`/`<p>` 等)会被当成一页,`<style>`/`<link>`/`<script>` 标签会被剥离。 | | `pageSize.width/height` | Number | \[选填] 页面尺寸(mm),传给设计器作为画布大小,最终决定 PDF 页面尺寸。默认 A4 (210×297)。 | | `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` | — | 与 `3.2.4 发送打印任务` 完全一致(渲染完 PDF 后再走这些参数调系统打印)。 | **调用示例(JavaScript):** ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); // 也可以是业务端 vue-print-designer 导出的 HTML(getPreviewHtml / exportHtml) const html = ` <div style="font-family: 'Segoe UI', sans-serif; padding: 24px; color:#111;"> <h1 style="margin:0 0 12px">订单确认</h1> <p>订单号:<b>SO-20260615-001</b></p> <p>金额:<span style="color:#d00">¥128.00</span></p> </div> <div style="page-break-after: always;"></div> <div style="font-family: 'Segoe UI', sans-serif; padding: 24px;"> <h2>第二页</h2> <p>任意 HTML 内容</p> </div> `; socket.addEventListener('open', () => { socket.send(JSON.stringify({ type: 'print_html', html, printer: 'Microsoft Print to PDF', key: '123456', pageSize: { width: 210, height: 297 }, // A4 job: { name: 'Order-001', copies: 1, intervalMs: 0 }, pages: { range: '', set: '' }, layout: { scale: 'fit', orientation: 'portrait' }, color: { mode: 'color' }, sides: { mode: 'simplex' }, paper: { size: 'A4' }, tray: { bin: '' } })); }); socket.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'print_result') { if (msg.status === 'success') { console.log('HTML printed successfully:', msg.message); } else { console.error('HTML print failed:', msg.message); } } }); ``` **响应:** 与 3.2.4 一致,服务端返回 `{type: "print_result", status: "success|error", message: "..."}`。 **与 print\_template 的对比:** - `print_template` 接收的是 vue-print-designer 的**模板结构**(`pages[].elements[]` 树),前端会先 `loadTemplateData` 再 `setTestData` 再渲染。 - `print_html` 接收的是**任意 HTML 字符串**(包括业务端用 `getPreviewHtml()` 导出的 HTML),直接塞进 `printApi.getPdfBlob()`。 - 两者下游(`processPrintRequest` → 系统打印)完全相同。 **与 print\_task 的对比:** - `print_task` 是已经渲染好的 Base64 PDF → 直接走系统打印。 - `print_html` 是 HTML 字符串 → 前端用 vue-print-designer 渲染成 PDF → 再走系统打印。 #### 3.2.7 发送模板打印任务 (Client -> Server, type = "print\_template") 业务端用 vue-print-designer 设计好模板(`data`)和填充值(`testData`),下发后客户端会用 vue-print-designer 把模板填充 → 渲染成 PDF → 走系统打印。 适合"同一份模板,订单数据每次不同"的场景(快递单、收据、发票等)。 **请求包:** ```json { "type": "print_template", "printer": "Microsoft Print to PDF", // [选填] 不填走系统默认 "key": "123456", // [选填] 连接时已验证可省略 "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "pages": { "range": "", "set": "" }, "layout": { "scale": "fit", "orientation": "portrait" }, "color": { "mode": "color" }, "sides": { "mode": "simplex" }, "paper": { "size": "A4" }, "tray": { "bin": "" }, "data": { /* vue-print-designer 模板结构,详见下方"模板结构"小节 */ }, "testData": { /* 占位符填充值,详见下方"testData"小节 */ } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :---------------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------------------------- | | `type` | String | 固定为 `print_template`。 | | `data` | Object | **必填**。vue-print-designer 的模板结构 JSON(`pages[].elements[]` 树)。通常由 vue-print-designer 导出或自己拼装。 | | `testData` | Object | \[选填] 用于填充 `data` 里的 `{{key}}` 占位符的键值对,例如 `{"orderNo":"SO-001"}`。 | | `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` | — | 与 3.2.4 / 3.2.6 完全一致(PDF 渲完后走这些参数调系统打印)。 | **模板结构(`data`):** `data` 是 vue-print-designer 导出的标准模板结构,**不是**任意 HTML 字符串(任意 HTML 走 `print_html`)。 ```json { "pages": [ { "name": "Page-1", "width": 210, "height": 297, "elements": [ { "type": "text", "x": 20, "y": 20, "width": 100, "height": 12, "content": "订单号:{{orderNo}}", "fontSize": 12 }, { "type": "qrcode", "x": 150, "y": 20, "width": 40, "height": 40, "content": "{{qrcodeText}}" } ] } ], "canvasSize": { "width": 210, "height": 297 }, "guides": [] } ``` 每个 `element` 是一个设计师元素对象,`type` 决定渲染方式: | `type` | 关键字段 | 说明 | | :------------ | :---------------------------------------------------- | :---------------------- | | `text` | `content`, `fontSize`, `fontWeight`, `align`, `color` | 文本,支持 `{{key}}` 占位符。 | | `image` | `src` | 图片,支持 URL / base64。 | | `qrcode` | `content` | 二维码,`{{key}}` 占位符替换为内容。 | | `barcode` | `content` | 条形码。 | | `table` | `rows` / `columns` | 表格。 | | `rect` | `width` / `height` / `color` | 矩形块。 | | `line` | `width` / `color` | 直线。 | | `circle` | `radius` / `color` | 圆形。 | | `page-number` | — | 当前页码标记。 | **`testData`:** `testData` 是纯 JSON 对象,键是占位符 `{{...}}` 里的 key,值是替换进去的内容(字符串、数字、布尔都支持)。 ```json { "orderNo": "SO-20260615-001", "qrcodeText": "https://example.com/o/SO-20260615-001" } ``` **调用示例(JavaScript):** ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); // 1) 模板结构(通常由 vue-print-designer 设计器提前导出并存储到业务端) const template = { pages: [ { name: 'Page-1', width: 210, height: 297, elements: [ { type: 'text', x: 20, y: 20, width: 100, height: 12, content: '订单号:{{orderNo}}', fontSize: 12 }, { type: 'qrcode', x: 150, y: 20, width: 40, height: 40, content: '{{qrcodeText}}' } ] } ], canvasSize: { "width": 210, "height": 297 }, guides: [] }; // 2) 运行时填充数据 const testData = { orderNo: 'SO-20260615-001', qrcodeText: 'https://example.com/o/SO-20260615-001' }; socket.addEventListener('open', () => { socket.send(JSON.stringify({ type: 'print_template', printer: 'Microsoft Print to PDF', key: '123456', job: { name: 'Order-20260615-001', copies: 1, intervalMs: 0 }, pages: { range: '', set: '' }, layout: { scale: 'fit', orientation: 'portrait' }, color: { mode: 'color' }, sides: { mode: 'simplex' }, paper: { size: 'A4' }, tray: { bin: '' }, data: template, testData: testData })); }); socket.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'print_result') { if (msg.status === 'success') { console.log('Template printed successfully:', msg.message); } else { console.error('Template print failed:', msg.message); } } }); ``` **响应:** 与 3.2.4 / 3.2.6 一致,服务端返回 `{type: "print_result", status: "success|error", message: "..."}`。 **与 `print_html` 的对比:** - `print_html` 接收**任意 HTML 字符串**,前端直接 `getPdfBlob` 渲染。 - `print_template` 接收 vue-print-designer **结构化模板**(`pages[].elements[]`),前端要 `loadTemplateData` + `setTestData` 走完整设计器渲染管线(自动支持占位符、表格分页、变量计算等)。 - 两者下游(`processPrintRequest` → 系统打印)完全相同。 **与** **`print_task`** **的对比:** - `print_task` 是已经渲染好的 Base64 PDF → 直接走系统打印。 - `print_template` 是模板 + 填充值 → 前端 vue-print-designer 渲染成 PDF → 走系统打印。 #### 3.2.8 标签打印 (Client -> Server, type = "print_label") 标签打印接收 vue-print-designer **模板 JSON**,由客户端后端直接转换为标签打印机指令(TSPL/TSPL2、ZPL、ESC/POS、CPCL),经系统打印队列以 RAW 方式发送到打印机。适用于热敏/热转印标签机(TSC、佳博、斑马、汉印、得力、芯烨等)。 与 `print_template` 的关键区别: - `print_template`:前端把模板渲染成 **PDF** → 系统打印(适合 A4 文档)。 - `print_label`:后端把模板转换为**打印机原生指令** → RAW 发送(条码/二维码为原生矢量,出标更快、扫描率更高,且不受驱动缩放影响)。 **请求包:** ```json { "type": "print_label", "printer": "TSC TE244", // [必填] 目标标签打印机 "key": "123456", // [选填] 鉴权密钥 "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "data": { /* vue-print-designer 模板结构,与 print_template 的 data 一致 */ }, "testData": { "sn": "A1" }, // [选填] 单条填充(与 labels 二选一) "labels": [ // [选填] 批量填充(与 testData 二选一) { "testData": { "sn": "A1" } }, { "testData": { "sn": "A2" } } ], "label": { "language": "auto", // [选填] auto | tspl | zpl | escpos | cpcl "dpi": 0, // [选填] 0 = 自动读取打印机分辨率 "speed": 0, // [选填] 打印速度(仅 TSPL) "density": 0, // [选填] 打印浓度(仅 TSPL) "gap": 2, // [选填] 间隙高度 mm(TSPL/ZPL) "bline": 0, // [选填] 黑标高度 mm,>0 时按黑标定位 "offset": 0, // [选填] 打印偏移 mm "textMode": "", // [选填] 空 = 按指令集默认 | device | bitmap "continuous": false, // [选填] true = 连续纸(按内容实测算高) "pageSize": { "width": 50, "height": 30 } // [选填] 标签纸宽高 mm,height=0 表示连续纸 } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `type` | String | 固定为 `print_label`。 | | `printer` | String | **必填**。标签打印机名称。 | | `data` | Object | **必填**。`print_template` 同款模板结构(`pages[].elements[]`)。可直接转发设计器 `getTemplateData()` 的返回值。 | | `testData` | Object | \[选填] 单条填充值,与 `labels` **二选一**(同时出现返回错误)。 | | `labels` | Array | \[选填] 批量填充,每个元素为 `{ "testData": {...} }`,一条数据打印一张(或一组)标签。 | | `job.name` | String | \[选填] 任务名称,默认 `Label Job`。 | | `job.copies` | Integer | \[选填] 份数,默认 1。 | | `job.intervalMs` | Integer | \[选填] 多张标签之间的间隔(毫秒)。>0 时逐张一个 RAW 作业。 | | `label.*` | — | \[选填] 见下表,未提供时按打印机名/驱动名自动推断。 | **`label` 子字段:** | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `language` | String | 指令集:`auto`(默认,按打印机名/驱动名推断)/ `tspl` / `zpl` / `escpos` / `cpcl`。推断失败必须显式指定。 | | `dpi` | Integer | 打印分辨率。0 或省略时读取打印机 DEVMODE 分辨率,读取失败按 203。 | | `speed` / `density` | Integer | 打印速度与浓度(TSPL 有效,0 表示用打印机当前设置)。 | | `gap` | Number | 间隙高度(mm),配合间隙纸使用。 | | `bline` | Number | 黑标高度(mm),>0 时按黑标定位(ZPL 用 `^MNY`,TSPL 用 `BLINE`)。 | | `offset` | Number | 打印起始偏移(mm)。 | | `textMode` | String | 文字渲染方式:`device`(机内字库,体积小)/ `bitmap`(位图,样式还原最准)。默认 TSPL、ZPL 走位图,ESC/POS、CPCL 走机内字库。 | | `continuous` | Boolean | `true` 表示连续纸:忽略 `pageSize.height`,按内容实测高度出纸。 | | `pageSize` | Object | 标签纸宽高(mm)。宽高都缺省时按模板 `canvasSize`(设计器 px 基准)换算。 | **语言自动推断规则(按顺序匹配打印机名 + 驱动名,忽略大小写):** | 顺序 | 指令集 | 命中关键词 | | :--- | :--- | :--- | | 1 | `cpcl` | `cpcl`、`zm2`、`zq5`、`qln`、`rw4`、`p4t`、`imz` | | 2 | `escpos` | `esc/pos`、`escpos`、`pos-`、`pos58`、`pos80`、`gb-`、`58mm`、`80mm`、`receipt`、`tm-`、`rp8` | | 3 | `zpl` | `zdesigner`、`zebra`、`zpl` | | 4 | `tspl` | `tspl`、`tsc`、`gprinter`、`gp-`、`xprinter`、`deli`、`得力`、`hprt`、`汉印`、`argox`、`godex`、`postek` | > 规则顺序即优先级:CPCL 先于 ZPL,是因为 Zebra 移动机的驱动名通常也含 `zebra`,若 ZPL 先匹配会把 CPCL 机型判错。未命中任何规则时返回错误,请在 `label.language` 中显式指定。 **调用示例(JavaScript,批量):** ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); const template = { pages: [{ width: 50, height: 30, elements: [ { type: 'text', x: 8, y: 8, width: 40, height: 14, content: 'SN: {{sn}}', fontSize: 12 }, { type: 'barcode', x: 8, y: 26, width: 60, height: 24, content: '{{sn}}' }, { type: 'qrcode', x: 76, y: 8, width: 40, height: 40, content: '{{url}}' } ] }], canvasSize: { width: 200, height: 120 } }; const labels = [ { testData: { sn: 'A0001', url: 'https://example.com/i/A0001' } }, { testData: { sn: 'A0002', url: 'https://example.com/i/A0002' } }, { testData: { sn: 'A0003', url: 'https://example.com/i/A0003' } } ]; socket.addEventListener('open', () => { socket.send(JSON.stringify({ type: 'print_label', printer: 'TSC TE244', key: '123456', job: { name: 'Order-20260615-001', copies: 1, intervalMs: 0 }, data: template, labels: labels, label: { language: 'auto', gap: 2, pageSize: { width: 50, height: 30 } } })); }); socket.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'print_result') { if (msg.status === 'success') console.log('标签打印成功:', msg.message); else console.error('标签打印失败:', msg.message); } }); ``` **响应:** 与 3.2.4 一致,服务端返回 `{type: "print_result", status: "success|error", message: "..."}`。 **打印机能力扩展(3.2.3 响应新增字段):** | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `guessedLanguage` | String | 按打印机名/驱动名推断出的指令集(未命中则无此字段)。 | | `dpi` | Integer | 打印机分辨率(读取失败则无此字段)。 | **已知限制:** - 模板中的 `customScript`(JS 数据加工脚本)不会被客户端执行,会被静默忽略;对应元素按其原有字段(`content` 等)照常渲染。 - 文字为「视觉等效」而非逐像素还原:客户端用内嵌固定字体渲染,换行点可能与浏览器偶有 ±1 字符差异,换来的是跨机器打印结果 100% 一致。 - 彩色内容按 ITU-R BT.601 灰度 + Floyd–Steinberg 抖动转为黑白(热敏纸物理特性)。 - 表格在标签路径**忽略 `autoPaginate`**:固定纸高逐行绘制、超出部分由标签纸裁剪;连续纸按内容整体撑高。 - `pageNumber` 元素在单页标签中无意义,会被静默忽略(不参与绘制)。 - 几何布局精确到 ≤0.5 dot;条码/二维码内容精确,但实际尺寸可能因模块宽取整相差 1–2%。 #### 3.2.9 预览 (Client -> Server, type = "preview") 客户端发送预览指令,服务端收到后将内容保存为临时文件,并打开内置预览窗口。预览窗口使用 Mozilla pdf.js 渲染 PDF,不依赖 WebView 自带 PDF 阅读器。支持三种模式: - **pdf**:Base64 编码的 PDF 内容,直接交给 pdf.js 预览。 - **html**:原始 HTML 字符串,先沿用客户端 vue-print-designer 渲染链路生成 PDF,再交给 pdf.js 预览。 - **json**:vue-print-designer 模板 JSON 字符串,先沿用模板渲染链路生成 PDF,再交给 pdf.js 预览。 预览窗口顶部提供系统打印选项栏。`preview` 请求可携带与打印任务一致的 `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` 字段,作为预览窗口点击“打印”时调用系统打印的默认参数。 **请求包:** ```json { "type": "preview", "mode": "pdf", "content": "<Base64 编码的 PDF 内容>", "title": "订单预览", "printer": "Microsoft Print to PDF", "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "pages": { "range": "", "set": "" }, "layout": { "scale": "fit", "orientation": "portrait" }, "color": { "mode": "color" }, "sides": { "mode": "simplex" }, "paper": { "size": "A4" }, "tray": { "bin": "" } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :---------------------------------------------------------------------------- | :----- | :--------------------------------------------------------------------------------------------------------------- | | `type` | String | 固定为 `preview`。 | | `mode` | String | **必填**。预览模式:`pdf` / `html` / `json`。 | | `content` | String | **必填**。`pdf` 模式为 Base64 编码的 PDF(支持 `data:application/pdf;base64,...` 前缀);`html` 模式为原始 HTML 字符串;`json` 模式为 vue-print-designer 模板 JSON 字符串。 | | `title` | String | \[选填] 预览窗口标题。 | | `printQuality` | String | \[选填] `html` / `json` 渲染为 PDF 时的质量:`fast` / `normal` / `high` / `ultra`。 | | `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` | — | \[选填] 与 `3.2.4 发送打印任务` 一致,作为预览窗口顶部打印选项栏的默认值,点击“打印”后用于调用系统打印。 | **响应:** ```json { "type": "preview_result", "status": "success", "message": "Preview opened" } ``` **调用示例(JavaScript):** ```javascript // PDF 预览 socket.send(JSON.stringify({ type: 'preview', mode: 'pdf', content: base64PdfString, title: '发票预览', printer: 'Microsoft Print to PDF', job: { name: 'Invoice-001', copies: 1, intervalMs: 0 }, pages: { range: '', set: '' }, layout: { scale: 'fit', orientation: 'portrait' }, color: { mode: 'color' }, sides: { mode: 'simplex' }, paper: { size: 'A4' }, tray: { bin: '' } })); // HTML 预览 socket.send(JSON.stringify({ type: 'preview', mode: 'html', content: '<h1>Hello</h1><p>这是预览内容</p>', title: '小票预览' })); // JSON 预览 socket.send(JSON.stringify({ type: 'preview', mode: 'json', content: JSON.stringify({ orderNo: 'SO-001', items: [...] }), title: '订单数据' })); ``` ## 4. 调用示例 (JavaScript) # PrintDot Client 使用指南与规范 ## 1. 项目简介 **PrintDot Client** 是一款面向本地打印场景设计的桌面端打印助手。它充当浏览器(或其他客户端)与操作系统打印机之间的桥梁,通过 WebSocket 协议接收打印指令,并调用系统打印机完成打印。 主要功能: - 自动获取操作系统已安装的打印机列表。 - 启动 WebSocket 服务监听打印请求(默认端口 1122)。 - 支持自定义服务端口和安全密钥(Secret Key)。 - **打印内容要求**:接收 Base64 编码的 PDF 文件内容,并调用系统打印命令进行打印。 - 支持高级打印参数:打印份数、份数间隔,以及更多打印设置。 - 提供可视化的管理界面,实时查看日志和打印机状态。 - **日志实时监控**:支持实时查看系统日志。 ## 2. 启动与配置 ### 2.1 启动应用 可以直接运行编译后的可执行文件(如 `print-dot-client.exe`)。 **注意**: 程序启动时会自动开启 WebSocket 服务(默认端口 1122)。 **Windows 打印说明**: - 请确保 Windows 打印后端可用且程序有权限访问。 - 安装包会在安装前尝试关闭正在运行的 `PrintDot.exe`,并覆盖安装目录中的核心二进制文件,避免旧版本残留。 ### 2.2 界面配置 启动后,界面提供以下配置项: - **Port**: WebSocket 服务监听端口(默认 `1122`)。 - **Secret Key**: 安全密钥(可选)。如果设置了密钥,客户端在连接或发送请求时必须通过鉴权。 - **Start/Stop Server**: 点击按钮即可启动或停止 WebSocket 服务。 - **Connection URL**: 服务启动后,界面会显示完整的连接地址(如 `wss://127.0.0.1:1122/ws?key=...`)。 **操作说明**: - **退出程序**: 使用菜单栏 `Menu` -> `Quit` (Ctrl+Q),或使用托盘菜单的 `Quit` 可完全退出程序。 - **后台运行**: 点击主窗口关闭按钮 (X) 不会退出程序,而是将程序最小化到系统托盘区。程序启动后会在系统托盘区显示图标,可用于快速唤起窗口或退出。 - **托盘菜单**: 在系统托盘图标上右键单击,可选择 `Show Main Window` 显示主窗口或 `Quit` 退出程序。 - **打开设置**: 使用菜单栏 `Menu` -> `Settings` (Ctrl+I) 可打开设置窗口。 - **查看日志**: 使用菜单栏 `Menu` -> `System Logs` (Ctrl+L) 可查看实时日志. ## 3. WebSocket 接口规范 ### 3.1 连接信息 - **协议**: Secure WebSocket (`wss://`) - **地址**: `wss://127.0.0.1:<PORT>/ws` - **鉴权**: - 如果设置了 `Secret Key`,建议在连接 URL 中携带:`wss://127.0.0.1:1122/ws?key=YOUR_PASSWORD` - 如果连接时未携带 key,也可以在发送的消息体中包含 `key` 字段(但不推荐,连接可能被拒绝)。 ### 3.2 消息类型 #### 3.2.1 连接成功响应 (Server -> Client) 连接建立后,服务端会立即发送当前的打印机列表: ```json { "type": "printer_list", "data": [ {"name": "Microsoft Print to PDF", "isDefault": true}, {"name": "ZDesigner GK888t", "isDefault": false} ] } ``` #### 3.2.2 获取打印机列表 (Client -> Server) 客户端可以随时发送以下 JSON 消息主动获取最新的打印机列表: ```json { "type": "get_printers" } ``` 服务端将回复与 **3.2.1** 相同格式的 `printer_list` 消息。 #### 3.2.3 获取打印机能力 (Client -> Server) 客户端可以请求指定打印机的可用参数: ```json { "type": "get_printer_caps", "printer": "Microsoft Print to PDF" } ``` 服务端响应示例: ```json { "type": "printer_caps", "printer": "Microsoft Print to PDF", "data": { "paperSizes": ["A4", "Letter"], "printerPaperNames": ["A4", "Letter"], "duplexSupported": false, "colorSupported": true } } ``` > 说明:不同系统返回字段会有差异,Windows 返回 Win32\_Printer/Win32\_PrinterConfiguration 信息;Linux/macOS 返回 lpoptions 解析结果。 #### 3.2.4 发送打印任务 (Client -> Server) 客户端发送的 JSON 数据包结构如下(按功能分类): ```json { "printer": "Microsoft Print to PDF", // [必填] 目标打印机名称 "content": "data:application/pdf;base64,JVBERi...", // [必填] Base64 编码的 PDF 内容 (支持带前缀或纯 Base64) "key": "123456", // [选填] 鉴权密钥 (若连接时已验证可省略) "job": { "name": "My Print Job 001", // [选填] 任务名称 (仅用于日志记录) "copies": 2, // [选填] 打印份数,默认 1 "intervalMs": 1000 // [选填] 份数间延迟(毫秒),用于手动隔张打印 }, "pages": { "range": "1-3,5", // [选填] 页码范围 (支持 N / N-M / N,M / 反向区间) "set": "odd" // [选填] odd | even }, "layout": { "scale": "fit", // [选填] noscale | shrink | fit "orientation": "portrait" // [选填] portrait | landscape }, "color": { "mode": "color" // [选填] color | monochrome }, "sides": { "mode": "duplex" // [选填] simplex | duplex | duplexshort | duplexlong }, "paper": { "size": "A4" // [选填] A4 | letter | legal | tabloid | statement | A2 | A3 | A5 | A6 }, "tray": { "bin": "2" // [选填] 纸盒编号或名称,例如 2 / Manual } } ``` > **注意**: > > 1. `content` 字段必须是 **PDF 文件的 Base64 编码字符串**。 > - 支持标准 Data URI 格式:`data:application/pdf;base64,JVBERi...` > - 也支持纯 Base64 字符串:`JVBERi...` > - 服务端会自动去除 `data:` 前缀(如果有)并校验解码后的内容是否以 `%PDF` 开头。 | 字段 | 类型 | 说明 | | :------------------- | :------ | :--------------------------------------------------------------- | | `printer` | String | 目标打印机名称。 | | `content` | String | **Base64 编码的 PDF 内容**。 | | `job.name` | String | 打印任务名称。 | | `job.copies` | Integer | **打印份数**。`intervalMs` 为 0 时由系统命令一次性处理。 | | `job.intervalMs` | Integer | **隔张间隔 (ms)**。大于 0 时服务端逐份打印。 | | `pages.range` | String | **页码范围**:`N` / `N-M` / `N,M` / 反向区间。 | | `pages.set` | String | **奇偶页**:`odd` / `even`。 | | `layout.scale` | String | **缩放**:`noscale` / `shrink` / `fit`。 | | `layout.orientation` | String | **方向**:`portrait` / `landscape`。 | | `color.mode` | String | **颜色模式**:`color` / `monochrome`。 | | `sides.mode` | String | **单双面**:`simplex` / `duplex` / `duplexshort` / `duplexlong`。 | | `paper.size` | String | **纸张**:如 `A4`、`letter`、`legal`。未提供时会尝试从 PDF 的 MediaBox 自动识别常见尺寸。 | | `tray.bin` | String | **纸盒**:编号或名称。 | ##### 3.2.4.1 平台支持说明 **Windows** - `pages.range` / `pages.set` / `layout.scale` / `layout.orientation` / `color.mode` / `sides.mode` / `paper.size` / `tray.bin` / `job.copies` 会被转换为 Windows 打印选项。 **Linux/macOS (lp/CUPS)** - `pages.range` -> `-P`。 - `pages.set` -> `-o page-set=odd|even`。 - `layout.scale` -> `fit-to-page` / `scaling=100`(`shrink` 使用默认行为)。 - `layout.orientation` -> `-o orientation-requested=3|4`(驱动可能忽略)。 - `color.mode` -> `-o ColorModel=Gray` / `-o ColorModel=RGB`(部分驱动可能忽略)。 - `sides.mode` -> `-o sides=...`。 - `paper.size` -> `-o media=...`。 - `tray.bin` -> `-o InputSlot=...`(依赖驱动支持)。 #### 3.2.5 服务端响应 (Server -> Client) 服务端响应统一采用带 `type` 的消息结构,前端应优先根据 `type` 做分发。 **打印任务响应类型:`print_result`** **成功响应:** ```json { "type": "print_result", "status": "success", "message": "Printed successfully" } ``` **失败响应:** ```json { "type": "print_result", "status": "error", "message": "Content must be a PDF file" } ``` > 说明: > > - 打印机列表消息类型为 `printer_list`。 > - 打印机能力消息类型为 `printer_caps`。 > - 与打印流程相关的成功/失败均返回 `print_result`,用于前端稳定地 resolve/reject `print()` Promise。 **Windows 队列跟踪说明:** Windows 下服务端会等待打印任务进入打印队列并完成后才返回 `success`。 若 120 秒内未入队或 5 分钟内未完成,将返回 `error` 并给出超时提示。 #### 3.2.6 发送 HTML 打印任务 (Client -> Server, type = "print\_html") PrintDot Client 集成了 vue-print-designer,因此可以直接接收任意 HTML 字符串。 HTML 会被丢到客户端 WebView 里的 vue-print-designer 渲染管线重新拍成 PDF,再走系统打印。 **请求包:** ```json { "type": "print_html", "html": "<div style=\"font-family:sans-serif;padding:24px\"><h1>订单确认</h1><p>订单号:SO-001</p></div><div style=\"page-break-after:always\"></div><div>第二页内容</div>", "printer": "Microsoft Print to PDF", // [选填] 不填走系统默认 "key": "123456", // [选填] 连接时已验证可省略 "pageSize": { "width": 210, "height": 297 }, // [选填] 自定义页面尺寸 (mm),默认 A4 "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "pages": { "range": "", "set": "" }, "layout": { "scale": "fit", "orientation": "portrait" }, "color": { "mode": "color" }, "sides": { "mode": "simplex" }, "paper": { "size": "A4" }, "tray": { "bin": "" } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :---------------------------------------------------------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------- | | `type` | String | 固定为 `print_html`。 | | `html` | String | **必填**。原始 HTML 字符串。每个顶层块级子元素(`<div>`/`<section>`/`<table>`/`<p>` 等)会被当成一页,`<style>`/`<link>`/`<script>` 标签会被剥离。 | | `pageSize.width/height` | Number | \[选填] 页面尺寸(mm),传给设计器作为画布大小,最终决定 PDF 页面尺寸。默认 A4 (210×297)。 | | `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` | — | 与 `3.2.4 发送打印任务` 完全一致(渲染完 PDF 后再走这些参数调系统打印)。 | **调用示例(JavaScript):** ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); // 也可以是业务端 vue-print-designer 导出的 HTML(getPreviewHtml / exportHtml) const html = ` <div style="font-family: 'Segoe UI', sans-serif; padding: 24px; color:#111;"> <h1 style="margin:0 0 12px">订单确认</h1> <p>订单号:<b>SO-20260615-001</b></p> <p>金额:<span style="color:#d00">¥128.00</span></p> </div> <div style="page-break-after: always;"></div> <div style="font-family: 'Segoe UI', sans-serif; padding: 24px;"> <h2>第二页</h2> <p>任意 HTML 内容</p> </div> `; socket.addEventListener('open', () => { socket.send(JSON.stringify({ type: 'print_html', html, printer: 'Microsoft Print to PDF', key: '123456', pageSize: { width: 210, height: 297 }, // A4 job: { name: 'Order-001', copies: 1, intervalMs: 0 }, pages: { range: '', set: '' }, layout: { scale: 'fit', orientation: 'portrait' }, color: { mode: 'color' }, sides: { mode: 'simplex' }, paper: { size: 'A4' }, tray: { bin: '' } })); }); socket.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'print_result') { if (msg.status === 'success') { console.log('HTML printed successfully:', msg.message); } else { console.error('HTML print failed:', msg.message); } } }); ``` **响应:** 与 3.2.4 一致,服务端返回 `{type: "print_result", status: "success|error", message: "..."}`。 **与 print\_template 的对比:** - `print_template` 接收的是 vue-print-designer 的**模板结构**(`pages[].elements[]` 树),前端会先 `loadTemplateData` 再 `setTestData` 再渲染。 - `print_html` 接收的是**任意 HTML 字符串**(包括业务端用 `getPreviewHtml()` 导出的 HTML),直接塞进 `printApi.getPdfBlob()`。 - 两者下游(`processPrintRequest` → 系统打印)完全相同。 **与 print\_task 的对比:** - `print_task` 是已经渲染好的 Base64 PDF → 直接走系统打印。 - `print_html` 是 HTML 字符串 → 前端用 vue-print-designer 渲染成 PDF → 再走系统打印。 #### 3.2.7 发送模板打印任务 (Client -> Server, type = "print\_template") 业务端用 vue-print-designer 设计好模板(`data`)和填充值(`testData`),下发后客户端会用 vue-print-designer 把模板填充 → 渲染成 PDF → 走系统打印。 适合"同一份模板,订单数据每次不同"的场景(快递单、收据、发票等)。 **请求包:** ```json { "type": "print_template", "printer": "Microsoft Print to PDF", // [选填] 不填走系统默认 "key": "123456", // [选填] 连接时已验证可省略 "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "pages": { "range": "", "set": "" }, "layout": { "scale": "fit", "orientation": "portrait" }, "color": { "mode": "color" }, "sides": { "mode": "simplex" }, "paper": { "size": "A4" }, "tray": { "bin": "" }, "data": { /* vue-print-designer 模板结构,详见下方"模板结构"小节 */ }, "testData": { /* 占位符填充值,详见下方"testData"小节 */ } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :---------------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------------------------- | | `type` | String | 固定为 `print_template`。 | | `data` | Object | **必填**。vue-print-designer 的模板结构 JSON(`pages[].elements[]` 树)。通常由 vue-print-designer 导出或自己拼装。 | | `testData` | Object | \[选填] 用于填充 `data` 里的 `{{key}}` 占位符的键值对,例如 `{"orderNo":"SO-001"}`。 | | `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` | — | 与 3.2.4 / 3.2.6 完全一致(PDF 渲完后走这些参数调系统打印)。 | **模板结构(`data`):** `data` 是 vue-print-designer 导出的标准模板结构,**不是**任意 HTML 字符串(任意 HTML 走 `print_html`)。 ```json { "pages": [ { "name": "Page-1", "width": 210, "height": 297, "elements": [ { "type": "text", "x": 20, "y": 20, "width": 100, "height": 12, "content": "订单号:{{orderNo}}", "fontSize": 12 }, { "type": "qrcode", "x": 150, "y": 20, "width": 40, "height": 40, "content": "{{qrcodeText}}" } ] } ], "canvasSize": { "width": 210, "height": 297 }, "guides": [] } ``` 每个 `element` 是一个设计师元素对象,`type` 决定渲染方式: | `type` | 关键字段 | 说明 | | :------------ | :---------------------------------------------------- | :---------------------- | | `text` | `content`, `fontSize`, `fontWeight`, `align`, `color` | 文本,支持 `{{key}}` 占位符。 | | `image` | `src` | 图片,支持 URL / base64。 | | `qrcode` | `content` | 二维码,`{{key}}` 占位符替换为内容。 | | `barcode` | `content` | 条形码。 | | `table` | `rows` / `columns` | 表格。 | | `rect` | `width` / `height` / `color` | 矩形块。 | | `line` | `width` / `color` | 直线。 | | `circle` | `radius` / `color` | 圆形。 | | `page-number` | — | 当前页码标记。 | **`testData`:** `testData` 是纯 JSON 对象,键是占位符 `{{...}}` 里的 key,值是替换进去的内容(字符串、数字、布尔都支持)。 ```json { "orderNo": "SO-20260615-001", "qrcodeText": "https://example.com/o/SO-20260615-001" } ``` **调用示例(JavaScript):** ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); // 1) 模板结构(通常由 vue-print-designer 设计器提前导出并存储到业务端) const template = { pages: [ { name: 'Page-1', width: 210, height: 297, elements: [ { type: 'text', x: 20, y: 20, width: 100, height: 12, content: '订单号:{{orderNo}}', fontSize: 12 }, { type: 'qrcode', x: 150, y: 20, width: 40, height: 40, content: '{{qrcodeText}}' } ] } ], canvasSize: { "width": 210, "height": 297 }, guides: [] }; // 2) 运行时填充数据 const testData = { orderNo: 'SO-20260615-001', qrcodeText: 'https://example.com/o/SO-20260615-001' }; socket.addEventListener('open', () => { socket.send(JSON.stringify({ type: 'print_template', printer: 'Microsoft Print to PDF', key: '123456', job: { name: 'Order-20260615-001', copies: 1, intervalMs: 0 }, pages: { range: '', set: '' }, layout: { scale: 'fit', orientation: 'portrait' }, color: { mode: 'color' }, sides: { mode: 'simplex' }, paper: { size: 'A4' }, tray: { bin: '' }, data: template, testData: testData })); }); socket.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'print_result') { if (msg.status === 'success') { console.log('Template printed successfully:', msg.message); } else { console.error('Template print failed:', msg.message); } } }); ``` **响应:** 与 3.2.4 / 3.2.6 一致,服务端返回 `{type: "print_result", status: "success|error", message: "..."}`。 **与 `print_html` 的对比:** - `print_html` 接收**任意 HTML 字符串**,前端直接 `getPdfBlob` 渲染。 - `print_template` 接收 vue-print-designer **结构化模板**(`pages[].elements[]`),前端要 `loadTemplateData` + `setTestData` 走完整设计器渲染管线(自动支持占位符、表格分页、变量计算等)。 - 两者下游(`processPrintRequest` → 系统打印)完全相同。 **与** **`print_task`** **的对比:** - `print_task` 是已经渲染好的 Base64 PDF → 直接走系统打印。 - `print_template` 是模板 + 填充值 → 前端 vue-print-designer 渲染成 PDF → 走系统打印。 #### 3.2.8 标签打印 (Client -> Server, type = "print_label") 标签打印接收 vue-print-designer **模板 JSON**,由客户端后端直接转换为标签打印机指令(TSPL/TSPL2、ZPL、ESC/POS、CPCL),经系统打印队列以 RAW 方式发送到打印机。适用于热敏/热转印标签机(TSC、佳博、斑马、汉印、得力、芯烨等)。 与 `print_template` 的关键区别: - `print_template`:前端把模板渲染成 **PDF** → 系统打印(适合 A4 文档)。 - `print_label`:后端把模板转换为**打印机原生指令** → RAW 发送(条码/二维码为原生矢量,出标更快、扫描率更高,且不受驱动缩放影响)。 **请求包:** ```json { "type": "print_label", "printer": "TSC TE244", // [必填] 目标标签打印机 "key": "123456", // [选填] 鉴权密钥 "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "data": { /* vue-print-designer 模板结构,与 print_template 的 data 一致 */ }, "testData": { "sn": "A1" }, // [选填] 单条填充(与 labels 二选一) "labels": [ // [选填] 批量填充(与 testData 二选一) { "testData": { "sn": "A1" } }, { "testData": { "sn": "A2" } } ], "label": { "language": "auto", // [选填] auto | tspl | zpl | escpos | cpcl "dpi": 0, // [选填] 0 = 自动读取打印机分辨率 "speed": 0, // [选填] 打印速度(仅 TSPL) "density": 0, // [选填] 打印浓度(仅 TSPL) "gap": 2, // [选填] 间隙高度 mm(TSPL/ZPL) "bline": 0, // [选填] 黑标高度 mm,>0 时按黑标定位 "offset": 0, // [选填] 打印偏移 mm "textMode": "", // [选填] 空 = 按指令集默认 | device | bitmap "continuous": false, // [选填] true = 连续纸(按内容实测算高) "pageSize": { "width": 50, "height": 30 } // [选填] 标签纸宽高 mm,height=0 表示连续纸 } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `type` | String | 固定为 `print_label`。 | | `printer` | String | **必填**。标签打印机名称。 | | `data` | Object | **必填**。`print_template` 同款模板结构(`pages[].elements[]`)。可直接转发设计器 `getTemplateData()` 的返回值。 | | `testData` | Object | \[选填] 单条填充值,与 `labels` **二选一**(同时出现返回错误)。 | | `labels` | Array | \[选填] 批量填充,每个元素为 `{ "testData": {...} }`,一条数据打印一张(或一组)标签。 | | `job.name` | String | \[选填] 任务名称,默认 `Label Job`。 | | `job.copies` | Integer | \[选填] 份数,默认 1。 | | `job.intervalMs` | Integer | \[选填] 多张标签之间的间隔(毫秒)。>0 时逐张一个 RAW 作业。 | | `label.*` | — | \[选填] 见下表,未提供时按打印机名/驱动名自动推断。 | **`label` 子字段:** | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `language` | String | 指令集:`auto`(默认,按打印机名/驱动名推断)/ `tspl` / `zpl` / `escpos` / `cpcl`。推断失败必须显式指定。 | | `dpi` | Integer | 打印分辨率。0 或省略时读取打印机 DEVMODE 分辨率,读取失败按 203。 | | `speed` / `density` | Integer | 打印速度与浓度(TSPL 有效,0 表示用打印机当前设置)。 | | `gap` | Number | 间隙高度(mm),配合间隙纸使用。 | | `bline` | Number | 黑标高度(mm),>0 时按黑标定位(ZPL 用 `^MNY`,TSPL 用 `BLINE`)。 | | `offset` | Number | 打印起始偏移(mm)。 | | `textMode` | String | 文字渲染方式:`device`(机内字库,体积小)/ `bitmap`(位图,样式还原最准)。默认 TSPL、ZPL 走位图,ESC/POS、CPCL 走机内字库。 | | `continuous` | Boolean | `true` 表示连续纸:忽略 `pageSize.height`,按内容实测高度出纸。 | | `pageSize` | Object | 标签纸宽高(mm)。宽高都缺省时按模板 `canvasSize`(设计器 px 基准)换算。 | **语言自动推断规则(按顺序匹配打印机名 + 驱动名,忽略大小写):** | 顺序 | 指令集 | 命中关键词 | | :--- | :--- | :--- | | 1 | `cpcl` | `cpcl`、`zm2`、`zq5`、`qln`、`rw4`、`p4t`、`imz` | | 2 | `escpos` | `esc/pos`、`escpos`、`pos-`、`pos58`、`pos80`、`gb-`、`58mm`、`80mm`、`receipt`、`tm-`、`rp8` | | 3 | `zpl` | `zdesigner`、`zebra`、`zpl` | | 4 | `tspl` | `tspl`、`tsc`、`gprinter`、`gp-`、`xprinter`、`deli`、`得力`、`hprt`、`汉印`、`argox`、`godex`、`postek` | > 规则顺序即优先级:CPCL 先于 ZPL,是因为 Zebra 移动机的驱动名通常也含 `zebra`,若 ZPL 先匹配会把 CPCL 机型判错。未命中任何规则时返回错误,请在 `label.language` 中显式指定。 **调用示例(JavaScript,批量):** ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); const template = { pages: [{ width: 50, height: 30, elements: [ { type: 'text', x: 8, y: 8, width: 40, height: 14, content: 'SN: {{sn}}', fontSize: 12 }, { type: 'barcode', x: 8, y: 26, width: 60, height: 24, content: '{{sn}}' }, { type: 'qrcode', x: 76, y: 8, width: 40, height: 40, content: '{{url}}' } ] }], canvasSize: { width: 200, height: 120 } }; const labels = [ { testData: { sn: 'A0001', url: 'https://example.com/i/A0001' } }, { testData: { sn: 'A0002', url: 'https://example.com/i/A0002' } }, { testData: { sn: 'A0003', url: 'https://example.com/i/A0003' } } ]; socket.addEventListener('open', () => { socket.send(JSON.stringify({ type: 'print_label', printer: 'TSC TE244', key: '123456', job: { name: 'Order-20260615-001', copies: 1, intervalMs: 0 }, data: template, labels: labels, label: { language: 'auto', gap: 2, pageSize: { width: 50, height: 30 } } })); }); socket.addEventListener('message', (event) => { const msg = JSON.parse(event.data); if (msg.type === 'print_result') { if (msg.status === 'success') console.log('标签打印成功:', msg.message); else console.error('标签打印失败:', msg.message); } }); ``` **响应:** 与 3.2.4 一致,服务端返回 `{type: "print_result", status: "success|error", message: "..."}`。 **打印机能力扩展(3.2.3 响应新增字段):** | 字段 | 类型 | 说明 | | :--- | :--- | :--- | | `guessedLanguage` | String | 按打印机名/驱动名推断出的指令集(未命中则无此字段)。 | | `dpi` | Integer | 打印机分辨率(读取失败则无此字段)。 | **已知限制:** - 模板中的 `customScript`(JS 数据加工脚本)不会被客户端执行,会被静默忽略;对应元素按其原有字段(`content` 等)照常渲染。 - 文字为「视觉等效」而非逐像素还原:客户端用内嵌固定字体渲染,换行点可能与浏览器偶有 ±1 字符差异,换来的是跨机器打印结果 100% 一致。 - 彩色内容按 ITU-R BT.601 灰度 + Floyd–Steinberg 抖动转为黑白(热敏纸物理特性)。 - 表格在标签路径**忽略 `autoPaginate`**:固定纸高逐行绘制、超出部分由标签纸裁剪;连续纸按内容整体撑高。 - `pageNumber` 元素在单页标签中无意义,会被静默忽略(不参与绘制)。 - 几何布局精确到 ≤0.5 dot;条码/二维码内容精确,但实际尺寸可能因模块宽取整相差 1–2%。 #### 3.2.9 预览 (Client -> Server, type = "preview") 客户端发送预览指令,服务端收到后将内容保存为临时文件,并打开内置预览窗口。预览窗口使用 Mozilla pdf.js 渲染 PDF,不依赖 WebView 自带 PDF 阅读器。支持三种模式: - **pdf**:Base64 编码的 PDF 内容,直接交给 pdf.js 预览。 - **html**:原始 HTML 字符串,先沿用客户端 vue-print-designer 渲染链路生成 PDF,再交给 pdf.js 预览。 - **json**:vue-print-designer 模板 JSON 字符串,先沿用模板渲染链路生成 PDF,再交给 pdf.js 预览。 预览窗口顶部提供系统打印选项栏。`preview` 请求可携带与打印任务一致的 `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` 字段,作为预览窗口点击“打印”时调用系统打印的默认参数。 **请求包:** ```json { "type": "preview", "mode": "pdf", "content": "<Base64 编码的 PDF 内容>", "title": "订单预览", "printer": "Microsoft Print to PDF", "job": { "name": "Order-001", "copies": 1, "intervalMs": 0 }, "pages": { "range": "", "set": "" }, "layout": { "scale": "fit", "orientation": "portrait" }, "color": { "mode": "color" }, "sides": { "mode": "simplex" }, "paper": { "size": "A4" }, "tray": { "bin": "" } } ``` **字段说明:** | 字段 | 类型 | 说明 | | :---------------------------------------------------------------------------- | :----- | :--------------------------------------------------------------------------------------------------------------- | | `type` | String | 固定为 `preview`。 | | `mode` | String | **必填**。预览模式:`pdf` / `html` / `json`。 | | `content` | String | **必填**。`pdf` 模式为 Base64 编码的 PDF(支持 `data:application/pdf;base64,...` 前缀);`html` 模式为原始 HTML 字符串;`json` 模式为 vue-print-designer 模板 JSON 字符串。 | | `title` | String | \[选填] 预览窗口标题。 | | `printQuality` | String | \[选填] `html` / `json` 渲染为 PDF 时的质量:`fast` / `normal` / `high` / `ultra`。 | | `printer` / `job` / `pages` / `layout` / `color` / `sides` / `paper` / `tray` | — | \[选填] 与 `3.2.4 发送打印任务` 一致,作为预览窗口顶部打印选项栏的默认值,点击“打印”后用于调用系统打印。 | **响应:** ```json { "type": "preview_result", "status": "success", "message": "Preview opened" } ``` **调用示例(JavaScript):** ```javascript // PDF 预览 socket.send(JSON.stringify({ type: 'preview', mode: 'pdf', content: base64PdfString, title: '发票预览', printer: 'Microsoft Print to PDF', job: { name: 'Invoice-001', copies: 1, intervalMs: 0 }, pages: { range: '', set: '' }, layout: { scale: 'fit', orientation: 'portrait' }, color: { mode: 'color' }, sides: { mode: 'simplex' }, paper: { size: 'A4' }, tray: { bin: '' } })); // HTML 预览 socket.send(JSON.stringify({ type: 'preview', mode: 'html', content: '<h1>Hello</h1><p>这是预览内容</p>', title: '小票预览' })); // JSON 预览 socket.send(JSON.stringify({ type: 'preview', mode: 'json', content: JSON.stringify({ orderNo: 'SO-001', items: [...] }), title: '订单数据' })); ``` ## 4. 调用示例 (JavaScript) ```javascript const socket = new WebSocket('wss://127.0.0.1:1122/ws?key=123456'); socket.onopen = () => { console.log('已连接'); }; socket.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'printer_list') { console.log('可用打印机:', msg.data); const targetPrinter = msg.data.find(p => p.isDefault) || msg.data[0]; // 示例:主动刷新打印机列表 // socket.send(JSON.stringify({ type: 'get_printers' })); // 获取打印机能力(纸张/单双面/彩色等) socket.send(JSON.stringify({ type: 'get_printer_caps', printer: targetPrinter?.name })); } else if (msg.type === 'printer_caps') { const caps = msg.data || {}; const sizes = caps.printerPaperNames || caps.paperSizes || []; const paperSize = sizes[0] || 'A4'; // 发送打印任务(按功能分组) socket.send(JSON.stringify({ printer: msg.printer, content: "JVBERi0xLjQKJ...", // Base64 PDF Data job: { name: "Test Job", copies: 2, intervalMs: 0 }, pages: { range: "1-3,5", set: "odd" }, layout: { scale: "fit", orientation: "portrait" }, color: { mode: "color" }, sides: { mode: "duplex" }, paper: { size: paperSize }, tray: { bin: "2" } })); } else { console.log('收到回复:', msg); } }; ```