# 曦寒打印设计器 · 全站正文 共 20 页。索引见 https://print.docs.xihanfun.com/llms.txt --- 来源:https://print.docs.xihanfun.com/changelog # 更新日志 本页记录 `@xihan-print/core`、`@xihan-print/runtime`、`@xihan-print/designer` 各版本使用者能感知的变更,每条标注 **新增 / 修复 / 调整 / 移除**。这几个包总是同一个版本号一起发布;逐包的完整记录见仓库里各包的 `CHANGELOG.md`。 ## v1.0.0-rc.1(2026-10-21) 首次发布,以原生 Web Components 交付,不绑定前端框架。模板是纯 JSON,按数据分页预览、在浏览器里打印,也可以经本地打印客户端静默打印。 - **新增** `@xihan-print/core`:打印模板的结构(文档、页面模板、页眉页脚及其变体、元素、表格)、单位换算与页面几何、元素类型登记表、模板校验与问题码、编辑操作与撤销重做、按数据分页排版。纯 TypeScript,不依赖 DOM。 - **新增** `@xihan-print/runtime`:`` 按模板与数据分页预览,调起浏览器打印;`printDocument`、`paginateDocument` 不经预览直接排版、打印,分页的每一步与打印的各个环节可经 `trace` 回调报告;与 XiHan.UI 内建语言一一对应的界面语言包(`@xihan-print/runtime/locale`);本地打印客户端的 SDK(`@xihan-print/runtime/client`),连不上客户端时退回浏览器打印。 - **新增** `@xihan-print/designer`:`` 可视化设计器,包括菜单栏、物料与按 `fields` 列出的字段物料,画布上的选择、移动、缩放、对齐吸附与参考线,表格编辑,属性面板,问题列表,页面设置,设置对话框,新手引导,以及按 `draftKey` 在本机自动保存的草稿。元素种类、编辑方式、校验规则、属性编辑器、命令、面板、设置分区、导入器、物料来源与菜单栏的菜单都能经 `extensions` 登记。 --- 来源:https://print.docs.xihanfun.com/ # 曦寒打印设计器 ```yaml layout: home title: 曦寒打印设计器 titleTemplate: 框架无关的打印模板设计、预览与打印 hero: name: 曦寒打印设计器 text: 框架无关的打印模板设计器 tagline: 原生 Web Components · 模板即 JSON · 按数据分页 · 浏览器打印与静默直打 image: src: /images/logo.png alt: 曦寒打印设计器 actions: - theme: brand text: 快速开始 link: /quickstart - theme: alt text: 介绍 link: /introduction features: - title: 框架无关 icon: 🧩 details: 以原生 Web Components 交付,Vue、React、Angular 或不用框架的页面都能直接使用;同一页面放多个实例互不影响。 link: /introduction linkText: 了解设计原则 - title: 模板即数据 icon: 📄 details: 模板是纯 JSON,不含也不执行任何代码;需要计算的地方用受限表达式,导入模板不会带来脚本注入。 link: /guide/template linkText: 了解模板结构 - title: 按数据分页 icon: 📑 details: 表格随数据续排到后续页,可重复表头、本页小计与合计、补空行;长文本与富文本按整行切到后续页;页眉页脚分首页、末页、奇偶页,页码分段编号。 link: /guide/runtime linkText: 了解分页规则 - title: 数据绑定与格式化 icon: 🔗 details: 文本可绑定字段、写占位模板或受限表达式;数字、百分比、日期、人民币大写与值映射,按十进制精确舍入。 link: /guide/binding linkText: 了解数据绑定 - title: 可视化设计器 icon: 🎨 details: 物料面板、标尺与参考线、吸附对齐、属性面板、问题列表、表格编辑与撤销重做;模板的保存与权限交给宿主。 link: /guide/designer linkText: 了解设计器 - title: 本地打印客户端 icon: 🖨️ details: Windows 托盘程序,只监听本机、按网站授权;静默打印到指定打印机,原始指令直送,导出 PDF。 link: /guide/client linkText: 了解打印客户端 ``` --- 来源:https://print.docs.xihanfun.com/introduction # 介绍 曦寒打印设计器(XiHan.PrintDesigner)是一款框架无关的打印设计器:以原生 Web Components 交付,可视化设计打印模板,在浏览器里按数据分页、预览与打印;配合本地打印客户端,还能不弹对话框直接打到指定的打印机。属于曦寒懿(XiHanFun)开源生态,采用 MIT 授权。 ## 能做什么 | 场景 | 用什么 | | --- | --- | | 设计与修改模板 | ``:物料面板、标尺与参考线、吸附对齐、属性面板、问题列表、表格编辑、撤销重做,可切到预览态 | | 在业务页面里预览、打印 | ``:按数据排出全部输出页逐页显示,调起浏览器的打印对话框 | | 不显示预览直接打印 | `printDocument(schema, data)`:在隐藏的 iframe 里排版并调起打印对话框 | | 静默打印、原始指令、导出 PDF | 本地打印客户端,经 `@xihan-print/runtime/client` 连接 | ## 设计原则 - **框架无关**:以原生 Web Components 交付,不绑定任何前端框架。元素用 Light DOM,样式只消费 [XiHan.UI](https://ui.docs.xihanfun.com) 的设计令牌。 - **零第三方依赖**:运行时只依赖 `@xihan-print/*` 与 `@xihan-ui/*`,条码、二维码、受限表达式与格式化都来自 XiHan.UI。 - **模板即数据**:模板是纯 JSON,不含也不执行任何代码。需要计算的地方用受限表达式,导入模板不会带来脚本注入。 - **数据由使用方掌控**:组件不发网络请求。模板存在哪里、数据从哪里取、谁能改模板,都由使用方决定,经属性与事件交给组件。 - **没有全局状态**:不往 `window` 上挂对象,不读写固定键名的本地存储,同一页面放多个实例互不影响。 ## 包与入口 | 入口 | 内容 | | --- | --- | | `@xihan-print/core` | 模板的类型定义、校验、编辑操作与分页算法。纯 TypeScript,不依赖 DOM,浏览器与 Node.js 里都能用 | | `@xihan-print/runtime` | 业务页面用的 ``、`printDocument` 与浏览器打印,样式在 `@xihan-print/runtime/style.css` | | `@xihan-print/runtime/client` | 连接本地打印客户端:静默打印、原始指令、导出 PDF;不用客户端的页面不会加载这部分代码 | | `@xihan-print/runtime/locale` | 各语言的语言包,如 `printZhCN`、`printJaJP`,与 XiHan.UI 的内建语言包一一对应,预览与设计器共用 | | `@xihan-print/designer` | ``,样式在 `@xihan-print/designer/style.css` | 依赖方向固定为 `designer → runtime → core`。业务页面只需要 runtime,设计器只在需要编辑模板的地方加载。 ## 工作流程 ```text 设计器 │ schema-change / save 事件 ▼ 宿主保存模板 JSON(数据库、文件,由宿主决定) │ ▼ 业务页面取模板与数据 ├─ 或 printDocument ──▶ 浏览器打印对话框 └─ @xihan-print/runtime/client ──▶ 本地打印客户端 ──▶ 打印机 / PDF ``` 1. 设计:在设计器里编辑模板,设计器派发 `schema-change` 与 `save`,宿主把模板 JSON 存起来。 2. 打印:业务页面取出模板与数据,交给 `` 预览与打印,或者直接调用 `printDocument`。 3. 静默打印:装了本地打印客户端时,交给客户端打到指定的打印机;连不上客户端时退回浏览器的打印对话框。 ## 运行环境 - `@xihan-print/core` 不依赖 DOM,可以在浏览器、Node.js 或其它 JavaScript 运行环境里校验模板、排出输出页。 - `@xihan-print/runtime` 与 `@xihan-print/designer` 要在支持 Custom Elements 的浏览器里运行,文字度量缺省用 Canvas 2D。 - 本地打印客户端是 Windows 托盘程序,有 x64 与 arm64 两种安装包,渲染用系统的 WebView2 运行时。 ## 下一步 - [快速开始](./quickstart):在任意前端项目里装包、渲染一张模板并打印。 - [模板结构](./guide/template):页面、页眉页脚、元素与表格。 - [富文本](./guide/rich-text):带格式的文字、字段里的 HTML 与存量模板的迁移。 --- 来源:https://print.docs.xihanfun.com/quickstart # 快速开始 这一页在任意前端项目里装上运行时,把一张送货单模板按数据排成输出页显示出来,再调起浏览器的打印对话框。页面上的示例就是下面这几段代码在运行。 ## 安装 ```bash pnpm add @xihan-print/core @xihan-print/runtime @xihan-ui/styles ``` | 包 | 用途 | | --- | --- | | `@xihan-print/core` | 模板的类型与构造函数,这一页用它写模板 | | `@xihan-print/runtime` | `` 与浏览器打印 | | `@xihan-ui/styles` | 设计令牌与组件皮肤,预览外框的颜色与间距都取自它 | 包都是 ESM,用 Vite、webpack 等打包器引入即可。模板也可以直接写成 JSON、或从接口取回,这时业务页面只需要 `@xihan-print/runtime`。 ## 注册元素并引入样式 元素不在 import 时注册,调用 `definePrintPreview()` 之后页面上才能用 ``,重复调用不报错。`../templates/delivery` 是下一节写的模板。 ```ts import { definePrintPreview } from '@xihan-print/runtime' import { data, schema } from '../templates/delivery' import '@xihan-ui/styles/index.css' import '@xihan-print/runtime/style.css' definePrintPreview() ``` ## 写一张模板 模板是纯 JSON。下面用 `@xihan-print/core` 的构造函数写:`createPage` 生成一张 A5 横向、四边留 10 mm 的页面模板,元素登记表的 `createElement` 按元素的缺省值生成元素,只覆盖需要改的属性。坐标与尺寸的单位都是 mm,原点在纸张左上角。 页眉放标题,页脚放页码,页码格式里的 `{{$page}}` 与 `{{$pages}}` 换成当前页码与总页数: ```ts const page = createPage({ id: 'delivery', paper: 'A5', orientation: 'landscape', margins: 10 }) page.header.default = { height: 14, elements: [ registry.createElement('text', { id: 'title', x: 10, y: 12, overrides: { width: 190, height: 10, content: { kind: 'static', text: '送货单' }, style: { ...textStyle, fontSize: 16, bold: true, align: 'center' }, }, }), ], } page.footer.default = { height: 8, elements: [ registry.createElement('pageNumber', { id: 'page-number', x: 80, y: 131, overrides: { width: 50, format: '第 {{$page}} 页 / 共 {{$pages}} 页' }, }), ], } ``` 正文放客户、日期与明细表格。客户用 `{{customer.name}}` 占位,日期按 `yyyy年M月d日` 格式化;表格绑定数组字段 `items`,第一列是跨页连续的行序号,金额列保留两位小数并加千分位,末页加一行合计: ```ts const money = { kind: 'number', decimals: 2, grouping: true } as const function column(id: string, title: string, width: number, field: string, align: TableColumn['align'] = 'left'): TableColumn { return { id, title, width, align, content: { kind: 'field', field, format: money }, cellType: 'text' } } page.elements.push( registry.createElement('text', { id: 'customer', x: 10, y: 26, overrides: { width: 120, content: { kind: 'template', template: '客户:{{customer.name}}' } }, }), registry.createElement('text', { id: 'date', x: 140, y: 26, overrides: { width: 60, content: { kind: 'field', field: 'date', format: { kind: 'date', pattern: 'yyyy年M月d日' } }, style: { ...textStyle, align: 'right' }, }, }), registry.createElement('table', { id: 'items', x: 10, y: 36, overrides: { width: 190, dataField: 'items', columns: [ { id: 'no', title: '序号', width: 14, align: 'center', content: { kind: 'rowNumber' }, cellType: 'text' }, { id: 'name', title: '品名', width: 80, align: 'left', content: { kind: 'field', field: 'name' }, cellType: 'text' }, { id: 'qty', title: '数量', width: 28, align: 'right', content: { kind: 'field', field: 'qty' }, cellType: 'text' }, column('price', '单价', 34, 'price', 'right'), column('amount', '金额', 34, 'amount', 'right'), ], footerRows: [ { id: 'total', placement: 'lastPage', height: 7, style: { ...registry.get('table')!.defaults.body.style, bold: true }, cells: [ { columnId: 'no', colSpan: 4, content: { kind: 'static', text: '合计' }, align: 'right' }, { columnId: 'amount', colSpan: 1, content: { kind: 'aggregate', fn: 'sum', field: 'amount', scope: 'all', format: money } }, ], }, ], }, }), ) ``` 最后合成文档。数据是任意 JSON,字段路径按点与下标读取: ```ts export const schema: PrintDocument = createDocument({ name: '送货单', pages: [page] }) ``` ```ts const products = ['A4 复印纸', '中性笔', '订书机', '文件夹', '便利贴', '计算器'] export const data = { customer: { name: '曦寒商贸有限公司' }, date: '2026-10-17', items: Array.from({ length: 18 }, (_, index) => { const qty = (index % 5) + 1 const price = 12.5 + index * 3 return { name: `${products[index % products.length]} ${index + 1} 号`, qty, price, amount: qty * price } }), } ``` ## 渲染与打印 把模板与数据交给 `` 的 `schema` 与 `data` 属性,它立即分页并逐页显示;属性变了会自动重新分页。复杂数据走属性,不走 attribute。 ```ts const preview = document.createElement('xh-print-preview') preview.schema = schema preview.data = data ``` `print()` 按当前属性重新分页,在隐藏的 iframe 里渲染后调起浏览器的打印对话框,`title` 是对话框与另存 PDF 时的缺省文件名: ```ts const button = document.createElement('button') button.type = 'button' button.textContent = '打印' button.addEventListener('click', () => preview.print({ title: '送货单' })) ``` 明细有 18 行,表格排满第一页后续排到第二页,表头在第二页重复,合计只出现在末页。 打印用的页面里,每种纸张尺寸写成一条具名的 `@page`,边距为 0,模板自己的页边距已经排进了输出页,打印对话框里的边距保持「默认」即可。 ## 不显示预览直接打印 页面上不需要预览时,用 `printDocument` 直接排版并打印: ```ts import type { PrintOutcome } from '@xihan-print/runtime' import { printDocument } from '@xihan-print/runtime' import { data, schema } from '../templates/delivery' export async function printDelivery(): Promise { // outcome.sheets 是送去打印的页数,outcome.diagnostics 是分页诊断 const outcome = await printDocument(schema, data, { title: '送货单' }) return outcome } ``` 模板有错误级问题或拿不到文字度量器时它会抛错,不打开打印对话框。 ## 界面语言 预览与设计器的界面文字缺省是英文。打印给 XiHan.UI 的每种内建语言各出一份语言包,名字是 `print` 加 UI 语言包的名字:`zhCN` 对 `printZhCN`,`jaJP` 对 `printJaJP`。每份已含设计器零件(`@xihan-ui/designer-shell`)同一语言的文案,把 UI 与打印同一语言的两份合并后全局设置,所有实例都跟着换。下面以中文为例。这一步要另外安装 `@xihan-ui/web-components`: ```ts import { printZhCN } from '@xihan-print/runtime/locale' import { mergeXhConfig, setXhConfig } from '@xihan-ui/web-components' import { zhCN } from '@xihan-ui/web-components/locale' setXhConfig(mergeXhConfig(zhCN, printZhCN)) ``` ## 下一步 - [模板结构](./guide/template):页面、页眉页脚、元素与表格。 - [元素](./reference/elements):每种元素的属性。 --- 来源:https://print.docs.xihanfun.com/ai/agents # For Agents 给 Claude Code、Cursor、Codex、GitHub Copilot 这类 AI 编程工具的入口:在项目里用打印设计器时,怎样让 AI 写对接法;参与打印仓库开发时,AI 该读什么。 打印设计器没有单独的命令行或 MCP 服务端,AI 工具要的资料在两处:本站构建时产出的[机读文档](./llms),以及仓库根的 `AGENTS.md`。打印用到的 XiHan.UI 另有自己的 [AI 辅助开发](https://ui.docs.xihanfun.com/ai/agents)资料。 ## 复制这段提示词 把下面这段放进对话,或写进项目的 `AGENTS.md` / `CLAUDE.md` / `.cursor/rules`: ```text 这个项目用曦寒打印设计器(@xihan-print/*)设计打印模板、按数据预览与打印。 写代码前先读 https://print.docs.xihanfun.com/llms.txt,按需读取其中列出的页面(站点地址后加 .md 取 Markdown)。 元素、属性、事件、模板结构与问题码以文档和项目里装的版本为准,不要凭记忆写,也不要套用别的打印组件的 API。 用到 XiHan.UI 的组件、设计令牌或语言包时,再读 https://ui.docs.xihanfun.com/llms.txt。 ``` ## 要装的包 | 用途 | 安装 | 注册 | 引入的样式 | | --- | --- | --- | --- | | 业务页面预览与打印 | `pnpm add @xihan-print/core @xihan-print/runtime @xihan-ui/styles` | `definePrintPreview()` | `@xihan-ui/styles/index.css`、`@xihan-print/runtime/style.css` | | 设计模板 | `pnpm add @xihan-print/designer @xihan-print/runtime @xihan-ui/styles @xihan-ui/designer-shell` | `definePrintDesigner()` | 上面两份,再加 `@xihan-ui/designer-shell/style.css`、`@xihan-print/designer/style.css` | | 界面语言 | 另装 `@xihan-ui/web-components` | 见下文「关键约定」 | — | `@xihan-print/core` 用来在代码里写模板;模板直接写成 JSON 或从接口取回时,业务页面只要 `@xihan-print/runtime`。`@xihan-ui/*` 是打印各包的 peer 依赖,由项目装唯一的一份,与项目自己用的 XiHan.UI 同一个版本,页面上不会出现两份 UI。 ## 关键约定 把这几条一并告诉 AI,常见的错写多出在这里: - **先注册再用**:元素不在 import 时注册,应用启动时调用一次 `definePrintPreview()` 或 `definePrintDesigner()`,重复调用不报错。 - **对象走属性**:模板、数据、字段树这类对象交给元素的属性(如 `schema`、`data`、`fields`),不写成 attribute。Vue 里用 `.prop` 修饰,并把 `xh-` 开头的标签声明为自定义元素,见[在前端框架里使用](../guide/host#在前端框架里使用)。 - **模板是纯 JSON**:不含函数与脚本;坐标、宽高与边距的单位是 mm,原点在纸张左上角,字号用 pt。保存前用 `validateDocument` 或设计器的 `validate()` 校验,有错误级问题的模板不能分页,问题码见[问题码与诊断](../reference/issues)。 - **宿主管保存与取数**:组件不发网络请求。设计器每次修改派发 `schema-change`,保存命令派发 `save`,宿主存好后调用 `markSaved()`;数据、存储与权限都由宿主给,见[与宿主集成](../guide/host)。 - **扩展走 `extensions`**:元素种类、属性编辑器、校验规则、命令、面板与菜单都经 `extensions` 登记,不改包内源码,见[设计器的扩展](../guide/designer#扩展)。 - **语言包合并后全局设置**:界面文字缺省是英文。语言包的名字是 `print` 加 XiHan.UI 语言包的名字(`zhCN` 对 `printZhCN`),与 UI 同一语言的那份合并:`setXhConfig(mergeXhConfig(zhCN, printZhCN))`,见[快速开始](../quickstart)。 ## 参与打印仓库开发 仓库根的 `AGENTS.md` 是写给 AI 的仓库规范: | 内容 | 说明 | | --- | --- | | 仓库概览与目录结构 | `print/` 是三个包的工作区,`docs/` 是本站,`client/` 是打印客户端 | | 常用命令 | 构建、类型检查、单测、Lint、产物契约与冒烟,以及文档站与客户端的命令 | | 分层与依赖 | `designer → runtime → core`,core 不碰 DOM,运行时依赖只允许 `@xihan-print/*` 与 `@xihan-ui/*` | | 硬性约定 | 模板即数据、无全局状态、数据由使用方提供、对外契约、语言包、样式、注释 | | 文档站、打印客户端、发版、Git 与提交 | 各自的做法与禁止事项 | `CLAUDE.md` 是指向 `AGENTS.md` 的链接,Claude Code 打开仓库即读到;Codex 直接读 `AGENTS.md`;Cursor、GitHub Copilot 等工具在各自的规则文件里写一句「先读仓库根的 AGENTS.md」即可。 ## 相关 - [LLMs.txt](./llms) · [快速开始](../quickstart) · [与宿主集成](../guide/host) - [XiHan.UI 的 AI 辅助开发](https://ui.docs.xihanfun.com/ai/agents) --- 来源:https://print.docs.xihanfun.com/ai/llms # LLMs.txt 文档站在构建时产出一组给大语言模型读的机读资产,内容与站点正文同源。AI 工具读取它们即可了解打印设计器的模板结构、元素、API 与用法。 ## 可用资源 | 文件 | 说明 | | --- | --- | | [llms.txt](https://print.docs.xihanfun.com/llms.txt) | 索引:全部页面的标题、一句话摘要与地址,以及下列资产的入口 | | [llms-full.txt](https://print.docs.xihanfun.com/llms-full.txt) | 全站正文,正文引用的示例代码已内联 | | [llms-guide.txt](https://print.docs.xihanfun.com/llms-guide.txt) | 指南:模板结构、数据绑定与格式化、富文本、运行时、设计器、打印客户端、与宿主集成 | | [llms-reference.txt](https://print.docs.xihanfun.com/llms-reference.txt) | 参考:元素、模板类型、问题码与诊断,以及预览与打印、设计器、打印客户端 SDK、模型层 core 的 API | **单页 Markdown**:任何一页的站点地址后加 `.md` 即可取到这一页的 Markdown,如 [guide/template.md](https://print.docs.xihanfun.com/guide/template.md)、[reference/elements.md](https://print.docs.xihanfun.com/reference/elements.md)。每页正文上方的「取本页 Markdown」也指向它。 `llms-full.txt` 体积较大,多数场景读 `llms.txt` 索引后按需取单页,或只取 `llms-guide.txt`、`llms-reference.txt` 之一。 站点在发版时部署,这些资产对应 npm 上最新发布的版本。打印用到 XiHan.UI 的样式、设计器零件与语言包,UI 的机读资产见 [ui.docs.xihanfun.com/llms.txt](https://ui.docs.xihanfun.com/llms.txt)。 ## 在 AI 工具中使用 所有工具都可以用同一段提示词: ```text 阅读 https://print.docs.xihanfun.com/llms.txt 了解曦寒打印设计器,写打印相关的代码时按需读取其中列出的页面(站点地址后加 .md 取 Markdown),以文档为准,不要凭记忆写元素、属性与模板结构;用到 XiHan.UI 时再读 https://ui.docs.xihanfun.com/llms.txt。 ``` | 工具 | 做法 | | --- | --- | | Claude Code | 写进项目的 `CLAUDE.md`,或用 `/memory` 记下 | | Cursor | 在对话中用 `@Docs` 添加 `https://print.docs.xihanfun.com/llms.txt`,或写进 `.cursor/rules` | | Codex | 写进项目的 `AGENTS.md` | | GitHub Copilot | 写进 `.github/copilot-instructions.md` | | Windsurf | 写进 `.windsurf/rules` | ## 相关 - [For Agents](./agents) --- 来源:https://print.docs.xihanfun.com/guide/binding # 数据绑定与格式化 模板只描述「哪里显示什么数据、显示成什么样」,数据在打印时才交给预览或打印函数。数据是任意 JSON,通常就是业务接口返回的对象。 ## 字段路径 字段路径按点与下标读数据: | 写法 | 读到 | | --- | --- | | `customer.name` | `data.customer.name` | | `items[0].qty` | 数组第一项的 `qty` | | `["unit price"]` | 不是标识符的键,用方括号加引号 | - 只读普通对象的自有属性与数组下标,读不到时得到空值,不报错。 - 字符串原样显示,数字、布尔转成文字,`null` 与 `undefined` 为空;对象与数组不能直接显示,显示为空并给出诊断 `data.notScalar`。 - 路径写法不对时校验报错 `field.invalidPath`。 ## 文本内容 文本、长文本、条码与二维码的内容都是一个「文本内容」,有四种来源: | 来源 | 写法 | 说明 | | --- | --- | --- | | 固定文字 | `{ kind: 'static', text }` | 原样显示 | | 字段 | `{ kind: 'field', field, format?, emptyText? }` | 读一个字段,可按格式显示;结果为空时显示 `emptyText` | | 占位模板 | `{ kind: 'template', template }` | 文字里用 `{{path}}` 嵌入字段,占位里只能写路径 | | 表达式 | `{ kind: 'expression', expression, format? }` | 受限表达式,变量是数据对象的顶层字段 | 占位模板适合「文字里嵌几个数据」,如 `客户:{{customer.name}}`;需要计算时用表达式,如 `qty * price`。表达式的结果是单个值,可以再按格式显示。 富文本的内容不是文本内容,另有三种来源,见[富文本](./rich-text#内容来源)。 下面这张模板的每一行,右边都是同一份数据按不同来源与格式显示的结果: ```ts const rows: [label: string, content: TextContent][] = [ ['数字', { kind: 'field', field: 'amount', format: { kind: 'number', decimals: 2, grouping: true } }], ['百分比', { kind: 'field', field: 'rate', format: { kind: 'percent', decimals: 1 } }], ['日期', { kind: 'field', field: 'signedAt', format: { kind: 'date', pattern: 'yyyy年M月d日 HH:mm' } }], ['人民币大写', { kind: 'field', field: 'paid', format: { kind: 'rmb-upper' } }], ['中文数字', { kind: 'field', field: 'qty', format: { kind: 'numeral', style: 'cn-upper' } }], ['值映射', { kind: 'field', field: 'status', format: { kind: 'map', entries: [{ value: 'pending', label: '待发货' }, { value: 'shipped', label: '已发货' }], fallback: '未知' }, }], ['表达式格式', { kind: 'field', field: 'qty', format: { kind: 'expression', expression: 'IF(value >= 3, \'多件\', \'单件\')' } }], ['空值', { kind: 'field', field: 'remark', emptyText: '无' }], ['占位模板', { kind: 'template', template: '{{customer.name}}({{customer.city}})' }], ['表达式', { kind: 'expression', expression: 'qty * price', format: { kind: 'number', decimals: 2 } }], ] ``` ```ts export const data = { amount: 1234567.891, rate: 0.1234, signedAt: '2026-10-17 08:30:00', paid: 1001.5, qty: 3, price: 19.9, status: 'shipped', remark: '', customer: { name: '曦寒商贸', city: '杭州' }, } ``` ## 值格式 字段与表达式的结果可以带 `format`: | 格式 | 写法 | 说明 | | --- | --- | --- | | 数字 | `{ kind: 'number', decimals?, minDecimals?, maxDecimals?, grouping?, rounding? }` | `decimals` 固定小数位;或舍入到 `maxDecimals` 位再去掉末尾的 0、至少留 `minDecimals` 位;`grouping` 加千分位 | | 百分比 | `{ kind: 'percent', … }` | 乘 100 后按数字格式输出并接 `%` | | 日期 | `{ kind: 'date', pattern }` | 占位符 `yyyy yy MM M dd d HH H mm m ss s SSS`,单引号里的内容原样输出 | | 人民币大写 | `{ kind: 'rmb-upper', rounding? }` | 舍入到分,整数金额以「整」结尾 | | 数字字形 | `{ kind: 'numeral', style }` | 整数写成罗马数字(`roman-upper`、`roman-lower`)或中文数字(`cn-lower`、`cn-upper`) | | 值映射 | `{ kind: 'map', entries, fallback? }` | `entries` 每项是 `{ value, label? }`,与选择类组件的选项同一形状、不带 `disabled`。按数据值查表显示 `label`,对上的一项没写 `label` 时显示为空;没对上时显示 `fallback`,没有 `fallback` 时显示原值 | | 表达式格式 | `{ kind: 'expression', expression }` | 用受限表达式把值转成文字,表达式里用 `value` 引用这个值 | - 数字类格式按十进制精确舍入,`1.005` 保留两位得 `1.01`。舍入方式 `rounding` 取 `half-up`(缺省)、`half-even`、`floor`、`ceil`、`down`。 - 值映射按显示文字比较,数字 `1` 与字符串 `'1'` 都能对上 `value: '1'`。同一个数据值写了几项时用第一项,校验报 `format.duplicateMapValue` 提醒。设计器里值映射是一张选项表,两列是数据值与显示文本,可以新增、删除与上移下移。 - 空值不经格式化,直接显示为空(再按 `emptyText` 补上);表达式格式例外,空值也交给表达式。 - 数字、百分比、日期、人民币大写与数字字形由 XiHan.UI 的 `formatValue` 完成,完整规则见 [XiHan.UI 的格式化](https://ui.docs.xihanfun.com/runtime/expression#格式化)。值格式不合法时校验报错,数据不合格式时显示原值并给出诊断 `format.failed`。 ### 区域与时区 预览元素与设计器的 `locale`、`time-zone` 两个 attribute(`printDocument` 的同名选项)交给格式化器: - `locale` 只决定小数点与千分位符号,数字始终是 ASCII 数字,缺省用 `.` 与 `,`。 - `timeZone` 取 `UTC`、`±HH:mm` 或 IANA 名称(如 `Asia/Shanghai`)。不带偏移的日期文本按原样的墙钟时间显示;带偏移的文本、毫秒时间戳在给了时区时换算到该时区。 ## 受限表达式 表达式由 XiHan.UI 的 `@xihan-ui/expression` 求值:只有运算符与白名单函数,不能访问全局对象,也不执行任何字符串代码,模板里存的只是源码文本。 - 文本与表达式格式里,变量是数据对象的顶层字段;表格单元格里,变量是这一行的顶层字段;表达式格式里只有 `value`。 - 常用函数:`IF`、`COALESCE`、`ROUND`、`SUM`、`AVG`、`CONCAT`、`LEFT`、`FORMAT_NUMBER`、`FORMAT_DATE`、`DATE_DIFF` 等,函数名不分大小写。文法与全部函数见 [XiHan.UI 的受限表达式](https://ui.docs.xihanfun.com/runtime/expression)。 - 表达式写错时校验报错 `expression.invalid`;求值失败(类型不对、除以零等)时显示为空,并给出错误级诊断 `expression.failed`。 ### 换一个表达式引擎 预览、设计器与 `printDocument` 缺省用 `createExpressionEngine()`,不提供当前时间,`TODAY()`、`NOW()` 会求值失败。要用当前时间或登记自己的函数,创建一个引擎交给 `expressionEngine` 属性: ```ts import type { XhPrintPreview } from '@xihan-print/runtime' import { createExpressionEngine, ExprDate } from '@xihan-ui/expression' export function useEngine(preview: XhPrintPreview): void { preview.expressionEngine = createExpressionEngine({ now: () => { const date = new Date() return ExprDate.fromParts(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()) }, functions: [ { name: 'MASK_PHONE', minArgs: 1, maxArgs: 1, call: ([phone], context) => { if (phone === null) return null if (typeof phone !== 'string') return context.typeMismatch(0) return phone.length === 11 ? `${phone.slice(0, 3)}****${phone.slice(7)}` : phone }, }, ], }) } ``` 宿主函数必须是纯函数,只依赖实参与当前时间。表格表尾的求和、平均、最小、最大也经表达式引擎计算,只启用部分内置函数时要留下 `SUM`、`AVG`、`MIN`、`MAX`。 ## 表格的数据 表格的 `dataField` 指向一个数组,每个元素一行。表体单元格的字段、占位与表达式都以行对象为根: - `field: 'qty'` 读这一行的 `qty`; - `{{name}}({{spec}})` 按行对象替换; - `qty * price` 的变量是这一行的顶层字段; - `rowNumber` 是从 1 开始、跨页连续的行序号。 表尾单元格的固定文字、字段与占位以整份数据为根,汇总则按 `scope` 取本页、截至本页或全部数据行,见[模板结构](./template#表尾行)。`dataField` 读到的不是数组时,表格按没有数据处理,并给出诊断 `data.notArray`。 ## 图片、条码与页码 - **图片**:来源是固定地址 `{ kind: 'static', url }` 或字段 `{ kind: 'field', field }`。地址只认 http、https、`blob:`、以 `data:image/` 开头的内联图片与相对地址,`javascript:` 等其它地址不显示,并给出 `image.unsafeSource`。 - **条码与二维码**:内容是文本内容,可以绑定字段。内容不能按所选码制编码时(如 EAN-13 位数不对)不画,并给出 `barCode.invalidValue` 或 `matrixCode.invalidValue`。 - **页码**:格式是占位模板,只能用 `{{$page}}` 与 `{{$pages}}`;`numeral` 把两者都写成罗马数字或中文数字。 - **水印**:文字里可以用 `{{path}}` 取数据。 ## 诊断 取值与格式化出问题时,分页照常进行,问题以诊断的形式随分页结果给出:没提供格式化器或表达式引擎、格式化失败、表达式求值失败、取到的不是单个值等。预览元素在 `paginated` 事件里带上诊断,全部问题码见[问题码与诊断](../reference/issues)。 --- 来源:https://print.docs.xihanfun.com/guide/client # 打印客户端 浏览器出于安全考虑,打印都要经过打印对话框,网页不能自己选打印机、也不能把原始指令发给小票机或标签机。本地打印客户端补上这一块:它是一个 Windows 托盘程序,业务页面经 `@xihan-print/runtime/client` 连上它,就能不弹对话框直接打到指定的打印机、写原始指令、导出 PDF。浏览器 SDK 的完整接口见[打印客户端 SDK](../reference/client)。 | 能力 | 说明 | | --- | --- | | 静默打印 | 打到指定的打印机,可设份数、逐份、单双面、彩色或灰度、页码范围;也可以弹出系统的打印对话框 | | 原始指令 | 把 ESC/POS、ZPL、TSPL 等指令原样写给打印机 | | 导出 PDF | 纸张尺寸不同的输出页导出成一份 PDF,每页保持自己的尺寸 | | 打印机列表 | 名称、是否默认、状态(就绪、暂停、脱机、缺纸、卡纸等),单台打印机可查纸张、双面、彩色与最大份数 | ## 安全模型 - 只监听本机的 `127.0.0.1`,不接受其它电脑的连接。端口缺省为 17620,被占用时依次试 17621 到 17623。 - 按网站授权:每个网站(协议、主机与端口都相同才算同一个)第一次连接时,客户端询问用户一次。只接受 http 与 https 的网站。 - 打印任务在一个不跑页面脚本、不联网的 WebView2 里渲染。文档用到的图片与字体由浏览器取好后随任务带去,渲染时访问任何别的地址都会被拦下。 - 原始指令只能发给装在 Windows 里的打印机,不能直连局域网里的 IP 与端口。 ## 安装 安装包在仓库的 [Release 页面](https://github.com/XiHanFun/XiHan.PrintDesigner/releases)发布,x64 与 arm64 各一个,文件名形如 `XiHanPrint-<版本>-x64-setup.exe`。 - 按当前用户安装到 `%LocalAppData%\Programs\XiHanFun\PrintClient`,不需要管理员权限。 - 需要系统的 WebView2 运行时:Windows 11 自带;没有时安装包调起微软的 WebView2 安装程序(要能联网)。 - 安装向导按系统界面语言显示简体中文或英文。附加任务里可以选开机时自动启动;装好后在开始菜单里有「曦寒打印客户端」。 - 装新版本时直接运行新的安装包:旧进程先退出,开机自启的设置与授权过的网站都保留。 - 在 Windows 的「应用」里卸载。卸载时询问是否一起删除设置、来源名单与日志;管理员下发的策略文件不删。 客户端运行后在托盘显示图标,右键菜单里有: | 菜单 | 作用 | | --- | --- | | 首行 | 版本与监听的端口 | | 设置… | 开机自启、检查新版本、日志级别,允许与拒绝的网站名单(可删除) | | 诊断信息… | 版本、WebView2 版本、端口、策略、来源名单与最近的任务 | | 打开日志目录 | 日志在 `%LocalAppData%\XiHanFun\PrintClient\logs`,按天滚动,保留 14 天;只记任务的来源、打印机、页数、耗时与错误,不记文档内容与原始指令 | | 导出诊断包… | 把诊断信息、日志、设置与策略文件打成 zip,形如令牌的值会打码 | | 测试打印 | 给选中的打印机打一页自检页 | | 检查更新… | 立即检查新版本;策略关闭检查更新时不显示 | | 开机自启 | 开关 | | 退出 | 退出客户端 | ## 首次授权 网站第一次连接时,客户端在桌面上弹出授权窗口,写明是哪个网站、它将能不弹对话框直接打印: - **允许**:记进允许名单,以后不再询问。 - **仅这次**:只放行这一次连接。 - **拒绝**:这次不放行;勾上「拒绝时不再询问这个网站」则记进拒绝名单。 - 60 秒内没人处理,当作这次不允许。 页面这边会收到 `consentpending` 事件,可以提示用户去点「允许」;被拒绝或超时时,连接以 `CONSENT_DENIED` 或 `CONSENT_TIMEOUT` 失败。名单在托盘的「设置」里查看和删除。 ## 在页面里连接 整个页面创建一个连接即可,创建时不连,第一次用到时才连: ```ts import type { XhPrintPreview } from '@xihan-print/runtime' import type { PrintClientJob, PrinterInfo } from '@xihan-print/runtime/client' import { createPrintClient, PrintClientError, printWithFallback } from '@xihan-print/runtime/client' import { data, schema } from '../templates/delivery' // 整个页面共用一个连接;创建时不连,第一次用到时才连 export const client = createPrintClient() export function watchConsent(notify: (message: string) => void): () => void { // 网站第一次连接时,客户端在桌面上弹出授权窗口 return client.on('consentpending', ({ origin }) => notify(`请在打印客户端弹出的窗口里允许 ${origin}`)) } ``` 最常用的写法是 `printWithFallback`:交给客户端静默打印,等它交给系统的打印队列;只有连不上客户端(没装、没运行,或协议版本不对)时退回浏览器的打印对话框。打印机出错、网站没被允许这类错误照常抛出,不退回,免得用户以为已经静默打出来了: ```ts export async function printDelivery(notify: (message: string) => void): Promise { try { const outcome = await printWithFallback(client, schema, data, { printer: '仓库打印机', copies: 2, title: '送货单' }) notify(outcome.via === 'client' ? `已交给打印机,共 ${outcome.sheets} 页` : '没有连上打印客户端,已改用浏览器打印') } catch (error) { if (!(error instanceof PrintClientError)) throw error switch (error.code) { case 'LOCAL_NETWORK_DENIED': notify('浏览器拦下了到本机打印客户端的连接,请在网站设置里允许访问本机应用') break case 'CONSENT_DENIED': case 'CONSENT_TIMEOUT': notify('打印客户端没有允许这个网站打印') break case 'PRINTER_NOT_FOUND': notify(`找不到打印机 ${String(error.details.printer)}`) break default: notify(`打印失败:${error.code}`) } } } ``` 打印选项: | 选项 | 说明 | | --- | --- | | `printer` | 打印机名称,缺省为默认打印机 | | `copies`、`collate` | 份数(1 到 999)与是否逐份打印 | | `duplex` | `simplex` 单面、`long-edge` 长边翻页、`short-edge` 短边翻页 | | `color` | `color` 彩色、`grayscale` 灰度 | | `pageRanges` | 页码范围,如 `1-3,5` | | `dialog` | 为 `true` 时弹出系统的打印对话框,用户选定后再打 | | `timeoutMs` | 交给打印队列的期限 | | `fitToNearestPaper` | 为 `true` 时,打印机没有某组纸张就改用最接近的纸张,见下文;缺省 `false` | ### 纸张怎么定 打印选项里没有纸张,纸张取自模板: - 每张页面模板的纸张就是它排出的输出页的尺寸。客户端把输出页按纸张尺寸分组,每组一个打印作业,按输出页的顺序依次送出。模板里换了纸张,打印跟着换;一份模板里混用几种纸张,也是一次交给客户端。 - 输出页的内容是按模板纸张排好的,所以不另设纸张选项去换成别的纸。 - 弹出系统打印对话框(`dialog: true`)时,采用对话框里选的打印机、份数、逐份、单双面、彩色与页码范围;纸张仍按模板,在打印机首选项里改纸张不起作用。 - 送出之前,客户端按打印机的纸张能力逐组核对:驱动报的纸张里有这组的尺寸(宽高各差 1 mm 以内,横放竖放都算),或者驱动接受这个尺寸,才照这组的纸张送出;横向的输出页可以用竖放的同尺寸纸张横过来打。核对时照送出时的设置问驱动会留下哪种纸,驱动会悄悄换成别的纸的,按对不上处理。 - 有一组对不上,整个任务以 `PAPER_UNSUPPORTED` 失败,一张都不打。`details` 带打印机名(`printer`)、第一个对不上的分组序号(`group`,从 0 起)与它的宽高(`widthMm`、`heightMm`),以及打印机报的全部纸张(`papers`,形状同 `PaperInfo`)。 - 宿主愿意放宽时,打印选项给 `fitToNearestPaper: true`:对不上的组改用最接近的纸张。横竖两种放法都算,放得下原大内容的纸取面积最小的一种,都放不下时取缩得最少的一种;内容比纸大的等比缩小到放得下,比纸小的原大居中。任务照常完成,`status.warnings` 里每个换了纸的组带一条 `PAPER_FITTED`,`details` 写着换成的纸张(`paper`)、是否横放(`landscape`)与缩放倍数(`scale`)。 - 驱动不报纸张能力,或者读不出驱动会留下哪种纸时,不拦,照模板的尺寸送出,`status.warnings` 里带 `PAPER_UNCHECKED`。 - 需要提前提示时,用 `client.printer(name)` 查这台打印机的纸张列表(`capabilities.papers`)。 - 原始指令的纸张由指令自己决定(如 ZPL 的 `^PW`、`^LL`,TSPL 的 `SIZE`),客户端原样下发,不涉及纸张。 要看任务进度或取消任务时,直接用 `printDocument`,它立即返回一个任务: ```ts export async function printToDefault(onStatus: (text: string) => void): Promise { const printers = await client.printers() const target = printers.find(item => item.isDefault) const job: PrintClientJob = client.printDocument(schema, data, { printer: target?.name, duplex: 'long-edge', pageRanges: '1' }) job.onStatus((status) => { onStatus(status.position === undefined ? status.state : `${status.state},前面还有 ${status.position} 个任务`) }) // completed 时兑现,表示已交给系统的打印队列;failed 与 cancelled 时以错误拒绝 await job.done return target } ``` 任务状态依次是 `queued`、`started`,最后是 `completed`、`failed` 或 `cancelled`。`completed` 表示已经交给系统的打印队列,不等于纸已经打出来;照常完成、但有宿主应当知道的情况(如换了纸、没能核对纸张)时,状态里带 `warnings`。断线后 SDK 自动重连并认回自己的任务,客户端里的任务不受影响。 预览元素与设计器有一个 `printClient` 属性,给了以后它们的打印也先交给客户端: ```ts export function usePrintClient(preview: XhPrintPreview): void { // preview.print() 先交给客户端,连不上时才调起浏览器的打印对话框 preview.printClient = client } ``` 给了 `printClient` 的设计器,设置对话框里多出打印方式一项与「本地客户端」一页:用户可以改用浏览器打印,也可以测试连接,见[设计器的设置](./designer#设置)。 ## 原始指令 `printRaw` 把字节原样写给打印机,适合小票机、标签机这类有自己指令集的打印机。指令由业务方自己拼,客户端不做任何解释: ```ts export function printReceipt(): PrintClientJob { const bytes = new Uint8Array([ 0x1B, 0x40, // ESC @ 初始化 ...new TextEncoder().encode('ORDER 20261017-001\n\n\n'), 0x1D, 0x56, 0x00, // GS V 0 切纸 ]) return client.printRaw('小票打印机', bytes, { docName: '小票' }) } ``` - 打印机要装在 Windows 里,用「Generic / Text Only」或厂商驱动都可以,端口可以是 USB、并口或网络端口。 - 传字符串时按 UTF-8 编码;打印机要 GBK 等编码时,先自己转成字节。 ## 导出 PDF `exportPdf` 在客户端里把模板排成一份 PDF,纸张尺寸不同的输出页各自保持尺寸,结果是一个 Blob: ```ts export async function downloadPdf(): Promise { const pdf = await client.exportPdf(schema, data) const url = URL.createObjectURL(pdf) const link = document.createElement('a') link.href = url link.download = '送货单.pdf' link.click() setTimeout(() => URL.revokeObjectURL(url), 1000) } ``` ## 打印是怎么完成的 1. 浏览器里照常分页,把输出页渲染成一份独立的 HTML。 2. 取来输出页用到的图片、背景图与模板字体族的 `@font-face`,地址改写成任务里的资源,随任务一起交给客户端。跨源的图片与字体文件要服务器允许 CORS,取不到时任务以 `RESOURCE_BLOCKED` 失败,不会悄悄少画。资源合计不超过 64 MiB。 3. 客户端在不跑页面脚本、不联网的 WebView2 里载入这份文档,等字体与图片就绪。 4. 按纸张尺寸分组,先逐组核对打印机的纸张,再每组一个打印作业,依次交给系统的打印队列;页码范围按全文页码换算到各组,没有选中页的组不核对。 几点限制: - 纸张尺寸不同的模板打多份时,是第一组打完全部份数再打第二组。 - 页面里读不到内容的跨源样式表会按 CORS 再取一次,仍取不到就跳过,里面声明的字体不会随任务带去。 - 浏览器与 WebView2 同为 Chromium,同一台电脑上排出来的折行一致;在 Firefox 里分页、交给客户端打印时,个别元素框里的折行可能不同。 ## 企业部署 ### 网站要用 HTTPS Chrome 与 Edge 从 147 版起,公网或局域网里的网页连接本机的 `127.0.0.1` 之前,要先过浏览器自己的「本机应用」权限(`loopback-network`),每个网站询问一次。这个权限只有安全上下文能请求,所以: - 业务系统要用 HTTPS 提供,`http://localhost` 除外。用 http 提供的局域网系统连不上本机客户端,浏览器也不会弹出询问。 - 连接被浏览器拦下时,SDK 报 `LOCAL_NETWORK_DENIED`,与「没装客户端」的 `CLIENT_UNAVAILABLE` 分开,宿主可以据此提示用户去网站设置里允许,而不是去安装客户端。 - 只有能确认连接是被浏览器拦下时才报 `LOCAL_NETWORK_DENIED`:页面不在本机回环地址上(`localhost` 及其子域、`127.0.0.0/8`、`[::1]`),所有端口都在建连期限(`connectTimeoutMs`)内失败,并且这个权限被拒或页面不是安全上下文。页面本身在本机回环地址上时这个权限不起作用;被拦下的连接会立即失败,有端口到期限还没有结果就说明连接没被拦。这两种情形连不上都报 `CLIENT_UNAVAILABLE`。 - `CLIENT_UNAVAILABLE` 的 `details` 带探测过的端口(`ports`)与浏览器报的这个权限的状态(`permission`:`granted`、`prompt` 或 `denied`,浏览器不认识这个权限时不带),宿主可以按它补一句提示。 - 企业环境可以用 Chrome 的策略 `LocalNetworkAccessAllowedForUrls` 预先放行业务系统的地址,免得每台电脑第一次使用时各弹一次。 页面嵌在 iframe 里时,每一层 iframe 的 `allow` 都要写上 `loopback-network`: ```html ``` ### 客户端策略 管理员可以在 `%ProgramData%\XiHanFun\PrintClient\policy.json` 放一份策略,优先于用户的设置。文件允许注释与末尾逗号: ```jsonc { // 预先放行的网站,不弹授权窗口 "allowedOrigins": ["https://erp.example.com"], // 为 false 时不认识的网站一律不放行,只认上面的名单与用户允许过的网站 "promptForUnknownOrigins": false, // 固定监听的端口;SDK 的 ports 选项要与它一致 "ports": [17620], "updates": { // 是否检查新版本 "check": true, // 自建的更新清单地址,只认 https,按顺序尝试 "manifestUrls": ["https://intranet.example.com/print-client/update.json"] } } ``` - 不合法的来源、端口与地址记一条警告日志后跳过;整个文件读不出时按没有策略处理,并记错误日志。 - 策略里的网站在设置窗口里标「(策略)」,用户不能删除。托盘的「诊断信息」显示当前生效的策略。 ### 批量安装 安装包支持 Inno Setup 的命令行参数,按当前用户静默安装与卸载: ```cmd XiHanPrint-<版本>-x64-setup.exe /VERYSILENT "%LocalAppData%\Programs\XiHanFun\PrintClient\unins000.exe" /VERYSILENT ``` `/LANG=en` 强制用英文向导。安装不需要管理员权限,所以要在每个使用打印的 Windows 用户下各装一次。 ### 新版本 客户端启动 2 分钟后检查一次新版本,之后每天一次,先查 Gitee、再查 GitHub 上的更新清单;发现新版本时弹出提示,写明版本与发布说明。用户可以在设置里关掉检查,管理员可以用策略关掉,或换成内网的清单地址。 --- 来源:https://print.docs.xihanfun.com/guide/designer # 设计器 `` 是可视化的模板设计器:画布按纸张显示一张页面模板的页眉、正文与页脚,旁边是物料、属性与问题列表。它只吃模板与数据、吐模板与事件,模板存在哪里、谁能改,都由宿主决定。属性、方法与事件的完整列表见[设计器参考](../reference/designer)。 ## 接入 ```bash pnpm add @xihan-print/designer @xihan-print/runtime @xihan-ui/styles @xihan-ui/designer-shell ``` 设计器用到 XiHan.UI 的设计器零件,样式要四份一起引入。`definePrintDesigner()` 注册设计器、它内部用到的预览与 UI 元素,重复调用不报错: ```ts import type { PrintDesignerEventMap } from '@xihan-print/designer' import type { DataField } from '@xihan-ui/designer-kit' import { definePrintDesigner } from '@xihan-print/designer' import { data, schema } from '../templates/delivery' import '@xihan-ui/styles/index.css' import '@xihan-ui/designer-shell/style.css' import '@xihan-print/runtime/style.css' import '@xihan-print/designer/style.css' definePrintDesigner() ``` 把模板交给 `schema`,可绑定的字段树交给 `fields`:属性面板里选字段时从这棵树里选,物料面板也多出「字段」一组,字段可以直接拖上画布(见下文「字段物料」): ```ts const fields: DataField[] = [ { name: 'customer', label: '客户', kind: 'object', children: [{ name: 'name', label: '名称', kind: 'value' }] }, { name: 'date', label: '日期', kind: 'value' }, { name: 'items', label: '明细', kind: 'list', children: [ { name: 'name', label: '品名', kind: 'value' }, { name: 'qty', label: '数量', kind: 'value' }, { name: 'price', label: '单价', kind: 'value' }, { name: 'amount', label: '金额', kind: 'value' }, ], }, ] ``` ```ts const designer = document.createElement('xh-print-designer') designer.schema = schema designer.fields = fields designer.sampleData = data designer.data = data ``` - `sampleData` 是画布上显示用的示例数据,给了就按它显示实际值,不给时字段显示成 `{path}` 占位。 - `data` 是切到预览态与打印时用的数据。 ## 界面 外框从上到下是菜单栏、工具栏、中间三栏(左边物料与页面结构、中间画布、右边属性)、下方的问题列表与最底下的状态栏。各块面板停在哪一栏、显不显示、收没收起、浮没浮出,记在一份面板布局里,见下文「面板布局」;预览态只留菜单栏、工具栏与画布。 | 部分 | 位置 | 面板 id | 作用 | | --- | --- | --- | --- | | 菜单栏 | 最上面 | — | 文件、编辑、视图、插入、页面、帮助几张菜单,见下文「菜单栏」;正中放宿主的模板标题,右边一段放宿主别的常显内容。常开 | | 工具栏 | 菜单栏下面 | — | 撤销重做;剪切、复制、粘贴、创建副本、删除;对齐(左、水平居中、右、顶端、垂直居中、底端)、分布(水平、垂直)与层级(置顶、上移一层、下移一层、置底),每条一颗图标钮;拖动工具、缩放、网格与吸附开关;设计与预览、页面设置、打印;末组是面板,设置与帮助在菜单栏里 | | 物料 | 左栏 | `palette` | 按分组列出元素:每组是组名一行,下面是元素的方形小卡片,种类的图标在上、名称在下,按面板宽度自动排成几列;悬停或用键盘走到一张卡片时提示它的名称。点一下把元素加到视口中央,拖到画布上放在落点。「字段」一组按字段树一行一个地显示,见下文「字段物料」。扩展的物料来源另加的几组排在后面,见下文「扩展」 | | 页面结构 | 左栏,物料下面 | `outline` | 按页面模板列出页眉、正文与页脚里的元素,同一处最上层的排最前;点一项选中它,在同一处里调层级,见下文「页面结构」 | | 画布 | 中间 | — | 按纸张显示当前页面模板的页眉、正文与页脚,区的标签上切换页眉页脚的变体 | | 标尺 | 画布上边与左边 | — | 毫米标尺;从标尺拖出参考线,参考线存进页面模板,只用于对位、不打印。设置里可以关掉 | | 翻页条 | 画布右侧中间 | `pages` | 画布上的浮动件,只能显示或隐藏。上一页、页码、下一页,新增、复制、删除页面模板。页码钮上写「第几个 / 共几个」,点它列出全部页面模板,点一项切过去,下面是上移、下移页面 | | 小地图 | 画布左下角 | `minimap` | 画布上的浮动件,只能显示或隐藏。按页显示:当前页面模板的纸一块淡色,当前显示的页眉页脚变体与正文里的元素各一块,框出画布此刻看到的范围;在上面按下或拖动,视口中心移到那里。键盘上,画布没有选中元素时按方向键平移视口 | | 选中工具条 | 选中框上方 | — | 选中元素时浮出:层级四颗钮、创建副本、复制与删除;文字类元素多出字号(钮上写当前字号)、文字样式与左中右对齐,多选多出对齐六颗与分布两颗,见「画布操作」 | | 尺寸徽标 | 选中框下方 | — | 选中元素时常驻宽 × 高(mm),单选时可以直接改,见「画布操作」 | | 属性 | 右栏 | `properties` | 编辑选中的元素,多选时只显示共同的属性;没有选中时编辑页面设置;表格编辑态里编辑选中的表格部位 | | 历史记录 | 右栏,属性下面 | `history` | 缺省隐藏,从「面板」菜单打开。按先后列出撤销历史的每一步,当前位置那一行选中,选一行就跳到那一步;已撤销的一步字色减淡。面板头是「清空历史」,先确认再清空。只读与预览态时只能看 | | 问题 | 下方 | `issues` | 校验问题列表,点一下切到问题所在的页面模板并定位到元素;错误、警告数在面板头上,点一下只看或不看这一级 | | 状态栏 | 最下面 | — | 左边写选中的对象:没有选中时是当前页面模板与上面的元素个数,选中一个是种类与位置尺寸(mm),选中多个是个数;右边写打印客户端的连接状态(给了 `printClient` 时)与缩放,点缩放回到实际尺寸。设置里可以关掉 | 物料、页面结构、属性、历史记录与问题是外框里的卡片:面板头写图标与标题,属性面板的标题下面写正在编辑的对象,「?」里是这块面板的用法。 ### 面板布局 - 面板头上的「×」隐藏这块面板,收起钮(或双击面板头)收成只剩标题条,「更多」菜单里换到别的栏、调先后、浮出成小窗或停靠回去。按住面板头拖:拖到外框边上换栏,拖到两块面板之间调先后,拖到画布上浮出成小窗;小窗不出外框。 - 工具栏的「面板」菜单列出物料、属性、问题、翻页条、小地图、页面结构与历史记录,勾选的显示;末尾是「显示 / 隐藏所有面板」(Ctrl / ⌘ + \)、「还原面板显隐」(Ctrl / ⌘ + Shift + \,位置与大小不动)与「恢复默认布局」。 - 布局一改,设计器就把整份布局写进设置的 `layout.panels`(`DESIGNER_SETTING_KEYS.layout`),派发 `settings-change`,宿主存起来、下次随 `settings` 赋回即可;给了 `settingsStorageKey` 时设计器自己存。拖分隔条只在松手时写一次。 - 宿主用 `defaultLayout` 给缺省布局:用户没存过布局时用它,「恢复默认布局」也回到它。用 `settingsPolicy` 管控:`layout.panels` 锁定整份布局,`layout.panels.<面板 id>` 写 `hidden` 就没有这块面板、写 `locked` 就固定这块面板。面板 id 见包入口导出的 `PRINT_DESIGNER_PANELS`。 ```ts export function wireLayout(designer: XhPrintDesigner): void { // 缺省收起问题列表;宿主的业务里用不到翻页条,也不许用户把它开出来 designer.defaultLayout = { version: 1, hidden: [PRINT_DESIGNER_PANELS.issues] } designer.settingsPolicy = { [`${DESIGNER_SETTING_KEYS.layout}.${PRINT_DESIGNER_PANELS.pages}`]: 'hidden' } } ``` 窗口窄时左右两栏收起,从「面板」菜单打开时以抽屉盖在画布上;浮出的面板在窄屏时按停靠位置收进抽屉。 工具栏缺省只显示图标,设置里可以换成文字或图标加文字;翻页条与选中工具条只显示图标。没有图标的按钮写一段短文字,比如页码钮写「1 / 3」、字号钮写当前字号,按钮的名字里含着这段字。按钮的名字与快捷键报给读屏,悬停或键盘聚焦到按钮时提示命令的文字与快捷键;会删掉内容的命令按危险色显示。工具栏放不下时,末尾的按钮收进「更多」。 有几条命令点开的不是一个动作,而是锚在按钮上的弹层或菜单: - 「页面设置」是弹层,改当前页面模板的纸张、方向与页边距,与属性面板里「纸张」一组是同一批属性,改动同样记撤销。 - 对齐、分布与层级每条一颗钮,一字排开,工具栏或选中工具条放不下时多的收进它们各自的「更多」。翻页条上的页码与选中工具条上的字号是菜单。 扩展登记的面板也放进外框,同样记在面板布局里,见下文「面板」。各区的属性见[元素](../reference/elements),属性面板的分组与中文名与它一致。 ### 菜单栏 菜单栏在外框最上面,菜单按命令表排: | 菜单 | 条目 | | --- | --- | | 文件 | 保存;导入字段;打印;设置 | | 编辑 | 撤销、重做;剪切、复制、粘贴、删除、创建副本;全选;编辑 JSON(开发者模式开着时) | | 视图 | 设计、预览;放大、缩小、适合窗口、实际大小;拖动;网格、吸附;面板(子菜单,就是工具栏上「面板」那一张)、外观(子菜单:显示工具栏与状态栏) | | 插入 | 各种元素,按物料分组各成一组,扩展的元素排在它的分组里;页眉、页脚(子菜单,见下) | | 页面 | 纸张、方向(子菜单,单选)、页面设置;上一页、下一页;新增、复制、删除页面;上移、下移页面 | | 帮助 | 开始引导、帮助、快捷键;关于 | - 条目写图标、名称与快捷键,开关式的命令(设计与预览、拖动、显示开关)带勾,此刻不可用的灰掉;没有条目的菜单不出现。预览态照样有菜单栏,从「视图」切回设计。 - 插入菜单里选一种元素,与点物料一样放在视口中央并选中。页眉、页脚的子菜单与画布上区标签的变体菜单做同一件事:上半是已有的变体,单选,当前显示的打勾;下半是还没有的变体,写「新增变体 · 首页」这样,选了新增并切过去;显示的不是常规时,末尾是「删除变体」。 - 页面菜单的纸张列出各种纸张预设,方向是纵向与横向,当前页面模板的打勾,选中写进当前页面模板、记一步撤销;自定义纸张与页边距在「页面设置」里改:它取消选择,在属性面板里编辑当前页面模板,属性面板藏着时把它显示出来。只读与预览态时插入菜单与纸张、方向不可用,只读时「页面设置」照样能看。 - 「快捷键」「关于」打开设置对话框的那一页。「视图 → 外观」的两条显示开关改的是设置(`toolbar.visible`、`general.statusBar`),和在对话框里改一样派发 `settings-change`;`settingsPolicy` 藏起或锁定了那一条时它不可用。 - 键盘:焦点在设计器里时按 F10 进菜单栏,焦点落到第一张菜单上;左右键换菜单,下、Enter、Space 展开,Esc 收起;再按 F10 或 Esc 回到进来前的地方。不用 Alt 与 Alt + 字母,它们留给浏览器。 - 窗口窄时整条收成一颗「菜单」钮,点开后每张菜单是一个子菜单。 - 菜单栏常开,设置里没有关掉它的开关。 - 宿主的内容写成设计器元素的子元素,按 `slot` 放进外框,后来加的也一样: - 模板标题写 `slot="menubar-center"`,按整条菜单栏居中,放不下截断、再窄就藏起,窄屏收成「菜单」钮时同样居中。 - 保存状态写 `slot="status-end"`,在状态栏右端、打印自己的状态项(打印客户端连接状态、缩放比例)前面;设置里关掉状态栏时跟着不显示。 - 别的要常显的写 `slot="menubar-end"`,在菜单栏右边一段。 - 上面的示例把标题与保存状态放在这两处,见下文「保存与事件」。 宿主的命令写成一个扩展的命令,`placement` 写 `menubar:<菜单 id>` 落进那张菜单(`file`、`edit`、`view`、`insert`、`page`、`help`;打印的两张菜单的 id 是导出的 `PRINT_MENUBAR_MENUS`),不在缺省排列里的自成一组,排在那张菜单收尾的一组之前,只有一组的菜单接在后面。要定位置时用 `commandLayout['menubar:<菜单 id>']` 写整份排列,给空数组就不要这张菜单: ```ts // 宿主自己的「新建」「返回列表」挂进文件菜单,新建同时上工具栏;再把文件菜单排成新建与保存、返回列表、设置三组 export function hostCommands(designer: XhPrintDesigner, actions: { readonly create: () => void, readonly back: () => void }): void { designer.extensions = [{ id: '@acme/host', commands: [ { id: '@acme/host.new', label: '新建', placement: ['menubar:file', 'toolbar'], run: actions.create }, { id: '@acme/host.back', label: '返回列表', placement: ['menubar:file'], run: actions.back }, ], }] designer.commandLayout = { 'menubar:file': [['@acme/host.new', 'save'], ['@acme/host.back'], ['settings']] } } ``` ### 页面结构 页面结构按页面模板分组,每个页面模板下分页眉、正文与页脚;页眉页脚加了首页、末页、奇偶页这些变体时,再按变体分。页眉、页脚里一个元素都没有时不列。同一处的元素按层级排,最上层的排最前;元素有名称时写名称、副文本是种类,没有名称时写「种类 · id」,行首是种类的图标。 - 点一项选中元素:它不在当前页面模板或当前显示的页眉页脚变体上时,先切过去再选中,并滚进可见区。悬停或用方向键走到一项时,画布上标出它。 - 能多选,与画布上的选择是同一份:按住 Ctrl(macOS 为 ⌘)点一项或按 Ctrl / ⌘ + Space 加选、取消,按住 Shift 点一项从上一次点的那项选到这一项,分组行不算。新选上的第一个滚进可见区,只取消几项时画布不动。选择只留当前页面模板与当前页眉页脚变体里的元素:加选别处的元素时切过去,原来的选择随之换掉。 - 拖动一项,或按 Alt 加上下键,在同一处里调层级,记一步撤销;要把元素从页眉挪到正文、或挪到别的页面模板,仍在画布上拖。锁定的元素不能挪、也不能从这里删。 - Delete 删除焦点所在的一项,焦点在选中的项上时删全部选中的,记一步撤销。 - 搜索框按名称与种类过滤。只读与预览态时只能选、不能删与挪。 ## 字段物料 给了 `fields` 时,物料面板最后多出「字段」一组,按字段树显示:带子字段的可以展开、收起,键盘与读屏按树的约定,搜索也能按字段名称与路径找。点一下把绑好字段的元素加到视口中央,拖到画布上放在落点: | 字段 | 生成 | | --- | --- | | `value`,`content` 不写或为 `text` | 文本,内容绑定这个字段 | | `value`,`content` 为 `image` | 图片,地址取这个字段 | | `value`,`content` 为 `barcode` | 条码(缺省码制),内容绑定这个字段 | | `value`,`content` 为 `qrcode` | 二维码,内容绑定这个字段 | | `list` | 表格:`dataField` 指向它,各列取它的 `value` 子字段(进入对象、不进入嵌套的列表),列标题取字段名称,列宽均分表格宽度;`content` 为图片、条码、二维码的子字段对应同类单元格 | | `object` | 不生成元素,只作分组 | - 字段写了 `label` 时,新元素的名称就是它;条目上同时显示字段路径。 - 列表的子字段相对数组的一行,只作浏览、不能拖出:要放行里的字段,先放表格,再在属性面板里改列。 - `content` 是 XiHan.UI 字段树 `DataField` 的可选项,说明这个值代表什么;`type` 是数据类型,与生成哪种元素无关。 ### 导入字段 物料面板头的「导入字段」(也在文件菜单里)打开导入对话框,从一份 JSON 样例或一份 JSON Schema 生成字段树,不用宿主手写 `fields`: - 「JSON 样例」「JSON Schema」两页各有一个输入框,粘贴进来,或把 `.json` 文件拖到输入框下面、点它选一个,文件在本机读,不发请求。解析失败时写出第几行第几列。 - 预览表一个字段一行,可以勾掉不要的、改显示名与类型;值字段的「内容」选文本、图片、条码或二维码,决定拖上画布生成哪种元素(见上表)。从样例推断时,图片地址(`data:image/…` 或以图片扩展名结尾的链接)预先标成图片。 - 已有字段时可选「合并到现有字段」(缺省)或「替换现有字段」:合并时同名字段保留现有的显示名,种类不同的保留现有的并在表下列出;替换可能让模板里已有的绑定找不到字段,问题列表会报出来。 - 来自样例时可以勾上「同时作为示例数据」(缺省勾上),画布马上按这份样例显示实际值。 - 按「导入」后设计器换上新的 `fields` 并派发 `fields-change { fields }`;带了示例数据的同样换上 `sampleData` 并派发 `sample-data-change { sampleData }`。字段与示例数据仍由宿主持有,不写进模板:宿主存起来,下次赋回。不记撤销。 - 字段来自后端元数据、不许用户改时写 `fields-readonly`:面板头没有这颗钮,命令 `import-fields` 不可用。只读与预览态时同样没有。 - `openFieldImport(source?)` 从别处打开对话框,`source` 是 `sample`、`json-schema` 或扩展字段来源的 id。 ```ts export function wireFields(designer: XhPrintDesigner, store: FieldStore, fromBackend: boolean): void { designer.fields = store.fields designer.sampleData = store.sample // 字段来自后端元数据时不许在设计器里改 designer.fieldsReadonly = fromBackend designer.addEventListener('fields-change', (event) => { store.save({ fields: (event as CustomEvent<{ fields: readonly DataField[] }>).detail.fields }) }) designer.addEventListener('sample-data-change', (event) => { store.save({ sample: (event as CustomEvent<{ sampleData: JsonValue }>).detail.sampleData }) }) } ``` 页码的格式是一行文本,行尾的「插入」菜单把当前页码 `{{$page}}` 或总页数 `{{$pages}}` 插到光标处,下面按第 1 页、共 3 页显示示例,数字写法跟着「数字写法」一栏。 边框分上、右、下、左四个页签,每边一个开关:打开时这一边照已有的边画线,还没有边时是 0.2 毫米黑色实线,下面可以改线宽、线型与颜色;四边都关掉就是没有边框。多选的元素边框不同时,从四边都不画开始改,改动对每个元素整体写入。 值格式先选种类:常用格式(数字、百分比、日期、人民币大写、数字字形)在下面填参数并显示示例;值映射是 XiHan.UI 设计器零件内建的选项表(`options`,不带禁用列),「数据值、显示文本」两列,可以新增、删除与上移下移,下面填没有命中时显示的文本;表达式以 `value` 引用待格式化的值,边输入边检查。换种类时整体换成这一种的缺省值,选「不设格式」即去掉格式。 ## 画布操作 - **选择**:点选元素;按住 Shift、Ctrl 或 ⌘ 点击切换某个元素的选中;在空白处拖动框选。 - **移动与变换**:拖动元素移动,拖控制柄缩放;文本、图片、矩形、椭圆、条码与二维码还能旋转。锁定的元素不能拖动与缩放。 - **选中工具条**:选中元素时,选中框上方浮着一条工具条:都有层级(置顶、上移一层、下移一层、置底)、创建副本、复制与删除;选中的全是文本、长文本、富文本或页码这类带文字样式的元素时,前面多出字号、粗体、斜体、下划线、删除线与左中右对齐,对选中的每个元素写同一个值,记一步撤销;选了几个元素、里面有别的种类时,多出对齐与分布。放不下的钮收进工具条自己的「更多」。贴顶放不下时翻到下方,拖动中与只读时收起。在画布上按 Alt+F10 把焦点交给它,在工具条里按 Esc 回到画布。排列怎么改见下文「命令」。 - **尺寸徽标**:选中元素时,选中框下方常驻宽 × 高(mm)。单选、元素能缩放时是两个数字框:点加减钮或按上下方向键改 1 mm,Shift 加上下方向键改 10 mm,连续改并成一步撤销;输入数字后按 Enter 或离开数字框才改,单独一步撤销,按 Esc 放弃。改的时候左上角不动,不小于这种元素的最小尺寸,二维码改一边另一边跟着变。锁定的元素、多选与只读时只显示尺寸。在选中工具条里按 Tab 进到徽标。 - **换区**:元素中心拖进页眉、正文或页脚,就放进那个区;表格、长文本与富文本只能留在正文区。 - **页眉页脚**:在区的标签上切换当前编辑的变体(缺省、首页、末页、奇数页、偶数页),可以新增或删除变体;拖动区的边界改高度。 - **平移与缩放**:设计器打开、换页面模板或重新装载模板时按实际大小显示(缩放 100%),纸的左上角落在画布左上角,上边与左边留出边距,左边让出页眉页脚标签;画布改尺寸时不重排。工具栏的「适合窗口」(`zoom-to-fit`,键 0)把整张纸缩放到画布里,此后画布改尺寸时跟着重新适配,直到平移、缩放或在画布上操作。滚轮平移,Ctrl / ⌘ + 滚轮以指针为中心缩放;按住空格或鼠标中键拖动平移。工具栏的「拖动」(手形,键 H)开着时,在画布上哪里按住拖动都是平移,按在元素、控制柄与表格把手上也一样:不选中、不挪元素,只点一下不取消选择,双击不进表格编辑;选中工具条、尺寸徽标与页眉页脚变体菜单照常能点。拖动途中按 Esc 只取消这次拖动,视口回到原处;不在拖动时按 Esc 关上拖动工具。 - **吸附**:拖动、新建与缩放元素时吸附到网格间距(设置「打印」一页,缺省 1 mm,写 0 不吸附网格);「吸附对齐」打开时还吸附其它元素、页边距、区边界与参考线,并显示对齐线。工具栏上的「网格」开关显示或收起网格线,「吸附」开关就是「吸附对齐」,两者改的都是设置。 - **表格编辑态**:双击表格或选中后按 Enter 进入。可以分别选中列、表头、表体、表尾行与表尾单元格,在属性面板里编辑;拖列边界改列宽;右键菜单里插入、删除、左右移动列,插入、删除表尾行,合并与拆分表尾单元格。按 Esc 退出。 - **右键菜单**:在空白处、元素上与表格编辑态里各有一套,带快捷键的条目在行尾显示快捷键。设计器先派发可取消的 `blank-contextmenu`、`element-contextmenu` 或 `table-contextmenu`(表格编辑态里,右键处的部位先被选中),宿主取消后不弹自带的菜单,可以换成自己的。 ## 设置 设置是用户偏好,每人每机一份,不随模板保存。`openSettings(section?)` 打开设置对话框,左边一个分区一页: - 「基础」(`general`):主题(跟随页面、跟随系统、亮色、暗色)、主题色(跟随页面、八种预设与自定义颜色)、状态栏开关、自动保存、开发者模式与分页、渲染两条调试日志。开发者模式缺省关,关着时编辑 JSON 这类调试查看的入口都不出现,两条调试日志也不能开,见下文「调试日志」。 - 「语言」(`language`):设计器的界面语言,缺省跟随页面;也可以跟随系统,或选 XiHan.UI 的十种语言之一。只作用于这台设计器:设计器自己、外壳零件与里面的元素(含弹出的菜单、对话框这些浮层)一起换,页面别处不变;跟随系统按浏览器的语言偏好挑最接近的一种,挑不到用英文。语言包选了才按需加载。键是 `language.locale`(`DESIGNER_SETTING_KEYS.language`)。 - 「布局」(`layout`):逐块面板一行,改显示、停在哪一栏、先后与收起,表下「恢复默认布局」;改的就是上文「面板布局」那一份。 - 「工具栏」(`toolbar`):按钮的显示方式(图标、文字、图标和文字,缺省只显示图标)、显示工具栏,以及按钮表:藏起不用的按钮、换先后与分组,「恢复默认」回到宿主排好的样子。显示方式与显示工具栏只走设置,没有单独的属性;画布上的翻页条总是只显示图标。按钮表从宿主用 `commandLayout` 排好的工具栏起步,只管顶部工具栏,菜单栏、选中工具条与右键菜单不在里面。 - 「打印」(`print`):打印方式、显示网格、网格间距、吸附对齐、显示边距线、标尺。打印方式只在给了 `printClient` 时才有,选本地客户端(缺省)还是浏览器:本地客户端直接交给打印机,连不上时退回浏览器的打印对话框;选浏览器时 `print()` 总是弹出浏览器的打印对话框。网格缺省不显示,间距缺省 1 mm,按当前缩放挤得太密时改按间距的整数倍画;边距线与标尺缺省显示。 - 「本地客户端」(`printClient`):给了 `printClient` 才有这一页,写连接状态:已连接、未连接或未安装。「测试连接」调宿主给的 `printClient` 的 `connect()` 连一次;没找到客户端(`CLIENT_UNAVAILABLE`)时写未安装,这时也可能是装了没有运行。没连上时下面有「下载客户端」链接,缺省指向本站的[本地打印客户端](./client)一页(`PRINT_CLIENT_DOWNLOAD_URL`),宿主用 `clientDownloadUrl`(attribute `client-download-url`)换成自己的地址。端口与来源授权仍由宿主建连接时与客户端管,设计器不自己建连接。 - 扩展登记的分区,排在后面。分区 id 与 `general`、`language`、`layout`、`toolbar`、`help`、`shortcuts`、`about`、`print`、`printClient` 或别的扩展重名时,给 `extensions` 赋值就抛 `DesignerExtensionError`。 - 「帮助」(`help`):几段操作说明,下面是「开始引导」与「帮助」两颗钮,按了先关上对话框再开始引导或打开本站的设计器一页。 - 「快捷键」(`shortcuts`):按命令表列出全部快捷键,分通用、编辑、打印设计器与扩展几组,第二快捷键写在同一行;打印设计器那组末尾补上不是命令的 Ctrl / ⌘ + C、X、V 复制剪切粘贴,Backspace 删除与 Enter 编辑表格;保存是外壳的命令,列在「通用」里;「通用」里方向键一行写明没有选中时平移画布,Esc 一行写明取消拖动、退出表格编辑或取消选择,其余照 designer-shell 的通用行(含 F10 进菜单栏)。 - 「关于」(`about`):打印设计器的版本(包入口导出的 `PRINT_VERSION`,与包版本一致)与许可证,后面接扩展给的几行。 ```ts export function openCanvasSettings(designer: XhPrintDesigner): void { designer.openSettings(PRINT_SETTINGS_SECTION) } ``` 值是扁平的键值,键是「分区 id.条目 id」,如 `general.theme`、`print.showGrid`,没写的键取缺省值。宿主给缺省、锁定条目或读值时用常量,不手写键:「打印」分区的 id 与各条目的键是包入口导出的 `PRINT_SETTINGS_SECTION` 与 `PRINT_SETTING_KEYS`,「本地客户端」分区的 id 是 `PRINT_CLIENT_SETTINGS_SECTION`,「基础」「布局」「工具栏」三页的键是 `@xihan-ui/designer-shell` 的 `DESIGNER_SETTING_KEYS`(如工具栏的显示方式 `toolbar.display`、显示工具栏 `toolbar.visible`、按钮表 `toolbar.layout`)。`settings` 赋值即生效;用户在对话框里、用「视图 → 外观」的显示开关或工具栏的网格、吸附开关改了一条,设计器换成新的一份并派发 `settings-change`,宿主存起来、下次赋回。`settingsPolicy` 隐藏或锁定条目,键是条目键或分区 id:`hidden` 不显示,一个分区的条目全藏了就没有这一页;`locked` 只读,说明换成「由所在系统管理」。 ```ts export function wireSettings(designer: XhPrintDesigner, saved: PrintSettings, store: (settings: PrintSettings) => void): void { // 宿主自己的缺省压在下面,用户存过的值盖在上面 designer.settings = { [PRINT_SETTING_KEYS.gridSize]: 5, [PRINT_SETTING_KEYS.showGrid]: true, ...saved } designer.settingsPolicy = { [DESIGNER_SETTING_KEYS.theme]: 'hidden', [DESIGNER_SETTING_KEYS.brand]: 'hidden', [PRINT_SETTING_KEYS.snap]: 'locked' } designer.addEventListener('settings-change', (event) => { store((event as CustomEvent<{ settings: PrintSettings }>).detail.settings) }) } ``` - **主题只作用于设计器**:主题与主题色写在 `` 自己的 `data-theme`、`data-brand` 上,页面其余部分不变;设置对话框、菜单这些浮层跟着设计器变。主题色内置:跟随页面(缺省,不写 `data-brand`)、XiHan.UI 的八种预设,或自定义一种颜色,宿主不用传预设;自定义的颜色注册成这台设计器自己的品牌,取不出色阶的颜色不生效。 - **扩展读设置**:命令、面板与扩展从上下文的 `settings` 读值,各分区没写的条目已经取了缺省值,如 `context.settings['print.gridSize']`。 独立使用、没有宿主替你存时,给 `settingsStorageKey`:连上文档时若没赋 `settings` 就从根所在窗口的 localStorage 读,用户改了设置就写回去;标了 `secret` 的文本条目(如密钥)不存。读写失败派发 `settings-storage-error`,设置留在内存里。不给就不读写存储。 ```ts export function standalone(designer: XhPrintDesigner): void { designer.settingsStorageKey = 'print-designer:settings' designer.addEventListener('settings-storage-error', (event) => { console.warn('设置没能读写本机存储,这次改的只在内存里', (event as CustomEvent<{ error: unknown }>).detail.error) }) } ``` ### 调试日志 开发者模式开着时,「基础」里能打开分页调试日志(`general.debugPagination`)与渲染调试日志(`general.debugRender`),缺省都关。打开后设计器把记录写到浏览器的控制台,每条折叠成一组,标题以 `[xh-print-designer] pagination` 或 `render` 开头;只写控制台,不上报。关掉开发者模式,两条日志随即停。 - 分页:切到预览或打印时,每个页面模板开始排、按假定的页数选的页眉页脚变体不对而重排、表格这类跨页元素在哪页放不下续排到哪页、元素在哪页放不下整块挪到哪页、排完共几页,组里是这一步的数据。 - 渲染:设计器每次重画(改了哪些属性)与设计态排版的耗时;预览与打印的各环节——分页、生成输出页的 DOM、等图片与字体就绪、合成交给本地客户端的文档、送打印——各记一组耗时,组里写输出页数、资源个数或字节数;一次打印另记一组总耗时与结果。 分页步骤与打印环节来自运行时的 `trace` 回调,宿主自己用预览元素或 `printDocument` 时也能接,见[运行时](./runtime#调试回调)。 ## 新手引导 设计器第一次在设计态显示出来(画布有了尺寸,放在没打开的页签里时等它打开)后,自动开始新手引导:先是各款共用的几步(欢迎、菜单栏、工具栏、物料、画布、属性、问题与状态栏),再是打印自己的几步——「插入」菜单、页眉与页脚、「页面」菜单、页面模板、字段物料与打印,最后一步是设置。一步高亮一块面板、一个区、菜单栏上的一张菜单或工具栏上的一颗钮,目标看不见(面板隐藏、按钮不在工具栏上)的那步自动跳过。 - 菜单栏一步高亮整条菜单栏,讲各张菜单里有什么、按 F10 进出菜单栏;「插入」「页面」两步高亮菜单栏上那张菜单的入口;设置一步高亮「文件」菜单的入口,说设置在里面、引导能从「帮助」重新开始。字段物料与打印两步的说明带一句它们也在「文件」菜单里。 - 窗口窄、菜单栏收成一颗「菜单」钮时,菜单栏一步高亮这一条,「插入」「页面」与设置几步都落在「菜单」钮上。 - 走完、跳过或关上之后,设计器把设置的 `help.tourDone`(`DESIGNER_TOUR_DONE_KEY`)写成 `true` 并派发 `settings-change`,宿主存起来,下次就不再自动开始。 - 引导限在设计器的外框里:遮罩只盖外框,高亮框裁在外框里,页面别处照常可读可点。同一页同时只有一个引导:别的设计器的引导开着时不自动开始。 - `autoTour`(attribute `auto-tour`)写 `false` 关掉自动开始。 - `startTour()` 从头开始,同一页里别的引导开着时先收起它,预览态或没有能走的步骤时返回 `false`;命令 `start-tour` 与设置「帮助」页的「开始引导」做同一件事,缺省在帮助菜单里、不上工具栏,宿主可以经 `commandLayout` 放上去。 - 扩展的 `tourSteps` 往后面加步骤,`order` 从 200 起;目标写 `{ panel }`、`{ region }`、`{ command }`、`{ selector }`,或 `{ menubar: '<菜单 id>' }`(菜单栏上那张菜单的入口,窄屏时是「菜单」钮,菜单栏上没有这张菜单时跳过)。步骤 id 写扩展 id 加点,与别的扩展重名或用了 `shell.` 前缀时赋值即抛 `DesignerExtensionError`。 ## 快捷键 焦点在设计器里、且不在输入框与面板里时生效,只在设计态。 | 按键 | 作用 | | --- | --- | | Ctrl / ⌘ + Z | 撤销 | | Ctrl / ⌘ + Shift + Z,Ctrl + Y | 重做 | | Ctrl / ⌘ + C、X、V | 复制、剪切、粘贴,走系统剪贴板 | | Ctrl / ⌘ + D | 创建副本 | | Ctrl / ⌘ + A | 全选当前页面模板里看得见的元素 | | Ctrl / ⌘ + S | 保存:执行外壳的 `save` 命令,派发 `save` | | Ctrl / ⌘ + Shift + J | 打开编辑 JSON 的对话框(开发者模式开着时;关着时交给浏览器) | | Ctrl / ⌘ + , | 打开设置对话框 | | Ctrl / ⌘ + ]、[ | 上移一层、下移一层 | | Ctrl / ⌘ + Shift + ]、[ | 置于顶层、置于底层 | | Delete、Backspace | 删除选中的元素 | | 方向键 | 选中的元素移动一格(网格间距,写 0 时 1 mm);没有选中时平移视口 | | Shift + 方向键 | 选中的元素移动 10 mm | | 按住空格拖动 | 平移画布;按住鼠标中键拖动同样 | | H | 打开或关上拖动工具 | | +、- | 放大、缩小 | | 0 | 适合窗口 | | Enter | 选中表格时进入表格编辑态 | | Esc | 取消正在进行的拖动;在表格编辑态里退出;否则取消选择 | | F10 | 焦点交给菜单栏;再按 F10 或 Esc 回到原处 | | Alt + F10 | 焦点交给选中框上方的选中工具条(工具条没有按钮时交给尺寸徽标);在里面按 Esc 回到画布 | 复制的元素以 JSON 放进系统剪贴板,可以在两个设计器之间粘贴;粘贴时先按当前的元素登记表校验,有错误的内容不粘贴。别的内容(图片、旧模板等)粘贴或拖到画布上时交给扩展的导入器,见下文「导入」。 ## 保存与事件 设计器不保存模板。每次修改派发 `schema-change`,`detail.schema` 是改完的模板;一次拖动、一次属性编辑合成一次。选「文件 → 保存」或在画布上按 Ctrl / ⌘ + S 时(都是外壳的 `save` 命令,预览态只能从菜单保存)派发 `save`,宿主保存成功后调用 `markSaved()`: ```ts // 写成设计器的子元素:slot="menubar-center" 的标题在菜单栏正中,slot="status-end" 的保存状态在状态栏右端 const title = document.createElement('span') title.slot = 'menubar-center' title.textContent = '送货单' const status = document.createElement('span') status.slot = 'status-end' status.textContent = '没有修改' designer.append(title, status) designer.addEventListener('dirty-change', (event) => { const { dirty } = (event as PrintDesignerEventMap['dirty-change']).detail status.textContent = dirty ? '有未保存的修改,选「文件 → 保存」或按 Ctrl / ⌘ + S' : '已保存' }) designer.addEventListener('save', () => { // 把 designer.schema 交给后端保存,成功后标记为已保存 designer.markSaved() }) ``` - 给 `schema` 赋值即装载新模板:清空撤销历史与选择,按实际大小显示、纸的左上角落在画布左上角,并派发 `kind` 为 `load` 的 `schema-change`。读取 `schema` 得到当前模板。 - `dirty` 表示有没有未保存的修改,变化时派发 `dirty-change`;撤销回到保存时的状态,`dirty` 也回到 `false`。 - 撤销历史的每一步带一句说明,如「删除文本 title」「修改「字号」」「移动 3 个元素」,随界面语言变;工具栏上撤销、重做的名字带上下一步的说明(「撤销:删除文本 title」)。拖动、缩放、旋转按下到松开是一步;属性面板里连续输入同一项是一步;方向键微移连续按、选中不变、相隔不超过 1 秒的并成一步,换了选中就另起一步。 - 「清空历史」(命令 `clear-history`)先弹确认框,按确定才清空撤销与重做,模板与 `dirty` 不变;它不在工具栏上,在历史记录面板的面板头上,宿主也可以经 `commandLayout` 放置。只读与预览态时撤销、重做与清空历史都不可用。 - `issues-change` 在模板改动后带上全部校验问题,宿主可以据此拦住有错误的模板,不让保存。 - 导入字段派发 `fields-change` 与 `sample-data-change`,见上文「导入字段」。 - 其余事件:`selection-change`、`page-change`、`mode-change`、`viewport-change`、`element-dblclick`。 也可以用方法操作:`execute(operations)` 执行一组编辑操作并记为一步撤销(操作见 `@xihan-print/core` 的 `PrintOperation`),`addElement`、`select`、`copy`、`paste`、`align`、`reorder`、`zoomToFit`、`scrollToElement` 等与界面上的命令一致。`print()` 按当前模板与 `data` 打印,给了 `printClient` 时交给本地打印客户端。 ### 自动保存本机草稿 设置「基础」里打开自动保存(`general.autoSave`,缺省关)后,有未保存的修改时停手约一秒,设计器把整份模板存成这台浏览器里的草稿,连着改只存最后一次;保存了(宿主调 `markSaved()`)草稿就删掉。下次宿主装进同一个草稿键的模板、和草稿不一样时,弹框问恢复还是丢弃:恢复把草稿整份换上,记一步撤销(草稿有错误级校验问题时不换,如扩展换了);丢弃即删;一样就悄悄删掉。只读时不问。 - 草稿键:设计器的 `draftKey`(attribute `draft-key`)写模板 id,区分同一页、同一种设计器里的不同模板;不写时按页面地址(路径、查询与锚点)区分。 - 草稿只存在本机,宿主不用存,也不用接新事件。要关掉这一项,用 `settingsPolicy` 把 `general.autoSave` 藏起或锁住,或在 `settings` 里预先写开或关。 ## 编辑 JSON 设置「基础」里开着开发者模式(`general.developer`,`DESIGNER_SETTING_KEYS.developer`)时,按 Ctrl / ⌘ + Shift + J,或在「编辑」菜单、画布空白处的右键菜单里选「编辑 JSON」,打开编辑整份模板 JSON 的对话框。开发者模式关着时这几处都没有它,快捷键交给浏览器;`openJsonEditor()` 照样能调。宿主要让用户用它,就在 `settings` 里预先写 `'general.developer': true`,或用 `settingsPolicy` 锁住这一条。 - 框里写出当前模板,缩进两格。边改边解析、按打印的校验报问题(与 `validate()` 同一套:结构、元素登记表与表达式),问题列在框下,点一条光标跳到那个路径在文字里的位置。 - 解析失败或有错误级的问题时「应用」不可用;没有错误时按「应用」整份换上,记一步撤销「编辑 JSON」,派发 `kind` 为 `execute` 的 `schema-change`。撤销就回到改之前的那份。 - 框下还有「格式化」与「复制」。只读时只能看,不能应用。 - 整份换上走 `@xihan-print/core` 的编辑操作 `{ type: 'replaceDocument', document }`,逆操作换回原来那份;新模板由调用方先校验。 ## 只读与预览 - `readonly`:可以选择、浏览、预览与打印,不能修改;物料不能拖出,属性面板只读。 - 写成 attribute 时按三态读,与 XiHan.UI 元素的布尔 attribute 同一读法:不写或移除时不只读;写 `readonly="false"` 也不只读;其余写法(`readonly`、`readonly=""`、`readonly="true"`)只读。 - 这是行为变化:以前按原生布尔 attribute 读,只要写了就只读,`readonly="false"` 也算只读。 - 经 property 赋值(`designer.readonly = false`)不受影响。 - `mode`:`design` 设计态,`preview` 预览态。预览态按 `data` 分页,用 `` 显示全部输出页。 ## 语言包 界面文字缺省是英文。把 XiHan.UI 与打印同一语言的语言包合并后全局设置,设计器、它内部的零件与预览都换成这种语言,见[快速开始](../quickstart#界面语言)。用户也可以在设置的「语言」页里只给这台设计器换语言,见上文「设置」。 只改一个实例的几处文案时,给它的 `translations` 属性,压在全局语言包之上: ```ts import type { XhPrintDesigner } from '@xihan-print/designer' export function renamePanels(designer: XhPrintDesigner): void { designer.translations = { palette: '控件', issues: '检查结果', } } ``` 全部文案键见[设计器参考](../reference/designer#printdesignertranslations)。属性面板里的属性名、选项名与单位也在里面,键是属性路径,如 `style.fontSize`。 ## 扩展 `extensions` 收一组扩展对象:元素种类、编辑方式、校验规则、属性编辑器、命令、面板、设置分区、导入器、字段来源、物料来源、菜单栏的菜单、引导步骤与「关于」页的几行都经它登记。内置元素、设计器自带的编辑器与命令也是扩展,自动排在最前。同一页面上的几个设计器各给各的,互不影响。 - 扩展对象的 `id` 全局唯一,用包名加功能名,如 `@acme/print-fonts`。它也是模板里扩展数据的键:文档、页面与元素都可以带 `extensions: { [扩展 id]: { ... } }`,校验只查它是对象,编辑与复制粘贴原样带着走。 - 赋值时当场合并查重:扩展 id、元素类型、编辑方式、命令 id 与快捷键、字段来源 id、物料来源 id、菜单栏的菜单 id 与内置的或别的扩展重名时,抛 `DesignerExtensionError`(`code` 为 `designer.extension-conflict`),设计器保持赋值前的状态。 - `setup(context)` 在设计器连上文档时调用,它返回的函数在断开、或这个扩展被去掉时调用。同一个扩展对象换了引用,当作卸下再装上,宿主要保持引用稳定。 - `rules` 是校验规则,收到文档与 `{ elements }`(合并后的元素登记表),返回问题。问题码以扩展 id 的末段加点开头,结构没有 error 时才跑。 - `controls` 是新的编辑方式:属性描述里用到这个 `kind` 时,按它的 `check` 校验取值;没登记的编辑方式报 `unknown-control`。 - `settings` 是扩展的设置分区,放进设置对话框,排在设计器自己的分区之后(`order` 从 200 起),值同样在 `settings` 里、键是「分区 id.条目 id」。要地址和密钥的收费附加包(如云打印)登记自己的分区:密钥写成 `{ kind: 'text', secret: true }` 的条目,画成密码框,`settingsStorageKey` 存设置时也不存它。本地客户端没有地址与密钥,「本地客户端」一页只写连接状态。 - `tourSteps` 是新手引导的步骤,见上文「新手引导」;`about` 是设置「关于」页里接在后面的几行 `{ label, value }`,收费附加包写自己的版本与授权。 - `fieldSources` 是导入字段对话框里的来源页,排在「JSON 样例」「JSON Schema」之后,如从接口请求取一份样例;页拿到内容后交回对话框,推断、预览与确认走对话框自己的。形状见 XiHan.UI designer-kit 的「字段来源」。 - `paletteSources` 是物料来源:往物料面板加几组条目,如收费附加包的「我的物料」、行业模板,排在内置分组与「字段」之后,几个来源之间按 `order`。物料面板每次刷新都按上下文现取它的分组;条目被点一下时交给来源的 `insert(item, context)`,拖到画布上放下时多给一个落点 `{ x, y, region }`:纸张坐标(mm),以及这一点落在页眉、正文还是页脚。插入什么、插在哪里由来源定;只读或不在设计态时不交给来源。分组或条目的 id 与物料面板里已有的重名时,刷新物料面板时抛 `TypeError`。形状见 XiHan.UI designer-shell 的「物料来源」。 - `menubarMenus` 往菜单栏加菜单 `{ id, label, order? }`:`label` 可以是收到上下文的函数,随语言变;`order` 定先后,内置的文件 0、编辑 10、视图 20、帮助 90,打印的插入 30、页面 40,扩展写在 30–80 之间,不写按 50。命令写 `placement: ['menubar:<菜单 id>']` 落进来,没有命令的菜单不出现,见上文「菜单栏」。 - 预览元素 `` 与 `paginate`、`validateDocument` 这些函数也收 `extensions`,与设计器给同一份。 ### 扩展元素 扩展的 `elements` 登记新的元素类型,`renderers` 给它们的渲染。原型与内置元素的一样,给出缺省值、尺寸下限、物料分组与属性描述;不跨页的元素(`flow` 为 `false`)另外必须给 `layout`:排版时按元素与数据算出画在元素框里的数据(只能是 JSON),元素框的位置与尺寸由排版填好。渲染收到排版盒与输出页所在的文档,返回要放进元素框的节点: ```ts import type { ExtensionElementPrototype } from '@xihan-print/core' import type { PrintDesignerExtension } from '@xihan-print/designer' import { behaviorProperties, layoutProperties, property } from '@xihan-print/core' const SVG = 'http://www.w3.org/2000/svg' const seal: ExtensionElementPrototype = { type: 'seal', category: 'media', order: 9, flow: false, minSize: { width: 10, height: 10 }, defaults: { type: 'seal', width: 40, height: 40, field: '', color: '#d40000' }, properties: [ ...layoutProperties({ rotation: false }), property(['field'], 'data', { kind: 'dataField', kinds: ['value'] }), property(['color'], 'appearance', { kind: 'color' }), ...behaviorProperties(), ], // 排版只给 JSON:从数据里取单位名,元素框由 core 填好 layout: (element, context) => ({ name: String(context.field(String(element.field)) ?? ''), color: String(element.color), }), // 报出引用的字段:collectDocumentFields 汇总模板字段时不会漏掉印章 fields: (element, context) => context.field(String(element.field)), } export const sealExtension: PrintDesignerExtension = { id: '@acme/print-seal', elements: [seal], palette: { seal: { label: '印章' } }, renderers: { seal: (box, { document }) => { const { name, color } = box.data as { name: string, color: string } const svg = document.createElementNS(SVG, 'svg') svg.setAttribute('viewBox', '0 0 100 100') svg.setAttribute('width', '100%') svg.setAttribute('height', '100%') const ring = document.createElementNS(SVG, 'circle') ring.setAttribute('cx', '50') ring.setAttribute('cy', '50') ring.setAttribute('r', '46') ring.setAttribute('fill', 'none') ring.setAttribute('stroke', color) ring.setAttribute('stroke-width', '4') const text = document.createElementNS(SVG, 'text') text.setAttribute('x', '50') text.setAttribute('y', '56') text.setAttribute('text-anchor', 'middle') text.setAttribute('fill', color) text.textContent = name svg.append(ring, text) return svg }, }, } ``` - 属性描述可以用 `layoutProperties`、`behaviorProperties` 与 `property` 拼,和内置元素用的是同一套。 - 引用了数据字段的元素给 `fields`:返回它引用的字段,`collectDocumentFields` 汇总模板字段时才不会漏掉它。上下文带 `field`、`template`、`expression` 与 `text` 四个帮手,分别把字段路径、`{{path}}` 模板串、表达式与文本内容来源换成引用,与内置元素同一套解析;引用的是列表时给 `rows`,里面是相对列表一行的引用。字段的值在元素里当图片地址、条码或二维码的内容用时,在引用上写 `content`(`image`、`barcode`、`qrcode`),汇总时一并记进字段树。 - 不跨页的元素没给 `layout`、跨页的元素没给 `prepare`(见下文)时,赋值时抛 `TypeError`。 - `palette` 给自己元素的物料项:名称与图标(`@xihan-ui/core` 的 `IconRecord`)。名称也用在画布与属性面板的可访问名和问题列表里,不给时显示类型名。`palette` 里写了不是本扩展登记的元素类型时,赋值时抛 `TypeError`。 - 原型的 `category` 可以是内置分组(`text`、`media`、`data`、`shape`、`code`),也可以是新的分组。新分组排在内置分组之后,按首次登记的顺序;名称写在扩展的 `categories` 里,没写时显示分组名。分组名与内置分组或别的扩展写的重名时,抛 `DesignerExtensionError`。 - 名称随扩展给出,不在设计器的语言包里。扩展包要支持多种语言,就按语言给出不同的扩展对象,例如导出一个收文案的工厂函数。 - 渲染只能用标准 HTML 与 SVG:浏览器打印与本地打印客户端用的是另一份文档,自定义元素在那里不会升级。 - 扩展去掉后,文档里已有的这类元素照样保留:设计器把它画成写着类型的虚线占位,校验报 `element.unknownType`,再装上就恢复。 跨页的元素(`flow` 为 `true`)给 `prepare` 而不是 `layout`:按元素与数据给出按顺序的若干块,每块是高度(mm)与交给渲染的数据,另可给一块 `repeat`,排在每页那一段的开头(如表头)。`prepare` 收到的上下文比 `layout` 多一个 `measureText(text, style, width)`,按文字样式与可用宽度折行量出各行与总高度: ```ts import type { ExtensionElementPrototype, TextStyle } from '@xihan-print/core' import type { PrintDesignerExtension } from '@xihan-print/designer' import { behaviorProperties, layoutProperties, property } from '@xihan-print/core' const style: TextStyle = { fontFamily: 'sans-serif', fontSize: 10, bold: false, italic: false, underline: false, lineThrough: false, color: '#000000', align: 'left', verticalAlign: 'top', lineHeight: 1.4, letterSpacing: 0, } interface Approval { name: string comment: string } const approvals: ExtensionElementPrototype = { type: 'approvals', category: 'data', order: 9, flow: true, minSize: { width: 40, height: 10 }, defaults: { type: 'approvals', width: 180, height: 20, field: 'approvals' }, properties: [ ...layoutProperties({ rotation: false }), property(['field'], 'data', { kind: 'dataField', kinds: ['list'] }), ...behaviorProperties(), ], // 每条记录一块:意见折行后的高度加上下各 2 mm;块不拆开,放不下就整块挪到下一页 prepare: (element, context) => { const list = context.field(String(element.field)) const items = (Array.isArray(list) ? list : []) as Approval[] return { repeat: { height: 8, data: ['审批人', '意见'] }, blocks: items.map(item => ({ height: context.measureText(item.comment, style, element.width - 40).height + 4, data: [item.name, item.comment], })), } }, } export const approvalsExtension: PrintDesignerExtension = { id: '@acme/print-approvals', elements: [approvals], palette: { approvals: { label: '审批记录' } }, renderers: { // 每页一段:data 是 { blocks, repeat },blocks 是这一页的那几块 approvals: (box, { document }) => { const { blocks, repeat } = box.data as { blocks: string[][], repeat: string[] } const table = document.createElement('table') table.style.width = '100%' for (const cells of [repeat, ...blocks]) { const row = table.insertRow() for (const text of cells) row.insertCell().textContent = text } return table }, }, } ``` - 块依次排进正文区,放不下的整块挪到下一页,与长文本、表格一样把下方的元素往下推;只有一段时高度不小于设计高度。 - 每页一段,渲染每段调一次:排版盒的 `data` 是 `{ blocks, repeat }`,`blocks` 是这一段的那几块的数据,`repeat` 是重复块的数据(没给时为 `null`);`fragment` 给出段序号与是不是最后一段。 - 一块比整页正文区还高时照样放下,分页诊断报 `element.overflow`;块高度不是不小于 0 的有限数时,排版抛 `TypeError`。 - 跨页的元素只能放在正文区,放进页眉页脚时校验报 `element.flowInBand`;设计态排成一段、按内容撑高。 ### 属性编辑器 扩展的 `editors` 给属性面板换编辑器,按扩展的顺序压在设计器自带的编辑器之上。查找顺序是对象种类加属性路径、属性路径、编辑方式,都没有时用面板内建的编辑器: | 表 | 键 | 适用 | | --- | --- | --- | | `byTarget` | 对象种类 → 属性路径 | 只对这种对象生效 | | `byPath` | 属性路径,如 `style.fontFamily` | 所有带这个属性的对象 | | `byControl` | 编辑方式,如 `color`、`stroke` | 所有这种编辑方式的属性 | 对象种类是 `page`(页面设置)、元素类型(如 `text`、`table`)与表格部位(`table-column`、`table-header`、`table-body`、`table-footer-row`、`table-footer-cell`);编辑方式见[元素](../reference/elements)的「取值」一栏。给了编辑器的复合属性不再拆开,整个值交给编辑器。 编辑器是一个函数:收到当前值与 `setValue`、`commit`,返回一个元素;值或只读状态变化时设计器调用 `update`,移除前调用 `destroy`。元素标了 `data-xh-part="control"` 时放进字段里,标签自动接上。下面的编辑器让字体只能从几种里选: ```ts import type { DesignerPropertyEditor } from '@xihan-ui/designer-shell' import { definePrintDesigner, PRINT_DESIGNER_PANELS } from '@xihan-print/designer' import { schema } from '../templates/minimal' const FONTS = ['宋体', '黑体', '楷体', '仿宋'] const fontEditor: DesignerPropertyEditor = (initial) => { let context = initial const select = document.createElement('select') select.dataset.xhPart = 'control' select.append(new Option('缺省字体', ''), ...FONTS.map(font => new Option(font, font))) const sync = (): void => { select.value = typeof context.value === 'string' ? context.value : '' select.disabled = context.readonly } select.addEventListener('change', () => { context.setValue(select.value === '' ? undefined : select.value) context.commit() }) sync() return { element: select, update: (next) => { context = next sync() }, } } ``` ```ts const designer = document.createElement('xh-print-designer') designer.defaultLayout = { version: 1, hidden: [PRINT_DESIGNER_PANELS.palette, PRINT_DESIGNER_PANELS.issues, PRINT_DESIGNER_PANELS.pages] } designer.extensions = [{ id: '@acme/print-fonts', editors: { byPath: { 'style.fontFamily': fontEditor } } }] designer.schema = schema ``` - `setValue(undefined)` 清除这个属性;连续的 `setValue` 合成一步撤销,`commit()` 之后的修改另起一步。 - `context.target` 是当前全部被编辑的对象,多选时有多个。 - `context.issues` 是落在这个属性里层的校验问题,路径相对这个属性(如值映射第 2 项的数据值是 `["entries", 1, "value"]`),没有时是空数组。复合编辑器据此把问题标到对应的子项上:子项旁边写出问题文字,子项的控件标无效(`aria-invalid="true"`),并经 `aria-describedby` 接上这段文字;读屏名仍是子项自己的名字。编辑器返回的实例写 `nestedIssues: true` 时,属性行下面不再重复这些问题;不写时,行下显示第一条。设计器自带的值格式(值映射由内建选项表标到那一格,未命中文本、常用格式与表达式标到各自下面)与表格列、表尾行的条目都这样标;四边边框按页签分边,里层问题仍显示在属性行下面。 ### 命令 扩展的 `commands` 登记命令,工具栏按钮、右键菜单项与快捷键共用一条。命令 id 用扩展 id 加点加名字;`label`、`enabled` 与 `run` 收到 `{ designer, translations, commands, settings, frame }`(`commands` 是四款设计器共用的命令文案,如 `commands.undo`;`settings` 是生效的设置;`frame` 是设计器的外框),`label` 也可以直接写成文字;给了 `checked` 的是开关式命令,工具栏按钮显示按下状态: ```ts import type { PrintDesignerExtension, XhPrintDesigner } from '@xihan-print/designer' import { PRINT_TOOLBAR_LAYOUT } from '@xihan-print/designer' export const lockExtension: PrintDesignerExtension = { id: '@acme/print-lock', commands: [{ id: '@acme/print-lock.lock', label: '锁定', placement: ['toolbar', 'menu:element'], shortcut: ['Mod', 'Shift', 'L'], enabled: ({ designer }) => !designer.readonly && designer.getSelection().elements.length > 0, run: ({ designer }) => { designer.execute(designer.getSelection().elements.map(id => ({ type: 'updateElement', id, changes: [{ path: ['locked'], value: true }] }))) }, }], } ``` - `placement` 是命令缺省出现的位置:工具栏 `toolbar`(工具栏只显示图标时,没有图标的命令写它的 `shortLabel`),右键菜单 `menu:blank`(画布空白处)、`menu:element`(元素上)、`menu:table`(表格编辑态里),选中工具条 `selection:text`(选中的全是文字类元素)、`selection:element`(单个别的元素)、`selection:multiple`(几个元素里有别的种类),或菜单栏的一张菜单 `menubar:<菜单 id>`(见上文「菜单栏」)。扩展命令按登记顺序排在内置命令之后,自成一组。 - `shortLabel` 是按钮上看得见的短文字,名字与提示仍取 `label`,所以短文字要出现在 `label` 里(不分大小写),比如 `label` 写「插入条码」、`shortLabel` 写「条码」;开发期不在时,控制台警告一次。 - `shortcut` 写成键数组,`Mod` 在 macOS 上是 ⌘、其余平台是 Ctrl。焦点在画布上、命令此刻可用时生效。 - 设计器的 `commandLayout` 改写排列:位置 → 若干组命令 id,内置命令与扩展命令可以混排;写了某个位置就只显示列出的命令,写了没登记的命令 id 时抛 `TypeError`。缺省排列是导出的 `PRINT_TOOLBAR_LAYOUT`、`printMenuLayout(subject, tableSelected)` 与 `printSelectionLayout(subject)`,菜单栏的是 `printMenubarLayout(commands, elements)`(在 `@xihan-ui/designer-shell` 的 `designerMenubarLayout(commands)` 上加了打印的几处,`elements` 是元素登记表,插入菜单按它的物料分组排),可以在它们的基础上改: ```ts export function lockFirst(designer: XhPrintDesigner): void { designer.extensions = [lockExtension] designer.commandLayout = { toolbar: [['@acme/print-lock.lock'], ...PRINT_TOOLBAR_LAYOUT] } } ``` 内置命令的 id: | 用途 | 命令 id | | --- | --- | | 编辑 | `undo` `redo` `clear-history` `cut` `copy` `paste` `duplicate` `delete` `select-all` | | 对齐与层级 | `align-left` `align-center` `align-right` `align-top` `align-middle` `align-bottom` `distribute-horizontal` `distribute-vertical` `bring-to-front` `bring-forward` `send-backward` `send-to-back`:工具栏上缺省每条一颗钮,对齐、分布与层级各成一组 | | 视图 | `pan-tool`(designer-shell 内置,拖动工具,按下状态跟外框的 `canvasTool`) `zoom-out` `zoom-in` `zoom-to-fit` `zoom-to-actual` `grid` `snap` `design` `preview` `page-setup` `print`:`grid` `snap` 也在视图菜单,`print` 也在文件菜单 | | 表格 | `edit-table` `exit-table-edit` `insert-column-left` `insert-column-right` `remove-column` `move-column-left` `move-column-right` `insert-footer-row-above` `insert-footer-row-below` `remove-footer-row` `merge-cell-right` `split-cell` | | 文字格式 | `font-size` `text-bold` `text-italic` `text-underline` `text-line-through` `text-align-left` `text-align-center` `text-align-right`:缺省排在 `selection:text` 的选中工具条上 | | 保存、面板、设置与帮助 | `save` `panels` `settings` `help`:designer-shell 内置。「保存」(Ctrl / ⌘ + S)派发 `save`,在文件菜单;其余三条在菜单栏里(面板在视图菜单,设置在文件菜单,帮助在帮助菜单),「面板」也排在工具栏末组。「面板」点开菜单开关各块面板,「设置」打开设置对话框(Ctrl / ⌘ + ,),「帮助」在新窗口打开本站的设计器一页(`PRINT_DESIGNER_HELP_URL`)。`panels-toggle-all` `panels-restore` `layout-reset` 排在「面板」菜单末尾。`start-tour` 开始新手引导,`shortcuts`、`about` 打开设置的快捷键页与关于页,都在帮助菜单;`edit-json` 打开编辑 JSON 的对话框(Ctrl / ⌘ + Shift + J),在编辑菜单与画布空白处右键菜单的末组,只在开发者模式开着时存在;`show-toolbar` `show-status-bar` 是显示开关,收在视图菜单的「外观」(`appearance`)子菜单里 | | 面板开关 | `panel:<面板 id>`:每块面板一条,含扩展登记的面板(`panel:<区名>`),文字是面板标题、勾选的是看得见的,宿主锁定的面板不可用;不排在任何位置,可以经 `commandLayout` 放上工具栏 | | 页面模板 | `previous-page` `page-menu` `next-page` `add-page` `duplicate-page` `remove-page`:翻页条上的排列固定(`PRINT_PAGE_STRIP_LAYOUT`),不经 `commandLayout`;`move-page-up` `move-page-down` 在页码菜单里。除了页码,这几条也在页面菜单里 | | 页面菜单 | `page-paper` `page-orientation`:纸张与方向的子菜单;`page-properties`:在属性面板里编辑当前页面模板 | | 插入 | `insert:<元素类型>`:每种元素一条,扩展登记的元素也有(`printInsertCommandId(type)`),放在视口中央并选中;`header-variants` `footer-variants`:页眉、页脚变体的子菜单。都在插入菜单里,不上工具栏,宿主可以经 `commandLayout` 放上去;扩展的命令不能用 `insert:` 开头的这些 id | ### 面板 扩展的 `panels` 往外框里加区,放印章库、套打对位这类面板。区是 `{ name, position, size?, minSize?, label?, title?, icon?, anchor?, defaultHidden?, closable?, movable?, floatable?, minimizable? }`:`position` 取 `top`、`start`、`end`、`bottom`,或 `overlay`(画布上的浮动件,`anchor` 定落在哪个角或右侧中间,只能显示或隐藏),同一位置的区按扩展顺序上下排;`label` 是分隔条可访问名里的区名,给了 `title` 就画成带面板头的卡片;`defaultHidden` 的缺省不显示,`closable`、`movable`、`floatable`、`minimizable` 写 `false` 就不能隐藏、换栏、浮出或收起。`create(context)` 返回放进区里的元素;设计器状态变化时调用 `update(context)`,扩展去掉时先调用 `destroy()` 再移除: ```ts import type { PrintDesignerExtension } from '@xihan-print/designer' export const selectionPanel: PrintDesignerExtension = { id: '@acme/print-selection', panels: [{ region: { name: 'acme-selection', position: 'end', label: '选中' }, create: ({ designer }) => { const element = designer.ownerDocument.createElement('section') const render = (ids: readonly string[]): void => { element.textContent = ids.length === 0 ? '没有选中' : ids.join('、') } render(designer.getSelection().elements) return { element, update: context => render(context.designer.getSelection().elements) } }, }], } ``` - 区名与内置区(`menubar`、`toolbar`、`pages`、`minimap`、`palette`、`outline`、`canvas`、`properties`、`history`、`issues`、`status`)或别的扩展的重名时,赋值时抛 `DesignerExtensionError`,设计器保持赋值前的状态。 - 扩展的区同样记在面板布局里,用户能隐藏、挪动与浮出,也各有一条开关命令 `panel:<区名>`;扩展自己登记了同名命令时同样抛 `DesignerExtensionError`。预览态只保留菜单栏、工具栏与画布。 ### 导入 扩展的 `importers` 把粘贴与拖到画布上的外部内容换成元素。来源是 `{ kind, mime?, name?, text?, file? }`:`kind` 为 `clipboard` 或 `drop`,文件带 `file`,文本按链接(`text/uri-list`)、HTML、纯文本依次给出。设计器按文件、链接、HTML、纯文本的顺序逐个问各导入器的 `accepts`,扩展靠前的先问,第一个接下的导入: ```ts import type { PrintDesignerExtension } from '@xihan-print/designer' import { createElementRegistry } from '@xihan-print/core' const registry = createElementRegistry() function readAsDataUrl(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(String(reader.result)) reader.onerror = () => reject(reader.error) reader.readAsDataURL(file) }) } export const imageImporter: PrintDesignerExtension = { id: '@acme/print-images', importers: [{ id: '@acme/print-images.file', accepts: source => source.file !== undefined && source.mime?.startsWith('image/') === true, import: async (source) => { const url = await readAsDataUrl(source.file!) const image = registry.createElement('image', { id: 'image', x: 0, y: 0, overrides: { source: { kind: 'static', url } } }) return { elements: [image] } }, }], } ``` - `import` 可以是异步的,返回 `{ elements, region? }`,返回 `undefined` 即什么都不放。元素整体挪到落点(粘贴时是视口中央),左上角吸附网格,再按粘贴的规则换新 id 放进当前页面模板并选中,记一步撤销。`region` 缺省按落点所在的区。 - 放下前按当前的元素登记表校验;`import` 抛错或元素没有通过校验时,设计器派发 `import-error`,带导入器的 id、来源与错误,不放任何元素。 - 设计器自己的剪贴板内容不经导入器。 - 导入器只把外部内容换成元素,放进当前页面模板,例如把拖进来的图片传到宿主的存储、换成图片元素。纸张、页眉页脚这类整份模板的设置不经导入器;整份存量模板的迁移见[与宿主集成](./host#存量模板迁移)。 --- 来源:https://print.docs.xihanfun.com/guide/host # 与宿主集成 组件只管设计、排版与打印,不发网络请求,也不碰存储与权限。把它嵌进业务系统时,下面这些由宿主来做: | 事情 | 宿主做什么 | 组件给什么 | | --- | --- | --- | | 模板的保存 | 存模板 JSON,管名称、分类、版本与审核 | `schema`、`schema-change`、`save`、`dirty`、`markSaved()` | | 取数 | 调接口取业务数据,整理成模板要的结构 | `data`、`sampleData`、`fields` | | 权限 | 决定谁能看、谁能改、谁能打印,服务端再复核 | `readonly`、`defaultLayout`、`settingsPolicy`、`extensions` | | 校验 | 保存前拦住有错误的模板,服务端复核 | `validate()`、`issues-change`、`validateDocument` | | 打印记录 | 记下谁在什么时候打了什么 | `print()` 的结果、客户端任务的状态 | ## 模板的保存 模板是一份完整的 JSON,原样存、原样取,不要拆成字段存。`schemaVersion` 记录模板格式的版本;`meta`(文档、页面与元素上都有)放宿主自己的数据,组件不读、原样保留。 打开时把取回的模板赋给 `schema`,保存时取 `schema` 交给后端,成功后 `markSaved()`: ```ts import type { PrintDocument, ValidationIssue } from '@xihan-print/core' import type { XhPrintDesigner } from '@xihan-print/designer' import type { JsonValue } from '@xihan-ui/core/json' import type { DataField } from '@xihan-ui/designer-kit' import { collectDocumentFields, validateDocument } from '@xihan-print/core' import { inferDataFields } from '@xihan-ui/designer-kit/field-inference' export async function openTemplate(designer: XhPrintDesigner, id: string): Promise { const response = await fetch(`/api/print-templates/${encodeURIComponent(id)}`) designer.schema = await response.json() as PrintDocument designer.addEventListener('save', async () => { const issues = designer.validate().filter(issue => issue.severity === 'error') if (issues.length > 0) return const saved = await fetch(`/api/print-templates/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(designer.schema), }) if (saved.ok) designer.markSaved() }) } ``` 设计器在内存里改模板,页面关掉修改就没了。有未保存的修改时提醒用户: ```ts export function guardUnsaved(designer: XhPrintDesigner): () => void { const onBeforeUnload = (event: BeforeUnloadEvent): void => { if (designer.dirty) event.preventDefault() } window.addEventListener('beforeunload', onBeforeUnload) return () => window.removeEventListener('beforeunload', onBeforeUnload) } ``` 用户在设置里打开自动保存时,未保存的修改另存一份这台浏览器里的草稿,下次打开同一个模板时问要不要恢复;宿主给设计器写 `draftKey`(模板 id)区分不同模板,存完照旧调 `markSaved()`,草稿随之删掉,见[自动保存本机草稿](./designer#自动保存本机草稿)。 ## 取数 - **打印时**:宿主调接口取业务数据,交给预览的 `data` 或 `printDocument`。字段路径对的是这份数据的结构,接口返回的结构与模板不一致时,在宿主这边转换好再交给组件。 - **设计时**:`fields` 告诉设计器有哪些字段可以绑定,物料面板的「字段」一组也按它列出,字段写了 `content`(`image`、`barcode`、`qrcode`)时拖上画布生成对应的元素,见[字段物料](./designer#字段物料);`sampleData` 让画布显示实际的值。用户可以在设计器里从 JSON 样例或 JSON Schema 导入字段(见[导入字段](./designer#导入字段)),宿主收 `fields-change` 与 `sample-data-change` 存起来;字段由后端定、不许用户改时写 `fields-readonly`。宿主也可以自己按一份示例数据生成字段树,用的是同一个推断函数: ```ts /** 按一份示例数据生成字段树:对象是 object,对象数组是 list(子字段取各项键的并集),其余是带类型的 value。 */ export function fieldsOf(sample: JsonValue): readonly DataField[] { return inferDataFields(sample, { detectImages: true }).fields } ``` - 反过来,`collectDocumentFields(document)` 列出一份模板引用了哪些字段,形状同 `fields`:文本、长文本、条码与二维码的内容(字段、`{{path}}` 模板串与表达式里的路径),图片与富文本的字段来源,表格的 `dataField`(记成 `list`,各列与汇总里的行内字段挂在它下面),以及页面水印。以 `$` 开头的宿主变量(如 `$page`)不算;只知道路径,所以不带 `label` 与 `type`。值字段带上 `content`:被图片的字段来源引用记 `image`,被条码、二维码的内容直接取用记 `barcode`、`qrcode`,表格里单元格类型是图片、条码、二维码的列同样;模板串与表达式里的字段只是内容的一部分,不记。一个字段被几种元素引用时取文字以外的那一种,文字以外的种类有冲突时取模板里先出现的那一种。没有数据源的模板可以按它生成示例数据的表单,有数据源的可以拿它核对模板里有没有数据源没有的字段。用了扩展元素时把 `extensions` 一并传进去: ```ts /** 列出模板引用的字段,与宿主给的字段树比对,找出数据源里没有的那些路径。 */ export function missingFields(document: PrintDocument, known: readonly DataField[]): string[] { const missing: string[] = [] const walk = (used: readonly DataField[], offered: readonly DataField[] | undefined, prefix: string): void => { for (const field of used) { const path = prefix === '' ? field.name : `${prefix}.${field.name}` const match = offered?.find(item => item.name === field.name) if (match === undefined) missing.push(path) else if (field.children) walk(field.children, match.children, path) } } walk(collectDocumentFields(document), known, '') return missing } ``` - 图片地址由宿主提供:放在宿主的文件服务里,模板里写地址或绑定数据里的地址字段。要经打印客户端打印的图片,文件服务要允许跨源读取(CORS)。 ## 存量模板迁移 整份存量模板(别的打印组件存的模板)的迁移由宿主写转换函数:把旧模板读成 `PrintDocument`,过 `validateDocument`,没有错误级问题时再赋给设计器的 `schema`。纸张、方向、页边距、页眉页脚变体与多页模板都在转换函数里一并换好: ```ts /** 打开一份存量模板:转换函数由宿主写,产出的模板先校验,没有错误级问题才交给设计器。 */ export function openLegacyTemplate(designer: XhPrintDesigner, legacy: unknown, convert: (legacy: unknown) => PrintDocument): ValidationIssue[] { const document = convert(legacy) const errors = validateDocument(document).issues.filter(issue => issue.severity === 'error') if (errors.length === 0) designer.schema = document return errors } ``` - 转换在宿主里跑,可以在前端打开旧模板时转,也可以在服务端批量转好再存;`@xihan-print/core` 不依赖 DOM,两边都能校验。 - 设计器的导入器只把粘贴、拖进来的外部内容换成元素,不负责整份模板。 - 旧模板里的 HTML 元素怎么换成富文本,见[富文本](./rich-text#从存量-html-元素迁移)。 ## 权限 - 只能看的用户把设计器设成 `readonly`:能选择、浏览、预览与打印,不能修改。 - 只让调整局部的场景,用 `settingsPolicy` 的 `layout.panels.<面板 id>` 去掉物料等面板(面板 id 见 `PRINT_DESIGNER_PANELS`),用自定义属性编辑器限定可选的值。 - 这些都只在前端生效。模板保存、删除与打印的权限要在服务端判断;保存时服务端也应复核模板本身。 ## 服务端复核 `@xihan-print/core` 不依赖 DOM,可以在 Node.js 后端直接校验收到的模板: ```ts /** 服务端收到模板时复核:只有错误级问题才拒收,返回这些问题的问题码与路径。 */ export function reviewTemplate(body: string): { valid: boolean, errors: string[] } { const report = validateDocument(JSON.parse(body) as PrintDocument) const errors = report.issues .filter(issue => issue.severity === 'error') .map(issue => `${issue.code} @ ${issue.path.join('.')}`) return { valid: report.valid, errors } } ``` 后端不是 JavaScript 时,前端在提交前用 `validate()` 拦住有错误的模板;服务端把模板当作不透明的 JSON 存取,需要时另起一个 Node.js 服务做复核。 ## 在前端框架里使用 预览与设计器是原生 Web Components,任何框架都能用:在应用启动时调用一次 `definePrintPreview()` 或 `definePrintDesigner()`,之后像普通元素一样放进模板。模板、数据这类对象要交给元素的属性,不要写成 attribute。 在 Vue 里,先告诉编译器 `xh-` 开头的标签是自定义元素,免得被当成组件去解析: ```ts // vite.config.ts 里 @vitejs/plugin-vue 的选项 vue({ template: { compilerOptions: { isCustomElement: tag => tag.startsWith('xh-'), }, }, }) ``` 对象用 `.prop` 修饰交给属性,事件用 `@` 监听: ```vue ``` React 19 起会把对象直接赋给自定义元素的同名属性;事件可以用 `ref` 拿到元素后 `addEventListener` 监听。 ## 一个页面里放多个实例 组件不往 `window` 上挂对象,也不读写固定键名的本地存储,同一页面里的多个预览、多个设计器互不影响。只有语言包是全局的:`setXhConfig` 对页面上所有元素生效,单个实例的文案用它的 `translations` 属性覆写。 --- 来源:https://print.docs.xihanfun.com/guide/rich-text # 富文本 富文本元素 `richText` 排一段带格式的文字:标题、段落、列表与分隔线,文字可以加粗、换颜色、改字号、加链接,还能嵌入行内字段。它和长文本一样随内容延展、跨页续排,只能放在正文区。 内容是 XiHan.UI 的富文本模型 `@xihan-ui/core/rich-text`,整份是 JSON,不存 HTML。模型的完整定义、HTML 白名单与问题码见 [XiHan.UI 的富文本模型](https://ui.docs.xihanfun.com/runtime/rich-text),本页只讲打印怎么用它。 ## 内容来源 | 来源 | 写法 | 说明 | | --- | --- | --- | | 写死的文档 | `{ kind: 'static', document }` | 模板里存一份富文本文档 | | 字段里的 HTML | `{ kind: 'field', field, source: 'html' }` | 字段值是 HTML 串,打印时按白名单换成文档 | | 字段里的文档 | `{ kind: 'field', field, source: 'document' }` | 字段值已是富文本文档对象 | ### 写死的文档 上面的停电检修通知,正文就是写死的文档: ```ts const notice: RichTextContent = { kind: 'static', document: { blocks: [ { type: 'heading', level: 1, align: 'center', runs: [{ type: 'text', text: '停电检修通知' }] }, { type: 'paragraph', runs: [ { type: 'text', text: '因供电线路检修,' }, { type: 'field', field: 'building', marks: { bold: true } }, { type: 'text', text: '将于 ' }, { type: 'field', field: 'start', format: { kind: 'date', pattern: 'M 月 d 日 HH:mm' }, marks: { color: '#c0392b' } }, { type: 'text', text: ' 起停电,预计持续 ' }, { type: 'field', field: 'hours' }, { type: 'text', text: ' 小时。请提前做好以下准备:' }, ], }, { type: 'list', ordered: true, items: [ { blocks: [{ type: 'paragraph', runs: [{ type: 'text', text: '保存正在编辑的文件,关闭电脑与显示器。' }] }] }, { blocks: [{ type: 'paragraph', runs: [{ type: 'text', text: '冰箱里的易腐物品', marks: { bold: true } }, { type: 'text', text: '请当天带走。' }] }] }, { blocks: [{ type: 'paragraph', runs: [{ type: 'text', text: '电梯暂停运行,请走楼梯。' }] }] }, ], }, { type: 'divider' }, { type: 'paragraph', align: 'right', runs: [{ type: 'text', text: '物业服务中心', marks: { italic: true } }] }, ], }, } ``` - 行内字段 `{ type: 'field', field, format?, marks? }` 按字段路径从数据的根取值,`format` 是[值格式](./binding#值格式),`marks` 是这截文字的样式;取到空值时什么都不显示。 - 文档里的链接只能是 `http`、`https`、`mailto`、`tel` 与相对地址,颜色写十六进制,字号单位 pt。 ### 字段里的 HTML 业务系统里用富文本编辑器写的内容,通常以 HTML 存着,直接绑定字段即可: ```ts page.elements = [ registry.createElement('richText', { id: 'body', x: 12, y: 12, overrides: { width: 124, height: 40, content: notice, style: { ...baseStyle, fontSize: 11 } }, }), registry.createElement('richText', { id: 'remark', x: 12, y: 58, overrides: { width: 124, height: 20, content: { kind: 'field', field: 'remark', source: 'html' }, style: { ...baseStyle, fontSize: 10, color: '#444444' } }, }), ] ``` ```ts export const data = { building: '3 号楼', start: '2026-10-24 08:30:00', hours: 4, remark: '

