Skip to content

Schema 与数据模型

Schema 不只是“组件配置列表”。它同时描述字段如何显示、数据存在哪里、初始模型是什么形状、如何校验,以及字段之间如何联动。SuperForm 会据此建立数据模型,再让输入、只读展示和页面组件共享同一份业务定义。

本章按“配置声明 → 字段路径 → 数据绑定”的顺序展开。第一次阅读建议顺序浏览;查阅具体能力时可直接使用右侧目录定位 fieldlabelFieldendFielddataSource 或字段级 Ref。

text
Schema 声明
├─ 结构:字段、容器、数组
├─ 视图:label、type、attrs、布局
├─ 模型:field、initialValue、关联字段
├─ 行为:rules、事件、响应式配置
└─ 场景:表单、表格、详情

这种设计的价值是:新增一个业务字段时,通常只需在 Schema 中补充一次定义,而不必分别维护表单控件、模型初始化、校验规则和只读文案。

从一项声明到完整行为

下面是一项典型字段配置:

ts
{
  type: 'Input',
  field: 'profile.name',
  label: '姓名',
  required: true,
  initialValue: '',
  span: 12,
  attrs: {
    maxlength: 30,
    allowClear: true,
  },
}

运行时它会同时产生以下结果:

  • type: 'Input' 选择文本输入控件。
  • field: 'profile.name' 建立 profile.name 数据路径;中间对象缺失时自动补齐。
  • label: '姓名' 生成表单标签,并为 Input 推导“请输入姓名”占位提示。
  • required: true 生成必填标识和默认必填规则,提示为“姓名不能为空!”。
  • initialValue: '' 进入 Schema 标准初始模型,供初始化和重置使用。
  • span: 12 让字段在 24 栅格中占半行。
  • attrs 继续传给底层 Ant Design Vue Input。

可以把其中的自动推导理解成下面的等价展开:

ts
{
  label: '姓名',
  required: true,
  attrs: {
    // 未显式填写 placeholder 时,Input 会根据 label 自动生成
    placeholder: '请输入姓名',
  },
  rules: [
    // required: true 会根据 label 自动生成默认提示
    { required: true, message: '姓名不能为空!' },
  ],
}

这段代码用于解释运行效果;组件不会改写传入的原始 Schema。显式配置始终优先,例如 attrs.placeholder: '填写联系人姓名' 或自定义 rules.message 会覆盖默认文案。

不同字段使用符合交互习惯的提示语:Input、Textarea、InputNumber、AutoComplete 默认使用“请输入…”,Select、TreeSelect 默认使用“请选择…”。各类型的默认值与专属配置见基础输入选择输入

根节点、容器与字段

text
SuperForm 根 Schema
└─ subItems
   ├─ 字段节点:绑定一个值
   ├─ 容器节点:组织一组 subItems
   └─ 数组节点:用 columns 描述数组元素

根节点对应 SuperForm,负责数据源、提交、重置和整体布局。它本身不是字段类型,也不需要在 subItems 中再写一个 Form 容器。

节点的角色由结构配置决定:

节点主要结构数据结果典型类型
普通字段field标量、对象或组件约定值Input、Select、Upload
对象容器subItems默认补为对象Group、Card、Tabs
数组容器columns默认补为空数组InputList、ListGroup、Table
辅助节点无需 field不进入提交模型InfoSlot、Buttons

完整的容器差异见布局容器,数组数据结构见数组与表格

配置分层

业务语义写在节点顶层,底层组件能力写在 attrs 中:

ts
{
  // SuperForm 识别的业务配置
  type: 'Input',
  field: 'name',
  label: '名称',
  required: true,
  hidden: ({ current }) => current.archived,
  span: 12,

  // 交给 Ant Design Vue Input 的属性
  attrs: {
    maxlength: 50,
    allowClear: true,
  },
}

不要把 fieldrequiredspanhidden 等 Schema 能力放进 attrs。反过来,底层组件的 allowClearmaxlengthmode 等属性也应留在 attrs,这样 Schema 层与 UI 组件层的职责清晰。

