Skip to content

Schema 参考

Schema 是 EasyInk 的模板模型。Designer、Viewer、打印和导出最终都围绕同一份 Schema 工作。

先看一个最小输入:

ts
import { normalizeDocumentSchema, validateSchemaIssues } from '@easyink/schema'

const schema = normalizeDocumentSchema({
  page: { width: 80 },
  elements: [],
})

const issues = validateSchemaIssues(schema)

归一化后,运行时拿到的是完整 DocumentSchema。宿主可以传宽松输入,但内部消费前应该先归一化。

输入和内部模型

宿主通常传 DocumentSchemaInput

ts
const input = {
  page: {
    mode: 'continuous',
    width: 80,
    height: 200,
  },
}

const schema = normalizeDocumentSchema(input)

DocumentSchemaInput 允许省略很多顶层字段。normalizeDocumentSchema() 会补齐:

  • version
  • unit
  • page
  • guides
  • elements
  • 页面模式相关的 pageModellayoutpaginationreflow

注意

version 不属于宿主可指定输入。归一化会写入当前 Schema 版本。

页面模式默认值

page.mode 会决定一组页面层默认配置:

ts
const fixed = normalizeDocumentSchema({
  page: { mode: 'fixed', width: 210, height: 297 },
})

const continuous = normalizeDocumentSchema({
  page: { mode: 'continuous', width: 80, height: 200 },
})

当前默认值是:

modepageModellayoutpaginationreflow
fixedpaged-paperabsolutefixed-sheetsmeasure-only
continuouscontinuous-paperstack-flownoneflow-y

这就是为什么连续纸不能只改宽高。它还会切换布局、分页和回流策略。

页面级渲染层

page.layers 保存整页级、非元素级的渲染层。它不是物料节点,也不会出现在 schema.elements 里。当前内置支持文字水印层:

json
{
  "page": {
    "mode": "fixed",
    "width": 210,
    "height": 297,
    "layers": [
      {
        "id": "page-watermark",
        "kind": "watermark",
        "type": "text",
        "enabled": true,
        "placement": "over-content",
        "zIndex": 0,
        "text": "DRAFT",
        "rotation": -30,
        "opacity": 0.1,
        "fontSize": 18,
        "gap": 60,
        "color": "#b8b8b8"
      }
    ]
  }
}

字段规则:

  • placement 可选 under-contentover-contenttop,分别位于内容层下方、内容层上方和最上层。
  • zIndex 只在所属 placement band 内排序,合法范围是 0..999
  • fontSizegap 跟随 schema.unit
  • opacity0..1

page.layers 适合文字水印这类整页装饰。如果你需要可选中、可拖拽、可绑定数据或可作为空白页保留条件的页眉、页脚、Logo、页码,请使用普通物料节点,并在需要每页出现时设置 node.repeat.scope='every-output-page'

校验和序列化

本地保存、导入导出和服务端接收模板时,优先用 schema 包里的入口:

ts
import {
  deserializeSchema,
  isValidSchema,
  serializeSchema,
  validateSchemaIssues,
} from '@easyink/schema'

const json = serializeSchema(schema)
const restored = deserializeSchema(json)

if (!isValidSchema(restored)) {
  console.warn(validateSchemaIssues(restored))
}

常用能力包括:

  • validateSchema():返回字符串错误数组。
  • validateSchemaIssues():返回结构化问题,包含 pathmessagecode
  • isValidSchema():类型守卫。
  • serializeSchema() / deserializeSchema():处理持久化 JSON。

外部输入解码

外部输入不要在业务层手写字段映射。先交给 codec:

ts
import { decodeBenchmarkInput } from '@easyink/schema'

const schema = decodeBenchmarkInput(input)

Schema codec 里包含 benchmark 输入的解码逻辑。普通 EasyInk JSON 仍然用 deserializeSchema(json)

元素节点

最常见的元素节点长这样:

json
{
  "id": "text-1",
  "type": "text",
  "x": 20,
  "y": 20,
  "width": 170,
  "height": 10,
  "props": {
    "content": "Hello EasyInk",
    "fontSize": 24,
    "fontFamily": "sans-serif"
  }
}

type 决定用哪个物料渲染,x/y/width/height 决定画布几何,props 保存物料自己的属性。

支持条件能力的物料还可以在节点根部保存 renderCondition。它会影响布局和分页,因此不属于物料私有 props。完整规则和运行语义见 条件渲染

自定义物料也走同一套结构:

ts
const node = {
  id: 'price-tag-1',
  type: 'price-tag',
  x: 20,
  y: 20,
  width: 48,
  height: 18,
  props: {
    label: '价格',
    amount: '¥ 99.00',
  },
}

只要 Designer 和 Viewer 都注册了同一个 type,这份节点就能被两边识别。

数据绑定字段

元素可以保存普通绑定引用。普通绑定适合 text / image / barcode / qrcode 这类“一个字段投影到一个 props”的物料:

