数据绑定
Designer 里的数据绑定,本质上是在做一件很朴素的事:把模板里的某个属性,连到运行时数据里的某条路径上。
如果你先把“设计时字段树”和“运行时数据”分开理解,这一层就不会绕。
字段树定义
Designer 接收的是 DataSourceDescriptor[]。它决定左侧数据源面板长什么样,也决定用户能拖哪些字段。
import type { DataSourceDescriptor } from '@easyink/designer'
const dataSources: DataSourceDescriptor[] = [
{
id: 'order',
name: '订单数据',
fields: [
{ name: 'orderNo', path: 'orderNo', title: '订单号', use: 'text' },
{
name: 'customer',
path: 'customer',
title: '客户',
fields: [
{ name: 'name', path: 'customer/name', title: '客户名称', use: 'text' },
{ name: 'phone', path: 'customer/phone', title: '联系电话', use: 'text' },
],
},
{
name: 'items',
path: 'items',
title: '商品列表',
tag: 'collection',
fields: [
{ name: 'name', path: 'items/name', title: '商品名称', use: 'text' },
{ name: 'qty', path: 'items/qty', title: '数量', use: 'text' },
],
},
],
},
]再把它传给 Designer:
<EasyInkDesigner
v-model:schema="schema"
:data-sources="dataSources"
/>上面这一步只是把字段树交给 Designer。它还没有产生任何运行时值。
哪些字段能拖拽
当前 Designer 的字段树有一个明确规则:叶子字段可以拖拽;带 union 的分组字段也可以拖拽。
const dataSources: DataSourceDescriptor[] = [
{
id: 'order',
name: '订单数据',
fields: [
{
name: 'items',
path: 'items',
title: '商品列表',
fields: [
{ name: 'name', path: 'items/name', title: '商品名称', use: 'text' },
],
},
{
name: 'summary',
path: 'summary',
title: '摘要组合',
fields: [
{ name: 'orderNo', path: 'orderNo', title: '订单号', use: 'text' },
{ name: 'grandTotal', path: 'grandTotal', title: '合计金额', use: 'text' },
],
union: [
{ path: 'orderNo', title: '订单号', use: 'text' },
{ path: 'grandTotal', title: '合计金额', use: 'text', offsetY: 8 },
],
},
],
},
]上面这个例子里,items 只是分组节点,默认负责展开子字段;summary 虽然也可以带子语义,但因为配置了 union,Designer 会允许它作为“一次拖拽生成多个绑定节点”的入口。
拖拽处理
先别急着想 Viewer。用户在画布里拖一个字段时,Designer 做的其实是把绑定信息写进模板。
可以把过程理解成这样:
字段树里的一个 field
-> 拖到元素上
-> Designer 把 binding 写进对应节点
-> schema 持久化下来这也是为什么绑定是模板的一部分,而不是运行时的一次性状态。
fieldPath 约定
到了运行时,Viewer 会统一解析节点上的绑定。
这里最重要的结论是:Viewer 按 fieldPath 从传入的 data 根对象里取值,不会再去看 Designer 的字段树。
先看一个最小心智模型:
const data = {
customer: {
name: 'Ada',
},
}如果节点上的绑定路径是 customer/name,Viewer 就会去读这条路径对应的值。
你可能会注意到绑定里还有 sourceId、sourceName 之类的信息。它们有用,但作用主要在设计时元数据和界面提示,不参与运行时根数据选择。
fieldPath 使用 / 分隔。解析时会从运行时 data 根对象开始逐段读取,也会避开 __proto__、constructor 这类危险路径。
稳定绑定约定
写模板时,Designer 关心的是:
- 字段树怎么展示
- 字段拖到哪里
- 绑定信息怎么写回 Schema
渲染模板时,Viewer 关心的是:
- 当前节点有哪些绑定
- 这些绑定的
fieldPath指向哪里 - 运行时
data里能不能取到值
这意味着一件很重要的事:预览、打印和导出时,不要再把 dataSources 传给 Viewer。
常用字段
第一次接入时,最常用的是这些:
| 字段 | 作用 |
|---|---|
name | 内部名称 |
path | 绑定路径 |
title | 面板显示名称 |
fields | 子字段 |
use | 推荐的物料用途 |
tag | 集合等语义提示 |
你当然还能继续往里放 format、bindIndex、union 之类更细的能力,但如果你现在只是先把绑定跑通,这些基础字段就足够了。
默认显示格式
如果某个字段每次拖出去都应该按同一种方式展示,可以直接在字段上写 format。
const dataSources: DataSourceDescriptor[] = [
{
id: 'invoice',
name: '发票数据',
fields: [
{
name: 'grandTotal',
path: 'grandTotal',
title: '合计金额',
use: 'text',
format: {
prefix: '¥',
fallback: '--',
mode: 'preset',
preset: {
type: 'number',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
},
},
],
},
]用户把 grandTotal 拖到文本、表格单元格或其他可绑定物料上时,Designer 会把这份格式复制到绑定里。
这里的关键词是“复制”。后面用户在属性面板里改显示格式,改的是模板里的 BindingRef.format,不会反写你的字段树。
字段函数模板
有时你不想直接给字段写死 format,而是想改变“自定义函数”里的默认示例。比如金额字段默认给一个金额函数,日期字段默认给一个中文日期函数。
这时把模板函数放到具体字段的 displayFormat.customTemplates 里:
const dataSources: DataSourceDescriptor[] = [
{
id: 'invoice',
name: '发票数据',
fields: [
{
name: 'grandTotal',
path: 'grandTotal',
title: '合计金额',
use: 'text',
displayFormat: {
defaultCustomTemplateId: 'invoice-money',
customTemplates: [
{
id: 'invoice-money',
label: '发票金额',
hint: '金额保留两位小数并添加人民币符号',
source: `function transform(value, data) {
var num = Number(value)
if (isNaN(num)) return ''
return '¥' + num.toFixed(2)
}`,
},
{
id: 'invoice-money-total',
label: '合计金额',
hint: '金额保留两位小数并加上合计前缀',
source: `function transform(value, data) {
var num = Number(value)
if (isNaN(num)) return ''
return '合计 ¥' + num.toFixed(2)
}`,
},
],
},
},
{
name: 'date',
path: 'invoice/date',
title: '开票日期',
use: 'text',
displayFormat: {
defaultCustomTemplateId: 'invoice-date-cn',
customTemplates: [
{
id: 'invoice-date-cn',
label: '中文开票日期',
source: `function transform(value, data) {
if (value == null || value === '') return ''
var text = String(value)
var match = text.match(/^(\\d{4})-(\\d{2})-(\\d{2})/)
if (!match) return text
return match[1] + '年' + match[2] + '月' + match[3] + '日'
}`,
},
],
},
},
],
},
]用户选中 grandTotal 这个字段产生的绑定,再打开“显示格式”,切到“自定义”时,默认示例会被字段自己的函数替换。字段可以只有一个模板,也可以有多个模板;有多个时,Designer 会优先采用 defaultCustomTemplateId 指向的那一个。自定义函数接收两个参数:value 是当前字段值,data 是当前 Viewer 正在消费的完整运行时数据。
原来的内置示例不会全部消失。字段模板只替换“默认转换函数”这一类入口,后面的“原始值转字符串”“数值格式化为货币”“日期格式化 YYYY-MM-DD”等示例仍然保留,用户随时可以切回去。
你可能会有个疑问:为什么保存的是函数内容,而不是模板 ID?
原因是模板要能脱离 Designer 和字段树独立渲染。用户点确定后,最终写进 Schema 的仍然是 BindingRef.format.custom.source。之后字段树里的模板函数变了,已经保存的模板不会被悄悄改写。
在线试一下
Playground 默认示例里的“流式发票”已经给发票金额、开票日期、税率这些字段加了各自的函数模板。打开示例后选中对应绑定字段,在属性面板里进入“显示格式”并切到“自定义”,就能看到字段自己的默认示例,同时还能看到其余内置示例。
绑定判断标准
当你调试绑定问题时,可以按这个顺序查:
- Designer 里字段树是不是正确展示了。
- 拖拽之后,Schema 里是不是写进了绑定信息。
- 运行时传给 Viewer 的
data,是不是能命中相同的fieldPath。
如果第三步对不上,再怎么改 Designer 侧字段树都不会生效。