关联字段
本文用于说明以关联为中心的字段 API:
RelationTableSelectTreeManyToOne/OneToOneOneToManyManyToMany- 关联查询 / 分页 / patch 行为
相关文档:
- Fields:核心
Fieldprops、条件、filters、Field.onChange、值契约 - Widget 矩阵:widget 兼容性与 widget 专属示例
- ModelForm:页面壳层与关联表单示例
- ModelTable:只读单元格与内联编辑行为
导入
import { Field, RelationTable } from "@/components/fields";RelationTable 仅在关联字段声明中使用,例如 Field.tableView。
RelationTable
RelationTable 是 OneToMany 与 ManyToMany 的关联表格视图定义。
请声明一个零 props 的 tableView 组件,并在其中返回 <RelationTable />。
示例:
function OptionItemsTableView() {
return (
<RelationTable orders={["sequence", "ASC"]} pageSize={10}>
<Field fieldName="sequence" />
<Field fieldName="itemCode" />
<Field fieldName="itemName" />
<Field fieldName="active" />
</RelationTable>
);
}Props
| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
children | ReactNode | 是 | 按顺序声明的 <Field /> 列,以及可选的 <Action /> 行级操作(见 行内操作)。 |
orders | OrderCondition | 否 | 关联表格默认排序。支持单个元组或多个元组。 |
pageSize | number | 否 | 关联表格页大小。仅对分页关联表格(isPaged={true})生效。 |
排序示例:
<RelationTable orders={["sequence", "ASC"]}>
<Field fieldName="sequence" />
<Field fieldName="itemCode" />
</RelationTable><RelationTable
orders={[
["sequence", "ASC"],
["itemCode", "DESC"],
]}
>
<Field fieldName="sequence" />
<Field fieldName="itemCode" />
</RelationTable>行为说明:
RelationTable.pageSize仅影响分页关联表格(isPaged)RelationTable.orders同时支持单个元组与多个元组- 列声明仍由子级
<Field />的顺序决定
行内操作 {#row-actions}
RelationTable 可与 <Field /> 并列接受 <Action /> 子节点。它们渲染在末尾的 Actions 列中,作为每行操作,并面向关联模型派发——动作的 operation / href / onClick 会以该行的 id 作为记录 id。
import { Action } from "@/components/actions/Action";
function OptionItemsTableView() {
return (
<RelationTable orders={["sequence", "ASC"]}>
<Field fieldName="itemCode" />
<Field fieldName="itemName" />
<Action
type="link"
labelName="打开"
placement="inline"
href="/config/option-item/{id}"
/>
<Action
labelName="归档"
operation="archive"
placement="more"
/>
</RelationTable>
);
}放置规则与 ModelTable 行级操作一致:
placement="inline"(行上默认)→ 显示在 Actions 列中的图标/按钮placement="more"→ 显示在 Actions 列的溢出下拉中placement="toolbar"/"header"会被忽略(关联表格没有工具栏)
行为说明:
- 仅当行存在
id时才渲染操作;草稿/未保存行对应单元格为空 - 动作派发使用关联模型名(而非父表单的模型),因此
operation针对关联实体执行,查询失效会刷新该模型 disabled/hidden条件基于已保存的行数据求值——不会跟踪未保存的内联编辑值(与ModelTable不同)- 此上下文中
ActionExecutionContext.scope报告为"model-table"(关联行复用同一套派发器)
ManyToOne / OneToOne
默认行为为可搜索的关联选择:
<Field fieldName="departmentId" />依赖型关联过滤示例:
<Field fieldName="companyId" />
<Field
fieldName="departmentId"
filters={[
["companyId", "=", "{{ companyId }}"],
"AND",
["active", "=", true],
]}
/>说明:
filters作用于默认的可搜索关联查询- 当依赖的
{{ fieldName }}在当前作用域没有值时,选择器保持查询禁用,而不会加载全部选项
filterBySource —— 由后端驱动的上下文过滤
当业务规则无法仅通过 filters / {{ fieldName }} 声明表达时(例如「男性员工不能选产假类型」「剩余年假须大于 0」),可使用 filterBySource,让后端根据调用方记录上下文自行过滤:
<Field fieldName="leaveTypeId" filterBySource />当 filterBySource 为 true 时,searchName 请求会携带 SourceRecord:
interface SourceRecord {
model: string; // metaField.modelName —— 拥有该字段的模型
recordId?: string | null; // 解析后的 recordId;创建模式下为 null
values?: Record<string, unknown>; // 该记录当前的内存表单值
}语义:
model与recordId描述直接拥有该字段的记录——根表单上的 Field 为根记录;OneToMany/ManyToMany行内的 Field 则为该行记录(使用行自身模型,而非父级)values为查询时刻的表单快照;任意表单值变更都会触发重新查询,使下拉随表单联动过滤- 默认为
false;按字段开启,因为「该查找是否应尊重宿主表单」由调用方决定,而非目标模型 - 可与声明式
filters同时使用;二者一并提交,由后端合并应用
在 filters 与 filterBySource 之间选择:
filters+{{ fieldName }}—— 简单跨字段引用,在前端解析(gender、status、departmentId等)。声明式、代码可读,无需改后端。filterBySource—— 需要计算、策略查询或后端跨模型关联的规则。对前端不透明,但业务规则可与在save上强制执行的后端服务放在一起。
示例 —— 请假类型下拉通过服务端规则按员工性别过滤:
<Field fieldName="employeeId" />
<Field fieldName="leaveTypeId" filterBySource />安全提示:filterBySource 会发送当前完整表单值映射。若表单含有目标后端不应看到的字段,请勿启用。
SelectTree
关联模型为层级结构时使用 SelectTree:
<Field fieldName="departmentId" widgetType="SelectTree" />带依赖过滤的常见写法:
<Field fieldName="companyId" />
<Field
fieldName="departmentId"
widgetType="SelectTree"
filters={[
["companyId", "=", "{{ companyId }}"],
"AND",
["active", "=", true],
]}
/>行为:
SelectTree是表单与内联编辑器中面向开发者的推荐树形入口- 仍通过
Field声明,而非直接渲染SelectTreePanel - 与可搜索关联字段使用相同的
Field.filters规则 - 依赖的
{{ fieldName }}缺失时,树选择器保持查询禁用,不会加载未过滤的整棵树 - 底层
Tree/SelectTreePanel属于内部基础设施
OneToMany
渲染为关联表格,支持行内编辑或对话框编辑。对外仍统一使用 Field。
示例:
function OptionItemsTableView() {
return (
<RelationTable orders={["sequence", "ASC"]} pageSize={10}>
<Field fieldName="sequence" />
<Field fieldName="itemCode" />
<Field fieldName="itemName" />
<Field fieldName="active" />
</RelationTable>
);
}
<Field fieldName="optionItems" tableView={OptionItemsTableView} />;常用 props:
tableView:零 props 的关联表格视图组件,内部渲染<RelationTable><Field /></RelationTable>formView:行创建/编辑用的对话框表单isPaged:启用分页 / 远程关联模式
默认提交行为为增量 patch map:
{
"Create": [{ "name": "new row" }],
"Update": [{ "id": "101", "name": "changed" }],
"Delete": ["102", "103"]
}行为:
false(默认):在getById中包含关联subQuery;关联表格 UI 不分页,渲染本地行true:关联表格启用分页 UI;当recordId + relatedModel + 作用域内关联过滤就绪时,由relatedModel.searchPage加载数据(远程模式),否则在本地做分页- 静态
Field.filters(不含{{ fieldName }}引用)会并入getById的 subQuery,首屏加载即带上过滤;含{{ fieldName }}的动态过滤仅在远程模式查询、且引用字段值可用时生效 - 可编辑单元格限制为已声明的
RelationTable列与关联模型可编辑字段的交集 - 未解析的
{{ expr }}依赖会暂停远程关联查询,直到父表单依赖值存在
ManyToMany
默认渲染为关联表格 + 选择器对话框。
示例:
function UserTableView() {
return (
<RelationTable orders={["username", "ASC"]} pageSize={10}>
<Field fieldName="username" />
<Field fieldName="nickname" />
<Field fieldName="email" />
<Field fieldName="status" />
</RelationTable>
);
}
<Field fieldName="userIds" tableView={UserTableView} />;默认提交行为为增量 patch map:
{
"Add": ["1", "2"],
"Remove": ["3"]
}TagList
widgetType="TagList" 将 ManyToMany 切换为可搜索的多选下拉,并在触发器下方以标签展示已选项。
<Field fieldName="userIds" widgetType="TagList" tableView={UserTableView} />行为:
- 可搜索下拉,支持多选交互
- 已选值在触发器下方渲染为标签
- 触发器文案保持紧凑,仅显示选中数量
- 字段布局默认遵循周围
FormSection列宽;需要独占整行时请显式传入fullWidth - 顶层
ModelForm的getById仅把字段名加入fields,不会追加关联subQuery - 字段 UI 值为
ModelReference[],顶层提交仍使用常规增量 patch map
查询说明
ManyToMany选择器对话框以AND合并有效字段过滤、内部关联作用域过滤、搜索过滤与列过滤- 未解析的
{{ expr }}依赖会暂停远程选择器与关联表格查询,直到源值存在 formView可选;在ManyToMany中,点击行以只读模式打开ModelDialog,新增/移除仍走选择器
共享的只读 / 内联行为
关联字段的共通行为:
ModelTable/RelationTable只读单元格将OneToMany与ManyToMany都渲染为紧凑标签列表,优先displayName,回退id,而非 JSON 字符串widgetProps不会传入RelationTable只读单元格渲染器- 关联表格复用与
ModelTable相同的紧凑文件/图片只读渲染 RelationTable.pageSize仅影响分页关联表格(isPaged=true)- 远程关联表格与选择器查询使用有效字段过滤(
Field.filters ?? metaField.filters)、关联作用域过滤以及运行时搜索/列过滤
表单视图示例
formView 通常与 ModelDialog 搭配:
function UserRoleUserIdsFormView() {
return (
<ModelDialog title="User Detail">
<FormSection labelName="General" hideHeader>
<Field fieldName="username" />
<Field fieldName="nickname" />
<Field fieldName="email" />
<Field fieldName="mobile" />
<Field fieldName="status" />
</FormSection>
</ModelDialog>
);
}