一份字段,多种页面场景

同一字段定义可以用于表单、表格和详情。exclude 用来声明不适用的场景:

ts
{
  type: 'Hidden',
  field: 'id',
  exclude: ['table', 'description'],
}

可用值为:

  • form:不进入编辑表单。
  • table:不生成表格列。
  • description:不进入详情展示。

需要回显和提交、但不应显示的主键或上下文字段,建议声明为 Hidden,不要只把它留在外部对象中。

何时拆分 Schema

优先维护一份共享字段定义;当不同页面的业务语义已经不同,再按场景拆分。例如列表中的“状态”可能只读并支持筛选,编辑页中的“状态”可能需要权限联动,此时可以共享基础字段后再组合:

ts
const statusField = {
  type: "Select",
  field: "status",
  label: "状态",
  options: statusOptions,
};

const editStatus = {
  ...statusField,
  required: true,
  disabled: ({ formData }) => !formData.canEditStatus,
};

这样保留字段名、选项和值语义的一致性,又不会强行把所有场景塞进大量条件函数。

类型辅助

ts
import { defineDetail, defineForm, defineTable } from "antdv-superform";

const schema = defineForm({
  subItems: [{ type: "Input", field: "name", label: "名称" }],
});

类型辅助函数只约束输入并改善编辑器提示,不改变运行时结果。动态 Schema 仍可以使用函数或异步函数交给相应组合函数。

下面继续从字段路径和数据绑定两个角度展开模型细节。前者解释 Schema 如何确定数据坐标与结构,后者解释业务对象如何成为当前模型并参与重置、提交和双向同步。

字段与数据路径

Schema 与数据模型是相辅相成的:Schema 决定模型应具备的结构,数据源提供当前业务值;模型变化又会驱动控件、校验、联动和只读展示。理解 field,就理解了整个系统的数据坐标。

field 是模型中的地址

field 表示当前节点在所属模型中的存储路径,支持点路径:

ts
{
  type: 'Input',
  field: 'profile.name',
  label: '姓名',
}

即使数据源最初是空对象,组件也会按 Schema 建立中间结构:

ts
const dataSource = {
  profile: {
    name: undefined,
  },
};

因此 field 不只是取值表达式,它还参与:

  • 建立初始模型结构。
  • 生成 Ant Design Vue FormItem 的校验路径。
  • 确定 effectData.fieldeffectData.value
  • 决定 setFieldsValueresetFields 能更新哪些字段。
  • 在表格和详情中读取对应单元格内容。

字段路径应保持稳定。不要在一次表单生命周期中动态改变同一节点的 field;业务条件变化应使用 hiddendisabled 或切换整份 Schema。

模型怎样被建立

每个节点会按下面的优先级确定初始值:

text
initialValue
  ↓ 未提供
value
  ↓ 未提供
columns ? [] : subItems ? {} : undefined

例如:

ts
const schema = {
  subItems: [
    { type: "Input", field: "name", initialValue: "" },
    {
      type: "Group",
      field: "address",
      subItems: [{ type: "Input", field: "city" }],
    },
    {
      type: "InputList",
      field: "contacts",
      columns: [{ type: "Input", field: "mobile" }],
    },
  ],
};

对应的标准初始模型为:

ts
{
  name: '',
  address: {
    city: undefined,
  },
  contacts: [],
}

数组和对象初始值建议使用函数,避免多次创建表单时共享同一引用:

ts
{
  type: 'InputList',
  field: 'contacts',
  initialValue: () => [{ name: '', mobile: '' }],
  columns: [
    { type: 'Input', field: 'name', label: '联系人' },
    { type: 'Input', field: 'mobile', label: '手机号' },
  ],
}

相对路径与嵌套上下文

进入带 field 的对象容器后,子项路径相对于该对象:

ts
{
  type: 'Group',
  field: 'receiver',
  subItems: [
    { type: 'Input', field: 'name', label: '收件人' },
    { type: 'Input', field: 'mobile', label: '手机号' },
  ],
}