json
{
  "id": "total-text",
  "type": "text",
  "x": 20,
  "y": 20,
  "width": 60,
  "height": 10,
  "props": { "content": "" },
  "binding": {
    "sourceId": "invoice",
    "fieldPath": "summary/total",
    "fieldLabel": "合计"
  }
}

Designer 负责把绑定写进节点。Viewer 在 open({ schema, data }) 时解析绑定,然后把结果交给物料渲染器。

结构化物料可以使用 data-contract binding。它不是普通绑定的替代品,而是给 chart-bar 这类物料描述目标数据模型和 source 映射:

json
{
  "id": "sales-chart",
  "type": "chart-bar",
  "x": 20,
  "y": 40,
  "width": 120,
  "height": 70,
  "props": {
    "barColor": "#2563eb",
    "backgroundColor": "#ffffff"
  },
  "binding": {
    "kind": "data-contract",
    "mappings": {
      "category": {
        "sourceId": "report",
        "select": { "path": "monthlySales/month", "label": "月份" }
      },
      "value": {
        "sourceId": "report",
        "select": { "path": "monthlySales/revenue", "label": "销售额" }
      }
    },
    "relation": { "kind": "auto" }
  }
}

mappings 的 key 是物料目标字段 id,不是源数据字段名。select.path 保存完整 source path;Resolver 在运行时推导共享集合或 index 对齐,不把 mode 写入 binding。

绑定格式配置保存在 BindingRef.formatDataContractFieldMapping.format。属性面板可编辑哪些格式 tab 不写入 Schema,而由物料注册时的 binding.formatEditor 声明;例如文本开放 preset/custom,自定义 SVG 与 chart 类物料通常只开放 custom

当前 MaterialNode.binding 的规范形态是:

ts
type MaterialBinding = BindingRef | BindingRef[] | DataContractBinding

interface DataContractBinding {
  kind: 'data-contract'
  mappings: Record<string, DataContractFieldMapping>
  relation?: { kind: 'auto' } | { kind: 'record' } | { kind: 'index' }
}

普通文本类旧 schema 仍然有效;只有声明了 binding.kind='data-contract' 的物料才会使用 DataContractBinding

表格节点

表格节点在 MaterialNode 外多了 table 属性:

ts
interface TableNode extends MaterialNode {
  type: 'table-static' | 'table-data'
  table: TableSchema
}

interface TableSchema {
  kind: 'static' | 'data'
  topology: TableTopologySchema
  layout: TableLayoutConfig
  diagnostics?: LayoutDiagnostic[]
}

单元格绑定保存在 cell 上:

ts
interface TableCellSchema {
  rowSpan?: number
  colSpan?: number
  content?: {
    text?: string
    elements?: MaterialNode[]
    editMode?: 'inline-text' | 'rich-text' | 'hosted'
  }
  props?: Record<string, unknown>
  binding?: BindingRef
  staticBinding?: BindingRef
}

table-data 通常用 binding 表示数据行字段。table-static 可以用 staticBinding 表示独立单元格绑定。

动画字段

动画挂在元素的可选字段里:

ts
interface AnimationSchema {
  trigger: string
  type: string
  duration?: number
  delay?: number
  options?: Record<string, unknown>
}

目前先把它理解成“元素上的播放配置”就够了。不同运行时可以按自己的能力消费这份配置。

工具函数

常用工具可以按用途记:

ts
import {
  createDefaultGuides,
  createDefaultPage,
  createDefaultSchema,
  getNodeProps,
  isCompatibleVersion,
  isTableDataNode,
  isTableNode,
  normalizeDocumentSchema,
  validateSchemaIssues,
} from '@easyink/schema'

它们大致分三类:

  • 默认值:createDefaultSchema()createDefaultPage()createDefaultGuides()
  • 类型和访问:isTableNode()isTableDataNode()getNodeProps<T>()
  • 校验和归一化:validateSchemaIssues()normalizeDocumentSchema()

完整示例

一份固定 A4 模板可以这样写:

json
{
  "version": "1.0.0",
  "unit": "mm",
  "page": {
    "mode": "fixed",
    "width": 210,
    "height": 297,
    "pageModel": {
      "kind": "paged-paper",
      "paper": { "width": 210, "height": 297 }
    },
    "layout": { "strategy": "absolute" },
    "pagination": { "strategy": "fixed-sheets" },
    "reflow": { "strategy": "measure-only" }
  },
  "guides": { "x": [], "y": [] },
  "elements": [
    {
      "id": "text-1",
      "type": "text",
      "x": 20,
      "y": 20,
      "width": 170,
      "height": 10,
      "props": {
        "content": "Hello EasyInk",
        "fontSize": 24,
        "fontFamily": "sans-serif"
      }
    }
  ]
}

如果你手写的是宽松输入,不必把所有默认层都写出来。交给 normalizeDocumentSchema() 补齐即可。

关于 Schema,目前知道这些就够用了。自定义物料如何保存自己的节点,可以继续看 自定义物料开发