跳转到内容

数据绑定与格式化 ​

模板只描述「哪里显示什么数据、显示成什么样」,数据在打印时才交给预览或打印函数。数据是任意 JSON,通常就是业务接口返回的对象。

字段路径 ​

字段路径按点与下标读数据:

写法读到
customer.namedata.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。表达式的结果是单个值,可以再按格式显示。

富文本的内容不是文本内容,另有三种来源,见富文本。

下面这张模板的每一行,右边都是同一份数据按不同来源与格式显示的结果:

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 的格式化。值格式不合法时校验报错,数据不合格式时显示原值并给出诊断 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 的受限表达式。
  • 表达式写错时校验报错 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 取本页、截至本页或全部数据行,见模板结构。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 事件里带上诊断,全部问题码见问题码与诊断。

Released under The MIT License