最终路径分别是 receiver.namereceiver.mobile。在子字段回调中:

  • currentreceiver 对象。
  • formData 始终是根表单对象。
  • parent 指向上一级响应上下文,而不是简单的数据对象副本。

数组的 columns 同样使用相对路径,每一行都会建立独立字段模型,并提供 indexrecord。详见数组与表格

一个控件绑定多个字段

有些交互展示为一个控件,但业务模型需要保存多个值。SuperForm 用关联字段显式表达这种关系。

labelField:同时保存值与显示文本

ts
{
  type: 'Select',
  field: 'departmentId',
  labelField: 'departmentName',
  label: '部门',
  options: departmentOptions,
}

选中后模型形态为:

ts
{
  departmentId: 12,
  departmentName: '研发中心',
}

field 保存提交值,labelField 保存显示文本。表格和详情的只读渲染也会优先读取 labelField,这能避免只有 ID 时再次查字典。支持范围、选项归一化和 labelAsValue 的关系见选择输入:通用选项

endField:把范围拆成两个业务字段

ts
{
  type: 'DateRange',
  field: 'startDate',
  endField: 'endDate',
  label: '有效期',
}

控件仍接收 [start, end],模型则保存为:

ts
{
  startDate: '2026-08-01',
  endDate: '2026-08-31',
}

回显时组件会重新把两个字段组合成范围值;只读模式显示为“开始值 - 结束值”。不配置 endField 时,范围字段也可以保存为数组或通过 stringifyValue 保存为逗号字符串。详见日期与时间:DateRange 值模式

vModelFields:扩展额外 v-model

ts
{
  type: 'ExtAddressPicker',
  field: 'districtCode',
  vModelFields: {
    provinceCode: 'provinceCode',
    cityCode: 'cityCode',
  },
}

键是扩展组件的 v-model 参数名,值可以是当前对象中的字段名或外部 Ref。适用于一个组件同时更新多个业务字段,具体契约见注册自定义字段:多个 v-model

无 field 的节点

并非每个节点都要进入模型:

  • InfoSlotButtons 等辅助节点通常没有 field
  • 只有 value: someRef、没有 field 的输入控件会直接绑定该 Ref,但不会进入表单提交模型。
  • 容器可以不设 field,此时只组织布局,子字段仍绑定当前对象。

需要提交或回显但不显示的值,应使用 Hidden 明确声明:

ts
{ type: 'Hidden', field: 'id' }

Hidden 不生成可见控件,但会让 id 成为标准模型的一部分,并参与重置和提交。详见展示与辅助:Hidden

表格列路径

表格列也使用 field 读取记录,支持点路径。未声明 type 的列按只读文本列处理;需要进入 Table 容器的行内编辑或弹窗表单时,列必须声明有效字段类型。

页面级 SuperTable 负责独立数据源、查询和 API 绑定;字段级 Table 数组容器 负责模型内部数组的显示与编辑,两者的数据边界不同。

数据源与双向绑定

SuperForm 的数据模型由两部分共同决定:Schema 给出稳定的结构和标准初始值,dataSource 给出本次业务记录。这样无论新增空记录、编辑不完整记录,还是切换到另一条记录,表单始终知道应有哪些字段以及如何重置。

建模与绑定顺序

内部流程可以概括为:

text
1. 从空对象开始
2. 按 Schema 建立完整字段结构
3. 克隆为“标准初始模型”
4. 读取并绑定 dataSource
5. 按 Schema 补齐 dataSource 缺失字段
ts
const record = ref({ id: 1, name: "张三" });

const [register, form] = useForm({
  dataSource: record,
  subItems: [
    { type: "Hidden", field: "id" },
    { type: "Input", field: "name", label: "姓名", initialValue: "" },
    { type: "Switch", field: "enabled", label: "启用" },
  ],
});

绑定后 record.value 会具备:

ts
{
  id: 1,
  name: '张三',
  enabled: undefined,
}

