# PrinterDemo **Repository Path**: tyousan_admin/PrinterDemo ## Basic Information - **Project Name**: PrinterDemo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-25 - **Last Updated**: 2026-11-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # S2Report 接入 Demo(PrinterDemo) 用于同时验证 S2Report 的两种打印模式: - **JSON 模板模式**:按当前环境配置的业务 `data` 模板展示只读 key、可编辑 value 的列表,点击一次“提交打印”完成 `resolveTemplate`、`createJsonRenderInput` 和 `sdk.print`。 - **DOM 采集模式**:用食品经营许可证底板 `spyjxkz.png` 模拟业务预览页,叠加 `text.field2`、`text.field3` 两个文本字段和 `image.qr` 二维码图片字段,点击时由 `captureDom` 抓取,再调用 `createDomRenderInput` 提交打印;可验证底板预览/打印、打印前确认、本地版式保存与复用。 ![PrinterDemo 界面](./screenshot.png) 页面本身是纯静态 HTML + ES Module,不需要构建。JSSDK 已复制到本工程的 `vendor/S2Report-sdk/`,**PrinterDemo 可以脱离 PrinterCenter 仓库单独部署运行**。打印参数按“环境”管理:一个环境同时保存 JSON 和 DOM 两套参数,顶部下拉切换后打印立即使用当前环境;设置里可以维护多套测试/正式环境。 ## 前置条件 1. **中心服务和 Agent 已启动**。 2. **Agent 已连接中心,并且是包含最新 DOM 采集/预览底板/握手修复的版本**。旧版 Agent 会拒绝带 `sourceIdentity`、`layoutStructureHash` 的 RenderInput,也可能在 handshake 中不返回中心地址。JSSDK 的中心地址来自 Agent handshake,业务页面不需要填写中心地址。 3. 在中心后台,把本 Demo 的访问 Origin(例如 `http://127.0.0.1:4180`)加入模板所属应用的 Origin 列表,否则会返回 `ORIGIN_NOT_ALLOWED`。 4. 模板已发布,只需要拿到 `templateId`;JSSDK 会根据 `templateId` 自动读取当前上线版本,业务配置不再填写 `templateReleaseId` / `activeDefaultLayoutHash`。 5. 如果 Agent 返回 `localTemplateStatus: "stale"`,Agent 会自动弹出强制的“服务器模板已更新”确认框,加载服务器最新默认版式;用户选择“使用最新默认版式”后在 Agent 中调整并保存新的本地版式,或取消本次打印。Demo 不在业务页面弹窗。 6. PrinterDemo 使用 `vendor/S2Report-sdk/` 下的 JSSDK 副本。如果刚改过 JSSDK,先在 PrinterCenter 构建,再在 PrinterDemo 执行 `npm run sync-sdk` 更新副本。 ## 启动 先进入 `PrinterDemo` 目录: ```bash cd PrinterDemo node serve.mjs ``` 或者使用 package 脚本: ```bash cd PrinterDemo npm start # 或 pnpm start ``` 也可以从仓库根目录执行: ```bash node PrinterDemo/serve.mjs ``` 默认地址: ```text http://127.0.0.1:4180/ ``` 旧的 `http://127.0.0.1:4180/PrinterDemo/` 也兼容。 `serve.mjs` 默认监听 `0.0.0.0:4180`(所有网卡),所以本机用 `127.0.0.1`、同事用内网 IP、容器/云主机用端口映射都能访问。 也可以指定端口和监听地址: ```bash PORT=4190 node serve.mjs HOST=127.0.0.1 PORT=4190 node serve.mjs # 只监听回环 ``` `serve.mjs` 以 PrinterDemo 自身目录为静态根,因此整个目录复制到任意位置都能独立启动,不再依赖仓库里的 `PrinterCenter/` 路径。 不要用 `file://` 直接打开 HTML;ES Module 和 Agent 通信都需要 HTTP 页面。 页面优先加载工程内的 SDK: ```text /PrinterDemo/vendor/S2Report-sdk/S2Report-sdk.esm.js ``` 只有在本地副本不存在时,位于 PrinterCenter 仓库中开发运行时才会回退到: ```text /PrinterCenter/admin/public/downloads/S2Report-sdk.esm.js /PrinterCenter/jssdk/src/index.mjs ``` ## 独立部署 PrinterDemo 目录已经包含运行所需的全部静态文件(`index.html`、`app.js`、`styles.css`、`assets/`、`vendor/S2Report-sdk/`),可以整体复制到任意静态服务器目录: ```bash # 任选一种 node serve.mjs # 或 nginx / caddy / python -m http.server 直接托管 PrinterDemo 目录 ``` 部署后仍需保证: - 中心服务和 Agent 正常运行; - 部署地址的 **Origin** 已加入模板所属应用的 Origin 白名单(路径不参与 Origin 判断); - 通过 HTTP 访问;Chrome 156 下 HTTP 页面无法授予本地网络权限,可安装 `PrinterCenter/browser-extension` 走 Native Messaging,或改用 HTTPS 页面;Safari 可直接使用 HTTP。不要用 `file://` 打开。 ### 容器 / 云主机 容器里必须让 Node 监听 `0.0.0.0`,否则宿主机通过端口映射访问会出现 `Recv failure: Connection reset by peer`(容器内一切正常,外面连不上): ```bash # 推荐:镜像里直接设环境变量,代码默认值已是 0.0.0.0,这两行是显式兜底 docker run -d --name PrinterDemo -p 4180:4180 \ -e HOST=0.0.0.0 -e PORT=4180 \ -v "$PWD":/app -w /app node:22-alpine npm start ``` docker compose 里对应写法: ```yaml services: PrinterDemo: image: node:22-alpine working_dir: /app volumes: [".:/app"] environment: HOST: 0.0.0.0 PORT: "4180" ports: ["4180:4180"] command: npm start ``` 排查顺序(容器内能开、宿主机 reset 就是监听地址问题): ```bash docker exec PrinterDemo wget -qO- http://127.0.0.1:4180/ | head -3 # 容器内自测 docker exec PrinterDemo sh -c 'awk "NR>1{print \$2}" /proc/net/tcp' # 00000000:1054 = 0.0.0.0:4180,0100007F:1054 = 仅回环 docker port PrinterDemo # 端口映射是否符合预期 ``` ## 更新 SDK PrinterCenter 里的 JSSDK 修改并重新构建后: ```bash cd PrinterCenter ./scripts/build-downloads.sh --jssdk-only cd ../PrinterDemo npm run sync-sdk ``` `npm run sync-sdk` 会复制最新的 `S2Report-sdk.esm.js`,并生成 `vendor/S2Report-sdk/version.json` 记录版本、同步时间和 SHA-256。也可以手动指定构建产物: ```bash node sync-sdk.mjs /path/to/S2Report-sdk.esm.js ``` ## 环境配置(测试 / 正式) 页面顶部不再有“接入配置”按钮,改成 **环境下拉 + 设置**: - 每个环境同时保存 JSON 模板和 DOM 采集两套参数; - 顶部下拉切换环境后立即生效,打印使用当前激活环境的参数; - “设置”里可以新建、复制、重命名、删除环境,或点“保存并启用”。 配置保存在 `localStorage` 的 `printer-demo-env-v1`: ```json { "version": 1, "activeId": "env_...", "preferences": { "debug": false }, "environments": [ { "id": "env_...", "name": "测试环境", "kind": "test", "json": { "applicationId": "...", "templateId": "...", "businessData": "{}" }, "dom": { "applicationId": "...", "templateId": "...", "rootSelector": "#dom-preview-root", "explicit": false } } ] } ``` 首次启动会预置 **测试环境** 和 **正式环境**。如果检测到旧版 `printer-demo-config-v2:dom` / `:json` 或更早的 `printer-demo-config-v1`,会自动合并成一个“默认环境(迁移)”,两个模式的参数和调试开关都会保留。 每个环境包含: | 字段 | 说明 | | --- | --- | | 环境名 | 必填且不能重名 | | 类型 | 测试 / 正式 / 其他,只影响列表徽标 | | JSON `applicationId` / `templateId` | JSON 模板模式使用 | | JSON 业务 data 模板 | 主界面按 key 展开成可编辑列表 | | DOM `applicationId` / `templateId` | DOM 采集模式使用 | | DOM 捕获根节点 | 默认 `#dom-preview-root` | | DOM 使用 `data-print-item-id` | 对比显式标注和普通 HTML 自动识别 | `SDK 调试面板` 是全局开关,不随环境保存。打印人固定为 `demo-user` / `演示用户`。SDK 按页面协议选择本机 Agent 入口:HTTP 页面使用 `ws://127.0.0.1:18181/ws`,HTTPS 页面使用 `wss://127.0.0.1:18182/ws`。HTTPS 页面首次使用前需要信任 Agent 本地 CA(Agent 设置页可打开证书目录)。中心地址不进入环境配置:SDK 会向本机 Agent handshake 自动获取。 > WebSocket 连不上时 SDK 会自动回退到 **扩展 + Native Messaging**(Chrome / Edge 等 Chromium 系浏览器默认开启,不用额外配置)。 > HTTP 页面在 Chromium 156+ 会被 Local Network Access 拦截,这时必须装 S2Report 扩展并保持 Agent 在运行; > 排错可先看 `netstat -ano | findstr 18181`(Windows)有没有 LISTENING,再用浏览器打开 `http://127.0.0.1:18181/` 确认 Agent 在响应。 ### 导入 / 导出 设置弹窗左侧提供: - **导出**:把全部环境(含当前激活环境和调试开关)下载为 `s2report-demo-env-<时间>.json`; - **导入**:选择导出的 JSON 文件,再选择: - **合并导入**(默认):保留现有环境,重名自动加后缀; - **替换全部**:覆盖当前环境;导入前会自动备份,之后可用“恢复导入前”还原。 - 导入会校验环境名和业务 data;文件不合法时不会改动现有配置。 切换环境会清空已解析的模板 / RenderInput 状态,避免拿旧环境的 manifest 打印;**已经提交的打印任务不会取消**,后台监听继续运行。 > 安全提示:如果 Agent 关闭了“打印前弹出预览确认”,点击“提交打印”会**直接出纸**。第一次联调建议先在 Agent 设置中打开确认开关。 ## JSON 模板模式 1. 切到“JSON 模板模式”。 2. 在顶部「设置」里选择或新建一个环境,填写 JSON 的 `applicationId` / `templateId`,并把接入代码里的 `data` 对象粘贴到“业务 data 模板”,保存并启用。 3. 主界面会按 `data` 的层级展开成列表: - key 只读,例如 `text.field2`、`image.logo3`; - value 可编辑,修改后直接用于本次打印。 4. 点击唯一的 **提交打印** 按钮。Demo 会一次性完成: - `resolveTemplate({ templateType: "json" })`; - 读取列表当前值组装 `data`; - `createJsonRenderInput`; - `sdk.print` 提交给 Agent。 5. Agent 弹出版式预览时按 Agent 的流程确认;提交结果通过按钮旁状态文字显示。 说明: - “业务 data 模板”可以直接粘贴 `const data = {...};`,Demo 会自动去掉 `const data =` 和末尾分号。 - 如果“业务 data 模板”为空,主界面列表为空,点击提交会提示先填写模板。 - 图片字段的 value 可以填写 `https` 地址或 `data:image/...` Base64。 - 打印的是**已发布版本**;后台草稿不会进入本次打印。 ## DOM 采集模式 1. 在后台“新增模板”里选择 **DOM 采集**,按需勾选 **打印底板**,保存并发布;环境配置只需要 `templateId`。 2. 切到“DOM 采集模式”,在顶部「设置」里填写当前环境的 DOM `applicationId` / `templateId` 并保存启用(可以和 JSON 使用不同的应用/模板)。 3. Demo 页面为 **A4 横向 297 × 210 mm**,底板 `assets/spyjxkz.png`,叠加 `text.field2`、`text.field3` 两个文本字段和 `image.qr` 二维码图片字段。Agent 版式编辑器中可以调整纸张大小和横纵向,保存本地版式后会随打印使用。 4. 点击唯一的主操作按钮 **提交打印**。Demo 会一键执行完整的 JSSDK 最终流程: - `resolveTemplate({ templateType: "dom" })`; - 从 `#dom-preview-root` 调用 `captureDom`; - 调用 `createDomRenderInput`; - 调用 `sdk.print` 提交给 Agent。 5. 如果 Agent 开启“打印前确认”,Agent 会打开版式编辑器: - 拖动/缩放字段; - 点击“保存本地版式”; - 确认打印。 6. 再次点击“提交打印”,同结构下 Agent 会自动套用保存的版式。 DOM 模式主界面只保留业务预览和操作按钮;JSON 模式也不展示“模板布局”“RenderInput”“打印任务”“运行日志”。抓取/生成/提交结果通过工具栏状态文字反馈;需要看请求日志时,在「设置」里打开“SDK 调试面板”(全局开关)。 ### 验证结构摘要 - 点击“模拟文字变化”:只改 `text.field2`/`text.field3`,**内容摘要变化、结构摘要不变**;旧版式仍可用。 - 点击“模拟结构变化”:增加/删除附加字段,**结构摘要变化**;Agent 应忽略旧版式并提示重新调整。 - 点击“恢复样例”:恢复初始预览 DOM。 ### 底板预览与打印 DOM 采集会把预览区的视觉底板一并采集,作为 **Agent 预览背景**;底板是否打印由“打印底板”开关控制: - 当前 Demo 使用 `assets/spyjxkz.png`(食品经营许可证完整底板)作为捕获根节点的 CSS `background-image`; - **打印底板由服务端 DOM 模板配置控制**:在后台“新增模板 → DOM 采集 → 打印底板”中开启/关闭; - 开启后 JSSDK 从 `domCaptureRule.printBaseboard` 读取,RenderInput 中 `baseboard.printable = true`,Agent 预览和 PDF 都会显示底板; - 关闭后 `printable = false`,Agent 仍显示底板用于对位,但 PDF 跳过底板; - 打印底板完全由服务端 DOM 模板配置决定,Demo 不再提供本地“打印底板”开关; - 自动识别:捕获根节点的 CSS `background-image`; - 显式识别:`data-print-baseboard` 元素,或 `data-print-layer="baseboard"` 元素; - 是否打印也可以用 `data-print-baseboard-printable="true|false"` 标记,Demo 的“打印底板”开关会同步这个属性; - 支持 PNG/JPEG,单张上限 2 MiB;Agent 请求上限已提升到 8 MiB;跨域图片需要允许 CORS,否则会跳过并给出 warning; - SDK 把底板写入 RenderInput 的 `baseboard`,`printable` 原样传递;Agent 编辑器显示底板,PDF 渲染器只绘制 `printable: true` 的底板。 业务预览区放两个文本字段和一个二维码图片字段: ```html
示例文本
示例文本
``` 底板承担许可证的静态文字、印章、边框和底纹;两个文本字段和一个二维码图片字段作为打印项叠加。这样 Agent 预览和打印都能接近业务预览,不再依赖 Agent 重绘伪元素或 CSS 装饰。 > 如果底板里的静态内容不需要打印,可以关闭“打印底板”;此时 PDF 只有叠加字段。需要静态内容打印时保持开启。 **仍然存在的边界**:`captureDom` 只采集真实 DOM 文本/图片项;如果字段标签是 `::before`/`::after` 伪元素、表格线是 CSS border,它们不会被单独采集。Demo 通过把静态内容放进底板解决;如果业务页不能用底板承载,就需要新增伪元素/整页快照采集能力。 该元素会被排除在打印项之外,只作为预览底板。 ### 显式标注对比 勾选“使用 data-print-item-id”后,Demo 会把 `data-field` 写到 `data-print-item-id`: - 显式模式下 `itemId` 是 `invoice.number` 这类业务 key; - 普通模式下 `itemId` 来自唯一 DOM `id` 或 `auto:`。 真实第三方页面如果既不能加标注、也没有稳定 `id`,就只能依赖 `auto` 路径;页面结构变化时版式会失效。 ## 常见问题 | 现象 | 处理 | | --- | --- | | `CENTER_ADDRESS_REQUIRED` | JSSDK 自动向 Agent 询问中心地址,但 Agent 的 handshake 没有返回 `centerBaseUrl`。检查 Agent 是否已配置中心并已连接;使用包含握手修复的 Agent 版本并重启。业务页面不填写中心地址 | | `ORIGIN_NOT_ALLOWED` | 把 `http://127.0.0.1:4180` 加入应用 Origin,注意端口和 `localhost`/`127.0.0.1` 是不同 Origin | | `TEMPLATE_NOT_FOUND` / `TEMPLATE_RELEASE_MISMATCH` | JSSDK 自动读取当前上线版本失败;确认模板已发布、`templateId` 正确,并重启到支持“按 templateId 读取最新发布”的 Agent | | `SESSION_INVALID` / `RENDER_INPUT_INVALID` | 在当前环境里重新保存参数后重试;旧 Agent 需要重新构建/重启 | | `DOM_CAPTURE_ROOT_REQUIRED` | 检查捕获根节点选择器和预览区是否在页面上 | | `DOM_CAPTURE_PAGE_INVALID` | 预览区没有 `data-page-*` 时需要在调用 `captureDom` 时传 `page`;本 Demo 的预览区已声明 A4 | | SDK 加载失败 | 确认 `vendor/S2Report-sdk/S2Report-sdk.esm.js` 存在;更新 SDK 后执行 `npm run sync-sdk` | | JSON 列表为空 | 在「设置」里给当前环境填写“业务 data 模板”后保存;key 只读,value 可编辑 | ## 文件 - `index.html`:页面结构、环境设置弹窗、预览区、结果面板 - `app.js`:环境切换、设置弹窗、SDK 调用、JSON/DOM 两种流程、任务监听 - `env-store.mjs`:环境数据模型、旧配置迁移、校验与持久化 - `env-store.test.mjs`:环境存储单测(`npm test`) - `styles.css`:布局与 A4 预览样式 - `serve.mjs`:零依赖本地静态服务器(以 PrinterDemo 自身为根目录) - `sync-sdk.mjs`:从 PrinterCenter 构建产物同步 JSSDK - `vendor/S2Report-sdk/`:随工程部署的 JSSDK ESM 副本与版本信息 - `assets/spyjxkz.png`:食品经营许可证底板(默认打印) - `screenshot.png`:界面截图 ## 中心断网时 Agent 会缓存最近一次已验签的模板发布,并保存中心心跳下发的离线租约(默认 24 小时)。 断网前至少在线解析过一次时,`resolveTemplate` 会返回 `offline: true`,SDK 随后以 `print.submit { offline: true }` 继续打印,不再向中心申请 grant。 离线租约到期或模板从未在线解析过时,必须重新连接中心;租约和发布缓存会保存到本地数据目录, Agent 重启后仍可复用。 离线打印只记录在 Agent 本地,不会补写中心审计。