补充说明

如有医疗设备等不能断电的情况,请在 10 月 22 日前联系物业登记。

', } ``` - 打印时按白名单把 HTML 换成文档:认识的标签与 `style` 属性换成块与样式,`script`、`img`、`iframe` 这类连同内容去掉,表格这类去掉标签、留下文字。规则见 [XiHan.UI 的解析 HTML](https://ui.docs.xihanfun.com/runtime/rich-text#解析-html)。 - 连同内容去掉或降级了标签时,给出警告诊断 `richText.droppedContent`,`params.field` 是字段路径,`params.names` 是去掉的标签名;`style` 属性与链接地址的取舍不报。上面的数据带了一段 `script`,所以会有这条警告。 - HTML 里的 `` 也是行内字段,按同一份数据取值。 ### 字段里的文档 字段值是富文本文档对象时用 `source: 'document'`。值先按模板里同样的规则检查,不合规时整段不画,给出错误级诊断 `richText.invalidData`:`params.field` 是字段路径,`params.code` 是问题码,`params.path` 是出问题的位置(如 `blocks[0].type`)。字段值是 JSON 串时也不合规,先在宿主这边解析成对象。 字段为空(`null`、`undefined` 或空串)时没有内容,不报诊断。 ## 基准样式 元素的 `style` 是基准样式:字体、字号、颜色、行高倍数、字间距、对齐与垂直对齐。块的 `align` 与文字的 `marks` 在它之上覆写,没写的取基准样式。 ## 排版规则 - 按元素内宽(扣掉左右内边距)逐块排。段落与标题的折行与普通文本是同一套规则:中日韩文字逐字可断,拉丁文按词断,一个单词跨两种样式时不拆开。 - 行高是这一行里最大的字号乘以行高倍数。上标与下标的字号缩到 0.7 倍,行高仍按原字号算。 - 一、二、三级标题的字号是基准字号的 1.6、1.35、1.15 倍,并且加粗;文字上写 `bold: false` 可以取消加粗。 - 顶层的块与块之间空半个基准字号;列表项之间、列表项里的块之间不空。 - 列表每层缩进两个基准字号。无序列表三层依次用 `•`、`◦`、`▪`,有序列表从 `start` 编号(缺省 1);符号或序号的右边离文字半个基准字号。 - 分隔线单独占一行基准行高,用基准颜色画 0.2 mm 的横线。 - 两端对齐的块里,最后一行与换行之前的那一行不拉伸。 - 链接写成带地址的链接,外观只取这截文字自己的样式。 这些倍数都是 `@xihan-print/core` 导出的常量,见[模型层 core](../reference/core#layout-rich-text)。 ## 跨页 跨页与长文本相同:按整行切到后续输出页,段落可以从中间切开;每段都带上下内边距,续排那一段的第一行不留段距;只有一段时高度不小于设计高度。富文本下面的元素随它下推,规则见[运行时](./runtime#正文元素的位置)。 不做孤行与寡行控制:标题可能单独留在一页的最后一行。 ## 在设计器里 - 属性面板的「内容」先选来源;选「字段」时再选绑定的字段与内容格式(HTML 或富文本文档)。 - 写死的文档在属性面板上不能编辑,用代码写进模板(如上面的例子),或从存量模板迁移过来。 - 画布按设计态排版显示;不给示例数据时,字段显示为 `{路径}` 占位。 ## 校验 | 问题码 | 级别 | 说明 | | --- | --- | --- | | `richText.invalidDocument` | 错误 | 文档结构不对,`params.code` 是 XiHan.UI 的[检查](https://ui.docs.xihanfun.com/runtime/rich-text#检查)给出的问题码 | | `field.invalidPath` | 错误 | 行内字段的路径写法不对 | | `format.*`、`invalid-option` 等 | 错误 | 行内字段的值格式不对,与文本内容的值格式同一套规则 | | `richText.empty` | 警告 | 写死的文档没有任何文字、行内字段、分隔线或列表项 | | `element.flowInBand` | 错误 | 放进了页眉或页脚 | ## 从存量 HTML 元素迁移 存量模板里的 HTML 元素,有的在模板里存一段 HTML,有的存一个返回 HTML 的函数。迁移到富文本由宿主完成:解析器是 `@xihan-ui/core/rich-text` 的 `parseRichTextHtml`,不依赖 DOM,Node.js 里也能跑,可以写成一次性的迁移脚本或放在服务端。 | 旧写法 | 迁移成 | | --- | --- | | 写死的 HTML | 解析成 `{ kind: 'static', document }` | | 绑定字段,字段值是 HTML | `{ kind: 'field', field, source: 'html' }`,打印时再解析 | | 函数只返回一个字符串字面量 | 静态取出这段字面量(不执行函数),再按写死的 HTML 处理 | | 函数里有拼接、条件、取数等逻辑 | 不能自动迁移,记进报告人工改写成富文本加行内字段、带表达式的文本或其它元素 | | HTML 里的表格、图片 | 解析时去掉并记进丢弃清单;表格改用表格元素,图片另起图片元素 | 迁移时不执行模板里的任何代码。 ### 迁移报告 每个元素迁移后留一条记录,汇总起来就是迁移报告。报告的形状由宿主自定,打印的包里没有发布这些类型与函数;下面的 `migrateHtmlContent`、`RichTextMigrationEntry` 与 `needsReview` 只是一种写法,可以照着改成自己的: ```ts import type { RichTextContent } from '@xihan-print/core' import type { RichTextDrop } from '@xihan-ui/core/rich-text' import { parseRichTextHtml } from '@xihan-ui/core/rich-text' /** 旧元素里与迁移有关的三种写法。 */ export type LegacyHtmlContent = | { kind: 'html', html: string } // 写死的 HTML,或从只返回一个字符串字面量的函数里静态取出的 HTML | { kind: 'field', field: string } // 绑定字段,字段值是 HTML | { kind: 'code', source: string } // 其余的函数源码 /** 迁移报告里的一条:一个元素一条。 */ export interface RichTextMigrationEntry { /** 原元素的标识。 */ elementId: string /** static:已换成写死的文档;field:改成绑定字段,打印时再解析;manual:要人工改写。 */ outcome: 'static' | 'field' | 'manual' /** 解析时丢掉的东西,即 parseRichTextHtml 的 dropped;绑定字段与人工改写的为空。 */ dropped: readonly RichTextDrop[] /** 人工改写时参考的原始内容。 */ source?: string } export function migrateHtmlContent(elementId: string, legacy: LegacyHtmlContent): { content?: RichTextContent, entry: RichTextMigrationEntry } { switch (legacy.kind) { case 'html': { const { document, dropped } = parseRichTextHtml(legacy.html) return { content: { kind: 'static', document }, entry: { elementId, outcome: 'static', dropped } } } case 'field': return { content: { kind: 'field', field: legacy.field, source: 'html' }, entry: { elementId, outcome: 'field', dropped: [] } } case 'code': return { entry: { elementId, outcome: 'manual', dropped: [], source: legacy.source } } } } /** 要人工复核的条目:要人工改写的,以及丢了标签或连同内容去掉了东西(表格、图片、脚本等)的。 */ export function needsReview(entry: RichTextMigrationEntry): boolean { return entry.outcome === 'manual' || entry.dropped.some(drop => drop.kind === 'element' || drop.kind === 'content') } ``` - `outcome` 是迁移结果:`static` 已换成写死的文档,`field` 改成绑定字段,`manual` 要人工改写,`source` 留着原来的内容供参考。 - `dropped` 直接取 `parseRichTextHtml` 的丢弃清单,每项是 `{ kind, name, count }`:`element` 去掉或降级了标签、文字保留,`content` 连同内容去掉,`style` 是不认识或取值不合法的 `style` 属性,`link` 是没通过判定的链接地址。同类同名合成一条并计数,详见 [XiHan.UI 的丢弃清单](https://ui.docs.xihanfun.com/runtime/rich-text#丢弃清单)。 - `dropped` 里有 `element` 或 `content` 的要人工复核,表格与图片要另外补上;只有 `style` 与 `link` 的通常可以忽略。 - 绑定字段的元素在迁移时还不知道数据里的 HTML 会丢什么,打印时由 `richText.droppedContent` 诊断报出。 --- 来源:https://print.docs.xihanfun.com/guide/runtime # 运行时 `@xihan-print/runtime` 是业务页面用的部分:`` 把模板按数据排成输出页逐页显示,`print()` 与 `printDocument` 调起浏览器的打印对话框。分页算法本身在 `@xihan-print/core`,不依赖 DOM。属性、方法与事件的完整列表见[预览与打印](../reference/runtime)。 ## 预览元素 调用 `definePrintPreview()` 注册后,把模板与数据交给 `schema` 与 `data`。下面的设备巡检记录排成多页,状态行显示每次分页的结果: ```ts const preview = document.createElement('xh-print-preview') const status = document.createElement('p') preview.addEventListener('paginated', (event) => { const { sheets, diagnostics } = (event as PrintPreviewEventMap['paginated']).detail const notes = diagnostics.map(item => `${item.code}(${item.severity})`).join('、') status.textContent = `共 ${sheets} 张输出页${notes === '' ? ',没有诊断' : `,诊断:${notes}`}` }) preview.addEventListener('pagination-error', (event) => { const { code, issues } = (event as PrintPreviewEventMap['pagination-error']).detail status.textContent = `分页失败:${code},校验问题 ${issues.length} 个` }) preview.schema = schema preview.data = data ``` - `schema`、`data`、`extensions` 与各注入项(`expressionEngine`、`formatter`、`measurer`、`locale`、`timeZone`)任一变化都重新分页。`extensions` 与设计器给同一份,见[设计器的扩展](./designer#扩展)。 - 分页成功派发 `paginated`,带输出页数与诊断;失败派发 `pagination-error` 并清空预览,带错误码与校验问题。 - `result` 是最近一次的分页结果,`paginate()` 立即按当前属性重新分页并返回结果。 - 元素是带滚动的块,在页面上给它一个高度,输出页多时在里面滚动。纸张比预览区宽时从左边开始排,可以横向滚动。 `zoom` 是显示缩放倍数,缺省 1,只影响预览、不影响打印: ```ts const zoomOut = document.createElement('button') const zoomIn = document.createElement('button') zoomOut.type = 'button' zoomIn.type = 'button' zoomOut.textContent = '缩小' zoomIn.textContent = '放大' const zoomBy = (factor: number): void => { preview.zoom = Math.min(2, Math.max(0.5, (preview.zoom ?? 1) * factor)) } zoomOut.addEventListener('click', () => zoomBy(1 / 1.25)) zoomIn.addEventListener('click', () => zoomBy(1.25)) ``` ## 分页规则 ### 页面模板与输出页 模板里的页面模板依次排,每张排出一张或多张输出页。每张输出页先选页眉与页脚的带区(首页、末页、奇偶页的选取见[模板结构](./template#首页、末页与奇偶页)),再按选中的带区高度算出这一页的正文区,所以首页页眉高一些时,首页的正文区就短一些。 ### 正文元素的位置 正文元素按设计时的上边缘从上到下依次排: 1. 一个元素的设计位置整个在另一个元素下方时(不论左右是否重叠),它排在那个元素实际结束的位置之后,并保持设计时两者之间的间距;同时在几个元素下方时,取最靠后的那个。 2. 不在任何元素下方的元素,留在第一张输出页的设计位置。 3. 固定尺寸的元素被推到正文区底边之外时,整个移到下一页正文区的顶部,不拆开。 4. 长文本、富文本与表格从起点开始续排,排满一页接着排到下一页。 所以表格下面的落款、长文本下面的表格,都会随上面内容的长短上下移动。巡检记录里,表格排在说明文字下面,落款排在表格之后。 ### 长文本 长文本按元素宽度换行,按整行切到后续输出页,每段都带上下内边距;只有一段时高度不小于设计高度。文字里的换行符照样换行。 ### 富文本 富文本按块排版,行高随这一行的字号变化,跨页的切法与长文本相同,续排那一段的第一行不留段距。排版规则见[富文本](./rich-text#排版规则)。 ### 表格 - 数据行不拆开跨页:一行放不下时整行移到下一页;在一页的正文区里也放不下时照样放上去,并给出警告 `table.rowOverflow`。 - 表头在第一段显示;`header.repeat` 为 `true` 时每一段都重复表头。 - `everyPage` 的表尾行在每一段末尾都出现,`lastPage` 的表尾行只在最后一段出现,并且与最后一行数据排在同一页:放不下时最后一行数据一起移到下一页。 - 行高方式为 `fixed` 时每行固定为行高;`minimum` 时以行高为下限,按单元格内容撑高。 - `maxRowsPerPage` 限制每段的数据行数;`fillEmptyRows` 用空行补齐到每页最大行数,没有设最大行数时补满可用高度。 - 只有一段时,表格高度不小于设计高度。 ### 页码 页码元素在全部输出页排完后才填入。`{{$page}}` 是这张输出页的页码,`{{$pages}}` 是所在编号段的总页数;`numbering.continuous` 为 `true` 的页面模板与上一张同属一个编号段。 ### 只在预览中显示 `previewOnly` 的元素在预览与设计器里显示,打印时不画,适合放套打用的底图或说明。 ### 输出页上限 整份模板最多排出 1000 张输出页(`printDocument` 的 `maxSheets` 可以改)。超出时停止排版,给出错误级诊断 `layout.sheetLimit`,已排的输出页照常显示与打印。 ## 诊断与错误 分页有两类结果: - **诊断**:分页照常完成,问题随结果给出。错误级表示按模板该画的内容没有画出或输出页被截断(如表达式求值失败、条码内容编不了、超出页数上限),警告表示内容画出了但与设计有出入(如文字超出元素框、表格一行跨不下一页)。 - **错误**:分页无法进行。模板有错误级校验问题时抛出 `PrintLayoutError`,码为 `document.invalid`,`issues` 里是全部错误级问题;没有给模板、拿不到文字度量器时抛出 `PrintRuntimeError`。 预览元素把错误转成 `pagination-error` 事件;`print()` 与 `printDocument` 直接抛出,不打开打印对话框。问题码见[问题码与诊断](../reference/issues)。 ## 调试回调 预览元素的 `trace` 属性与 `printDocument`、`paginateDocument`、打印客户端各方法的 `trace` 选项收一组回调,给调试日志用,不给就不报,结果照旧: - `step(step)`:分页的每一步,形状是 `@xihan-print/core` 的 `PaginationStep`——`page` 开始排一个页面模板,`reflow` 按假定页数选的页眉页脚变体不对而重排,`split` 跨页元素续排到后续输出页,`move` 元素放不下整块挪到下一页,`done` 这个页面模板排完。重排时只报最后采用的那一次的 `split` 与 `move`。底层的 `paginate` 也收同样的 `trace` 选项。 - `phase(phase)`:一个环节开始时调,返回在它结束时调的函数,结束时带上这一环节的数据。环节有 `paginate`(分页,带输出页数)、`render`(生成输出页的 DOM,带输出页数)、`resources`(等图片与字体就绪;交给本地客户端时是把它们取来随任务带去,带资源个数)、`document`(合成交给本地客户端的文档,带字节数)与 `print`(交给浏览器的打印对话框)。预览元素重新分页时报 `paginate` 与 `render`,`print()` 另报后面的环节。 设计器的「调试日志」就是接在这里,见[设计器](./designer#调试日志)。 ## 浏览器打印 `print()`、`printDocument` 与 `printSheets` 都这样打印: 1. 在页面上挂一个看不见的 iframe,把输出页渲染进去,`previewOnly` 的元素不画。 2. 每种纸张尺寸写一条具名的 `@page`,边距为 0,背景色照样打印。 3. 把页面里的 `@font-face` 带进 iframe:读得到的规则把地址换成绝对地址抄过去,读不到的跨源样式表整张链接过去。 4. 等图片解码、字体就绪,最多等 10 秒,然后调起打印对话框。 5. 对话框关闭后移除 iframe;有的浏览器不派发 `afterprint`,最多等 60 秒。 `title` 选项是打印对话框与另存 PDF 时的缺省文件名,缺省沿用当前页面的标题。浏览器打印由用户在对话框里选打印机;要不弹对话框直接打到指定的打印机,或者纸张尺寸不同的输出页要分别按各自的纸张送去打印,用[打印客户端](./client)。 ### 一次打多份 一份数据打一份单据;要一次打出多份,每份数据各用 `paginateDocument(schema, data, options?)` 排一次,`mergePaginationResults(results)` 按顺序接成一份,再交给 `printSheets`: ```ts import type { MergedPaginationResult } from '@xihan-print/runtime' import type { PrintClient } from '@xihan-print/runtime/client' import { mergePaginationResults, paginateDocument, printSheets } from '@xihan-print/runtime' import { schema } from '../templates/delivery' /** 每份数据各排一次再合并:页码与总页数各自从第 1 页算起;分页的缺省注入项与 printDocument 相同。 */ export function paginateAll(records: readonly unknown[]): MergedPaginationResult { return mergePaginationResults(records.map(record => paginateDocument(schema, record))) } export async function printAll(records: readonly unknown[]): Promise { await printSheets(paginateAll(records), { title: '送货单' }) } ``` - `paginateDocument` 只分页、不打印,缺省注入项与 `printDocument` 同一份:度量器用 `options.document`(缺省为当前页面)里的 Canvas 度量器,表达式引擎与格式化器同预览元素,`measurer`、`expressionEngine`、`formatter`、`locale`、`timeZone`、`maxSheets` 与 `extensions` 照样可以给。出错的情形也与 `printDocument` 相同。 - 每份的页码与总页数各自从第 1 页算起,合并后不变。合并结果的 `parts` 给出每份占的输出页(起点与张数);诊断各带 `part`,是来自第几份(从 0 开始),`path` 与 `target` 相对那一份的文档。 - `printDocument` 只收一份数据,不收数组:模板的数据根本身就可能是数组(比如整份数据就是一张明细表),收数组会分不清是一份还是多份。 - 合并结果同样可以交给 `renderSheets` 显示,或交给[打印客户端](./client)的 `printSheets`: ```ts export async function printAllViaClient(client: PrintClient, records: readonly unknown[]): Promise { await client.printSheets(paginateAll(records), { printer: 'Delivery Printer' }).done } ``` ## 字体与文字度量 分页时要量每段文字的宽度来换行,缺省用页面里的 Canvas 度量器,字体就是模板里写的字体族,没写时用 `sans-serif`。 - 预览元素在页面的字体加载完成后自动重新分页,网页字体晚到也能排对。 - `printDocument` 只排一次。模板用了网页字体时,先用 `document.fonts.load()` 把要用的字体加载好再调用,否则会按替代字体换行。 - 输出页里每个元素都按 mm 绝对定位、尺寸固定,只有元素框内部的折行受字形宽度影响。换一台字体不同的电脑,折行位置可能不同。 - 需要在没有 Canvas 的环境里分页时,用 `@xihan-print/core` 的 `paginate` 并传入自己的度量器(`measure(text, font)` 返回宽度与上下高度,单位 CSS 像素)。 --- 来源:https://print.docs.xihanfun.com/guide/template # 模板结构 模板是一份纯 JSON:不含函数、不含脚本,可以原样存进数据库、放进文件或经接口传输。这一章讲模板由哪些部分组成,每个字段的类型见[模板类型](../reference/schema),每种元素的属性见[元素](../reference/elements)。 ## 单位与坐标 | 量 | 单位 | | --- | --- | | 坐标、宽高、边距、带区高度、线宽、间距 | mm | | 字号、字间距 | pt | | 旋转角度 | 度,元素绕中心顺时针旋转;水印逆时针 | | 文本行高 | 字号的倍数;表格单元格的行高是 mm | | 颜色 | `#RGB`、`#RGBA`、`#RRGGBB`、`#RRGGBBAA` 或 `transparent` | 所有元素的坐标都相对纸张左上角,页眉、正文与页脚里的元素都一样。纸张横放时,坐标按横放后的纸张算。 ## 一份最小的模板 一张 A6 纵向页面,正文里一行绑定数据的文字。这就是完整的模板: ```ts export const schema: PrintDocument = { schemaVersion: 1, name: '取件凭条', pages: [ { id: 'page-1', paper: { preset: 'A6', width: 105, height: 148 }, orientation: 'portrait', margins: { top: 8, right: 8, bottom: 8, left: 8 }, header: { default: { height: 0, elements: [] }, singleSheet: 'first' }, footer: { default: { height: 0, elements: [] }, singleSheet: 'last' }, numbering: { continuous: true, start: 1 }, elements: [ { id: 'pickup-code', type: 'text', x: 8, y: 12, width: 89, height: 12, content: { kind: 'template', template: '取件码 {{code}}' }, style: { fontSize: 18, bold: true, italic: false, underline: false, lineThrough: false, color: '#000000', align: 'center', verticalAlign: 'middle', lineHeight: 1.2, letterSpacing: 0, }, wrap: false, overflow: 'shrink', rotation: 0, }, ], }, ], } ``` ## 文档 `PrintDocument` 是模板的根: | 字段 | 说明 | | --- | --- | | `schemaVersion` | 模板 schema 版本,当前为 1。不认识的版本校验报错 | | `name` | 模板名称,可省 | | `pages` | 页面模板,至少一张 | | `meta` | 宿主附加数据,组件不读,原样保留 | 一张页面模板按数据排出一张或多张输出页。有多张页面模板时依次排:比如第一张是封面、第二张是明细,明细多了就排出多张输出页,接在封面后面。 ## 页面模板 | 字段 | 说明 | | --- | --- | | `id` | 页面标识,整份模板内唯一,与元素标识也不能重复 | | `paper` | 纸张:`width`、`height` 写纵向尺寸;`preset` 记来源预设,自定义纸张不填。预设见[纸张预设](../reference/elements#page) | | `orientation` | `portrait` 纵向、`landscape` 横向;横向时宽高互换 | | `margins` | 四边页边距 | | `header`、`footer` | 页眉与页脚,见下一节 | | `elements` | 正文元素 | | `numbering` | 页码编号:`continuous` 是否接续上一张页面模板的页码,`start` 是不接续时的起始页码 | | `watermark` | 平铺文字水印,可省 | | `guides` | 设计器里的参考线,只用于对位,不参与分页也不打印 | | `meta` | 宿主附加数据 | 用 `createPage` 生成时,缺省是 A4 纵向、四边 10 mm,页码接续上一张页面模板。 ### 页码编号 页码元素的格式里,`{{$page}}` 是当前页码,`{{$pages}}` 是所在编号段的总页数。`numbering.continuous` 为 `true` 的页面模板接着上一张的页码往下编,并与它算作同一个编号段;为 `false` 时从 `start` 重新开始一个编号段。 比如封面 `continuous: false, start: 1`、正文 `continuous: false, start: 1`:封面自己是「第 1 页 / 共 1 页」,正文从第 1 页重新编。 ### 水印 水印在输出页上平铺文字,文字里可以用 `{{path}}` 占位取数据。`fontSize` 单位 pt,`opacity` 为 0 到 1,`rotation` 逆时针旋转,`gapX`、`gapY` 是相邻水印的间距(mm)。 ## 页眉、正文与页脚 ```text ┌──────────────── 纸张 ────────────────┐ │ 上边距 │ │ ┌──────── 页眉 header ──────────┐ │ │ ├──────── 正文 elements ────────┤ │ │ │ │ │ │ ├──────── 页脚 footer ──────────┤ │ │ 下边距 │ └──────────────────────────────────────┘ ``` - 页眉在上边距之下,页脚在下边距之上,高度由带区的 `height` 决定;两者之间是正文区。 - 页眉、页脚里的元素每张输出页都画一遍;正文元素按数据排,长文本、富文本与表格可以续排到后面的输出页。 - 长文本、富文本与表格只能放在正文区,放进页眉页脚校验报错。 ### 首页、末页与奇偶页 页眉与页脚各有一个缺省带区 `default`,还可以有四个变体:`first` 首页、`last` 末页、`odd` 奇数页、`even` 偶数页。每张输出页按下面的顺序选带区: 1. 是首页且有 `first`,或是末页且有 `last`:用对应的变体;既是首页又是末页、两个变体都有时,按 `singleSheet` 选 `first` 或 `last`。 2. 否则按页码奇偶,有 `odd` 或 `even` 就用。 3. 都没有时用 `default`。 首页、末页按这张页面模板排出的输出页算,奇偶按页码算。带区高度不同时,每张输出页的正文区跟着变:首页页眉高一些,首页的正文区就短一些。 ## 元素 每个元素都有这些字段: | 字段 | 说明 | | --- | --- | | `id` | 元素标识,整份模板内唯一 | | `type` | 元素类型,如 `text`、`table` | | `name` | 设计器里显示的名称,可省 | | `x`、`y`、`width`、`height` | 元素框,相对纸张左上角 | | `locked` | 锁定后在设计器里不能拖动与缩放 | | `previewOnly` | 只在设计器与预览里显示,不打印,适合画套打底图 | | `meta` | 宿主附加数据 | 其余字段随元素类型不同,见[元素](../reference/elements)。 - 同一区里的元素按数组顺序叠放,越靠后的画在越上面。 - 元素框超出纸张或超出所在的页眉、页脚,校验给出警告,照样分页。 - 固定尺寸的文本放不下时按 `overflow` 处理:`clip` 裁掉,`shrink` 缩小字号直到放下(最小 4 pt),`visible` 照常显示超出的部分。 ### 用代码生成元素 元素登记表登记了全部内置元素的缺省值。`createElement` 按缺省值生成元素,`overrides` 只覆盖顶层属性;要改样式里的一项,先展开缺省的样式: ```ts import type { PrintDocument, TableColumn } from '@xihan-print/core' import { createDocument, createElementRegistry, createPage } from '@xihan-print/core' const registry = createElementRegistry() const textStyle = registry.get('text')!.defaults.style ``` 快速开始里的送货单就是这样写的:标题用缺省文本样式,只改字号、粗体与对齐。 ## 表格 表格绑定数据里的一个数组,每个元素一行,行数多了续排到后面的输出页。 | 部分 | 说明 | | --- | --- | | `dataField` | 数组的字段路径,如 `items`、`order.lines` | | `columns` | 列:标题、列宽、对齐、单元格内容与单元格类型 | | `header` | 表头:是否显示、跨页时是否每页重复、行高、样式 | | `body` | 表体:行高与行高方式(`fixed` 固定,`minimum` 以行高为下限按内容撑高)、样式 | | `footerRows` | 表尾行:每页都出现(`everyPage`)或只在表格结束的那一页出现(`lastPage`) | | `maxRowsPerPage` | 每页最多几行数据,缺省按可用高度排满 | | `fillEmptyRows` | 数据行不足时用空行补齐到每页最多行数或可用高度 | | `border` | 网格线:线型,以及外框、横线、竖线各画不画 | 列的宽度合计应等于表格宽度,不一致时校验给出警告;在设计器里拖动表格宽度时列宽按比例缩放。 ### 单元格 表体单元格的内容取自行对象: | 内容 | 说明 | | --- | --- | | `field` | 行内字段,可带格式与空值时显示的文字 | | `template` | 按行对象替换 `{{path}}` 占位 | | `rowNumber` | 从 1 开始、跨页连续的行序号 | | `expression` | 受限表达式,变量是行对象的顶层字段 | 单元格类型 `cellType` 决定怎么画:`text` 文字,`image` 把内容当图片地址,`barCode` 按列的 `barCodeFormat` 画一维条码,`matrixCode` 画 QR 码。 ### 表尾行 表尾单元格从起始列 `columnId` 开始向右跨 `colSpan` 列,内容可以是固定文字、根数据的字段或模板,也可以是汇总: | 汇总 | 说明 | | --- | --- | | `fn` | `sum` 求和、`avg` 平均、`count` 计数、`min` 最小、`max` 最大 | | `field` | 参与汇总的行内字段;`count` 不给字段时统计行数 | | `scope` | `page` 本页的数据行,`cumulative` 截至本页的累计,`all` 全部数据 | 「本页小计」放在 `everyPage` 的表尾行里、用 `scope: 'page'`;「合计」放在 `lastPage` 的表尾行里、用 `scope: 'all'`。送货单的表格: ```ts const money = { kind: 'number', decimals: 2, grouping: true } as const function column(id: string, title: string, width: number, field: string, align: TableColumn['align'] = 'left'): TableColumn { return { id, title, width, align, content: { kind: 'field', field, format: money }, cellType: 'text' } } page.elements.push( registry.createElement('text', { id: 'customer', x: 10, y: 26, overrides: { width: 120, content: { kind: 'template', template: '客户:{{customer.name}}' } }, }), registry.createElement('text', { id: 'date', x: 140, y: 26, overrides: { width: 60, content: { kind: 'field', field: 'date', format: { kind: 'date', pattern: 'yyyy年M月d日' } }, style: { ...textStyle, align: 'right' }, }, }), registry.createElement('table', { id: 'items', x: 10, y: 36, overrides: { width: 190, dataField: 'items', columns: [ { id: 'no', title: '序号', width: 14, align: 'center', content: { kind: 'rowNumber' }, cellType: 'text' }, { id: 'name', title: '品名', width: 80, align: 'left', content: { kind: 'field', field: 'name' }, cellType: 'text' }, { id: 'qty', title: '数量', width: 28, align: 'right', content: { kind: 'field', field: 'qty' }, cellType: 'text' }, column('price', '单价', 34, 'price', 'right'), column('amount', '金额', 34, 'amount', 'right'), ], footerRows: [ { id: 'total', placement: 'lastPage', height: 7, style: { ...registry.get('table')!.defaults.body.style, bold: true }, cells: [ { columnId: 'no', colSpan: 4, content: { kind: 'static', text: '合计' }, align: 'right' }, { columnId: 'amount', colSpan: 1, content: { kind: 'aggregate', fn: 'sum', field: 'amount', scope: 'all', format: money } }, ], }, ], }, }), ) ``` 表格跨页的规则见[运行时](./runtime#表格)。 ## 校验 模板保存前、导入时都可以先校验。`validateDocument` 返回全部问题,每个问题有问题码、严重程度与路径;有错误级问题的模板不能分页。问题码见[问题码与诊断](../reference/issues)。 ```ts import type { PrintDocument, ValidationIssue } from '@xihan-print/core' import { validateDocument } from '@xihan-print/core' import { createExpressionEngine } from '@xihan-ui/expression' export function checkTemplate(schema: PrintDocument): ValidationIssue[] { const report = validateDocument(schema, { expressionEngine: createExpressionEngine() }) // report.valid 为 false 时 issues 里至少有一个 severity 为 error 的问题 return report.issues } ``` --- 来源:https://print.docs.xihanfun.com/reference/client # 打印客户端 SDK `@xihan-print/runtime/client` 连接本地打印客户端:静默打印、写原始指令、导出 PDF、查打印机。这个子入口单独打包,不用客户端的页面不会加载它。 ## 创建连接 ### createPrintClient() {#createprintclient} ```ts declare function createPrintClient(options?: PrintClientOptions): PrintClient ``` 创建本地打印客户端的连接;不会立即连接,第一次调用时才连。 ### PrintClientOptions {#printclientoptions} createPrintClient 的选项。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `ports?` | `readonly number[]` | 按顺序探测的端口,缺省 17620–17623。 | | `timeoutMs?` | `number` | 请求等响应的期限,缺省 10 秒。 | | `connectTimeoutMs?` | `number` | 每个端口建连的期限,缺省 3 秒。 | | `consentTimeoutMs?` | `number` | 客户端弹出授权窗口后等用户决定的期限,缺省 75 秒。 | | `reconnect?` | `boolean` | 断线后是否自动重连,缺省 true。 | | `WebSocket?` | `WebSocketConstructor` | WebSocket 构造函数,缺省为全局 WebSocket。 | | `permissions?` | `PermissionsLike \| null` | 查本机网络权限的入口,缺省为 navigator.permissions;为 null 时不查。 | | `secureContext?` | `boolean` | 页面是否是安全上下文,缺省为全局 isSecureContext。 | | `origin?` | `string` | 页面的来源,缺省为全局 location.origin;页面本身在本机回环地址上时,连不上不按本机网络权限判定。 | | `fetch?` | `typeof fetch` | 打包文档时取资源用的 fetch,缺省为全局 fetch。 | ### PrintClient {#printclient} 本地打印客户端的连接。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `state` | `PrintClientState` | 连接状态。 | | `welcome` | `ClientWelcome \| undefined` | 最近一次握手的结果;还没连上过时为 undefined。 | | 方法 | 说明 | | --- | --- | | `connect(): Promise` | 连接并握手;已连着时直接返回握手结果。连不上抛 PrintClientError,码为 CLIENT_UNAVAILABLE、LOCAL_NETWORK_DENIED、PROTOCOL_UNSUPPORTED、CONSENT_DENIED、CONSENT_TIMEOUT 等。 | | `close(): void` | 断开连接,不再重连;未结束的任务以 CONNECTION_LOST 拒绝,客户端里的任务照常执行完。 | | `printers(): Promise` | 列出打印机,不含能力明细。 | | `printer(name: string): Promise` | 查一台打印机,含能力明细。 | | `printDocument(schema: PrintDocument, data: unknown, options?: PrintClientDocumentOptions): PrintClientJob` | 按数据排出模板并交给客户端打印。 | | `printSheets(result: PaginationResult, options?: PrintClientDocumentOptions): PrintClientJob` | 把排好的输出页交给客户端打印。 | | `printRaw(printer: string, data: Uint8Array \| ArrayBuffer \| string, options?: PrintRawOptions): PrintClientJob` | 把原始指令原样写给打印机;字符串按 UTF-8 编码,要别的编码时先转成字节。 | | `exportPdf(schema: PrintDocument, data: unknown, options?: PrintClientDocumentOptions): Promise` | 按数据排出模板并导出为 PDF。 | | `exportSheetsPdf(result: PaginationResult, options?: PrintClientDocumentOptions): Promise` | 把排好的输出页导出为 PDF。 | | `job(jobId: string): Promise` | 查任务状态。 | | `on(type: K, listener: (detail: PrintClientEventMap[K]) => void): () => void` | 订阅事件,返回退订函数。 | ### PrintClientJob {#printclientjob} 一个交给客户端的任务。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `id` | `string` | 任务标识,由 SDK 生成。 | | `status` | `JobStatus \| undefined` | 最近一次的状态;还没交给客户端时为 undefined。 | | `done` | `Promise` | completed 时兑现;failed 时以客户端给出的错误码拒绝,cancelled 时以 JOB_CANCELLED 拒绝,发送前出错时以对应错误拒绝。 | | 方法 | 说明 | | --- | --- | | `onStatus(listener: (status: JobStatus) => void): () => void` | 订阅状态变化,返回退订函数。 | | `cancel(): Promise` | 取消任务:排队中的立即取消并返回 true;执行中的尽力取消并返回 false,结果以状态为准。 | ### PrintClientEventMap {#printclienteventmap} 客户端派发的事件。 用 `client.on(type, listener)` 订阅。连接状态 `PrintClientState` 取 `idle`、`connecting`、`open`、`reconnecting`、`closed`。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `state` | `PrintClientState` | 连接状态变化。 | | `consentpending` | `{ readonly origin: string }` | 来源还没授权,客户端已弹出授权窗口。 | ## 退回浏览器打印 ### printWithFallback() {#printwithfallback} ```ts declare function printWithFallback(client: PrintClient, schema: PrintDocument, data: unknown, options?: PrintWithFallbackOptions): Promise ``` 先交给本地打印客户端打印,等到交给打印池;只在连不上客户端(CLIENT_UNAVAILABLE、PROTOCOL_UNSUPPORTED)时退回浏览器的打印对话框。打印机报错、拒绝授权等其它错误照常抛出,免得用户以为已经静默打出来了。 ### PrintWithFallbackOptions {#printwithfallbackoptions} printWithFallback 的选项:客户端打印选项、分页注入项,退回浏览器打印时另用 title。 继承 `PrintClientDocumentOptions`、`Pick`。 ### PrintWithFallbackOutcome {#printwithfallbackoutcome} printWithFallback 的结果:走了哪条路,与送去打印的页数。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `via` | `'client' \| 'browser'` | | | `sheets` | `number` | | ### shouldFallBack() {#shouldfallback} ```ts declare function shouldFallBack(error: unknown): boolean ``` 这个错误是否表示连不上客户端、应该退回浏览器打印;打印机报错、拒绝授权等一律不退回。 ## 错误码 出错时抛出或以 `PrintClientError` 拒绝,`code` 为下表之一,`details` 带补充信息。「SDK」一栏的码在浏览器里产生,其余由客户端给出。 | 错误码 | 出自 | 说明 | 退回浏览器打印 | | --- | --- | --- | --- | | `CLIENT_UNAVAILABLE` | SDK | 所有端口都连不上(客户端没装或没运行),details 带端口 ports 与浏览器报的「本机应用」权限状态 permission(查不到时不带) | 是 | | `LOCAL_NETWORK_DENIED` | SDK | 连接在浏览器里就被拦下:页面不在本机回环地址上、所有端口都在建连期限内失败,且「本机应用」权限被拒或页面不是安全上下文 | 否 | | `CONNECTION_LOST` | SDK | 等结果时断线,或断线后没能在期限内重连上 | 否 | | `TOO_MANY_CONNECTIONS` | SDK | 同一来源的连接数超过客户端上限 | 否 | | `TIMEOUT` | SDK | 请求没有在期限内得到响应 | 否 | | `JOB_CANCELLED` | SDK | 任务被取消 | 否 | | `BAD_REQUEST` | 客户端 | 消息格式或字段不合法 | 否 | | `CONSENT_PENDING` | 客户端 | 授权窗口还开着时又发了业务请求 | 否 | | `CONSENT_DENIED` | 客户端 | 用户拒绝,或来源在拒绝名单 | 否 | | `CONSENT_TIMEOUT` | 客户端 | 授权窗口在期限内没人处理 | 否 | | `SESSION_EXPIRED` | 客户端 | 重连令牌过期或不属于这个来源,已当作新会话 | 否 | | `PROTOCOL_UNSUPPORTED` | 客户端 | 协议主版本不匹配 | 是 | | `NO_PRINTERS` | 客户端 | 本机没有任何打印机 | 否 | | `PRINTER_NOT_FOUND` | 客户端 | 指定的打印机不存在 | 否 | | `PRINTER_OFFLINE` | 客户端 | 打印机脱机、暂停或出错 | 否 | | `PAPER_UNSUPPORTED` | 客户端 | 打印机没有任务需要的纸张,且驱动不接受自定义尺寸 | 否 | | `DOCUMENT_TOO_LARGE` | 客户端 | 超过消息上限或资源总量上限 | 否 | | `RESOURCE_BLOCKED` | 客户端 | 文档里引用了随任务带来的资源之外的地址 | 否 | | `RENDER_FAILED` | 客户端 | 渲染引擎载入或等待就绪失败 | 否 | | `RENDER_TIMEOUT` | 客户端 | 渲染超时 | 否 | | `SPOOL_FAILED` | 客户端 | 交给打印池失败 | 否 | | `PDF_FAILED` | 客户端 | 导出 PDF 失败 | 否 | | `JOB_DUPLICATE` | 客户端 | 任务标识重复 | 否 | | `JOB_NOT_FOUND` | 客户端 | 查无此任务 | 否 | | `QUEUE_FULL` | 客户端 | 同一会话排队的任务超过上限 | 否 | | `CAPABILITY_UNSUPPORTED` | 客户端 | 请求了客户端没有的能力 | 否 | | `INTERNAL` | 客户端 | 未预期的异常 | 否 | ## 提醒码 HTML 通道的任务照常完成、但有宿主应当知道的情况时,`JobStatus` 的 `warnings` 里带若干条提醒(`ClientWarningPayload`),`code` 为下表之一,`details` 带补充信息。 | 提醒码 | 说明 | | --- | --- | | `PAPER_UNCHECKED` | 驱动没有报纸张能力,或读不出驱动会留下哪种纸,没有核对纸张就照常下发了 | | `PAPER_FITTED` | 按 fitToNearestPaper 把某组改用了最接近的纸张:比纸大的内容按 scale 等比缩小,比纸小的原大居中 | ## 打印选项与结果 ### ClientPrintOptions {#clientprintoptions} 交给客户端的打印选项。纸张不在选项里:每组输出页按模板里的纸张尺寸下发;原始指令的纸张由指令自己决定。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `printer?` | `string` | 打印机名称,缺省为默认打印机。 | | `copies?` | `number` | 份数,1–999。 | | `collate?` | `boolean` | 多份时逐份打印,缺省 true。 | | `duplex?` | `'simplex' \| 'long-edge' \| 'short-edge'` | | | `color?` | `'color' \| 'grayscale'` | | | `pageRanges?` | `string` | 页码范围,如 1-3,5。 | | `dialog?` | `boolean` | 弹系统打印对话框,缺省静默打印。 | | `timeoutMs?` | `number` | 交给打印池的期限,毫秒。 | | `fitToNearestPaper?` | `boolean` | 打印机没有某组的纸张、驱动也不接受这个尺寸时,改用最接近的纸张,比纸大的内容等比缩小到放得下,比纸小的原大居中;缺省 false,这时整个任务以 PAPER_UNSUPPORTED 失败。 | ### PrintClientDocumentOptions {#printclientdocumentoptions} 打印与导出的选项:客户端打印选项、分页注入项与打包文档的选项。 继承 `ClientPrintOptions`、`PrintRenderOptions`、`Pick`。 ### PrintRawOptions {#printrawoptions} 写原始指令的选项。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `docName?` | `string` | 打印池里显示的文档名,缺省为任务标识。 | ### JobStatus {#jobstatus} 任务的当前状态。 任务状态 `JobState` 取 `queued`、`started`、`completed`、`failed`、`cancelled`。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `jobId` | `string` | | | `state` | `JobState` | | | `position?` | `number` | 排队时同一台打印机前面还有几个任务。 | | `sheets?` | `number` | HTML 通道交给打印池的页数。 | | `bytes?` | `number` | 原始通道写出的字节数,或导出的 PDF 大小。 | | `error?` | `ClientErrorPayload` | | | `warnings?` | `readonly ClientWarningPayload[]` | HTML 通道完成时的提醒,如没能核对纸张、按 fitToNearestPaper 换了纸;没有时缺省。 | ## 打印机与客户端信息 ### PrinterInfo {#printerinfo} 一台打印机。 打印机状态 `PrinterState` 取 `ready`、`paused`、`offline`、`error`、`paperOut`、`paperJam`、`busy`、`unknown`。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `name` | `string` | | | `isDefault` | `boolean` | | | `status` | `PrinterState` | | | `statusFlags` | `number` | 系统给出的原始状态位。 | | `location?` | `string` | | | `comment?` | `string` | | | `driver?` | `string` | | | `port?` | `string` | | | `isNetwork` | `boolean` | | | `capabilities?` | `PrinterCapabilities` | | ### PrinterCapabilities {#printercapabilities} 打印机能力,只在查单台打印机时给出。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `color` | `boolean` | | | `duplex` | `boolean` | | | `maxCopies` | `number` | | | `papers` | `readonly PaperInfo[]` | | ### PaperInfo {#paperinfo} 打印机支持的一种纸张。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `id` | `number` | | | `name` | `string` | | | `widthMm` | `number` | | | `heightMm` | `number` | | ### ClientWelcome {#clientwelcome} 握手成功后客户端的回复。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `protocol` | `number` | | | `minor` | `number` | | | `client` | `ClientInfo` | | | `capabilities` | `ClientCapabilities` | | | `resumed` | `readonly string[]` | 重连认回会话时仍未结束的任务。 | ### ClientInfo {#clientinfo} 客户端信息。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `version` | `string` | | | `os` | `string` | | | `arch` | `string` | | | `renderer?` | `string` | 渲染引擎版本;没有渲染引擎时缺省。 | ### ClientCapabilities {#clientcapabilities} 客户端能力。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `html` | `boolean` | | | `raw` | `boolean` | | | `pdf` | `boolean` | | | `duplex` | `boolean` | | | `printerStatus` | `boolean` | | ## 交给客户端的文档 ### buildClientDocument() {#buildclientdocument} ```ts declare function buildClientDocument(result: PaginationResult, options?: ClientDocumentOptions): Promise ``` 把分页结果打包成交给客户端的文档:输出页渲染进一份独立的 HTML(只在预览中显示的元素不画),图片、背景图与模板用到的字体取来随任务带去,地址改写成客户端里的资源地址。 ### ClientDocumentOptions {#clientdocumentoptions} 打包文档的选项。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `document?` | `Document` | 发起打印的文档:从它的样式表里找模板用到的字体,相对地址也按它解析;缺省为全局 document。 | | `fetch?` | `typeof fetch` | 取资源用的 fetch,缺省为全局 fetch。 | | `extensions?` | `readonly PrintRuntimeExtension[]` | 扩展:扩展元素按它们给的渲染画出,与分页用同一份。 | | `trace?` | `PrintTrace` | 各环节的回调:报生成 DOM、取资源与合成文档三个环节的起止,见 PrintTrace。 | ### ClientDocument {#clientdocument} 交给客户端的文档:自包含的输出页、资源与按出现顺序的纸张分组。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `html` | `string` | | | `resources` | `readonly ClientResource[]` | | | `sheets` | `readonly ClientSheetGroup[]` | | ### ClientResource {#clientresource} 随任务带去的一份资源,data 为 base64。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `id` | `string` | | | `type` | `string` | | | `data` | `string` | | ### ClientSheetGroup {#clientsheetgroup} 连续的若干张同尺寸输出页。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `widthMm` | `number` | | | `heightMm` | `number` | | | `count` | `number` | | ## 常量 | 名称 | 值 | 说明 | | --- | --- | --- | | `DEFAULT_PORTS` | `[17620, 17621, 17622, 17623]` | 客户端监听的端口,按顺序探测。 | | `SUB_PROTOCOL` | `'xihan-print.v1'` | 带主版本的子协议。 | | `PROTOCOL_VERSION` | `1` | 协议主版本。 | | `PROTOCOL_MINOR` | `0` | 协议次版本:只加不减。 | ## 全部导出 ### client/client | 名称 | 种类 | 说明 | | --- | --- | --- | | `createPrintClient` | 函数 | 创建本地打印客户端的连接;不会立即连接,第一次调用时才连。 | | `PermissionsLike` | 接口 | 查浏览器权限的入口,形同 navigator.permissions。 | | `PrintClient` | 接口 | 本地打印客户端的连接。 | | `PrintClientDocumentOptions` | 接口 | 打印与导出的选项:客户端打印选项、分页注入项与打包文档的选项。 | | `PrintClientEventMap` | 接口 | 客户端派发的事件。 | | `PrintClientJob` | 接口 | 一个交给客户端的任务。 | | `PrintClientOptions` | 接口 | createPrintClient 的选项。 | | `PrintClientState` | 类型 | 连接状态。 | | `PrintRawOptions` | 接口 | 写原始指令的选项。 | | `WebSocketConstructor` | 类型 | 能当 WebSocket 用的构造函数,单测可注入假实现。 | ### client/document | 名称 | 种类 | 说明 | | --- | --- | --- | | `buildClientDocument` | 函数 | 把分页结果打包成交给客户端的文档:输出页渲染进一份独立的 HTML(只在预览中显示的元素不画),图片、背景图与模板用到的字体取来随任务带去,地址改写成客户端里的资源地址。 | | `ClientDocumentOptions` | 接口 | 打包文档的选项。 | ### client/errors | 名称 | 种类 | 说明 | | --- | --- | --- | | `FALLBACK_CODES` | 常量 | 连不上客户端、可以退回浏览器打印的错误码:客户端没装或没运行,或协议主版本不对。 | | `PrintClientError` | 类 | 打印客户端相关的错误,`code` 与 `details` 供调用方判断与生成文案。 | | `shouldFallBack` | 函数 | 这个错误是否表示连不上客户端、应该退回浏览器打印;打印机报错、拒绝授权等一律不退回。 | ### client/fallback | 名称 | 种类 | 说明 | | --- | --- | --- | | `printWithFallback` | 函数 | 先交给本地打印客户端打印,等到交给打印池;只在连不上客户端(CLIENT_UNAVAILABLE、PROTOCOL_UNSUPPORTED)时退回浏览器的打印对话框。 | | `PrintWithFallbackOptions` | 接口 | printWithFallback 的选项:客户端打印选项、分页注入项,退回浏览器打印时另用 title。 | | `PrintWithFallbackOutcome` | 接口 | printWithFallback 的结果:走了哪条路,与送去打印的页数。 | ### client/protocol | 名称 | 种类 | 说明 | | --- | --- | --- | | `ClientCapabilities` | 接口 | 客户端能力。 | | `ClientDocument` | 接口 | 交给客户端的文档:自包含的输出页、资源与按出现顺序的纸张分组。 | | `ClientErrorCode` | 类型 | 客户端给出的错误码。 | | `ClientErrorPayload` | 接口 | 响应与任务事件里的错误。 | | `ClientInfo` | 接口 | 客户端信息。 | | `ClientPrintOptions` | 接口 | 交给客户端的打印选项。 | | `ClientResource` | 接口 | 随任务带去的一份资源,data 为 base64。 | | `ClientSheetGroup` | 接口 | 连续的若干张同尺寸输出页。 | | `ClientWarningCode` | 类型 | 客户端在完成的任务里给出的提醒码。 | | `ClientWarningPayload` | 接口 | 任务照常完成、但宿主应当知道的情况,code 取 ClientWarningCode。 | | `ClientWelcome` | 接口 | 握手成功后客户端的回复。 | | `DEFAULT_PORTS` | 常量 | 客户端监听的端口,按顺序探测。 | | `JobState` | 类型 | 任务状态。 | | `JobStatus` | 接口 | 任务的当前状态。 | | `PaperInfo` | 接口 | 打印机支持的一种纸张。 | | `PrintClientErrorCode` | 类型 | 打印客户端相关的全部错误码。 | | `PrinterCapabilities` | 接口 | 打印机能力,只在查单台打印机时给出。 | | `PrinterInfo` | 接口 | 一台打印机。 | | `PrinterState` | 类型 | 打印机状态。 | | `PROTOCOL_MINOR` | 常量 | 协议次版本:只加不减。 | | `PROTOCOL_VERSION` | 常量 | 协议主版本。 | | `SdkErrorCode` | 类型 | SDK 自己给出的错误码: | | `SUB_PROTOCOL` | 常量 | 带主版本的子协议。 | --- 来源:https://print.docs.xihanfun.com/reference/core # 模型层 core `@xihan-print/core` 是纯 TypeScript 的模型层:模板类型、校验、编辑操作、元素登记表与分页算法,不依赖 DOM,浏览器、Node.js 与其它 JavaScript 运行环境里都能用。模板类型的字段见[模板类型](./schema),元素属性见[元素](./elements)。下表按源码模块列出全部导出。 ## 全部导出 ### extensions | 名称 | 种类 | 说明 | | --- | --- | --- | | `createPrintModel` | 函数 | 合出打印模型:内置扩展 PRINT_CORE_EXTENSION 在前,extensions 按顺序在后。 | | `PRINT_CORE_EXTENSION` | 常量 | 内置扩展:全部内置元素。 | | `PRINT_CORE_EXTENSION_ID` | 常量 | 内置扩展的 id。 | | `PRINT_EXTENSION_SPACES` | 常量 | 打印各层扩展共用的名字空间:元素类型。 | | `PrintCoreExtension` | 接口 | 打印 core 层的扩展:元素原型、编辑方式与校验规则。 | | `PrintModel` | 接口 | 一组扩展合出的打印模型:元素登记表与编辑方式。 | | `PrintRuleContext` | 接口 | 规则拿到的上下文:合并后的元素登记表。 | ### factory | 名称 | 种类 | 说明 | | --- | --- | --- | | `collectDocumentIds` | 函数 | 收集文档中已使用的页面与元素标识。 | | `createDocument` | 函数 | 生成当前 schema 版本的文档。 | | `CreateDocumentOptions` | 接口 | 文档创建选项。 | | `createEdges` | 函数 | 生成四边相等或逐边指定的边距。 | | `createPage` | 函数 | 生成页面模板:页眉单页时优先首页变体,页脚单页时优先末页变体,页码接续上一页面模板。 | | `createPageBandSet` | 函数 | 生成只含空 default 带区的页眉或页脚。 | | `CreatePageOptions` | 接口 | 页面模板创建选项。 | | `createPaper` | 函数 | 按预设名生成纸张。 | ### fields | 名称 | 种类 | 说明 | | --- | --- | --- | | `collectDocumentFields` | 函数 | 列出一份模板引用了哪些数据字段,合成去重的字段树,形状同设计器的 fields。 | | `CollectDocumentFieldsOptions` | 接口 | collectDocumentFields 的选项。 | ### geometry | 名称 | 种类 | 说明 | | --- | --- | --- | | `getBandRect` | 函数 | 取页眉或页脚带区在纸张上的矩形。 | | `getBodyRect` | 函数 | 取给定页眉、页脚带区之间的正文区。 | | `getContentRect` | 函数 | 取页边距以内的内容区。 | | `getElementRect` | 函数 | 取元素框。 | | `getPageBand` | 函数 | 取页眉或页脚中指定键的带区。 | | `getPaperSize` | 函数 | 取页面按方向换算后的纸张尺寸。 | | `resolvePageBand` | 函数 | 按输出页位置选出页眉或页脚应使用的带区。 | | `resolvePageBandKey` | 函数 | 按输出页位置选出页眉或页脚应使用的带区键。 | | `SheetPosition` | 接口 | 输出页在所属页面模板中的位置。 | ### layout/boxes | 名称 | 种类 | 说明 | | --- | --- | --- | | `MIN_SHRINK_FONT_SIZE` | 常量 | 缩小字号以容纳文字时的最小字号,单位 pt。 | | `SHRINK_FONT_STEP` | 常量 | 缩小字号的步长,单位 pt。 | ### layout/design | 名称 | 种类 | 说明 | | --- | --- | --- | | `DESIGN_FILLER_ROWS` | 常量 | 设置了补空行的表格在设计态示意的空行数。 | | `DESIGN_SAMPLE_ROWS` | 常量 | 表格缺省示意的数据行数。 | | `DesignLayout` | 接口 | 设计态排版结果:各区的布局盒按元素层级排列,坐标为设计坐标。 | | `DesignLayoutOptions` | 接口 | 设计态排版的选项:度量器、表达式引擎、格式化器、区域与时区同分页。 | | `layoutDesign` | 函数 | 按设计坐标排出页面模板的每个元素,页眉页脚取 options.bands 指定的变体。 | ### layout/paginate | 名称 | 种类 | 说明 | | --- | --- | --- | | `DEFAULT_MAX_SHEETS` | 常量 | 输出页总数上限的缺省值。 | | `paginate` | 函数 | 按数据排出模板文档的全部输出页,返回每页每个元素的布局盒。 | ### layout/rich-text | 名称 | 种类 | 说明 | | --- | --- | --- | | `RICH_TEXT_BLOCK_GAP` | 常量 | 顶层块与块之间的段距,基准字号的倍数;列表项之间与列表项里的块之间不留段距。 | | `RICH_TEXT_DIVIDER_WIDTH` | 常量 | 分隔线的线宽,单位 mm。 | | `RICH_TEXT_HEADING_SCALES` | 常量 | 一到三级标题的字号,基准字号的倍数。 | | `RICH_TEXT_LIST_INDENT` | 常量 | 每层列表的缩进,基准字号的倍数。 | | `RICH_TEXT_MARKER_GAP` | 常量 | 项目符号或序号与文字之间的间距,基准字号的倍数。 | | `RICH_TEXT_SCRIPT_SCALE` | 常量 | 上标与下标的字号,所在文字字号的倍数。 | ### layout/text-measure | 名称 | 种类 | 说明 | | --- | --- | --- | | `DEFAULT_FONT_FAMILY` | 常量 | 文字样式没写字体时,量字与渲染共用的字体族。 | ### layout/types | 名称 | 种类 | 说明 | | --- | --- | --- | | `BarCodeLayoutBox` | 接口 | 一维条码布局盒。 | | `EllipseLayoutBox` | 接口 | 椭圆布局盒。 | | `ExtensionLayoutBox` | 接口 | 扩展元素的布局盒,交给运行时层同一类型的渲染;类型没登记时 data 为 null。 | | `ImageLayoutBox` | 接口 | 图片布局盒。 | | `LayoutBand` | 接口 | 输出页选中的页眉或页脚带区。 | | `LayoutBox` | 类型 | | | `LayoutBoxBase` | 接口 | 布局盒共有的字段。 | | `LayoutFragment` | 接口 | 跨页续排元素的分段位置。 | | `LayoutSheet` | 接口 | 一张输出页。 | | `LayoutWatermark` | 类型 | 已替换占位的水印。 | | `LineLayoutBox` | 接口 | 直线布局盒。 | | `MatrixCodeLayoutBox` | 接口 | 二维码布局盒。 | | `PaginateOptions` | 接口 | 分页选项。 | | `PAGINATION_DIAGNOSTIC_SEVERITIES` | 常量 | 全部分页诊断码及其固定的严重程度:error 为按模板该画的内容没有画出或输出页被截断,warning 为内容画出了但与设计有出入。 | | `PaginationDiagnostic` | 类型 | 分页诊断:与校验问题同形的 @xihan-ui/designer-kit/schema 的 SchemaIssue,不含文案,严重程度与 target 总是给出。 | | `PaginationDiagnosticCode` | 类型 | 分页诊断码。 | | `PaginationDiagnosticTarget` | 接口 | 分页诊断所在的页面(kind 为 page)或元素(kind 为 element)。 | | `PaginationResult` | 接口 | 分页结果。 | | `PaginationStep` | 类型 | 分页的一步,按发生的先后交给 PaginateOptions.trace。 | | `PrintLayoutError` | 类 | 分页无法进行时抛出的错误。 | | `PrintLayoutErrorCode` | 类型 | 分页无法进行的原因:文档有 error 级校验问题、选项不合法、测量器或格式化器返回了不合法的结果。 | | `RectLayoutBox` | 接口 | 矩形布局盒。 | | `RichTextLayoutBox` | 接口 | 富文本布局盒,跨页时每页一段。 | | `RichTextLine` | 接口 | 富文本排出的一行,长度单位 mm。 | | `RichTextMarker` | 接口 | 列表项的项目符号或序号。 | | `RichTextSpan` | 接口 | 富文本一行里同一样式的一截文字:字号与字间距单位 pt。 | | `TableCellLayout` | 接口 | 表格单元格布局,坐标相对纸张左上角。 | | `TableLayoutBox` | 接口 | 表格布局盒,跨页时每页一段。 | | `TableRowLayout` | 接口 | 表格行布局:表头、数据行、补空行或表尾行。 | | `TextLayoutBox` | 接口 | 文本、长文本与页码的布局盒。 | | `TextMeasurement` | 接口 | 按文本样式排出的文字,长度单位 mm。 | ### operations/apply | 名称 | 种类 | 说明 | | --- | --- | --- | | `applyOperation` | 函数 | 以纯函数方式执行编辑操作:不修改传入的文档,返回新文档与逆操作数组。 | | `ApplyOperationOptions` | 接口 | 操作选项。 | | `applyOperations` | 函数 | 按顺序执行一组编辑操作,整体成功或整体失败(@xihan-ui/designer-kit 的 applyInSequence)。 | ### operations/errors | 名称 | 种类 | 说明 | | --- | --- | --- | | `PrintOperationError` | 类 | 操作无法执行时抛出的错误,`code` 与 `params` 供调用方判断与生成文案。 | | `PrintOperationErrorCode` | 类型 | 操作无法执行的原因。 | ### operations/table-plans | 名称 | 种类 | 说明 | | --- | --- | --- | | `MIN_TABLE_COLUMN_WIDTH` | 常量 | 调整列宽时每列的最小宽度,单位 mm。 | | `planInsertFooterRow` | 函数 | 在第 index 个表尾行之前插入一行(index 等于行数时追加)。 | | `planInsertTableColumn` | 函数 | 在第 index 列之前插入一列(index 等于列数时追加在末尾);跨过插入位置的表尾单元格多跨一列,其余表尾行在新列处留空。 | | `planMergeFooterCells` | 函数 | 合并表尾行里 columnIds 覆盖的列范围:与范围相交的单元格(含留空的列)合成一个,合并后的范围扩到它们的并集;保留最左边那个单元格的内容,最左边是留空的列时新建空白单元格。 | | `planMoveTableColumn` | 函数 | 把一列移到第 index 位(按移动后的顺序数)。 | | `planRemoveFooterRow` | 函数 | 删除一个表尾行。 | | `planRemoveTableColumn` | 函数 | 删除一列:只占这一列的表尾单元格删除,跨过它的缩短一列,从它起跨的改由下一列起跨。 | | `planResizeTableColumn` | 函数 | 把一列调到 width 宽,不小于 MIN_TABLE_COLUMN_WIDTH。 | | `planSplitFooterCell` | 函数 | 把表尾行里从 columnId 起跨的单元格拆成只占一列,空出来的列留空。 | | `TableColumnFit` | 类型 | 列宽怎样跟着变:keep 保持表格宽度、各列按比例缩放;resize 改变表格宽度、其余列不动。 | | `TablePlanRefusal` | 类型 | 计划不能执行的原因:只剩一列、移动会拆开跨列的表尾单元格、所选范围只有一个单元格、单元格本来就不跨列。 | | `TablePlanResult` | 类型 | 计划结果:成功时给出 updateElement 的属性修改。 | ### operations/types | 名称 | 种类 | 说明 | | --- | --- | --- | | `AddBandOperation` | 接口 | 为页眉或页脚添加变体带区。 | | `AddElementOperation` | 接口 | 向容器添加元素,`index` 缺省时放在最上层。 | | `AddPageOperation` | 接口 | 添加页面模板,`index` 缺省时放在末尾。 | | `MoveElementsOperation` | 接口 | 平移一组元素,单位 mm。 | | `MovePageOperation` | 接口 | 移动页面模板到目标下标。 | | `OperationResult` | 类型 | 操作结果,即 @xihan-ui/designer-kit 的 OperationResult:新文档,以及按顺序作用于新文档即可还原出传入文档的逆操作。 | | `PrintOperation` | 类型 | 编辑操作。 | | `PrintOperationType` | 类型 | 编辑操作的种类。 | | `PropertyChange` | 接口 | 一处属性修改。 | | `RemoveBandOperation` | 接口 | 删除页眉或页脚的变体带区及其中的元素。 | | `RemoveElementOperation` | 接口 | 删除元素。 | | `RemovePageOperation` | 接口 | 删除页面模板。 | | `ReorderElementOperation` | 接口 | 调整元素在容器内的层级。 | | `ReorderTarget` | 类型 | 层级调整目标:最上层、最下层、上移一层、下移一层,或容器内的目标下标。 | | `ReplaceDocumentOperation` | 接口 | 整份换成另一份文档,如在 JSON 编辑框里改完应用;新文档由调用方先校验(validateDocument),操作只记下换之前的那份作逆操作。 | | `ResizeElementOperation` | 接口 | 把元素缩放到新的元素框,元素原型可附带专属缩放行为。 | | `UpdateDocumentOperation` | 接口 | 修改文档属性;不能修改 `schemaVersion` 与 `pages`。 | | `UpdateElementOperation` | 接口 | 修改元素属性;不能修改 `id` 与 `type`。 | | `UpdatePageOperation` | 接口 | 修改页面设置:名称、纸张、方向、边距、页码规则、水印、参考线、宿主附加数据,以及页眉页脚的 `singleSheet` 和已有带区的 `height`。 | ### registry/builtins | 名称 | 种类 | 说明 | | --- | --- | --- | | `createBuiltinElementPrototypes` | 函数 | 生成全部内置元素原型,每次调用返回互不共享的新对象。 | | `scaleTableColumns` | 函数 | 按新的总宽度等比缩放列宽,末列吸收舍入误差;原列宽合计不为正时均分。 | ### registry/part-properties | 名称 | 种类 | 说明 | | --- | --- | --- | | `pageProperties` | 函数 | 页面设置的属性描述,路径相对页面模板;页眉页脚各变体的高度只在该变体存在时显示。 | | `tableBodyProperties` | 函数 | 表体的属性描述,路径相对表体。 | | `tableColumnProperties` | 函数 | 表格列的属性描述,路径相对列;条码码制只在单元格类型为条码时显示。 | | `tableFooterCellProperties` | 函数 | 表尾单元格的属性描述,路径相对单元格。 | | `tableFooterRowProperties` | 函数 | 表尾行的属性描述,路径相对表尾行;单元格另按表尾格编辑。 | | `tableHeaderProperties` | 函数 | 表头的属性描述,路径相对表头;表头隐藏时只显示「是否显示」。 | ### registry/properties | 名称 | 种类 | 说明 | | --- | --- | --- | | `behaviorProperties` | 函数 | 名称、锁定与只预览不打印。 | | `boxProperties` | 函数 | `box` 下的背景、边框与内边距。 | | `choices` | 函数 | 一组取值写成 select 编辑方式的选项。 | | `layoutProperties` | 函数 | 坐标、宽高与可选的旋转角度。 | | `property` | 函数 | 生成一条属性描述,缺省为元素上必须有的属性。 | | `PropertyOptions` | 接口 | 属性描述的可选项。 | | `textStyleProperties` | 函数 | `style` 下的文本样式。 | ### registry/registry | 名称 | 种类 | 说明 | | --- | --- | --- | | `CreateElementInit` | 接口 | 创建元素的参数。 | | `createElementRegistry` | 函数 | 创建元素类型登记表,默认登记全部内置元素;类型重复时抛出错误。 | | `ElementCategoryGroup` | 接口 | 物料面板中的一个分组。 | | `ElementRegistry` | 接口 | 元素类型登记表。 | ### registry/types | 名称 | 种类 | 说明 | | --- | --- | --- | | `AnyElementPrototype` | 类型 | 任一元素的原型:内置元素的,或扩展元素的。 | | `ELEMENT_CATEGORIES` | 常量 | 内置的物料分组,按面板中的显示顺序;扩展自定的分组排在它们之后。 | | `ElementCategory` | 类型 | 内置的物料分组:文字、图片、数据、图形、条码。 | | `ElementDefaults` | 类型 | 元素默认值:不含标识与坐标。 | | `ElementFieldReference` | 接口 | 元素引用的一个数据字段。 | | `ElementFieldsContext` | 接口 | 列字段引用时拿到的帮手:把常见的内容来源换成引用,扩展元素不必自己解析模板串与表达式。 | | `ElementFlowContext` | 接口 | 跨页扩展元素取值时拿到的上下文:在排版上下文之上还能量文字。 | | `ElementLayoutContext` | 接口 | 扩展元素排版时拿到的上下文。 | | `ElementPrototype` | 接口 | 元素原型:一种元素的默认值、尺寸约束、可编辑属性与专属行为。 | | `ElementValidationContext` | 接口 | 元素专属规则的校验上下文。 | | `ExtensionElementPrototype` | 类型 | 扩展元素的原型:不跨页的给 layout,跨页的给 prepare。 | | `ExtensionFlowBlock` | 接口 | 跨页扩展元素的一块:块不拆开,height 是它占的高度(mm),data 交给渲染,要是 JSON。 | | `ExtensionFlowContent` | 接口 | 跨页扩展元素排出的内容:blocks 依次排;repeat 给了时排在每一段的开头(如表头),算进每段的高度。 | | `FixedExtensionElementPrototype` | 接口 | 不跨页的扩展元素的原型:必须给出 layout。 | | `FlowExtensionElementPrototype` | 接口 | 跨页的扩展元素的原型:必须给出 prepare;只能放在正文区。 | | `NumberControl` | 类型 | 数值属性:designer-kit 的 number 编辑方式,单位取打印的单位。 | | `PAGE_PROPERTY_GROUPS` | 常量 | 页面设置的属性分组取值,按面板中的显示顺序。 | | `PROPERTY_GROUPS` | 常量 | 元素与表格部位的属性分组取值,按面板中的显示顺序。 | | `PropertyControl` | 类型 | 属性编辑方式:@xihan-ui/designer-kit/properties 的内建编辑方式(数值的单位取打印的单位),加上打印专属的复合类型;复合类型由设计器提供对应的专用编辑器。 | | `PropertyControlKind` | 类型 | 属性编辑方式的种类。 | | `PropertyDescriptor` | 接口 | 可编辑属性描述:@xihan-ui/designer-kit/properties 的 PropertyDescriptor,path 相对元素;分组总是给出,显示条件写在 visibleWhen;required(显示时必须填写)不用,空值由打印自己的规则报告。 | | `PropertyGroup` | 类型 | 属性面板分组:元素与表格部位用布局、内容、数据、文字、表格、条码、外观、行为,页面设置用页面、纸张、页眉页脚、页码、水印。 | | `PropertyUnit` | 类型 | 数值属性的单位:mm、pt、角度、倍数或个数。 | | `SidesControl` | 类型 | 四边距离:designer-kit 的 sides 编辑方式,单位取打印的单位。 | ### schema/common | 名称 | 种类 | 说明 | | --- | --- | --- | | `BorderSides` | 接口 | 四边边框,缺省的一边不画线。 | | `BoxStyle` | 接口 | 元素外框样式。 | | `Color` | 类型 | 颜色:`#RGB`、`#RGBA`、`#RRGGBB`、`#RRGGBBAA` 或 `transparent`。 | | `Edges` | 接口 | 四边距离,单位 mm。 | | `ExpressionFormat` | 接口 | 表达式格式:以受限表达式把值转成显示文本,表达式中以 `value` 引用待格式化的值。 | | `ExpressionTextContent` | 接口 | 以受限表达式计算的文本,表达式中的变量为数据对象的顶层字段。 | | `FieldImageSource` | 接口 | 从数据字段读取图片地址。 | | `FieldTextContent` | 接口 | 绑定单个数据字段。 | | `ImageSource` | 类型 | 图片来源。 | | `MapFormat` | 接口 | 值映射格式:按数据值查表显示文本。 | | `StaticImageSource` | 接口 | 固定地址的图片。 | | `StaticTextContent` | 接口 | 固定文本。 | | `Stroke` | 接口 | 描边。 | | `TemplateTextContent` | 接口 | 含 `{{path}}` 占位的文本模板,占位按数据字段替换。 | | `TextContent` | 类型 | 文本内容来源。 | | `TextStyle` | 接口 | 文本样式。 | | `ValueFormat` | 类型 | 值格式:数字、百分比、日期与人民币大写即 @xihan-ui/expression 的 FormatSpec,交给注入的格式化器;值映射在本包内完成,表达式格式经表达式接入点。 | | `ValueMapEntry` | 接口 | 值映射的一项:数据值(按字符串比较)与显示文本,与选择类组件的选项同一形状(不带 disabled)。 | ### schema/document | 名称 | 种类 | 说明 | | --- | --- | --- | | `PageBand` | 接口 | 页眉或页脚的一个带区。 | | `PageBandSet` | 接口 | 页眉或页脚:`default` 为常规带区,其余为按输出页命中的变体。 | | `PageGuides` | 接口 | 参考线:设计时对位用,不参与分页也不打印;坐标相对纸张左上角(按页面方向换算后的纸张),单位 mm。 | | `PageNumbering` | 接口 | 页码编号规则。 | | `PaperSpec` | 接口 | 纸张。 | | `PRINT_SCHEMA_VERSION` | 常量 | 当前模板 schema 版本。 | | `PrintDocument` | 接口 | 打印模板文档。 | | `PrintPage` | 接口 | 页面模板:一份页面模板按数据量排出一张或多张输出页。 | | `PrintSchemaVersion` | 类型 | 模板 schema 版本号类型。 | | `Watermark` | 接口 | 平铺文字水印。 | ### schema/elements | 名称 | 种类 | 说明 | | --- | --- | --- | | `BarCodeElement` | 接口 | 一维条码。 | | `BarCodeTextStyle` | 接口 | 条码下方文字的样式。 | | `ElementBase` | 接口 | 全部元素共有的字段。 | | `EllipseElement` | 接口 | 椭圆,内切于元素框。 | | `ExtensionElement` | 接口 | 扩展登记的元素:通用字段之外的属性由扩展的原型定义,取值都是 JSON。 | | `ImageElement` | 接口 | 图片。 | | `LineElement` | 接口 | 直线,走向由元素框和方向共同决定。 | | `LongTextElement` | 接口 | 随内容纵向延展并可跨页续排的长文本。 | | `MatrixCodeElement` | 接口 | 二维码:QR、Data Matrix、PDF417、Aztec。 | | `PageNumberElement` | 接口 | 页码。 | | `PRINT_ELEMENT_TYPES` | 常量 | 内置元素类型列表。 | | `PrintElement` | 类型 | 内置元素。 | | `PrintElementOf` | 类型 | 按类型取元素接口。 | | `PrintElementType` | 类型 | 内置元素类型。 | | `RectElement` | 接口 | 矩形。 | | `RichTextContent` | 类型 | 富文本的内容来源:模板里写死的文档,或从数据字段读取。 | | `RichTextElement` | 接口 | 随内容纵向延展并可跨页续排的富文本:段落、标题、列表、分隔线与带样式的文字。 | | `TableElement` | 接口 | 绑定数组数据、随行数延展并可跨页续排的表格。 | | `TextElement` | 接口 | 固定尺寸的文本。 | ### schema/options | 名称 | 种类 | 说明 | | --- | --- | --- | | `AGGREGATE_FUNCTIONS` | 常量 | 表格汇总函数取值。 | | `AGGREGATE_SCOPES` | 常量 | 表格汇总范围取值。 | | `AggregateFunction` | 类型 | 表格汇总函数。 | | `AggregateScope` | 类型 | 表格汇总范围:page 本页数据,cumulative 截至本页的累计数据,all 全部数据。 | | `BarCodeFormat` | 类型 | 转导出自 @xihan-ui/headless | | `HORIZONTAL_ALIGNS` | 常量 | 单元格水平对齐取值。 | | `HorizontalAlign` | 类型 | 单元格水平对齐。 | | `IMAGE_FITS` | 常量 | 图片适配方式取值。 | | `ImageFit` | 类型 | 图片在元素框内的适配方式。 | | `LINE_DIRECTIONS` | 常量 | 直线方向取值。 | | `LineDirection` | 类型 | 直线在元素框内的走向:horizontal 沿垂直中线,vertical 沿水平中线,diagonalDown 左上到右下,diagonalUp 左下到右上。 | | `MatrixCodeFormat` | 类型 | 转导出自 @xihan-ui/headless | | `MatrixCodeLevel` | 类型 | 转导出自 @xihan-ui/headless | | `PAGE_BAND_VARIANTS` | 常量 | 页眉页脚的页面变体键。 | | `PAGE_ORIENTATIONS` | 常量 | 页面方向取值。 | | `PageBandKey` | 类型 | 页眉页脚的全部带区键,`default` 为未命中任何变体时使用的带区。 | | `PageBandVariant` | 类型 | 页眉页脚变体:first 首页、last 末页、odd 奇数页、even 偶数页(按页码奇偶)。 | | `PageOrientation` | 类型 | 页面方向:portrait 纵向,landscape 横向(宽高互换)。 | | `RICH_TEXT_SOURCES` | 常量 | 富文本字段的内容格式取值。 | | `RichTextSource` | 类型 | 富文本字段的内容格式:html 为 HTML 串,按白名单换成文档;document 为富文本文档对象。 | | `SINGLE_SHEET_PREFERENCES` | 常量 | 同一输出页既是首页又是末页时优先采用的变体取值。 | | `SingleSheetPreference` | 类型 | 同一输出页既是首页又是末页时优先采用的变体。 | | `STROKE_STYLES` | 常量 | 线型取值。 | | `StrokeStyle` | 类型 | 线型。 | | `TABLE_CELL_TYPES` | 常量 | 表格列单元格的呈现类型取值。 | | `TABLE_FOOTER_PLACEMENTS` | 常量 | 表尾行出现位置取值。 | | `TABLE_ROW_HEIGHT_MODES` | 常量 | 表体行高模式取值。 | | `TableCellType` | 类型 | 表格列单元格的呈现类型。 | | `TableFooterPlacement` | 类型 | 表尾行出现位置:everyPage 表格所跨的每一页,lastPage 仅表格结束的那一页。 | | `TableRowHeightMode` | 类型 | 表体行高模式:fixed 每行固定为设定的行高,minimum 以设定的行高为下限、按单元格内容撑高。 | | `TEXT_ALIGNS` | 常量 | 文本水平对齐取值。 | | `TEXT_OVERFLOWS` | 常量 | 固定尺寸文本的溢出处理取值。 | | `TextAlign` | 类型 | 文本水平对齐。 | | `TextOverflow` | 类型 | 固定尺寸文本的溢出处理:clip 裁切,shrink 缩小字号以容纳,visible 允许溢出显示。 | | `VERTICAL_ALIGNS` | 常量 | 垂直对齐取值。 | | `VerticalAlign` | 类型 | 垂直对齐。 | ### schema/table | 名称 | 种类 | 说明 | | --- | --- | --- | | `AggregateCellContent` | 接口 | 表尾单元格的汇总内容。 | | `CellStyle` | 接口 | 单元格样式。 | | `TableBody` | 接口 | 表体。 | | `TableBorder` | 接口 | 表格网格线。 | | `TableCellContent` | 类型 | 表体单元格内容来源。 | | `TableColumn` | 接口 | 表格列。 | | `TableExpressionCellContent` | 接口 | 表体单元格以受限表达式计算,表达式中的变量为行对象的顶层字段。 | | `TableFieldCellContent` | 接口 | 表体单元格显示行内字段。 | | `TableFooterCell` | 接口 | 表尾单元格。 | | `TableFooterCellContent` | 类型 | 表尾单元格内容:固定文本、根数据字段、根数据模板或汇总值。 | | `TableFooterRow` | 接口 | 表尾行,可承载固定文字、根数据字段与汇总值。 | | `TableHeader` | 接口 | 表头。 | | `TableRowNumberCellContent` | 接口 | 表体单元格显示从 1 开始、跨页连续的行序号。 | | `TableTemplateCellContent` | 接口 | 表体单元格按行对象替换 `{{path}}` 占位。 | ### traverse | 名称 | 种类 | 说明 | | --- | --- | --- | | `ElementContainerRef` | 类型 | 元素容器:页面正文,或页眉、页脚中的某个带区。 | | `ElementLocation` | 接口 | 元素在文档中的位置。 | | `ElementRegion` | 类型 | 元素所在区域:正文、页眉或页脚。 | | `findElement` | 函数 | 按标识查找元素。 | | `findPage` | 函数 | 按标识查找页面及其下标。 | | `forEachElement` | 函数 | 按页面、正文、页眉、页脚的顺序遍历全部元素。 | | `getContainerElements` | 函数 | 取容器的元素数组;页面或带区不存在时返回 undefined。 | | `getContainerPath` | 函数 | 取容器元素数组相对页面的 JSON 路径。 | | `listContainers` | 函数 | 列出页面上的全部容器,按正文、页眉、页脚的顺序。 | | `listPageBandKeys` | 函数 | 列出页眉或页脚中已存在的带区键。 | | `PAGE_BAND_KEYS` | 常量 | 页眉页脚带区键,按 default、first、last、odd、even 的顺序。 | ### validation/checks | 名称 | 种类 | 说明 | | --- | --- | --- | | `isValidColor` | 函数 | 判断颜色是否为 `transparent` 或以 `#` 开头的 3、4、6、8 位十六进制色,判据即 designer-kit 的 color 编辑方式。 | ### validation/issues | 名称 | 种类 | 说明 | | --- | --- | --- | | `ExtensionIssueCode` | 类型 | 扩展自己的问题码:扩展 id 的末段加点开头,如 seal.noImage;没有登记严重程度,缺省按 error。 | | `IssueParams` | 类型 | 问题参数,供各语言文案插值。 | | `VALIDATION_ISSUE_SEVERITIES` | 常量 | 全部问题码及其固定的严重程度:不带前缀的是 @xihan-ui/designer-kit/schema 的通用问题码,带前缀的是打印专属的问题码。 | | `ValidationIssue` | 类型 | 校验问题:@xihan-ui/designer-kit/schema 的 SchemaIssue,严重程度总是给出。 | | `ValidationIssueCode` | 类型 | 问题码。 | | `ValidationReport` | 接口 | 校验报告。 | ### validation/validate | 名称 | 种类 | 说明 | | --- | --- | --- | | `validateDocument` | 函数 | 校验模板文档:先检查 JSON 纯度,再按 schema、属性描述与元素专属规则逐层校验。 | | `ValidateDocumentOptions` | 接口 | 校验选项。 | --- 来源:https://print.docs.xihanfun.com/reference/designer # 设计器 `@xihan-print/designer` 的公开接口。样式在 `@xihan-print/designer/style.css`,还要同时引入 `@xihan-ui/designer-shell/style.css` 与 `@xihan-print/runtime/style.css`。 ## `` 注册 <xh-print-designer>;重复调用不报错。 ### 属性 | 属性 | attribute | 类型 | 说明 | | --- | --- | --- | --- | | `data` | — | `unknown` | 预览分页用的数据。 | | `sampleData` | — | `unknown` | 示例数据:给出时画布按它显示实际值,不给时字段显示 {path} 占位。 | | `fields` | — | `readonly DataField[] \| undefined` | 可绑定的字段树:物料面板「字段」组与属性面板的字段选择器读它;导入字段时设计器换掉它并派发 fields-change。 | | `fieldsReadonly` | `fields-readonly` | `boolean` | 字段由宿主管着、不许在设计器里导入:物料面板头没有导入字段钮,import-fields 命令不可用。attribute 为 fields-readonly。 | | `expressionEngine` | — | `ExpressionEngine \| undefined` | 受限表达式引擎;缺省为 @xihan-ui/expression 的 createExpressionEngine()。 | | `formatter` | — | `ValueFormatter \| undefined` | 值格式化器;缺省为 @xihan-ui/expression 的 formatValue。 | | `measurer` | — | `TextMeasurer \| undefined` | 文字度量器;缺省用所在文档的 Canvas 度量器。 | | `printClient` | — | `PrintClient \| undefined` | 本地打印客户端:给了时打印先交给它静默打印,连不上客户端才退回浏览器打印;设置里的打印方式选了浏览器时直接用浏览器打印。给了它,设置对话框才有打印方式一项与「本地客户端」一页。 | | `clientDownloadUrl` | `client-download-url` | `string \| undefined` | 设置对话框「本地客户端」一页里「下载客户端」链接的地址,缺省 PRINT_CLIENT_DOWNLOAD_URL;attribute 为 client-download-url。 | | `locale` | `locale` | `string \| undefined` | 交给格式化器的区域,只决定小数点与千分位符号;同名 attribute。 | | `timeZone` | `time-zone` | `string \| undefined` | 交给格式化器的时区:`UTC`、`±HH:mm` 或 IANA 名称;attribute 为 time-zone。 | | `translations` | — | `Partial \| undefined` | 元素自己的文案,压在全局配置之上。 | | `readonly` | `readonly` | `boolean` | 只读:可以选择、浏览与预览,不能修改。attribute 三态:不写或移除为不只读,写 "false" 为不只读,其余写法为只读。 | | `mode` | `mode` | `PrintDesignerMode` | 设计态或预览态,缺省 design;同名 attribute。 | | `defaultLayout` | — | `DesignerLayout \| undefined` | 宿主的缺省面板布局:用户没存过布局(settings 里没有 layout.panels)时用它,「恢复默认布局」也回到它;写了的字段整项覆盖内置缺省。面板 id 见 PRINT_DESIGNER_PANELS。 | | `commandLayout` | — | `DesignerCommandLayout \| undefined` | 工具栏与右键菜单的排列:位置(toolbar、menu:blank、menu:element、menu:table)→ 若干组命令 id,内置与扩展的命令可以混排。写了某个位置就只显示列出的命令;写了没登记的命令 id 时抛 TypeError。 | | `settings` | — | `DesignerSettings \| undefined` | 设置的值:扁平的键值,键是「分区 id.条目 id」,如 general.theme、print.showGrid;没写的键取条目的缺省值。赋值即生效;用户在设置对话框里或用工具栏上的网格、吸附开关改了以后,设计器换成新的一份并派发 settings-change。 | | `settingsPolicy` | — | `DesignerSettingsPolicy \| undefined` | 宿主对设置条目的管控:键是条目键或分区 id,hidden 不显示,locked 只读、说明换成「由所在系统管理」。 | | `settingsStorageKey` | `settings-storage-key` | `string \| undefined` | 设置存在根所在窗口 localStorage 的哪个键下:给了,连上文档时若没赋 settings 就从这里读,用户改了设置就写回去,标了 secret 的条目不存;读写失败派发 settings-storage-error。不给就不读写。同名 attribute settings-storage-key。 | | `draftKey` | `draft-key` | `string \| undefined` | 自动保存草稿的键:区分同一页、同一种设计器里的不同模板(宿主写模板 id);不给时按页面地址区分。设置里开着自动保存时,未保存的修改停手约一秒存成这台浏览器里的草稿,保存(markSaved)后删掉;下次装进同一个键的模板、和草稿不一样时问恢复还是丢弃。同名 attribute draft-key。 | | `autoTour` | `auto-tour` | `boolean \| undefined` | 第一次在设计态显示出来(画布有了尺寸)后自动开始新手引导,缺省开;设置里记过 help.tourDone(走完、跳过或关上过一次)时不再自动开始。写 false 关掉,start-tour 命令、帮助页的「开始引导」与 startTour() 照样能开始。同名 attribute auto-tour,写 "false" 是关。 | ### 存取器 | 属性 | 读写 | 类型 | 说明 | | --- | --- | --- | --- | | `xhConfig` | 只读 | `XhConfig` | 设计器这棵子树的配置:设置里选了语言时是这一语言的 UI、designer-shell 与打印语言包,跟随页面时是空对象。设计器自己、里面的元素与它弹出的浮层都按它取 locale 与文案,页面别处不受影响。 | | `schema` | 可读写 | `PrintDocument` | 打印模板。赋值即装载:清空历史与选择,按实际大小放到视口左上(赋回当前这一份不重新装载);读取时得到当前模板。 | | `activePageId` | 可读写 | `string` | 当前页面模板的标识;赋值切换页面模板。 | | `canUndo` | 只读 | `boolean` | 是否有可以撤销的修改。 | | `canRedo` | 只读 | `boolean` | 是否有可以重做的修改。 | | `dirty` | 只读 | `boolean` | 有未保存的修改。 | | `extensions` | 可读写 | `readonly PrintDesignerExtension[] \| undefined` | 扩展:元素种类、编辑方式、规则、属性编辑器、命令与面板都从这里来,设计器自带的扩展与内置元素自动排在最前。赋值时当场合并查重,冲突抛 DesignerExtensionError、什么都不变;各扩展的 setup 在元素连上文档时调用、断开时清理。同一个扩展对象换了引用当作卸下再装上,宿主要保持引用稳定。 | ### 方法 | 方法 | 说明 | | --- | --- | | `execute(operations: readonly PrintOperation[], options?: ExecuteOptions): PrintDocument` | 执行一组操作并记为一步撤销;返回新的模板。 | | `undo(): boolean` | 撤销一步;没有可撤销的修改时返回 false。 | | `redo(): boolean` | 重做一步;没有可重做的修改时返回 false。 | | `markSaved(): void` | 把当前状态记为已保存。 | | `addElement(type: string, at?: Point, options?: { readonly region?: ElementRegion }): PrintElement` | 新建元素并选中:at 是元素中心的纸张坐标(mm),缺省放在视口中央;位置吸附网格,region 缺省按落点所在区决定。 | | `select(selection: Partial): void` | 替换当前选择;不在当前页面模板正文与当前显示的页眉页脚变体里的元素会被忽略。 | | `getSelection(): PrintSelection` | 当前选择。 | | `deleteSelection(): boolean` | 删除选中的元素;只读或没有选中时返回 false。 | | `copy(): PrintClipboard \| undefined` | 复制选中的元素;没有选中时返回 undefined。 | | `cut(): PrintClipboard \| undefined` | 复制并删除选中的元素;只读或没有选中时返回 undefined。 | | `paste(clipboard?: PrintClipboard): PrintSelection` | 粘贴到当前页面模板并选中粘贴出的元素;缺省用最近一次复制的内容,连续粘贴依次错开。只读时不粘贴。 | | `duplicate(): PrintSelection` | 复制选中的元素并立即粘贴,不改动剪贴板;只读时什么都不做。 | | `align(mode: AlignMode): void` | 对齐选中的元素;少于两个或只读时什么都不做。 | | `distribute(axis: DistributeAxis): void` | 等距分布选中的元素;少于三个或只读时什么都不做。 | | `reorder(target: ReorderTarget): void` | 调整选中元素的层级。 | | `openSettings(section?: string): void` | 打开设置对话框,给了分区 id 就显示那一页:基础是 general,设计器自己的「画布」是 print。 | | `openJsonEditor(): void` | 打开编辑整份模板 JSON 的对话框:写出当前模板,边改边按打印的校验报问题;没有错误时「应用」整份换上,记一步撤销。只读时只能看、不能应用。 | | `startTour(): boolean` | 从头开始新手引导:通用步骤加打印与扩展的步骤,同一页里别的引导开着时先收起它;预览态或没有能走的步骤时返回 false。 | | `openFieldImport(source?: string): void` | 打开导入字段对话框,给了页 id 就显示那一页:内置的是 sample 与 json-schema,扩展的字段来源用它的 id。 | | `validate(): ValidationIssue[]` | 按当前登记表与表达式引擎校验模板。 | | `getViewport(): Viewport` | 视口:屏幕坐标 = 纸张坐标(mm)× zoom × 96 / 25.4 + (x, y);zoom 为 1 时是实际尺寸。 | | `setViewport(viewport: Viewport): void` | 设置视口;此后尺寸变化不再自动适配。 | | `zoomToFit(): void` | 让整张纸连同左侧的页眉页脚标签落在视口中央,让开浮在画布一侧的翻页条(让出的宽度与四周留白取较大的那个),不放大到实际尺寸以上;此后尺寸变化时重新适配,直到设置视口或在画布上操作。 | | `zoomIn(): void` | 以视口中心放大一档。 | | `zoomOut(): void` | 以视口中心缩小一档。 | | `zoomToActual(): void` | 实际尺寸,纸张居中。 | | `scrollToElement(id: string): void` | 切到元素所在的页面模板与带区变体,并把元素居中。 | | `revealElement(id: string): void` | 切到元素所在的页面模板与带区变体;元素没整个落在视口内时把它居中。 | | `clientToWorld(clientX: number, clientY: number): Point` | 浏览器视口坐标换成纸张坐标(mm)。 | | `print(options?: PrintPreviewPrintOptions): Promise` | 按当前模板与 data 打印,等同 <xh-print-preview> 的 print。 | ### 事件 事件都是冒泡的 `CustomEvent`。 | 事件 | detail | 说明 | | --- | --- | --- | | `schema-change` | `{ schema: PrintDocument, operations: readonly PrintOperation[], kind: PrintSchemaChangeKind }` | 模板变了:schema 为变化后的模板,operations 为实际应用的操作;一次拖动、一次属性编辑等事务提交时合成一次,宿主装载新模板时 kind 为 load、operations 为空。 | | `selection-change` | `PrintSelection` | 选择变了,detail 为当前选择。 | | `dirty-change` | `{ dirty: boolean }` | 是否有未保存的修改变了。 | | `issues-change` | `{ issues: readonly ValidationIssue[] }` | 模板改动或登记表、表达式引擎换了之后重新校验,detail 为全部校验问题。 | | `viewport-change` | `{ viewport: Viewport }` | 视口变了,取值同 getViewport()。 | | `save` | `Record` | 执行了保存命令 save:选了菜单栏的「文件 → 保存」,或设计态下焦点在设计器里(输入框与面板之外)时按了 Ctrl / ⌘ + S;设计器不保存,宿主保存后调用 markSaved()。 | | `element-dblclick` | `{ id: string }` | 双击元素;双击表格时同时进入表格编辑态。 | | `element-contextmenu` | `{ id: string, x: number, y: number }` | 在元素上打开右键菜单,未选中的元素先被选中;可取消,取消后不弹设计器自带的菜单。 | | `blank-contextmenu` | `{ x: number, y: number }` | 在画布空白处打开右键菜单;可取消,取消后不弹设计器自带的菜单。 | | `table-contextmenu` | `{ id: string, part: PrintTablePart, x: number, y: number }` | 在表格编辑态里打开右键菜单,右键处的表格部位先被选中;可取消,取消后不弹设计器自带的菜单。 | | `import-error` | `{ importer: string, source: DesignerImportSource, error: unknown }` | 导入器没能导入粘贴或拖放进来的内容:它抛了错,或给出的元素没有通过校验;importer 是导入器的 id。 | | `page-change` | `{ pageId: string }` | 当前页面模板变了。 | | `mode-change` | `{ mode: PrintDesignerMode }` | 在设计态与预览态之间切换了。 | | `settings-change` | `{ key: string, value: JsonValue \| undefined, settings: DesignerSettings }` | 改了一条设置(设置对话框里,或工具栏上的网格、吸附开关):设计器已换成新的 settings 并生效;value 为 undefined 表示删掉这个键、回到缺省值。宿主存起来,下次赋回 settings。 | | `settings-storage-error` | `VisualEnvironmentStorageError` | 读写 settingsStorageKey 指的本机存储失败(内容不是对象、无痕窗口、配额满);设置留在内存里。 | | `fields-change` | `{ fields: readonly DataField[] }` | 在导入字段对话框里导入了字段:设计器已换成新的 fields。宿主存起来,下次赋回 fields。 | | `sample-data-change` | `{ sampleData: JsonValue }` | 导入字段时勾了「同时作为示例数据」:设计器已换成新的 sampleData。宿主存起来,下次赋回 sampleData。 | ## 类型 ### PrintDesignerMode {#printdesignermode} 设计态或预览态。取值:`design`、`preview`。 ### PrintSchemaChangeKind {#printschemachangekind} schema-change 的种类:执行操作、撤销、重做,或宿主装载了新模板。取值:`execute`、`undo`、`redo`、`load`。 ### PrintSelection {#printselection} 选择:都在当前页面模板的正文与当前显示的页眉页脚变体里;tablePart 只在表格编辑态下出现。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `elements` | `readonly string[]` | | | `tablePart?` | `PrintTablePart` | | ### PrintTablePart {#printtablepart} 表格编辑态下选中的部位。 `| { readonly kind: 'column', readonly elementId: string, readonly columnId: string } | { readonly kind: 'header' | 'body', readonly elementId: string } | { readonly kind: 'footerRow', readonly elementId: string, readonly rowId: string } | { readonly kind: 'footerCell', readonly elementId: string, readonly rowId: string, readonly columnId: string }` ### PrintClipboard {#printclipboard} 剪贴板内容:被复制的元素与它们所在的区域。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `elements` | `readonly PrintElement[]` | | | `region` | `ElementRegion` | | ### PrintDesignerTranslations {#printdesignertranslations} 设计器的文案。 文案键是 `xh-print-designer`,中文在 `@xihan-print/runtime/locale` 的 `printZhCN` 里。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `label` | `string` | 设计器的可访问名。 | | `canvas` | `string` | 画布区域的可访问名。 | | `grid` | `string` | 显示网格的开关。 | | `snap` | `string` | 吸附其它元素与参考线的开关。 | | `print` | `string` | | | `pages` | `string` | 画布右侧翻页条的可访问名。 | | `minimap` | `string` | 画布左下角小地图的名字:面板菜单与设置的「布局」页里用。 | | `menuInsert` | `string` | 菜单栏上「插入」菜单的名字:各种元素与页眉页脚的变体。 | | `menuPage` | `string` | 菜单栏上「页面」菜单的名字:纸张、方向与页面模板。 | | `previousPage` | `string` | | | `nextPage` | `string` | | | `addPage` | `string` | | | `duplicatePage` | `string` | | | `removePage` | `string` | | | `movePageUp` | `string` | | | `movePageDown` | `string` | | | `regions` | `Readonly>` | 页眉、正文、页脚的名称。 | | `bandVariants` | `Readonly>` | 页眉页脚变体的名称。 | | `addBandVariant` | `string` | | | `removeBandVariant` | `string` | | | `elements` | `Readonly>` | 元素种类的名称。 | | `categories` | `Readonly>` | 物料分组的名称。 | | `tableParts` | `Readonly>` | 表格部位的名称。 | | `editTable` | `string` | | | `exitTableEdit` | `string` | | | `insertColumnLeft` | `string` | | | `insertColumnRight` | `string` | | | `removeColumn` | `string` | | | `moveColumnLeft` | `string` | | | `moveColumnRight` | `string` | | | `insertFooterRowAbove` | `string` | | | `insertFooterRowBelow` | `string` | | | `removeFooterRow` | `string` | | | `mergeCellRight` | `string` | | | `splitCell` | `string` | | | `history` | `PrintHistoryTranslations` | 撤销历史里一步的说明。 | | `palette` | `string` | 物料面板的标题。 | | `fieldGroup` | `string` | 物料面板里按 fields 列出的字段那一组的名称。 | | `properties` | `string` | 属性面板的标题。 | | `issues` | `string` | 问题列表的标题。 | | `paletteHelp` | `string` | 物料面板头里的帮助。 | | `propertiesHelp` | `string` | 属性面板头里的帮助。 | | `issuesHelp` | `string` | 问题列表面板头里的帮助。 | | `settingsPrint` | `string` | 设置对话框里设计器自己那一页(打印方式与画布偏好)的名称。 | | `settingsShowGrid` | `string` | | | `settingsShowGridHint` | `string` | | | `settingsGridSize` | `string` | | | `settingsGridSizeHint` | `string` | | | `settingsSnap` | `string` | | | `settingsSnapHint` | `string` | | | `settingsShowMargins` | `string` | | | `settingsShowMarginsHint` | `string` | | | `settingsRulers` | `string` | 设置「打印」分区:标尺开关。 | | `settingsRulersHint` | `string` | | | `tourSteps` | `Readonly>` | 新手引导里打印自己的几步:页眉页脚、页面模板、字段物料与打印。 | | `helpNotes` | `readonly string[]` | 设置对话框「帮助」页的几段操作说明。 | | `shortcutNudge` | `string` | 快捷键表「通用」里方向键那一行:移动选中的元素,没有选中时平移画布。 | | `shortcutCancel` | `string` | 快捷键表「通用」里 Esc 那一行:取消拖动、退出表格编辑或取消选择。 | | `settingsVia` | `string` | 「打印」页里打印方式一项:经本地打印客户端,还是浏览器的打印对话框。 | | `settingsViaHint` | `string` | | | `settingsViaOptions` | `Readonly>` | | | `settingsClient` | `string` | 设置对话框里「本地客户端」那一页的名称。 | | `settingsClientStatus` | `string` | 「本地客户端」页里连接状态一项。 | | `settingsClientStatusHint` | `string` | | | `settingsClientStates` | `Readonly>` | 连接状态的三种说法:已连接;未连接;最近一次测试连接没找到客户端时是未安装。 | | `settingsClientTest` | `string` | | | `settingsClientDownload` | `string` | | | `statusZoom` | `string` | 状态栏右段缩放一项的标签。 | | `statusClient` | `string` | 状态栏右段打印客户端一项的标签。 | | `statusClientStates` | `Readonly>` | 打印客户端连接状态的说法。 | | `selectionToolbar` | `string` | 选中元素后浮在选中框上方的选中工具条的名字。 | | `alignTextLeft` | `string` | | | `alignTextCenter` | `string` | | | `alignTextRight` | `string` | | | `propertyGroups` | `Readonly>` | 属性分组的名称。 | | `propertyNames` | `Readonly>` | 属性名:键为 formatJsonPath(path);「对象种类:路径」覆盖同一路径在该种类上的名字,对象种类是 page、元素种类或 table-column 等表格部位。复合属性拆开后的分项用「复合种类/分项」作键,如 stroke/width、edges/top。 | | `propertyOptions` | `Readonly>` | select 选项名:键为选项值;「路径=选项值」覆盖该属性上的名字。 | | `units` | `Readonly>` | 数值属性的单位。 | | `pageSettings` | `string` | 属性面板在没有选中时编辑页面设置,标题下的一行。 | | `pageNumberInsert` | `string` | 页码格式编辑器里插入变量的菜单按钮。 | | `pageNumberTokens` | `Readonly<{ page: string, pages: string }>` | 页码格式里能插入的两个变量:当前页码与总页数。 | | `valueFormatKind` | `string` | 值格式编辑器里种类下拉的名字。 | | `valueFormatKinds` | `Readonly<{ none: string, format: string, map: string, expression: string }>` | 值格式的种类:不设格式、常用格式(数字、百分比、日期、人民币大写、数字字形)、值映射、表达式。 | | `valueMapColumns` | `Readonly<{ value: string, label: string }>` | 值映射表的两列:数据值与显示文本。 | | `valueMapFallback` | `string` | 值映射里没有命中任何一项时显示的文本。 | | 方法 | 说明 | | --- | --- | | `pagePosition(position: number, count: number, name: string): string` | 翻页条上页码按钮的名字:含按钮上写的「第几个 / 共几个」,再加页面名称(没有名称时是 pageTitle),position 从 1 开始。按钮上看得见的字要包含在名字里。 | | `pageTitle(position: number): string` | 没有名称的页面模板的标题,position 从 1 开始。 | | `resizeBand(region: 'header' \| 'footer'): string` | 拖动调整页眉或页脚高度的把手的可访问名。 | | `elementLabel(type: string, name: string): string` | 画布上一个元素的可访问名:种类名加元素名称或标识。 | | `resizeColumn(title: string): string` | 拖动调整列宽的把手的可访问名。 | | `newColumnTitle(position: number): string` | 新插入的列的标题,position 从 1 开始。 | | `statusElements(count: number): string` | 状态栏左段没有选中时的值:当前页面模板上可见的元素个数。 | | `statusGeometry(x: string, y: string, width: string, height: string): string` | 状态栏左段选中一个元素时的值:入参是格式化后的位置与尺寸(mm)。 | | `fontSizeMenu(size: string): string` | 格式工具条上字号菜单的名字:含按钮上写的字号,选中的几处字号不一时是 —。 | | `issue(issue: SchemaIssue): string` | 校验问题与分页诊断的说明,按问题码与参数生成。 | | `propertyPart(parent: string, part: string): string` | 复合属性拆开后分项的名字,如「页边距 · 上」。 | | `selectedCount(count: number): string` | 属性面板在选中多个元素时标题下的一行。 | | `pageNumberSample(text: string): string` | 页码格式的示例,入参是按第 1 页、共 3 页填好的文字。 | | `valueMapCell(index: number, column: string): string` | 值映射表里一格的名字,index 从 1 起,column 是列名。 | ## 编辑模型 设计器内部的编辑模型也一并导出,不显示界面、只想按操作改模板时可以直接用。创建打印编辑模型。 ```ts declare function createPrintEditor(options: PrintEditorOptions): PrintEditor ``` ### PrintEditorOptions {#printeditoroptions} | 属性 | 类型 | 说明 | | --- | --- | --- | | `document` | `PrintDocument` | | | `extensions?` | `readonly PrintCoreExtension[]` | 扩展:元素种类、编辑方式与规则,内置扩展自动排在最前。 | | `historyLimit?` | `number` | 撤销步数上限,缺省 100。 | | `mergeWithin?` | `number` | 同一合并键相隔多少毫秒内并入上一步,缺省 1000。 | | 方法 | 说明 | | --- | --- | | `now(): number` | | | `describe(operations: readonly PrintOperation[], before: PrintDocument): HistoryLabel \| undefined` | 没给说明的一步,按正向操作与应用前的文档生成说明。 | ### PrintEditor {#printeditor} | 属性 | 类型 | 说明 | | --- | --- | --- | | `document` | `PrintDocument` | | | `elements` | `ElementRegistry` | 当前扩展合出的元素登记表。 | | `history` | `History` | | | `selection` | `PrintSelection` | | | `activePageId` | `string` | | | `page` | `PrintPage` | 当前页面模板。 | | `bands` | `PrintBandView` | | | 方法 | 说明 | | --- | --- | | `setExtensions(extensions: readonly PrintCoreExtension[]): void` | 换一组扩展:之后的编辑操作与校验都按它;冲突时抛 DesignerExtensionError、什么都不变。 | | `load(document: PrintDocument): void` | 装载新文档:清空历史与选择,当前页面回到第一张;文档变化的 cause 为 reset。 | | `execute(operations: readonly PrintOperation[], options?: ExecuteOptions): PrintDocument` | 执行一组操作并记为一步撤销;返回新文档。 | | `undo(): boolean` | | | `redo(): boolean` | | | `seal(): void` | 封口:此后带同一合并键的修改另起一步。 | | `setActivePage(pageId: string): void` | 切换当前页面模板并清空选择;页面不存在时抛错。 | | `setBandVariant(region: 'header' \| 'footer', key: PageBandKey): void` | 切换页眉或页脚当前显示的变体;变体不存在时用 default。选择里不再显示的元素随之取消选择。 | | `select(selection: Partial, mode?: PrintSelectMode): void` | | | `selectAll(): void` | | | `clearSelection(): void` | | | `visibleElements(): { readonly element: PrintElement, readonly region: ElementRegion }[]` | 当前页面模板里可见的元素(正文与当前显示的页眉页脚变体),按页眉、正文、页脚的层级顺序。 | | `addElement(type: string, init: PrintAddElementInit, options?: ExecuteOptions): PrintElement` | 按元素类型新建元素并选中它;类型未登记时抛错。 | | `deleteSelection(options?: ExecuteOptions): boolean` | 删除选中的元素;没有选中时返回 false。 | | `moveElements(ids: readonly string[], dx: number, dy: number, options?: ExecuteOptions): void` | 平移一组元素,单位 mm;位移为零或元素为空时什么都不做。 | | `reorder(target: ReorderTarget, options?: ExecuteOptions): void` | 调整选中元素的层级;多个元素按选中先后依次调整。 | | `align(mode: AlignMode, options?: ExecuteOptions): void` | 对齐选中的元素;少于两个时什么都不做。 | | `distribute(axis: DistributeAxis, options?: ExecuteOptions): void` | 等距分布选中的元素;少于三个时什么都不做。 | | `copy(): PrintClipboard \| undefined` | 复制选中的元素;没有选中时返回 undefined。 | | `cut(options?: ExecuteOptions): PrintClipboard \| undefined` | 复制并删除选中的元素。 | | `paste(clipboard?: PrintClipboard, options?: ExecuteOptions & { readonly offset?: number }): PrintSelection` | 粘贴到当前页面模板并选中粘贴出的元素;缺省用最近一次 copy 的内容,每次粘贴比上一次多偏移 offset(mm)。 | | `duplicate(options?: ExecuteOptions & { readonly offset?: number }): PrintSelection` | 复制选中内容并立即粘贴,不改动剪贴板。 | | `addPage(index?: number, options?: ExecuteOptions): PrintPage` | 在 index 处(缺省末尾)新增一张页面模板并切到它。 | | `duplicatePage(pageId: string, options?: ExecuteOptions): PrintPage` | 复制页面模板:页面与其中的元素换新 id,放在原页之后并切到它;页面不存在时抛错。 | | `removePage(pageId: string, options?: ExecuteOptions): boolean` | 删除页面模板;只剩一张时不删并返回 false。 | | `movePage(pageId: string, index: number, options?: ExecuteOptions): void` | | | `addBand(region: 'header' \| 'footer', variant: PageBandVariant, options?: ExecuteOptions): void` | 为当前页面模板的页眉或页脚新增变体(高度取 default 的高度)并切到它;已存在时只切换。 | | `removeBand(region: 'header' \| 'footer', variant: PageBandVariant, options?: ExecuteOptions): void` | 删除当前页面模板页眉或页脚的变体及其中的元素。 | | `setBandHeight(region: 'header' \| 'footer', key: PageBandKey, height: number, options?: ExecuteOptions): void` | 设置当前页面模板某个带区的高度,单位 mm。 | | `applyTablePlan(tableId: string, plan: TablePlanResult, options?: ExecuteOptions): boolean` | 按 core 的表格计划修改表格;计划被拒时不改文档并返回 false。 | | `validate(): ValidationIssue[]` | | | `subscribe(listener: (change: PrintEditorChange) => void): () => void` | | ## 全部导出 ### editor | 名称 | 种类 | 说明 | | --- | --- | --- | | `createPrintEditor` | 函数 | 创建打印编辑模型。 | | `elementRect` | 函数 | 元素外框:带旋转角的元素按旋转后的形状参与对齐与分布。 | | `PrintAddElementInit` | 接口 | 新建元素的参数:位置为左上角,单位 mm;region 缺省为正文,页眉页脚放进当前显示的变体。 | | `PrintBandView` | 接口 | 当前页面模板显示的页眉与页脚变体。 | | `PrintClipboard` | 接口 | 剪贴板内容:被复制的元素与它们所在的区域。 | | `PrintEditor` | 接口 | | | `PrintEditorChange` | 类型 | 编辑模型的变化:文档变化带上历史变化的种类与实际应用的操作;视图变化指当前页面、带区变体或选择变了。 | | `PrintEditorOptions` | 接口 | | | `PrintSelection` | 接口 | 选择:都在当前页面模板的正文与当前显示的页眉页脚变体里;tablePart 只在表格编辑态下出现。 | | `PrintSelectMode` | 类型 | 选择方式:替换、追加或切换。 | | `PrintTablePart` | 类型 | 表格编辑态下选中的部位。 | ### extensions | 名称 | 种类 | 说明 | | --- | --- | --- | | `createPrintDesignerModel` | 函数 | 合出设计器用的模型;任何一处重名(含设置分区与设计器自己的 print、内建的 general 重名,引导步骤 id 重名或用了 shell. 前缀,物料来源 id 重名,菜单栏的菜单 id 重名,扩展的命令占了 panel:<区名> 或 insert:<元素类型>)都抛 DesignerExtensionError,物料项给了别处登记的元素类型时抛 TypeError。 | | `PRINT_DESIGNER_EXTENSION_ID` | 常量 | 设计器自带的扩展的 id:内置命令,以及表格列与表尾行的条目、页码格式、四边边框与值格式的编辑器。 | | `PRINT_DESIGNER_HELP_URL` | 常量 | 内置命令 help 打开的帮助页:打印文档站的设计器一页。 | | `PRINT_DESIGNER_PANELS` | 常量 | 设计器的面板 id:宿主写 defaultLayout、settingsPolicy(layout.panels.<面板 id>)时用。 | | `PRINT_MENUBAR_MENUS` | 常量 | 打印自己在菜单栏上的两张菜单的 id:插入、页面;宿主写 commandLayout 的 menubar:<菜单 id> 时用。 | | `PRINT_PAGE_STRIP_LAYOUT` | 常量 | 画布右侧翻页条上的命令,排列固定:上一页、页码、下一页,新增、复制、删除页面。 | | `PRINT_TOOLBAR_LAYOUT` | 常量 | 工具栏的缺省排列;commandLayout 没写 toolbar 时用它,再把 placement 含 toolbar 的扩展命令追加成一组。 | | `PrintDesignerCommand` | 类型 | 设计器的命令:工具栏按钮、右键菜单项与快捷键共用。 | | `PrintDesignerContext` | 接口 | 扩展装上时拿到的上下文。 | | `PrintDesignerExtension` | 接口 | 打印设计器层的扩展:运行时层的全部字段(元素种类、渲染、编辑方式与规则),加上属性编辑器、命令、面板、设置分区、导入器、物料项、物料来源、菜单栏的菜单与 setup。 | | `PrintDesignerImporter` | 类型 | 导入器:把粘贴或拖放进来的外部内容换成一组元素;返回 undefined 即不放任何元素。 | | `PrintDesignerModel` | 接口 | 一组设计器扩展合出的东西:交给 core 函数与预览的列表、运行时的模型、属性编辑器表、命令表、导入器、物料项与物料来源。 | | `PrintImportFragment` | 接口 | 导入的结果:一组元素,整体挪到落点(粘贴时为视口中央)后按粘贴的规则放进当前页面模板。 | | `printInsertCommandId` | 函数 | 插入一种元素的命令 id:insert:<元素类型>。 | | `printMenubarLayout` | 函数 | 菜单栏的缺省排列:在 designer-shell 四张菜单的缺省排列上,文件菜单的设置之前多出导入字段、打印两组,视图菜单的拖动之后多出网格与吸附一组;插入菜单按物料分组(elements 的 groupByCategory)各成一组列出插入元素的命令,最后一组是页眉、页脚的变体;页面菜单是纸张、方向与页面设置,上一页、下一页,新增、复制、删除页面,上移、下移页面。 | | `printMenuLayout` | 函数 | 右键菜单的缺省排列;选中的是一张表格时,元素菜单最前面加「编辑表格」。 | | `PrintMenuSubject` | 类型 | 右键菜单落在哪类对象上:画布空白处、元素,或表格编辑态里的表格部位。 | | `PrintPaletteDrop` | 接口 | 物料来源的条目拖到画布上放下的落点:纸张坐标(mm,以纸张左上角为原点),以及这一点落在页眉、正文还是页脚。 | | `PrintPaletteEntry` | 接口 | 物料面板里的一项:扩展元素的名称与图标。 | | `printSelectionLayout` | 函数 | 选中工具条(命令落点 selection:<主体>)的缺省排列:文字类元素多出字号、粗体、斜体、下划线、删除线与左中右对齐;多选多出对齐与分布;都有层级、创建副本、复制与删除。 | | `PrintSelectionSubject` | 类型 | 选中工具条落在哪类选中上:都是文字类元素(文本、长文本、富文本、页码)、单个别的元素,或几个元素里有别的种类。 | ### print-designer | 名称 | 种类 | 说明 | | --- | --- | --- | | `definePrintDesigner` | 函数 | 注册 <xh-print-designer>;重复调用不报错。 | | `PRINT_DESIGNER_TAG` | 常量 | 设计器的标签名。 | | `PrintDesignerEventMap` | 接口 | 设计器派发的事件;x / y 为浏览器视口坐标。 | | `PrintDesignerMode` | 类型 | 设计态或预览态。 | | `PrintSchemaChangeKind` | 类型 | schema-change 的种类:执行操作、撤销、重做,或宿主装载了新模板。 | | `XhPrintDesigner` | 类 | | ### settings | 名称 | 种类 | 说明 | | --- | --- | --- | | `PRINT_CLIENT_DOWNLOAD_URL` | 常量 | 「下载客户端」链接的缺省地址:打印文档站的客户端一页。 | | `PRINT_CLIENT_SETTINGS_SECTION` | 常量 | 设计器「本地客户端」分区的 id,扩展同样不能再用;只在宿主给了 printClient 时出现。 | | `PRINT_CLIENT_STATUS_CONTROL` | 常量 | 「本地客户端」分区里连接状态那一条的编辑方式种类:设计器自己铺状态、「测试连接」钮与下载链接,这一条不存值。 | | `PRINT_SETTING_KEYS` | 常量 | 「打印」分区各条目的键,即 settings 里的键(分区 id.条目 id)。 | | `PRINT_SETTINGS_SECTION` | 常量 | 设计器「打印」分区的 id,扩展不能再用。 | | `printClientSettingsSection` | 函数 | 「本地客户端」分区:一条连接状态,按 PRINT_CLIENT_STATUS_CONTROL 铺。 | | `PrintClientSettingsSectionOptions` | 接口 | printClientSettingsSection 的选项。 | | `PrintClientStatusControl` | 接口 | 连接状态那一条的编辑方式:带着「下载客户端」链接的地址。 | | `printSettingsSection` | 函数 | 「打印」分区:打印方式缺省经本地客户端;网格间距缺省 1 mm,吸附对齐、边距线与标尺缺省打开,网格缺省不显示。 | | `PrintSettingsSectionOptions` | 接口 | printSettingsSection 的选项。 | | `PrintVia` | 类型 | 打印方式:经本地打印客户端,或浏览器的打印对话框。 | ### version | 名称 | 种类 | 说明 | | --- | --- | --- | | `PRINT_VERSION` | 常量 | @xihan-print/designer 的版本号。 | --- 来源:https://print.docs.xihanfun.com/reference/elements # 元素 内置元素按物料面板的分组列出。属性路径相对元素,嵌套属性用点连接;「名称」是属性面板里的中文名,「新建时」是在设计器里新建元素时的取值,过长的值以 … 代替,详见各元素的类型。 每个元素还有共有字段 `id`、`type`、`name`、`x`、`y`、`width`、`height`、`locked`、`previewOnly`、`meta`,见 [ElementBase](./schema#elementbase)。坐标相对纸张左上角,长度单位为 mm,字号与字间距单位为 pt。 ## 文字 ### 文本 `text` {#text} 固定尺寸,可放在页眉、正文与页脚 · 最小 1 × 1 mm · 新建时 60 × 8 mm · 类型 [TextElement](./schema#textelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `60` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `8` | | `rotation` | 旋转 | 数值(度),≥ -360,≤ 360 | 是 | `0` | | `content` | 内容 | [文本内容](./schema#textcontent) | 是 | `{"kind":"static","text":""}` | | `style.fontFamily` | 字体 | 文本 | 可省 | — | | `style.fontSize` | 字号 | 数值(pt),> 0,≤ 500 | 是 | `10.5` | | `style.bold` | 粗体 | 布尔 | 是 | `false` | | `style.italic` | 斜体 | 布尔 | 是 | `false` | | `style.underline` | 下划线 | 布尔 | 是 | `false` | | `style.lineThrough` | 删除线 | 布尔 | 是 | `false` | | `style.color` | 文字颜色 | [颜色](./schema#color) | 是 | `"#000000"` | | `style.align` | 水平对齐 | `left` 左、`center` 居中、`right` 右、`justify` 两端对齐 | 是 | `"left"` | | `style.verticalAlign` | 垂直对齐 | `top` 上、`middle` 居中、`bottom` 下 | 是 | `"middle"` | | `style.lineHeight` | 行高 | 数值(倍),> 0,≤ 10 | 是 | `1.2` | | `style.letterSpacing` | 字间距 | 数值(pt),≥ -10,≤ 100 | 是 | `0` | | `wrap` | 自动换行 | 布尔 | 是 | `false` | | `overflow` | 放不下时 | `clip` 裁掉、`shrink` 缩小字号、`visible` 照常显示 | 是 | `"clip"` | | `box.background` | 背景 | [颜色](./schema#color) | 可省 | — | | `box.border` | 边框 | [四边边框](./schema#bordersides) | 可省 | — | | `box.padding` | 内边距 | [四边距离](./schema#edges),每边是数值(mm),≥ 0 | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ### 长文本 `longText` {#longtext} 随内容纵向延展、可跨页续排,只能放在正文区 · 最小 5 × 3 mm · 新建时 120 × 20 mm · 类型 [LongTextElement](./schema#longtextelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `120` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `20` | | `content` | 内容 | [文本内容](./schema#textcontent) | 是 | `{"kind":"static","text":""}` | | `style.fontFamily` | 字体 | 文本 | 可省 | — | | `style.fontSize` | 字号 | 数值(pt),> 0,≤ 500 | 是 | `10.5` | | `style.bold` | 粗体 | 布尔 | 是 | `false` | | `style.italic` | 斜体 | 布尔 | 是 | `false` | | `style.underline` | 下划线 | 布尔 | 是 | `false` | | `style.lineThrough` | 删除线 | 布尔 | 是 | `false` | | `style.color` | 文字颜色 | [颜色](./schema#color) | 是 | `"#000000"` | | `style.align` | 水平对齐 | `left` 左、`center` 居中、`right` 右、`justify` 两端对齐 | 是 | `"left"` | | `style.verticalAlign` | 垂直对齐 | `top` 上、`middle` 居中、`bottom` 下 | 是 | `"top"` | | `style.lineHeight` | 行高 | 数值(倍),> 0,≤ 10 | 是 | `1.5` | | `style.letterSpacing` | 字间距 | 数值(pt),≥ -10,≤ 100 | 是 | `0` | | `box.background` | 背景 | [颜色](./schema#color) | 可省 | — | | `box.border` | 边框 | [四边边框](./schema#bordersides) | 可省 | — | | `box.padding` | 内边距 | [四边距离](./schema#edges),每边是数值(mm),≥ 0 | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ### 富文本 `richText` {#richtext} 随内容纵向延展、可跨页续排,只能放在正文区 · 最小 5 × 3 mm · 新建时 120 × 20 mm · 类型 [RichTextElement](./schema#richtextelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `120` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `20` | | `content` | 内容 | [富文本内容](./schema#richtextcontent) | 是 | … | | `style.fontFamily` | 字体 | 文本 | 可省 | — | | `style.fontSize` | 字号 | 数值(pt),> 0,≤ 500 | 是 | `10.5` | | `style.bold` | 粗体 | 布尔 | 是 | `false` | | `style.italic` | 斜体 | 布尔 | 是 | `false` | | `style.underline` | 下划线 | 布尔 | 是 | `false` | | `style.lineThrough` | 删除线 | 布尔 | 是 | `false` | | `style.color` | 文字颜色 | [颜色](./schema#color) | 是 | `"#000000"` | | `style.align` | 水平对齐 | `left` 左、`center` 居中、`right` 右、`justify` 两端对齐 | 是 | `"left"` | | `style.verticalAlign` | 垂直对齐 | `top` 上、`middle` 居中、`bottom` 下 | 是 | `"top"` | | `style.lineHeight` | 行高 | 数值(倍),> 0,≤ 10 | 是 | `1.5` | | `style.letterSpacing` | 字间距 | 数值(pt),≥ -10,≤ 100 | 是 | `0` | | `box.background` | 背景 | [颜色](./schema#color) | 可省 | — | | `box.border` | 边框 | [四边边框](./schema#bordersides) | 可省 | — | | `box.padding` | 内边距 | [四边距离](./schema#edges),每边是数值(mm),≥ 0 | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ### 页码 `pageNumber` {#pagenumber} 固定尺寸,可放在页眉、正文与页脚 · 最小 1 × 1 mm · 新建时 30 × 6 mm · 类型 [PageNumberElement](./schema#pagenumberelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `30` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `6` | | `format` | 格式 | 页码模板,可用 `{{$page}}` 与 `{{$pages}}` | 是 | `"{{$page}}/{{$pages}}"` | | `numeral` | 数字写法 | `roman-upper` 罗马数字(I、II)、`roman-lower` 罗马数字(i、ii)、`cn-lower` 中文小写(一、二)、`cn-upper` 中文大写(壹、贰) | 可省 | — | | `style.fontFamily` | 字体 | 文本 | 可省 | — | | `style.fontSize` | 字号 | 数值(pt),> 0,≤ 500 | 是 | `9` | | `style.bold` | 粗体 | 布尔 | 是 | `false` | | `style.italic` | 斜体 | 布尔 | 是 | `false` | | `style.underline` | 下划线 | 布尔 | 是 | `false` | | `style.lineThrough` | 删除线 | 布尔 | 是 | `false` | | `style.color` | 文字颜色 | [颜色](./schema#color) | 是 | `"#000000"` | | `style.align` | 水平对齐 | `left` 左、`center` 居中、`right` 右、`justify` 两端对齐 | 是 | `"center"` | | `style.verticalAlign` | 垂直对齐 | `top` 上、`middle` 居中、`bottom` 下 | 是 | `"middle"` | | `style.lineHeight` | 行高 | 数值(倍),> 0,≤ 10 | 是 | `1.2` | | `style.letterSpacing` | 字间距 | 数值(pt),≥ -10,≤ 100 | 是 | `0` | | `box.background` | 背景 | [颜色](./schema#color) | 可省 | — | | `box.border` | 边框 | [四边边框](./schema#bordersides) | 可省 | — | | `box.padding` | 内边距 | [四边距离](./schema#edges),每边是数值(mm),≥ 0 | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ## 图片 ### 图片 `image` {#image} 固定尺寸,可放在页眉、正文与页脚 · 最小 1 × 1 mm · 新建时 40 × 30 mm · 类型 [ImageElement](./schema#imageelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `40` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `30` | | `rotation` | 旋转 | 数值(度),≥ -360,≤ 360 | 是 | `0` | | `source` | 图片来源 | [图片来源](./schema#imagesource) | 是 | `{"kind":"static","url":""}` | | `fit` | 填充方式 | `contain` 完整显示、`cover` 铺满裁切、`fill` 拉伸铺满、`none` 原始大小 | 是 | `"contain"` | | `box.background` | 背景 | [颜色](./schema#color) | 可省 | — | | `box.border` | 边框 | [四边边框](./schema#bordersides) | 可省 | — | | `box.padding` | 内边距 | [四边距离](./schema#edges),每边是数值(mm),≥ 0 | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ## 数据 ### 表格 `table` {#table} 随内容纵向延展、可跨页续排,只能放在正文区 · 最小 10 × 5 mm · 新建时 180 × 22 mm · 类型 [TableElement](./schema#tableelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `180` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `22` | | `dataField` | 数据字段 | 指向数组的字段路径 | 是 | `""` | | `columns` | 列 | [列](#table-column)的数组 | 是 | … | | `header.visible` | 显示表头 | 布尔 | 是 | `true` | | `header.repeat` | 每页重复表头 | 布尔;「显示表头」打开时显示 | 是 | `true` | | `header.height` | 表头行高 | 数值(mm),> 0;「显示表头」打开时显示 | 是 | `8` | | `header.style` | 表头样式 | [单元格样式](./schema#cellstyle);「显示表头」打开时显示 | 是 | … | | `body.rowHeight` | 行高 | 数值(mm),> 0 | 是 | `7` | | `body.rowHeightMode` | 行高方式 | `fixed` 固定、`minimum` 至少 | 是 | `"fixed"` | | `body.style` | 表体样式 | [单元格样式](./schema#cellstyle) | 是 | … | | `footerRows` | 表尾行 | [表尾行](#table-footer-row)的数组 | 是 | `[]` | | `maxRowsPerPage` | 每页最多行数 | 整数(个),≥ 1 | 可省 | — | | `fillEmptyRows` | 用空行补齐 | 布尔 | 是 | `false` | | `border.stroke` | 边框线 | [描边](./schema#stroke) | 是 | `{"width":0.2,"style":"solid","color":"#000000"}` | | `border.outer` | 外框 | 布尔 | 是 | `true` | | `border.horizontal` | 横线 | 布尔 | 是 | `true` | | `border.vertical` | 竖线 | 布尔 | 是 | `true` | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ## 图形 ### 直线 `line` {#line} 固定尺寸,可放在页眉、正文与页脚 · 最小 0 × 0 mm · 新建时 80 × 0 mm · 类型 [LineElement](./schema#lineelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `80` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `0` | | `direction` | 方向 | `horizontal` 水平、`vertical` 竖直、`diagonalDown` 左上到右下、`diagonalUp` 左下到右上 | 是 | `"horizontal"` | | `stroke` | 边线 | [描边](./schema#stroke) | 是 | `{"width":0.3,"style":"solid","color":"#000000"}` | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ### 矩形 `rect` {#rect} 固定尺寸,可放在页眉、正文与页脚 · 最小 1 × 1 mm · 新建时 40 × 20 mm · 类型 [RectElement](./schema#rectelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `40` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `20` | | `rotation` | 旋转 | 数值(度),≥ -360,≤ 360 | 是 | `0` | | `stroke` | 边线 | [描边](./schema#stroke) | 可省 | `{"width":0.3,"style":"solid","color":"#000000"}` | | `fill` | 填充 | [颜色](./schema#color) | 可省 | — | | `radius` | 圆角 | 数值(mm),≥ 0 | 是 | `0` | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ### 椭圆 `ellipse` {#ellipse} 固定尺寸,可放在页眉、正文与页脚 · 最小 1 × 1 mm · 新建时 30 × 20 mm · 类型 [EllipseElement](./schema#ellipseelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `30` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `20` | | `rotation` | 旋转 | 数值(度),≥ -360,≤ 360 | 是 | `0` | | `stroke` | 边线 | [描边](./schema#stroke) | 可省 | `{"width":0.3,"style":"solid","color":"#000000"}` | | `fill` | 填充 | [颜色](./schema#color) | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ## 条码 ### 条码 `barCode` {#barcode} 固定尺寸,可放在页眉、正文与页脚 · 最小 5 × 3 mm · 新建时 50 × 15 mm · 类型 [BarCodeElement](./schema#barcodeelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `50` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `15` | | `rotation` | 旋转 | 数值(度),≥ -360,≤ 360 | 是 | `0` | | `format` | 码制 | `code128`、`ean13`、`ean8`、`upca`、`upce`、`itf14`、`code39` | 是 | `"code128"` | | `content` | 内容 | [文本内容](./schema#textcontent) | 是 | `{"kind":"static","text":"1234567890"}` | | `showText` | 显示文字 | 布尔 | 是 | `true` | | `textStyle.fontFamily` | 文字字体 | 文本;「显示文字」打开时显示 | 可省 | — | | `textStyle.fontSize` | 文字字号 | 数值(pt),> 0,≤ 100;「显示文字」打开时显示 | 是 | `9` | | `color` | 颜色 | [颜色](./schema#color) | 是 | `"#000000"` | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ### 二维码 `matrixCode` {#matrixcode} 固定尺寸,可放在页眉、正文与页脚 · 最小 5 × 5 mm · 新建时 25 × 25 mm · 类型 [MatrixCodeElement](./schema#matrixcodeelement)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `x` | X | 数值(mm) | 是 | — | | `y` | Y | 数值(mm) | 是 | — | | `width` | 宽 | 数值(mm),≥ 0 | 是 | `25` | | `height` | 高 | 数值(mm),≥ 0 | 是 | `25` | | `rotation` | 旋转 | 数值(度),≥ -360,≤ 360 | 是 | `0` | | `content` | 内容 | [文本内容](./schema#textcontent) | 是 | `{"kind":"static","text":"1234567890"}` | | `format` | 码制 | `qr`、`data-matrix`、`pdf417`、`aztec` | 是 | `"qr"` | | `level` | 纠错级别 | 纠错级别,见 [MatrixCodeElement](./schema#matrixcodeelement) | 可省 | `"M"` | | `quietZone` | 静区 | 整数(个),≥ 0,≤ 20 | 是 | `1` | | `color` | 颜色 | [颜色](./schema#color) | 是 | `"#000000"` | | `background` | 背景 | [颜色](./schema#color) | 可省 | — | | `name` | 名称 | 文本 | 可省 | — | | `locked` | 锁定 | 布尔 | 可省 | — | | `previewOnly` | 只在预览中显示 | 布尔 | 可省 | — | ## 页面设置 {#page} 没有选中元素时,属性面板编辑当前页面模板。属性路径相对页面模板,见 [PrintPage](./schema#printpage)。 | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `name` | 名称 | 文本 | 可省 | — | | `paper` | 纸张 | [纸张](./schema#paperspec) | 是 | — | | `orientation` | 方向 | `portrait` 纵向、`landscape` 横向 | 是 | — | | `margins` | 页边距 | [四边距离](./schema#edges),每边是数值(mm),≥ 0 | 是 | — | | `header.default.height` | 页眉高度 | 数值(mm),≥ 0 | 是 | — | | `header.first.height` | 首页页眉高度 | 数值(mm),≥ 0;设置了 `header.first` 时显示 | 可省 | — | | `header.last.height` | 末页页眉高度 | 数值(mm),≥ 0;设置了 `header.last` 时显示 | 可省 | — | | `header.odd.height` | 奇数页页眉高度 | 数值(mm),≥ 0;设置了 `header.odd` 时显示 | 可省 | — | | `header.even.height` | 偶数页页眉高度 | 数值(mm),≥ 0;设置了 `header.even` 时显示 | 可省 | — | | `header.singleSheet` | 只有一页时的页眉 | `first` 首页、`last` 末页 | 是 | — | | `footer.default.height` | 页脚高度 | 数值(mm),≥ 0 | 是 | — | | `footer.first.height` | 首页页脚高度 | 数值(mm),≥ 0;设置了 `footer.first` 时显示 | 可省 | — | | `footer.last.height` | 末页页脚高度 | 数值(mm),≥ 0;设置了 `footer.last` 时显示 | 可省 | — | | `footer.odd.height` | 奇数页页脚高度 | 数值(mm),≥ 0;设置了 `footer.odd` 时显示 | 可省 | — | | `footer.even.height` | 偶数页页脚高度 | 数值(mm),≥ 0;设置了 `footer.even` 时显示 | 可省 | — | | `footer.singleSheet` | 只有一页时的页脚 | `first` 首页、`last` 末页 | 是 | — | | `numbering.continuous` | 接续上一页面模板的页码 | 布尔 | 是 | — | | `numbering.start` | 起始页码 | 整数(个),≥ 0 | 是 | — | | `watermark` | 水印 | [水印](./schema#watermark) | 可省 | — | 纸张预设: | 预设名 | 宽 × 高(mm,纵向) | | --- | --- | | `A3` | 297 × 420 | | `A4` | 210 × 297 | | `A5` | 148 × 210 | | `A6` | 105 × 148 | | `B4` | 250 × 353 | | `B5` | 176 × 250 | | `Letter` | 215.9 × 279.4 | | `Legal` | 215.9 × 355.6 | ## 表格部位 {#table-parts} 双击表格进入表格编辑态后,可以分别选中列、表头、表体、表尾行与表尾单元格编辑。属性路径相对该部位。 ### 列 {#table-column} | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `title` | 标题 | 文本 | 是 | `""` | | `content` | 内容 | [单元格内容](./schema#tablecellcontent) | 是 | `{"kind":"field","field":""}` | | `cellType` | 单元格类型 | `text` 文字、`image` 图片、`barCode` 条码、`matrixCode` 二维码 | 是 | `"text"` | | `barCodeFormat` | 条码码制 | `code128`、`ean13`、`ean8`、`upca`、`upce`、`itf14`、`code39`;「单元格类型」为 `barCode` 时显示 | 可省 | — | | `width` | 列宽 | 数值(mm),> 0 | 是 | `60` | | `align` | 对齐 | `left` 左、`center` 居中、`right` 右 | 是 | `"left"` | ### 表头 {#table-header} | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `visible` | 显示 | 布尔 | 是 | `true` | | `repeat` | 每页重复 | 布尔;「显示」打开时显示 | 是 | `true` | | `height` | 行高 | 数值(mm),> 0;「显示」打开时显示 | 是 | `8` | | `style` | 样式 | [单元格样式](./schema#cellstyle);「显示」打开时显示 | 是 | … | ### 表体 {#table-body} | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `rowHeight` | 行高 | 数值(mm),> 0 | 是 | `7` | | `rowHeightMode` | 行高方式 | `fixed` 固定、`minimum` 至少 | 是 | `"fixed"` | | `style` | 样式 | [单元格样式](./schema#cellstyle) | 是 | … | ### 表尾行 {#table-footer-row} | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `placement` | 出现在 | `everyPage` 每一页、`lastPage` 只在最后一页 | 是 | — | | `height` | 行高 | 数值(mm),> 0 | 是 | — | | `style` | 样式 | [单元格样式](./schema#cellstyle) | 是 | — | ### 表尾单元格 {#table-footer-cell} | 属性 | 名称 | 取值 | 必有 | 新建时 | | --- | --- | --- | --- | --- | | `content` | 内容 | [表尾单元格内容](./schema#tablefootercellcontent) | 是 | — | | `colSpan` | 跨列数 | 整数(个),≥ 1 | 是 | — | | `align` | 对齐 | `left` 左、`center` 居中、`right` 右 | 可省 | — | --- 来源:https://print.docs.xihanfun.com/reference/issues # 问题码与诊断 校验问题由 `validateDocument` 与设计器的问题列表给出,分页诊断随分页结果一起给出。问题都不带文案,只有问题码、严重程度、路径与参数;下表的说明来自中文语言包 `printZhCN`,其中的 {名} 是参数,实际文案只含问题给出的参数。 ## 校验问题 有错误级问题的模板不能分页:`paginate` 抛出 `PrintLayoutError`(码为 `document.invalid`),预览元素派发 `pagination-error`。警告不影响分页。 | 问题码 | 严重程度 | 说明 | 参数 | | --- | --- | --- | --- | | `required` | 错误 | 缺少必填项 | — | | `invalid-type` | 错误 | 类型不对 | — | | `invalid-value` | 错误 | 取值无效 | — | | `out-of-range` | 错误 | 应不小于 {min}、大于 {exclusiveMin}、不大于 {max} | `min`、`exclusiveMin`、`max` | | `not-integer` | 错误 | 应为整数 | — | | `invalid-option` | 错误 | 不是可选的值 | — | | `invalid-color` | 错误 | 不是有效的颜色 | — | | `empty` | 错误 | 不能为空 | — | | `unknown-key` | 警告 | 未知的属性 {key} | `key` | | `unknown-control` | 错误 | 未知的编辑方式 | — | | `duplicate-key` | 错误 | 键 {key} 重复 | `key` | | `duplicate-id` | 错误 | 标识 {id} 重复 | `id` | | `duplicate-value` | 错误 | 选项的值重复 | — | | `json.circular` | 错误 | 数据有循环引用 | — | | `schema.unsupportedVersion` | 错误 | 不支持模板版本 {version}(应为 {supported}) | `version`、`supported` | | `field.empty` | 警告 | 没有绑定字段 | — | | `field.invalidPath` | 错误 | 字段路径 {field} 写法不对 | `field` | | `template.unclosedPlaceholder` | 错误 | 模板里有没闭合的 {{ | — | | `template.emptyPlaceholder` | 错误 | 模板里有空的 {{}} | — | | `template.invalidPath` | 错误 | 模板里的字段路径写法不对 | — | | `expression.invalid` | 错误 | 表达式有误 | — | | `format.duplicateMapValue` | 警告 | 值映射里 {value} 重复 | `value` | | `format.invalidOption` | 错误 | 格式选项无效 | — | | `format.conflict` | 错误 | 格式选项相互冲突 | — | | `document.noPages` | 错误 | 模板里没有页面 | — | | `page.presetMismatch` | 警告 | 纸张尺寸与 {preset} 不符 | `preset` | | `page.noContentArea` | 错误 | 页边距占满了纸张 | — | | `page.bandsOverflow` | 错误 | 页眉页脚比版心还高 | — | | `watermark.emptyText` | 警告 | 水印文字为空 | — | | `element.unknownType` | 错误 | 未知的元素种类 {type} | `type` | | `element.tooSmall` | 错误 | 小于最小尺寸 … × … mm | `minWidth`、`minHeight` | | `element.outsidePaper` | 警告 | 超出纸张 | — | | `element.outsideBand` | 警告 | 超出{region}区 | `region` | | `element.flowInBand` | 错误 | 跨页元素只能放在正文区 | — | | `element.flowOutsideBody` | 警告 | 跨页元素超出正文区 | — | | `line.zeroLength` | 错误 | 直线长度为 0 | — | | `shape.invisible` | 警告 | 图形既没有边线也没有填充 | — | | `image.emptySource` | 警告 | 图片地址为空 | — | | `image.unsafeSource` | 错误 | 图片地址不允许使用 | — | | `table.noColumns` | 警告 | 表格没有列 | — | | `table.duplicateColumnId` | 错误 | 列标识 {id} 重复 | `id` | | `table.duplicateRowId` | 错误 | 表尾行标识 {id} 重复 | `id` | | `table.columnWidthMismatch` | 警告 | 列宽合计 … mm,与表格宽度 … mm 不一致 | `columnsWidth`、`width` | | `table.unknownColumn` | 错误 | 表尾单元格指向不存在的列 {columnId} | `columnId` | | `table.cellSpanOverflow` | 错误 | 跨 {colSpan} 列,右侧只剩 {available} 列 | `colSpan`、`available` | | `table.cellOverlap` | 错误 | 表尾单元格与其它单元格重叠 | — | | `table.aggregateFieldMissing` | 错误 | 汇总 {fn} 缺少字段 | `fn` | | `table.rowExceedsBody` | 警告 | 表头加一行需要 … mm,正文区只有 … mm | `required`、`available` | | `barCode.invalidValue` | 错误 | 内容无法按 {format} 编码 | `format` | | `matrixCode.invalidValue` | 错误 | 内容无法按 {format} 编码 | `format` | | `matrixCode.invalidLevel` | 错误 | 纠错级别 {level} 不适用于 {format} | `level`、`format` | | `matrixCode.levelIgnored` | 警告 | {format} 没有纠错级别,设置的级别不起作用 | `format` | | `matrixCode.notSquare` | 警告 | 二维码宽高不等 | — | | `pageNumber.noToken` | 警告 | 页码格式里没有 {{$page}} 或 {{$pages}} | — | | `pageNumber.unknownToken` | 警告 | 页码格式里有未知的变量 {token} | `token` | | `richText.invalidDocument` | 错误 | 富文本写法不对 | `code` | | `richText.empty` | 警告 | 富文本没有内容 | — | ## 分页诊断 分页诊断不打断分页:错误表示按模板该画的内容没有画出或输出页被截断,警告表示内容画出了但与设计有出入。同一位置的同一问题只报告一次。 | 问题码 | 严重程度 | 说明 | 参数 | | --- | --- | --- | --- | | `data.notArray` | 警告 | 字段 {field} 不是数组 | `field` | | `data.notScalar` | 警告 | 字段 {field} 不是单个值 | `field` | | `format.unavailable` | 警告 | 没有提供格式化器 | — | | `format.failed` | 警告 | 格式化失败 | — | | `expression.unavailable` | 警告 | 没有提供表达式引擎 | — | | `expression.failed` | 错误 | 表达式求值失败 | — | | `image.unsafeSource` | 错误 | 图片地址不允许使用 | — | | `barCode.invalidValue` | 错误 | 内容无法按 {format} 编码 | `format` | | `matrixCode.invalidValue` | 错误 | 内容无法按 {format} 编码 | `format` | | `text.overflow` | 警告 | 文字缩到 {fontSize} pt 仍超出元素框 | `fontSize` | | `richText.droppedContent` | 警告 | 字段 {field} 的 HTML 里去掉了 {names} | `field`、`names` | | `richText.invalidData` | 错误 | 字段 {field} 不是合规的富文本文档 | `field` | | `table.rowOverflow` | 警告 | 第 … 行在一页里放不下 | `dataIndex` | | `element.overflow` | 警告 | 内容超出元素框 | — | | `layout.sheetLimit` | 错误 | 输出页数超过上限 {limit} | `limit` | ## 分页与打印失败 分页无法进行的原因:文档有 error 级校验问题、选项不合法、测量器或格式化器返回了不合法的结果。 `PrintLayoutError` 的码:`document.invalid`、`option.invalid`、`measurer.invalidResult`、`formatter.invalidResult`。 运行时无法完成分页或打印的原因:拿不到文字度量器、没有给模板。 `PrintRuntimeError` 的码:`measurer.unavailable`、`schema.missing`。 --- 来源:https://print.docs.xihanfun.com/reference/runtime # 预览与打印 `@xihan-print/runtime` 的公开接口:预览元素、直接打印的函数与相关类型。样式在 `@xihan-print/runtime/style.css`。 ## `` 注册 <xh-print-preview>;同一构造器重复注册不报错,没有 customElements 的环境跳过。 ### 属性 | 属性 | attribute | 类型 | 说明 | | --- | --- | --- | --- | | `schema` | — | `PrintDocument \| undefined` | 打印模板。 | | `data` | — | `unknown` | 绑定的数据。 | | `expressionEngine` | — | `ExpressionEngine \| undefined` | 受限表达式引擎;缺省为 @xihan-ui/expression 的 createExpressionEngine()。 | | `formatter` | — | `ValueFormatter \| undefined` | 值格式化器;缺省为 @xihan-ui/expression 的 formatValue。 | | `measurer` | — | `TextMeasurer \| undefined` | 文字度量器;缺省用所在文档的 Canvas 度量器,字体加载完成后自动重排。 | | `translations` | — | `Partial \| undefined` | 元素自己的文案,压在全局配置之上。 | | `locale` | `locale` | `string \| undefined` | 交给格式化器的区域。 | | `timeZone` | `time-zone` | `string \| undefined` | 交给格式化器的时区。 | | `zoom` | `zoom` | `number \| undefined` | 显示缩放倍数,缺省 1;写在宿主的 --xh-print-zoom 上,由 style.css 作用到输出页,只影响预览、不影响打印。 | | `printClient` | — | `PrintClient \| undefined` | 本地打印客户端(@xihan-print/runtime/client 的 createPrintClient):给了时 print() 先交给它静默打印,连不上客户端才退回浏览器打印。 | | `trace` | — | `PrintTrace \| undefined` | 分页与打印各环节的回调,给调试日志用:重新分页时报分页的每一步与分页、生成 DOM 两个环节,print() 另报后面的环节。换它不重新分页。 | ### 存取器 | 属性 | 读写 | 类型 | 说明 | | --- | --- | --- | --- | | `extensions` | 可读写 | `readonly PrintRuntimeExtension[] \| undefined` | 扩展:元素种类、渲染、编辑方式与规则,内置扩展自动排在最前。赋值时当场合并查重,冲突抛 DesignerExtensionError、什么都不变;同一个数组只合并一次,宿主要保持引用稳定。 | | `result` | 只读 | `PaginationResult \| undefined` | 最近一次分页的结果;还没分页或分页失败时为 undefined。 | ### 方法 | 方法 | 说明 | | --- | --- | | `paginate(): PaginationResult \| undefined` | 立即按当前属性重新分页并渲染,返回分页结果;没有模板或分页失败时返回 undefined,失败另派发 pagination-error。 | | `print(options?: PrintPreviewPrintOptions): Promise` | 打印当前模板:按当前属性重新分页后送去打印,只在预览中显示的元素不打印。没有模板或分页失败时抛错。给了 printClient 时先交给本地打印客户端,等到交给打印池;只在连不上客户端时退回浏览器打印,其它错误照常抛出。 | ### 事件 事件都是冒泡的 `CustomEvent`。 | 事件 | detail | 说明 | | --- | --- | --- | | `paginated` | `{ readonly sheets: number, readonly diagnostics: readonly PaginationDiagnostic[] }` | 分页并渲染完成。 | | `pagination-error` | `PaginationErrorDetail` | 分页失败,已清空预览。 | ## 函数 ### printDocument() {#printdocument} ```ts declare function printDocument(schema: PrintDocument, data: unknown, options?: PrintOptions): Promise ``` 按数据排出模板并直接打印,不需要页面上有预览元素;分页同 paginateDocument。文档不合法或拿不到度量器时抛错,不打开打印对话框。 ### printSheets() {#printsheets} ```ts declare function printSheets(result: PaginationResult, options?: PrintSheetsOptions): Promise ``` 把排好的输出页送去打印:在隐藏 iframe 里渲染(只在预览中显示的元素不画),纸张尺寸不同的输出页分到各自的具名 @page,带上源文档的字体,等样式表、图片与字体就绪后调起打印对话框,对话框关闭后移除 iframe。 ### definePrintPreview() {#defineprintpreview} ```ts declare function definePrintPreview(): void ``` 注册 <xh-print-preview>;同一构造器重复注册不报错,没有 customElements 的环境跳过。 ### renderSheets() {#rendersheets} ```ts declare function renderSheets(result: PaginationResult, container: Element, options?: RenderSheetsOptions): HTMLDivElement[] ``` 把分页结果渲染进 container:先清空 container,再按顺序放入每张输出页,返回这些输出页元素。输出页按纸张尺寸定宽高,元素按毫米绝对定位;只用标准 HTML 与 SVG,任何文档里都能渲染,不依赖自定义元素注册。 ## 类型 ### PrintRenderOptions {#printrenderoptions} 分页用到的注入项:预览元素的属性与 printDocument 的选项共用这一组。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `measurer?` | `TextMeasurer` | 文字度量器;缺省用所在文档的 Canvas 度量器(@xihan-ui/viz 的 createCanvasMeasurer),拿不到 2D 上下文时报 measurer.unavailable。 | | `expressionEngine?` | `ExpressionEngine` | 受限表达式引擎;缺省为 @xihan-ui/expression 的 createExpressionEngine()。 | | `formatter?` | `ValueFormatter` | 值格式化器;缺省为 @xihan-ui/expression 的 formatValue。 | | `locale?` | `string` | 交给格式化器的区域。 | | `timeZone?` | `string` | 交给格式化器的时区:`UTC`、`±HH:mm` 或 IANA 名称。 | | `maxSheets?` | `number` | 输出页总数上限,缺省同 @xihan-print/core 的 paginate。 | | `extensions?` | `readonly PrintRuntimeExtension[]` | 扩展:元素种类、渲染、编辑方式与规则,内置扩展自动排在最前。 | | `trace?` | `PrintTrace` | 分页与打印各环节的回调,给调试日志用,见 PrintTrace。 | ### PrintOptions {#printoptions} printDocument 的选项:分页注入项加上打印选项。 继承 `PaginateDocumentOptions`、`PrintSheetsOptions`。 ### PrintSheetsOptions {#printsheetsoptions} printSheets 的选项。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `document?` | `Document` | 发起打印的文档,缺省为全局 document;隐藏 iframe 挂在它的 body 上。 | | `title?` | `string` | 打印文档的标题,即打印对话框与另存 PDF 时的缺省文件名;缺省沿用发起打印的文档标题。 | | `extensions?` | `readonly PrintRuntimeExtension[]` | 扩展:扩展元素按它们给的渲染画出,与分页用同一份。 | | `trace?` | `PrintTrace` | 打印各环节的回调:报生成 DOM、等资源与送打印三个环节的起止,见 PrintTrace。 | ### PrintOutcome {#printoutcome} 一次打印的结果:送去打印的输出页数与分页诊断。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `sheets` | `number` | | | `diagnostics` | `readonly PaginationDiagnostic[]` | | | `via?` | `'client' \| 'browser'` | 送去打印的途径:client 为本地打印客户端,browser 为浏览器的打印对话框。 | ### PrintPreviewPrintOptions {#printpreviewprintoptions} print() 的选项:浏览器打印时的标题,以及交给本地打印客户端的打印选项。 继承 `Pick`、`ClientPrintOptions`。 ### PaginationErrorDetail {#paginationerrordetail} 分页失败的原因:core 的 PrintLayoutError 或运行时的 PrintRuntimeError。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `code` | `PrintLayoutErrorCode \| PrintRuntimeErrorCode` | | | `params` | `IssueParams` | | | `issues` | `readonly ValidationIssue[]` | 文档不合法时的校验问题。 | ### PrintPreviewTranslations {#printpreviewtranslations} 预览元素的文案。 | 属性 | 类型 | 说明 | | --- | --- | --- | | `label` | `string` | 预览区域的可访问名。 | | 方法 | 说明 | | --- | --- | | `sheetLabel(position: number, count: number): string` | 一张输出页的可访问名;position 从 1 开始,count 为输出页总数。 | ## 全部导出 ### designer-locale | 名称 | 种类 | 说明 | | --- | --- | --- | | `DEFAULT_PRINT_DESIGNER_TRANSLATIONS` | 常量 | 设计器的内建英文文案,与 printEnUS 里设计器的那份相同。 | ### extensions | 名称 | 种类 | 说明 | | --- | --- | --- | | `createPrintRuntimeModel` | 函数 | 合出运行时用的打印模型;同一元素类型在两个扩展里都给了渲染时抛 DesignerExtensionError。 | | `PrintElementRenderContext` | 接口 | 渲染扩展元素时拿到的上下文。 | | `PrintElementRenderer` | 类型 | 扩展元素的渲染:按布局盒建出画在元素框里的节点,元素框的位置、尺寸与只预览标记由运行时摆好。 | | `PrintRuntimeExtension` | 接口 | 打印运行时层的扩展:core 层的全部字段,加上扩展元素的渲染。 | | `PrintRuntimeModel` | 接口 | 运行时用的打印模型:core 的模型加上各元素类型的渲染。 | ### merge | 名称 | 种类 | 说明 | | --- | --- | --- | | `MergedPaginationDiagnostic` | 类型 | 合并结果里的一条诊断:原样带着,另记它来自第几份结果。 | | `MergedPaginationPart` | 接口 | 一份结果在合并结果里占的输出页。 | | `MergedPaginationResult` | 接口 | 合并后的分页结果。 | | `mergePaginationResults` | 函数 | 按顺序把几份分页结果接成一份。 | ### options | 名称 | 种类 | 说明 | | --- | --- | --- | | `PrintPhase` | 类型 | 打印的环节:paginate 分页,render 把输出页画成 DOM,resources 等图片与字体就绪(交给本地客户端时是把它们取来随任务带去),document 把输出页、资源与样式合成交给本地客户端的一份文档,print 交给浏览器的打印对话框。 | | `PrintRenderOptions` | 接口 | 分页用到的注入项:预览元素的属性与 printDocument 的选项共用这一组。 | | `PrintRuntimeError` | 类 | 运行时无法完成分页或打印时抛出的错误,`code` 与 `params` 供调用方判断与生成文案。 | | `PrintRuntimeErrorCode` | 类型 | 运行时无法完成分页或打印的原因:拿不到文字度量器、没有给模板。 | | `PrintTrace` | 接口 | 分页与打印各环节的回调,给调试日志用;不给就不报,结果照旧。 | ### print | 名称 | 种类 | 说明 | | --- | --- | --- | | `paginateDocument` | 函数 | 按数据排出模板,返回分页结果,不打印;缺省注入项与 printDocument 相同:度量器用 options.document(缺省全局 document)里的 Canvas 度量器,表达式引擎与格式化器同预览元素。 | | `PaginateDocumentOptions` | 接口 | paginateDocument 的选项:分页注入项,外加缺省度量器所在的文档。 | | `printDocument` | 函数 | 按数据排出模板并直接打印,不需要页面上有预览元素;分页同 paginateDocument。 | | `PrintOptions` | 接口 | printDocument 的选项:分页注入项加上打印选项。 | | `PrintOutcome` | 接口 | 一次打印的结果:送去打印的输出页数与分页诊断。 | | `printSheets` | 函数 | 把排好的输出页送去打印:在隐藏 iframe 里渲染(只在预览中显示的元素不画),纸张尺寸不同的输出页分到各自的具名 @page,带上源文档的字体,等样式表、图片与字体就绪后调起打印对话框,对话框关闭后移除 iframe。 | | `PrintSheetsOptions` | 接口 | printSheets 的选项。 | ### print-preview | 名称 | 种类 | 说明 | | --- | --- | --- | | `definePrintPreview` | 函数 | 注册 <xh-print-preview>;同一构造器重复注册不报错,没有 customElements 的环境跳过。 | | `PaginationErrorDetail` | 接口 | 分页失败的原因:core 的 PrintLayoutError 或运行时的 PrintRuntimeError。 | | `PRINT_PREVIEW_TAG` | 常量 | 预览元素的标签名。 | | `PrintPreviewEventMap` | 接口 | 预览元素派发的事件。 | | `PrintPreviewPrintOptions` | 接口 | print() 的选项:浏览器打印时的标题,以及交给本地打印客户端的打印选项。 | | `XhPrintPreview` | 类 | | ### render/boxes | 名称 | 种类 | 说明 | | --- | --- | --- | | `renderBox` | 函数 | 按布局盒的种类渲染一个元素;扩展元素按 renderers 里同一类型的渲染。 | ### render/sheets | 名称 | 种类 | 说明 | | --- | --- | --- | | `renderSheets` | 函数 | 把分页结果渲染进 container:先清空 container,再按顺序放入每张输出页,返回这些输出页元素。 | | `RenderSheetsOptions` | 接口 | renderSheets 的选项。 | | `sheetPageName` | 函数 | 纸张尺寸对应的 CSS 具名页面,尺寸相同的输出页共用一个名字。 | ### render/watermark | 名称 | 种类 | 说明 | | --- | --- | --- | | `renderWatermark` | 函数 | 按模板的水印生成覆盖整张输出页的一层;没有可印的文字时返回 undefined。 | ### translations | 名称 | 种类 | 说明 | | --- | --- | --- | | `PrintDesignerTranslations` | 接口 | 设计器的文案。 | | `PrintHistoryTranslations` | 接口 | 撤销历史里一步的说明:单个元素写「种类 名称」(见 elementLabel),几个元素写个数。 | | `PrintPreviewTranslations` | 接口 | 预览元素的文案。 | | `PrintTablePartKind` | 类型 | 表格编辑态里可以选中的部位。 | | `PrintTourStepKey` | 类型 | 新手引导里打印自己那几步的键。 | | `PrintTourStepTranslations` | 接口 | 新手引导一步的标题与说明。 | ## `@xihan-print/runtime/locale` 的导出 ### locale | 名称 | 种类 | 说明 | | --- | --- | --- | | `printDeDE` | 常量 | 打印的德语文案:预览、设计器与 designer-shell 零件。 | | `printEnUS` | 常量 | 打印的英文文案:预览、设计器与 designer-shell 零件。 | | `printEsES` | 常量 | 打印的西班牙语文案:预览、设计器与 designer-shell 零件。 | | `printFrFR` | 常量 | 打印的法语文案:预览、设计器与 designer-shell 零件。 | | `printJaJP` | 常量 | 打印的日语文案:预览、设计器与 designer-shell 零件。 | | `printKoKR` | 常量 | 打印的韩语文案:预览、设计器与 designer-shell 零件。 | | `printPtBR` | 常量 | 打印的巴西葡萄牙语文案:预览、设计器与 designer-shell 零件。 | | `printRuRU` | 常量 | 打印的俄语文案:预览、设计器与 designer-shell 零件。 | | `printZhCN` | 常量 | 打印的简体中文文案:预览、设计器与 designer-shell 零件。 | | `printZhTW` | 常量 | 打印的繁体中文文案:预览、设计器与 designer-shell 零件。 | --- 来源:https://print.docs.xihanfun.com/reference/schema # 模板类型 模板文档 `PrintDocument` 的全部类型,按 `@xihan-print/core` 的源码列出。当前 schema 版本 `PRINT_SCHEMA_VERSION` 为 `1`。 长度单位为 mm,字号与字间距单位为 pt,角度单位为度;颜色写作 `#RGB`、`#RGBA`、`#RRGGBB`、`#RRGGBBAA` 或 `transparent`。 ## 文档与页面 ### PrintSchemaVersion {#printschemaversion} 模板 schema 版本号类型。 类型:typeof PRINT_SCHEMA_VERSION ### PaperSpec {#paperspec} 纸张。宽高为纵向尺寸,单位 mm;横向时宽高互换。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `preset` | PaperPresetName | 可省 | 来源预设,自定义纸张不填。 | | `width` | number | 是 | | | `height` | number | 是 | | ### PageBand {#pageband} 页眉或页脚的一个带区。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `height` | number | 是 | 带区高度,单位 mm。 | | `elements` | PrintElement[] | 是 | 带区内的元素,坐标相对纸张左上角。 | ### PageBandSet {#pagebandset} 页眉或页脚:`default` 为常规带区,其余为按输出页命中的变体。命中顺序为首页、末页、奇偶页,都未命中时使用 `default`。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `default` | PageBand | 是 | | | `first` | PageBand | 可省 | | | `last` | PageBand | 可省 | | | `odd` | PageBand | 可省 | | | `even` | PageBand | 可省 | | | `singleSheet` | SingleSheetPreference | 是 | 输出页既是首页又是末页且两种变体都存在时采用哪一个。 | ### PageNumbering {#pagenumbering} 页码编号规则。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `continuous` | boolean | 是 | 是否接续上一页面模板的页码;为 false 或没有上一页面模板时从 `start` 开始。 | | `start` | number | 是 | 起始页码。 | ### Watermark {#watermark} 平铺文字水印。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `text` | string | 是 | 水印文字,支持 `{{path}}` 占位。 | | `fontFamily` | string | 可省 | | | `fontSize` | number | 是 | 字号,单位 pt。 | | `color` | Color | 是 | | | `opacity` | number | 是 | 不透明度,0 到 1。 | | `rotation` | number | 是 | 逆时针旋转角度。 | | `gapX` | number | 是 | 相邻水印的水平间距,单位 mm。 | | `gapY` | number | 是 | 相邻水印的垂直间距,单位 mm。 | ### PageGuides {#pageguides} 参考线:设计时对位用,不参与分页也不打印;坐标相对纸张左上角(按页面方向换算后的纸张),单位 mm。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `x` | number[] | 是 | 竖线的横坐标。 | | `y` | number[] | 是 | 横线的纵坐标。 | ### PrintPage {#printpage} 页面模板:一份页面模板按数据量排出一张或多张输出页。页眉位于上边距之下,页脚位于下边距之上,两者之间为正文区。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 是 | 页面标识,在整份文档内唯一。 | | `name` | string | 可省 | | | `paper` | PaperSpec | 是 | | | `orientation` | PageOrientation | 是 | | | `margins` | Edges | 是 | 页边距,单位 mm。 | | `header` | PageBandSet | 是 | | | `footer` | PageBandSet | 是 | | | `elements` | PrintElement[] | 是 | 正文元素,坐标相对纸张左上角。 | | `numbering` | PageNumbering | 是 | | | `watermark` | Watermark | 可省 | | | `guides` | PageGuides | 可省 | | | `meta` | JsonObject | 可省 | | | `extensions` | JsonObject | 可省 | 扩展自己的数据,键是扩展 id。 | ### PrintDocument {#printdocument} 打印模板文档。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `schemaVersion` | PrintSchemaVersion | 是 | | | `name` | string | 可省 | | | `pages` | PrintPage[] | 是 | | | `meta` | JsonObject | 可省 | | | `extensions` | JsonObject | 可省 | 扩展自己的数据,键是扩展 id。 | ## 元素 ### ElementBase {#elementbase} 全部元素共有的字段。坐标相对纸张左上角,单位 mm。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 是 | 元素标识,在整份文档内唯一。 | | `type` | TType | 是 | | | `name` | string | 可省 | 设计器中显示的名称。 | | `x` | number | 是 | | | `y` | number | 是 | | | `width` | number | 是 | | | `height` | number | 是 | | | `locked` | boolean | 可省 | 是否锁定,锁定的元素在设计器中不可拖动与缩放。 | | `previewOnly` | boolean | 可省 | 是否只在设计与预览中显示、不参与打印。 | | `meta` | JsonObject | 可省 | 宿主附加数据。 | | `extensions` | JsonObject | 可省 | 扩展自己的数据,键是扩展 id;core 只检查它是对象,里面不看。 | ### TextElement {#textelement} 固定尺寸的文本。 继承 ElementBase<'text'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `content` | TextContent | 是 | | | `style` | TextStyle | 是 | | | `wrap` | boolean | 是 | 是否自动换行。 | | `overflow` | TextOverflow | 是 | | | `rotation` | number | 是 | 绕中心顺时针旋转的角度。 | | `box` | BoxStyle | 可省 | | ### LongTextElement {#longtextelement} 随内容纵向延展并可跨页续排的长文本。 继承 ElementBase<'longText'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `content` | TextContent | 是 | | | `style` | TextStyle | 是 | | | `box` | BoxStyle | 可省 | | ### RichTextContent {#richtextcontent} 富文本的内容来源:模板里写死的文档,或从数据字段读取。字段值是 HTML 串时(source 为 html)按白名单换成文档,字段值已是富文本文档对象时 source 为 document。文档是 @xihan-ui/core/rich-text 的 RichTextDocument,其中行内字段的 format 按值格式解释。 类型:| { kind: 'static', document: RichTextDocument } | { kind: 'field', field: string, source: RichTextSource } ### RichTextElement {#richtextelement} 随内容纵向延展并可跨页续排的富文本:段落、标题、列表、分隔线与带样式的文字。 继承 ElementBase<'richText'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `content` | RichTextContent | 是 | | | `style` | TextStyle | 是 | 基准样式:块与行内的样式在它之上覆写。 | | `box` | BoxStyle | 可省 | | ### TableElement {#tableelement} 绑定数组数据、随行数延展并可跨页续排的表格。 继承 ElementBase<'table'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `dataField` | string | 是 | 数组数据的字段路径。 | | `columns` | TableColumn[] | 是 | | | `header` | TableHeader | 是 | | | `body` | TableBody | 是 | | | `footerRows` | TableFooterRow[] | 是 | | | `maxRowsPerPage` | number | 可省 | 每页最多显示的数据行数,缺省按可用高度排满。 | | `fillEmptyRows` | boolean | 是 | 数据行不足时是否用空行补齐到每页最大行数或可用高度。 | | `border` | TableBorder | 是 | | ### ImageElement {#imageelement} 图片。 继承 ElementBase<'image'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `source` | ImageSource | 是 | | | `fit` | ImageFit | 是 | | | `rotation` | number | 是 | | | `box` | BoxStyle | 可省 | | ### LineElement {#lineelement} 直线,走向由元素框和方向共同决定。 继承 ElementBase<'line'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `direction` | LineDirection | 是 | | | `stroke` | Stroke | 是 | | ### RectElement {#rectelement} 矩形。 继承 ElementBase<'rect'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `stroke` | Stroke | 可省 | 边线,缺省不画边。 | | `fill` | Color | 可省 | 填充色,缺省不填充。 | | `radius` | number | 是 | 圆角半径,单位 mm。 | | `rotation` | number | 是 | | ### EllipseElement {#ellipseelement} 椭圆,内切于元素框。 继承 ElementBase<'ellipse'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `stroke` | Stroke | 可省 | | | `fill` | Color | 可省 | | | `rotation` | number | 是 | | ### BarCodeTextStyle {#barcodetextstyle} 条码下方文字的样式。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `fontFamily` | string | 可省 | | | `fontSize` | number | 是 | 字号,单位 pt。 | ### BarCodeElement {#barcodeelement} 一维条码。 继承 ElementBase<'barCode'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `format` | BarCodeFormat | 是 | 码制,取值见 @xihan-ui/headless 的 BAR_CODE_FORMATS。 | | `content` | TextContent | 是 | | | `showText` | boolean | 是 | 是否在条码下方显示编码文字。 | | `textStyle` | BarCodeTextStyle | 是 | | | `color` | Color | 是 | | | `rotation` | number | 是 | | ### MatrixCodeElement {#matrixcodeelement} 二维码:QR、Data Matrix、PDF417、Aztec。 继承 ElementBase<'matrixCode'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `format` | MatrixCodeFormat | 是 | | | `content` | TextContent | 是 | | | `level` | MatrixCodeLevel | 可省 | 纠错级别:qr 为 L / M / Q / H,pdf417 为 0–8,aztec 为纠错码字至少占的百分比 5–95,data-matrix 没有级别;缺省取码制的默认值。 | | `color` | Color | 是 | | | `background` | Color | 可省 | | | `quietZone` | number | 是 | 四周静区宽度,单位为模块数。 | | `rotation` | number | 是 | | ### PageNumberElement {#pagenumberelement} 页码。 继承 ElementBase<'pageNumber'> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `format` | string | 是 | 页码格式:`{{path}}` 文本模板,可用的宿主变量为 `$page`(当前页码)与 `$pages`(所在编号段的总页数),如 `第 {{$page}} 页 / 共 {{$pages}} 页`。 | | `numeral` | NumeralStyle | 可省 | 页码的数字字形,即 @xihan-ui/expression 的 numeral 格式(罗马数字、中文数字);缺省为阿拉伯数字。 | | `style` | TextStyle | 是 | | | `box` | BoxStyle | 可省 | | ### ExtensionElement {#extensionelement} 扩展登记的元素:通用字段之外的属性由扩展的原型定义,取值都是 JSON。 继承 ElementBase<string> 的字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `` | JsonValue | undefined | 是 | | ### PrintElement {#printelement} 内置元素。 取其一:TextElement、LongTextElement、RichTextElement、TableElement、ImageElement、LineElement、RectElement、EllipseElement、BarCodeElement、MatrixCodeElement、PageNumberElement。 ### PrintElementType {#printelementtype} 内置元素类型。 类型:PrintElement['type'] ### PrintElementOf {#printelementof} 按类型取元素接口。 类型:Extract<PrintElement, { type: TType }> ## 表格 ### CellStyle {#cellstyle} 单元格样式。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `fontFamily` | string | 可省 | | | `fontSize` | number | 是 | 字号,单位 pt。 | | `bold` | boolean | 是 | | | `italic` | boolean | 是 | | | `color` | Color | 是 | | | `background` | Color | 可省 | | | `align` | HorizontalAlign | 可省 | 水平对齐,缺省跟随列的对齐方式。 | | `verticalAlign` | VerticalAlign | 是 | | | `padding` | number | 是 | 左右内边距,单位 mm。 | | `lineHeight` | number | 是 | 单元格文字每一行的高度,单位 mm。 | ### TableFieldCellContent {#tablefieldcellcontent} 表体单元格显示行内字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'field' | 是 | | | `field` | string | 是 | 行对象内的字段路径。 | | `format` | ValueFormat | 可省 | | | `emptyText` | string | 可省 | | ### TableTemplateCellContent {#tabletemplatecellcontent} 表体单元格按行对象替换 `{{path}}` 占位。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'template' | 是 | | | `template` | string | 是 | | ### TableRowNumberCellContent {#tablerownumbercellcontent} 表体单元格显示从 1 开始、跨页连续的行序号。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'rowNumber' | 是 | | ### TableExpressionCellContent {#tableexpressioncellcontent} 表体单元格以受限表达式计算,表达式中的变量为行对象的顶层字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'expression' | 是 | | | `expression` | string | 是 | 受限表达式源码。 | | `format` | ValueFormat | 可省 | 计算结果的格式。 | ### TableCellContent {#tablecellcontent} 表体单元格内容来源。 取其一:TableFieldCellContent、TableTemplateCellContent、TableRowNumberCellContent、TableExpressionCellContent。 ### TableColumn {#tablecolumn} 表格列。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 是 | 列标识,在所属表格内唯一。 | | `title` | string | 是 | 表头文本。 | | `width` | number | 是 | 列宽,单位 mm。 | | `align` | HorizontalAlign | 是 | | | `content` | TableCellContent | 是 | | | `cellType` | TableCellType | 是 | | | `barCodeFormat` | BarCodeFormat | 可省 | 条码码制,`cellType` 为 barCode 时必填;matrixCode 单元格按 QR 编码。 | ### TableHeader {#tableheader} 表头。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `visible` | boolean | 是 | | | `repeat` | boolean | 是 | 表格跨页时是否在每一页重复表头。 | | `height` | number | 是 | 表头行高,单位 mm。 | | `style` | CellStyle | 是 | | ### TableBody {#tablebody} 表体。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `rowHeight` | number | 是 | 行高,单位 mm;`rowHeightMode` 为 minimum 时作为最小行高。 | | `rowHeightMode` | TableRowHeightMode | 是 | | | `style` | CellStyle | 是 | | ### AggregateCellContent {#aggregatecellcontent} 表尾单元格的汇总内容。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'aggregate' | 是 | | | `fn` | AggregateFunction | 是 | | | `field` | string | 可省 | 参与汇总的行内字段路径;`fn` 为 count 时可省略,表示统计行数。 | | `scope` | AggregateScope | 是 | | | `format` | ValueFormat | 可省 | | ### TableFooterCellContent {#tablefootercellcontent} 表尾单元格内容:固定文本、根数据字段、根数据模板或汇总值。 取其一:TextContent、AggregateCellContent。 ### TableFooterCell {#tablefootercell} 表尾单元格。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `columnId` | string | 是 | 起始列的标识。 | | `colSpan` | number | 是 | 向右跨越的列数,至少为 1。 | | `content` | TableFooterCellContent | 是 | | | `align` | HorizontalAlign | 可省 | 水平对齐,缺省跟随行样式再跟随起始列。 | ### TableFooterRow {#tablefooterrow} 表尾行,可承载固定文字、根数据字段与汇总值。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 是 | 行标识,在所属表格内唯一。 | | `placement` | TableFooterPlacement | 是 | | | `height` | number | 是 | 行高,单位 mm。 | | `style` | CellStyle | 是 | | | `cells` | TableFooterCell[] | 是 | | ### TableBorder {#tableborder} 表格网格线。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `stroke` | Stroke | 是 | | | `outer` | boolean | 是 | 是否画外框。 | | `horizontal` | boolean | 是 | 是否画行之间的横线。 | | `vertical` | boolean | 是 | 是否画列之间的竖线。 | ## 通用类型 ### Color {#color} 颜色:`#RGB`、`#RGBA`、`#RRGGBB`、`#RRGGBBAA` 或 `transparent`。 类型:string ### Edges {#edges} 四边距离,单位 mm。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `top` | number | 是 | | | `right` | number | 是 | | | `bottom` | number | 是 | | | `left` | number | 是 | | ### Stroke {#stroke} 描边。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `width` | number | 是 | 线宽,单位 mm。 | | `style` | StrokeStyle | 是 | | | `color` | Color | 是 | | ### BorderSides {#bordersides} 四边边框,缺省的一边不画线。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `top` | Stroke | 可省 | | | `right` | Stroke | 可省 | | | `bottom` | Stroke | 可省 | | | `left` | Stroke | 可省 | | ### BoxStyle {#boxstyle} 元素外框样式。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `background` | Color | 可省 | 背景色,缺省为透明。 | | `border` | BorderSides | 可省 | | | `padding` | Edges | 可省 | 内边距,单位 mm。 | ### TextStyle {#textstyle} 文本样式。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `fontFamily` | string | 可省 | 字体族,缺省使用运行时默认字体。 | | `fontSize` | number | 是 | 字号,单位 pt。 | | `bold` | boolean | 是 | | | `italic` | boolean | 是 | | | `underline` | boolean | 是 | | | `lineThrough` | boolean | 是 | | | `color` | Color | 是 | | | `align` | TextAlign | 是 | | | `verticalAlign` | VerticalAlign | 是 | | | `lineHeight` | number | 是 | 行高,字号的倍数。 | | `letterSpacing` | number | 是 | 字间距,单位 pt。 | ### ValueMapEntry {#valuemapentry} 值映射的一项:数据值(按字符串比较)与显示文本,与选择类组件的选项同一形状(不带 disabled)。没有 label 时命中这一项显示为空。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `value` | string | 是 | | | `label` | string | 可省 | | ### MapFormat {#mapformat} 值映射格式:按数据值查表显示文本。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'map' | 是 | | | `entries` | ValueMapEntry[] | 是 | | | `fallback` | string | 可省 | 未命中任何映射时显示的文本,缺省显示原值。 | ### ExpressionFormat {#expressionformat} 表达式格式:以受限表达式把值转成显示文本,表达式中以 `value` 引用待格式化的值。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'expression' | 是 | | | `expression` | string | 是 | 受限表达式源码。 | ### ValueFormat {#valueformat} 值格式:数字、百分比、日期与人民币大写即 @xihan-ui/expression 的 FormatSpec,交给注入的格式化器;值映射在本包内完成,表达式格式经表达式接入点。 取其一:FormatSpec、MapFormat、ExpressionFormat。 ### StaticTextContent {#statictextcontent} 固定文本。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'static' | 是 | | | `text` | string | 是 | | ### FieldTextContent {#fieldtextcontent} 绑定单个数据字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'field' | 是 | | | `field` | string | 是 | 字段路径,写法同 @xihan-ui/core/json 的 formatJsonPath,如 `customer.name`、`items[0].qty`。 | | `format` | ValueFormat | 可省 | | | `emptyText` | string | 可省 | 字段值为空时显示的文本。 | ### TemplateTextContent {#templatetextcontent} 含 `{{path}}` 占位的文本模板,占位按数据字段替换。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'template' | 是 | | | `template` | string | 是 | | ### ExpressionTextContent {#expressiontextcontent} 以受限表达式计算的文本,表达式中的变量为数据对象的顶层字段。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'expression' | 是 | | | `expression` | string | 是 | 受限表达式源码。 | | `format` | ValueFormat | 可省 | 计算结果的格式。 | ### TextContent {#textcontent} 文本内容来源。 取其一:StaticTextContent、FieldTextContent、TemplateTextContent、ExpressionTextContent。 ### StaticImageSource {#staticimagesource} 固定地址的图片。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'static' | 是 | | | `url` | string | 是 | 图片地址:http(s)、data:image、blob 或相对地址。 | ### FieldImageSource {#fieldimagesource} 从数据字段读取图片地址。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `kind` | 'field' | 是 | | | `field` | string | 是 | | ### ImageSource {#imagesource} 图片来源。 取其一:StaticImageSource、FieldImageSource。 ## 取值 ### PageOrientation {#pageorientation} 页面方向:portrait 纵向,landscape 横向(宽高互换)。 取值见常量 `PAGE_ORIENTATIONS`: | 取值 | 中文名 | | --- | --- | | `portrait` | 纵向 | | `landscape` | 横向 | ### PageBandVariant {#pagebandvariant} 页眉页脚变体:first 首页、last 末页、odd 奇数页、even 偶数页(按页码奇偶)。 取值见常量 `PAGE_BAND_VARIANTS`: | 取值 | 中文名 | | --- | --- | | `first` | 首页 | | `last` | 末页 | | `odd` | 奇数页 | | `even` | 偶数页 | ### PageBandKey {#pagebandkey} 页眉页脚的全部带区键,`default` 为未命中任何变体时使用的带区。 类型:'default' | PageBandVariant ### TextAlign {#textalign} 文本水平对齐。 取值见常量 `TEXT_ALIGNS`: | 取值 | 中文名 | | --- | --- | | `left` | 左 | | `center` | 居中 | | `right` | 右 | | `justify` | 两端对齐 | ### HorizontalAlign {#horizontalalign} 单元格水平对齐。 取值见常量 `HORIZONTAL_ALIGNS`: | 取值 | 中文名 | | --- | --- | | `left` | 左 | | `center` | 居中 | | `right` | 右 | ### VerticalAlign {#verticalalign} 垂直对齐。 取值见常量 `VERTICAL_ALIGNS`: | 取值 | 中文名 | | --- | --- | | `top` | 上 | | `middle` | 居中 | | `bottom` | 下 | ### StrokeStyle {#strokestyle} 线型。 取值见常量 `STROKE_STYLES`: | 取值 | 中文名 | | --- | --- | | `solid` | 实线 | | `dashed` | 虚线 | | `dotted` | 点线 | | `double` | 双线 | ### TextOverflow {#textoverflow} 固定尺寸文本的溢出处理:clip 裁切,shrink 缩小字号以容纳,visible 允许溢出显示。 取值见常量 `TEXT_OVERFLOWS`: | 取值 | 中文名 | | --- | --- | | `clip` | 裁掉 | | `shrink` | 缩小字号 | | `visible` | 照常显示 | ### RichTextSource {#richtextsource} 富文本字段的内容格式:html 为 HTML 串,按白名单换成文档;document 为富文本文档对象。 取值见常量 `RICH_TEXT_SOURCES`: | 取值 | 中文名 | | --- | --- | | `html` | HTML | | `document` | 富文本文档 | ### ImageFit {#imagefit} 图片在元素框内的适配方式。 取值见常量 `IMAGE_FITS`: | 取值 | 中文名 | | --- | --- | | `contain` | 完整显示 | | `cover` | 铺满裁切 | | `fill` | 拉伸铺满 | | `none` | 原始大小 | ### LineDirection {#linedirection} 直线在元素框内的走向:horizontal 沿垂直中线,vertical 沿水平中线,diagonalDown 左上到右下,diagonalUp 左下到右上。 取值见常量 `LINE_DIRECTIONS`: | 取值 | 中文名 | | --- | --- | | `horizontal` | 水平 | | `vertical` | 竖直 | | `diagonalDown` | 左上到右下 | | `diagonalUp` | 左下到右上 | ### BarCodeFormat {#barcodeformat} 转导出自 `@xihan-ui/headless`。取值:`code128`、`ean13`、`ean8`、`upca`、`upce`、`itf14`、`code39`。 ### MatrixCodeFormat {#matrixcodeformat} 转导出自 `@xihan-ui/headless`。取值:`qr`、`data-matrix`、`pdf417`、`aztec`。 ### MatrixCodeLevel {#matrixcodelevel} 转导出自 `@xihan-ui/headless`,取值随元素说明。 ### AggregateFunction {#aggregatefunction} 表格汇总函数。 取值见常量 `AGGREGATE_FUNCTIONS`: | 取值 | 中文名 | | --- | --- | | `sum` | 求和 | | `avg` | 平均 | | `count` | 计数 | | `min` | 最小 | | `max` | 最大 | ### AggregateScope {#aggregatescope} 表格汇总范围:page 本页数据,cumulative 截至本页的累计数据,all 全部数据。 取值见常量 `AGGREGATE_SCOPES`: | 取值 | 中文名 | | --- | --- | | `page` | 本页 | | `cumulative` | 累计到本页 | | `all` | 全部 | ### TableFooterPlacement {#tablefooterplacement} 表尾行出现位置:everyPage 表格所跨的每一页,lastPage 仅表格结束的那一页。 取值见常量 `TABLE_FOOTER_PLACEMENTS`: | 取值 | 中文名 | | --- | --- | | `everyPage` | 每一页 | | `lastPage` | 只在最后一页 | ### TableRowHeightMode {#tablerowheightmode} 表体行高模式:fixed 每行固定为设定的行高,minimum 以设定的行高为下限、按单元格内容撑高。 取值见常量 `TABLE_ROW_HEIGHT_MODES`: | 取值 | 中文名 | | --- | --- | | `fixed` | 固定 | | `minimum` | 至少 | ### TableCellType {#tablecelltype} 表格列单元格的呈现类型。 取值见常量 `TABLE_CELL_TYPES`: | 取值 | 中文名 | | --- | --- | | `text` | 文字 | | `image` | 图片 | | `barCode` | 条码 | | `matrixCode` | 二维码 | ### SingleSheetPreference {#singlesheetpreference} 同一输出页既是首页又是末页时优先采用的变体。 取值见常量 `SINGLE_SHEET_PREFERENCES`: | 取值 | 中文名 | | --- | --- | | `first` | 首页 | | `last` | 末页 |