也就是说,传入对象不是只读快照,而是当前表单模型本身;用户输入和 Schema 补齐都会反映到该对象。若业务需要保留原始记录,应在传入前自行克隆。

对象与 Ref 的差异

dataSource 可以是普通对象或 Ref:

ts
// 固定绑定一个响应式对象
dataSource: reactive({ name: "" });

// 支持整体切换记录
dataSource: currentRecord;

Ref 会被整体解包。当 currentRecord.value 指向新对象时,SuperForm 清除当前校验状态并切换模型,随后按 Schema 补齐新对象缺失的字段:

ts
currentRecord.value = { id: 2, name: "李四" };

这适合弹窗复用同一个表单编辑多条记录。useForm 只接收 Schema;不要使用旧式 useForm(schema, record),外部对象统一通过 Schema 的 dataSource 绑定。

标准初始模型与当前模型

两者用途不同:

模型来源用途
标准初始模型Schema 的 initialValue / value / 结构默认值无参数重置、缺省值回退
当前模型当前 dataSource 或内部对象输入绑定、联动、提交

例如编辑记录中 name 为“张三”,但 Schema 的 initialValue'';调用无参数 resetFields() 后,字段恢复为 '',不是恢复到第一次传入的“张三”。需要把一条记录作为重置目标时,应显式传入:

ts
form.resetFields(recordSnapshot);

数据动作的边界

动作语义是否增加模型外字段
getData() / dataSource读取当前绑定模型不适用
setFieldsValue(partial)只更新已建立且本次提供的字段
resetFields()按已建立字段恢复标准初始值
resetFields(record)按已建立字段从记录回填,缺项回退初始值
submit()校验后返回当前模型深拷贝
ts
form.setFieldsValue({
  name: "王五",
  unknown: 123, // Schema 模型中没有该字段,不会被加入
});

对象会按已建立结构递归更新,数组和新对象会深拷贝后替换,避免直接复用传入集合引用。setFieldsValue 只处理参数中实际出现的字段;未提供的字段保持不变。

需要提交 id、版本号等不可见字段时,请用 Hidden 把它们加入 Schema 模型,而不是依赖动作保留任意外部属性。详见字段与数据路径:无 field 的节点

字段级 Ref

节点的 value 可以直接绑定外部 Ref。

同时配置 field

ts
const keyword = ref('')

{
  type: 'Input',
  field: 'keyword',
  value: keyword,
  label: '关键词',
}

此时存在双向同步:

text
输入控件 ↔ 表单模型 keyword ↔ 外部 Ref

字段会进入校验、重置和提交模型。

只有 value,没有 field

ts
{
  type: 'Input',
  value: keyword,
}

控件直接更新 keyword.value,但它没有模型路径,不进入表单提交数据。适合临时筛选器或只服务于页面交互的控件。

复合值的双向转换

有些字段在控件值和业务模型之间存在转换层。

范围拆分

ts
{
  type: 'DateRange',
  field: 'startDate',
  endField: 'endDate',
}

控件使用 [startDate, endDate],模型保存两个字段。任一模型字段在外部变化时,控件范围都会重新同步。详见DateRange 值模式

数组与逗号字符串

ts
{
  type: 'Select',
  field: 'roleIds',
  stringifyValue: true,
  attrs: { mode: 'multiple' },
}

控件使用数组,模型保存逗号字符串:

text
['admin', 'editor'] ↔ 'admin,editor'

选项标签同步

ts
{
  type: 'Select',
  field: 'departmentId',
  labelField: 'departmentName',
  options: departments,
}

一次选择同时更新值字段与文本字段;完整语义见选择输入:通用选项

扩展组件的多个 v-model

自定义字段可以通过 vModelFields 将额外 v-model 映射到同级字段或 Ref,见注册自定义字段

提交不是重新组装任意对象

submit() 的结果是当前标准模型的深拷贝。这个约束使提交字段可由 Schema 审核,也保证重置、校验和提交围绕同一套路径工作。若后端参数结构不同,建议在 API 层显式转换,相关约定见接口与数据适配

基于 MIT 许可发布