Skip to Content
文档前端开发字段 & 控件字段概览

字段

ModelForm、关联对话框、内联编辑器和展示场景使用的、基于元数据驱动的字段系统。

Field 是应用代码中面向业务的主要入口。

本文用于说明:

  • Field props 与覆盖方式,三种渲染模式(表单 / 展示 / 声明)
  • required / readonly / hidden
  • dependsOn(...)
  • filters(关联查询与 Option / MultiOption 客户端选项过滤)
  • 远程 Field.onChange
  • 运行时值契约
  • 按字段类型划分的前端行为

相关文档:

  • 关联字段RelationTableSelectTreeOneToManyManyToMany
  • WidgetsFieldType -> WidgetType 矩阵与 widget 专属示例
  • ModelForm:页面壳层
  • ModelTable:只读单元格、内联编辑与侧栏
  • ModelSideForm:侧栏 + 内嵌表单视图
  • TreeSideTreeSelectTree 使用的内部树形原语

导入

推荐的业务侧导入方式:

import { Field } from "@/components/fields";

额外的公开导出:

import { Field, useDisplayRecord, useDisplayRecordValue, RelationTable, type FieldCondition, type FieldConditionContext, type FieldOnChangeProp, type RelationFormView, type RelationTableProps, } from "@/components/fields";

内部说明:

  • ResolvedFields 为内部能力,应只留在基础设施代码一侧,而不应成为面向业务的字段 API

字段渲染模式

Field 会根据 React 上下文自动判断渲染模式:

模式条件行为
表单模式存在 FieldPropsContext可编辑字段,含校验与条件
展示模式存在 RecordContext,且无 FieldPropsContext通过 FieldDisplayValue 只读展示行内值
声明模式上述上下文均不存在返回 null;由父级通过 children 收集声明

因此同一 <Field fieldName="name" /> 无需 mode prop,即可在表单、侧栏、卡片模板和表格列声明中复用。

展示模式

Field 位于 RecordContext 内(例如 SideCardSideListFormHeader 的子节点),会渲染为 FieldDisplayValue——只读行内值,与表格列使用相同的单元格渲染器。

import { RecordContextProvider } from "@/components/contexts/RecordContext"; <RecordContextProvider record={data} metaModel={metaModel}> <Field fieldName="name" /> {/* 展示为只读值 */} <Field fieldName="status" /> {/* 展示为只读值 */} </RecordContextProvider>

FieldDisplayScope

ModelForm 中已提供 FieldPropsContext。若需将 Field 强制为展示模式(例如在 FormHeader 子节点中),请用 FieldDisplayScope 包裹:

import { FieldDisplayScope } from "@/components/fields/display"; <FieldDisplayScope> <Field fieldName="status" /> {/* 虽在 ModelForm 内,仍以展示模式渲染 */} </FieldDisplayScope>

FormHeader 会用 FieldDisplayScope 自动包裹其 children

FieldDisplayScope 内若编写自定义仅展示组件,请使用 useDisplayRecordValue(path)useDisplayRecord(paths) 仅订阅所需字段,而不要读取整份表单记录:

import { Field, useDisplayRecordValue } from "@/components/fields"; function HeaderStatusBadge() { const status = useDisplayRecordValue<string>("status"); return <span>{status}</span>; } <FormHeader> <Field fieldName="employeeCode" /> <HeaderStatusBadge /> </FormHeader>

Group

用于在同一行内横向组合多个 Field 的行内 flex 容器。内部 Field 的标签一律隐藏——请改用 GrouplabelName

位置: @/components/fields/composition

Group 属性

Prop类型必填默认值说明
labelNamestring-渲染在行内组合上方的标签,替代各 Field 独立标签
separatorReactNode-子节点之间的分隔符(如 "-""·""/"
classNamestring-flex 容器额外类名
childrenReactNode-一般为 Field 元素

标签行为

  • 展示模式FormHeaderSideCard 等):Field 仅渲染为值的 <span>,默认无标签。
  • 表单模式FormBody 内):Group 会克隆子 Field 并设 hideLabel={true}。若提供 labelName,则在组合上方渲染单个 FormLabel

示例

展示模式——在 FormHeader 中(无标签):

<FormHeader> <Group separator="·"> <Field fieldName="employeeCode" /> <Field fieldName="departmentName" /> </Group> </FormHeader>

表单模式——共享标签:

import { Group } from "@/components/fields/composition"; <FormSection labelName="名称"> <Group labelName="姓名" separator="-"> <Field fieldName="firstName" /> <Field fieldName="lastName" /> </Group> </FormSection>

表单模式——无标签(仅值):

<Group separator="/"> <Field fieldName="countryCode" /> <Field fieldName="areaCode" /> <Field fieldName="phoneNumber" /> </Group>

推荐用法

在应用代码中,将 Field 作为唯一入口:

<Field fieldName="name" /> <Field fieldName="description" widgetType="Text" /> <Field fieldName="avatar" widgetType="Image" /> <Field fieldName="notes" widgetType="Markdown" />

运行时会自动解析:

  • 从元数据中获取 fieldType
  • 根据 fieldType 选择默认字段适配器
  • 根据 widgetType 选择可选 widget 渲染器

直接使用适配器组件和底层 widget,只建议出现在字段基础设施内部。

快速关联示例:

<Field fieldName="departmentId" widgetType="SelectTree" />
function UserTableView() { return ( <RelationTable orders={["username", "ASC"]} pageSize={10}> <Field fieldName="username" /> <Field fieldName="email" /> </RelationTable> ); } <Field fieldName="userIds" tableView={UserTableView} />;

核心 Props

Field 基于元数据驱动,并支持字段级覆盖和运行时条件。

Prop类型必填说明
fieldNamestring当前模型中的元数据字段 key。
fieldTypeFieldType可选的字段类型覆盖。若省略,则运行时使用元数据中的 fieldType
widgetTypeWidgetType可选的 widget 覆盖。必须与解析后的 fieldType 兼容。
widgetPropsRecord<string, unknown>仅用于 widget 专属配置。表单 widget 和内联编辑器会使用;表格只读单元格不会使用。
placeholderstring字段级输入占位文案。优先于 widgetProps.placeholder
hideLabelboolean隐藏整个标签区域。
fullWidthboolean文本类字段和关联字段的布局提示。
labelNamestring元数据标签覆盖。
requiredFieldCondition动态必填控制。支持 booleanFilterConditiondependsOn(...)
readonlyFieldCondition动态只读控制。支持 booleanFilterConditiondependsOn(...)
hiddenFieldCondition动态可见性控制。隐藏字段不会渲染,同时会抑制其校验。
defaultValueunknown仅创建态使用的默认值覆盖。优先级高于 metaField.defaultValue 和对话框 / 页面级 defaultValues
filtersstring | FilterCondition过滤条件覆盖。用于关联查询,以及 Option / MultiOption 在客户端按 itemCode 过滤已加载的选项(再渲染)。Field.filters 会覆盖 metaField.filters。支持 JSON 字符串形式的元数据过滤条件以及 {{ expr }}(如 {{ fieldName }})引用。
filterBySourceboolean为 true 时,在 searchName 等服务端搜索调用中附带 SourceRecord 快照(所属模型 + recordId + 当前表单值),供后端按调用表单上下文应用业务规则。默认 false;按调用点开启。详见 关联字段 — filterBySource
onChangeFieldOnChangeProp远程字段联动。支持简写 string[]{ update?, with? }
tableViewRelationTableViewOneToMany / ManyToMany 的关联表格视图。须为零 props 的组件,且渲染 <RelationTable />。详见 关联字段
formViewRelationFormView关联对话框 / 详情视图配置。详见 关联字段
isPagedbooleanOneToMany / ManyToMany 启用分页关联表格模式。详见 关联字段

FieldCondition

type FieldCondition = | boolean | FilterCondition | ((ctx: FieldConditionContext) => boolean);

FieldConditionContext

interface FieldConditionContext { fieldName: string; metaField: MetaField; values: Record<string, unknown>; value: unknown; scope: "form" | "model-table" | "relation-table"; rowIndex?: number; rowId?: string; isEditing: boolean; recordId?: string; }

行为说明:

  • boolean:最简单直接的形式
  • FilterCondition:推荐用于常见业务规则的声明式写法
  • dependsOn([...], evaluator):当需要显式字段订阅的函数式条件时推荐使用
  • 无效的 FilterCondition 配置会触发开发态警告,并解析为 false
  • 不支持裸函数条件;请使用 dependsOn([...], evaluator) 包裹
  • 相同的条件模型也用于表单和表格工具栏中的 Action.disabledAction.hidden
  • hidden 会同时抑制渲染和校验
  • ModelTable / RelationTable 的内联编辑中,条件里的 values 是当前行对象,而不是整个表单对象
  • 在表格声明中,hidden 只支持 boolean,且会隐藏整列
  • widgetProps 不会透传给 ModelTable / RelationTable 的只读单元格渲染器
  • defaultValue 适用于静态的字段级创建默认值。对于路由参数、父行值或未渲染字段等运行时 / 上下文预填,请使用对话框 / 页面级 defaultValues
  • required={false} 可以放宽元数据里的 requiredreadonly={false} 可以覆盖元数据里的只读设置

示例:

import { dependsOn, Field } from "@/components/fields"; <Field fieldName="status" readonly={true} /> <Field fieldName="itemTone" hidden={["active", "=", false]} /> <Field fieldName="description" readonly={[ ["status", "IN", ["approved", "archived"]], "OR", [["type", "=", "SYSTEM"], "AND", ["editable", "!=", true]], ]} /> <Field fieldName="itemName" required={dependsOn(["active", "itemCode"], ({ values, isEditing }) => !isEditing && values.active === true && values.itemCode !== "Temp" )} />

dependsOn(...)

当某个字段规则依赖其他值,且你希望显式声明订阅字段时,请使用 dependsOn([...], evaluator)

import { dependsOn, Field } from "@/components/fields"; <Field fieldName="itemName" required={dependsOn(["active", "itemCode"], ({ values, isEditing }) => !isEditing && values.active === true && values.itemCode !== "Temp" )} />

为什么优先用 dependsOn(...),而不是裸函数:

  • 依赖列表是显式的
  • 运行时订阅更精确
  • evaluator 依然保留完整的编程能力

优先级建议:先用 boolean,再用 FilterCondition 表达声明式业务规则,只有真正需要计算逻辑时再使用 dependsOn(...)

filters

filters 用于:

  • 关联字段 —— 约束后端查询
    • ManyToOne / OneToOne 可搜索引用查询
    • SelectTree 关联选择查询
    • OneToMany / ManyToMany 远程关联表格查询
    • ManyToMany 选择器对话框查询
  • Option / MultiOption 字段 —— 在客户端按 itemCode 过滤已加载的 OptionReference[] 后再渲染(适用于 OptionSelectRadioStatusBarCheckBox widget;Badge 为纯展示,不受影响)。未传 filters 时展示全部选项。

可接受输入:

  • 应用代码中的 FilterCondition
  • 来自元数据 / 后端 payload 的 JSON 字符串形式

过滤值中推荐使用的声明式值语法:

  • {{ fieldName }}:在发请求前从当前前端作用域解析(统一模板语法 {{ expr }}
  • TODAYNOWUSER_IDUSER_EMP_IDUSER_POSITION_IDUSER_DEPT_IDUSER_COMP_ID:原样透传,由后端替换环境变量
  • 字面量:使用 {{ 'value' }} 或后端 token 如 {{ NOW }};保留字段引用在支持的场景下使用 {{ @fieldName }}

示例 —— 关联:

<Field fieldName="departmentId" filters={[ ["companyId", "=", "{{ companyId }}"], "AND", ["active", "=", true], "AND", ["effectiveDate", "<=", "TODAY"], "AND", ["type", "=", "{{ TODAY }}"], ]} />

示例 —— Option / MultiOption(过滤条件中的字段名须为 itemCode):

<Field fieldName="country" filters={[["itemCode", "IN", ["US", "CA", "UK"]]]} /> <Field fieldName="tags" filters={[["itemCode", "!=", "archived"]]} />

行为:

  • Field.filters 会覆盖 metaField.filters
  • 若省略 Field.filters,关联 widget 会回退到 metaField.filtersOption / MultiOption widget 会原样展示全部选项
  • {{ fieldName }} 会基于当前作用域值解析:
    • ModelForm:当前表单值
    • ModelTable 内联编辑:当前编辑行
    • RelationTable 内联编辑:当前关联行
  • 解析后的字段值会在请求前做归一化:
    • ManyToOne / OneToOne -> id
    • Option -> itemCode
    • MultiOption -> itemCode[]
  • 如果任意 {{ expr }} 依赖缺失,则关联查询会视为未就绪,不会发送请求
  • 前端不会求值后端环境 token,例如 TODAY;它们会原样透传
  • 对未分页的 OneToMany 字段,不含 {{ fieldName }} 引用的静态过滤条件也会写入 getById 的 subQuery,首屏即可拿到已过滤的关联数据;含 {{ fieldName }} 引用的过滤仅在远程模式查询时生效

远程 Field.onChange

Field 通过顶层 onChange prop 支持远程联动:

type FieldOnChangeProp = | string[] | { update?: string[]; with?: string[] | "all"; };

常见示例:

<Field fieldName="itemCode" onChange={["itemName", "itemTone"]} /> <Field fieldName="itemCode" onChange={{ update: ["itemName"], with: ["active"] }} /> <Field fieldName="itemCode" onChange={{ with: "all" }} />

行为:

  • onChange={["a", "b"]}onChange={{ update: ["a", "b"] }} 的简写
  • 存在 update:只从响应 values 中提取这些字段
  • 省略 update:会应用当前作用域内响应 values 的全部 key
  • 省略 with:请求只在编辑态发送 id,并附带当前字段 value
  • with: ["a", "b"]:请求会附带这些字段对应的 values,并使用提交 / API 形态
  • with: "all":请求会附带当前作用域的所有值,并使用提交 / API 形态

当前支持的作用域:

  • ModelForm
  • ModelTable 的当前内联编辑行
  • RelationTable 的当前内联编辑行

当前非目标:

  • 顶层独立 OneToMany / ManyToMany 容器交互不能作为触发源
  • 独立对话框表单目前不会自动提供这套运行时能力

自动触发规则:

  • blur:文本类和编辑器类字段,如 StringMultiString、数值输入、JSONFiltersOrdersCodeMarkdownRichTextTemplateEditor
  • change:提交型字段,如 BooleanDateDateTimeTimeOptionMultiOptionManyToOneOneToOneFileMultiFile

前端使用的后端契约:

POST /<modelName>/onChange/<fieldName>

请求体:

{ "id": "123", "value": "ITEM-001", "values": { "active": true } }

响应体:

{ "values": { "itemName": "Open", "itemTone": "Success" }, "readonly": { "itemName": true }, "required": { "itemTone": true } }

响应规则:

  • values 只 patch 返回的 key;缺失的 key 保持不变
  • 返回 null 表示明确清空
  • readonly / required 会独立于 update 应用
  • 远程返回的 readonly / required 会覆盖元数据和本地条件,直到后续响应或作用域重置

作用域说明:

  • ModelForm 中,with: "all" 使用当前表单的提交形态;已注册的顶层关联字段会序列化为关联 patch payload,而不是原始 UI 行
  • ModelTable / RelationTable 内联编辑中,valueswith: "all" 都只针对当前行,而不是整个表格或父表单

级联字段路径(展示) {#cascaded-field-path-display}

fieldName 中使用点号记法,可从关联记录(经 ManyToOne / OneToOne)读取字段并以只读展示 —— 无需手写 SubQuery、无需额外展示 widget、无需自研 hook。

{/* 在每条 AppEnv 卡片上渲染 DesignDeployment.deployStatus */} <Field fieldName="lastDeploymentId.deployStatus" widgetType="StatusIcon" /> {/* 深度 3:AppEnv → Owner → Department.name */} <Field fieldName="ownerId.departmentId.name" />

行为:

  • 始终只读 —— 不向 react-hook-form 注册,不会出现在 formState.dirtyFields,不参与校验
  • 生效的 metaField最深层模型上的叶子字段(例如 DesignDeployment.deployStatus);fieldType / widgetType / optionSetCode / labelName 均来自叶子,<Field> 上的 props(label / widgetType / hideLabel / className)照常覆盖
  • 支持于 ModelFormModelSideFormModelTableModelBoardModelCard —— 宿主遍历器自动将匹配的 SubQuery 折叠进数据请求,CascadedResolutionsProvider 为每条路径注入叶子元数据
  • 中间为 null 时按叶子类型的空占位渲染(与普通 null 字段一致),不为「已删除引用」单独渲染墓碑
  • 路径仅穿越 *-to-One 关联;路径中出现 OneToMany / ManyToMany 会被后端拒绝(TRAVERSE_TO_MANY),字段回退为 "-" 占位 + 开发态 console.error
  • 最大深度由后端约束;前端不做预校验

需要自定义渲染 / 回退时可命令式读取路径:

import { getValueAtPath } from "@/utils/object-path"; function HealthDot() { const { record } = useRecordContext(); const deployStatus = getValueAtPath(record, "lastDeploymentId.deployStatus"); // ... 根据 deployStatus 派生 UI }

在宿主视图 JSX 某处声明 <Field fieldName="lastDeploymentId.deployStatus"> 才会向遍历器注册路径以便折叠 SubQuery —— getValueAtPath 只是对嵌套对象结果的类型化访问。

限制:

  • 嵌套在 formView 回调内部的级联路径(深度 > 0)尚未解析 —— 遍历器会打开发态 console.warn,字段渲染空占位。请将级联路径放在顶层表单 / 列表作用域。

级联字段自动回填(写入)

MetaField.cascadedField 可以在编辑作用域中启用隐式自动回填,而无需源字段显式声明 Field.onChange这与上文「级联字段路径(展示)」不同 —— 前者是读侧 / 展示,本条是写侧 / 选型后的值传播。

deptId.cascadedField = "employeeId.departmentId"; companyId.cascadedField = "employeeId.department.companyId";

行为:

  • 支持于 ModelFormModelTable 内联编辑和 RelationTable 内联编辑
  • 源字段必须是 ManyToOneOneToOne,并且定义了 relatedModel
  • 当源字段变化时,前端会请求一次 /<relatedModel>/getById,并从响应中读取所有依赖的级联路径
  • 多个目标依赖同一个源字段时,会在一次查询中统一解析
  • 如果源字段同时声明了 Field.onChange,两种效果会并行执行
  • 如果两者都写入同一个目标字段,则 cascadedField 优先
  • 清空源字段时,会直接清空所有依赖的级联目标,不会调用 getById
  • 无效的级联元数据会被忽略,并给出开发态警告

语法说明:

  • 格式为 <sourceField>.<path>
  • <sourceField> 必须是当前作用域中的同级字段
  • <path> 从源模型的 getById 响应中读取,可以是嵌套路径

字段类型概览

本节说明不同 fieldType 的默认前端行为。widget 专属变体和 props 表请见 Widgets

字符串与文本

  • String:默认单行文本输入
  • MultiString:标签式输入;值会在 Enter, 或 blur 时提交,并以逗号分隔字符串存入表单状态
  • 常见的 String widget 变体:
    • URL
    • Email
    • Color
    • Text
    • RichText
    • TemplateEditor
    • Markdown
    • Code

示例:

<Field fieldName="name" /> <Field fieldName="homepage" widgetType="URL" /> <Field fieldName="description" widgetType="Text" /> <Field fieldName="notes" widgetType="Markdown" /> <Field fieldName="content" widgetType="RichText" />

数值类型

  • IntegerLongDouble:数字类输入
  • BigDecimal:保留小数字符串语义
  • 常见的数值类 widget 变体:
    • Monetary
    • Percentage
    • Slider
<Field fieldName="amount" widgetType="Monetary" /> <Field fieldName="ratio" widgetType="Percentage" /> <Field fieldName="score" widgetType="Slider" />

布尔与选项类型

  • Boolean:默认是 Switch
  • Option:默认单选下拉
  • MultiOption:默认是复选组风格的多选

常见 widget 变体:

  • CheckBox
  • Radio
  • StatusBar
  • StatusIcon — 只读、仅图标的指示器,由选项元数据中的 itemToneitemIcon 驱动

只读展示是 自动的——Option / MultiOption 在任何只读上下文(表格单元格、卡片 / 看板单元格、只读表单)中渲染时,若选项带有 itemTone 会自动采用彩色 StatusBadge,否则为纯文本。无需声明 widgetType

<Field fieldName="active" /> <Field fieldName="active" widgetType="CheckBox" /> <Field fieldName="status" widgetType="Radio" /> <Field fieldName="status" /> // 只读上下文 → 自动 Badge / 文本 <Field fieldName="status" widgetType="StatusIcon" />

日期与时间类型

  • Date:标准日期选择器
  • DateTime:日期时间输入
  • Time:时间输入

特殊的格式导向 widget:

  • Dateyyyy-MMMM-dd
  • TimeHH:mmHH:mm:ss
<Field fieldName="birthday" /> <Field fieldName="period" widgetType="yyyy-MM" /> <Field fieldName="startTime" widgetType="HH:mm" />

引用类型

  • ManyToOne / OneToOne:默认是可搜索的关联选择器
  • 使用 filters 为查询添加依赖约束
  • 使用 widgetType="SelectTree" 处理层级选择
<Field fieldName="departmentId" /> <Field fieldName="departmentId" widgetType="SelectTree" />

SelectTreeRelationTableOneToManyManyToMany 请见 关联字段

文件类型

  • File:默认通用上传;Image 为图片模式
  • MultiFile:默认多文件上传;MultiImage 为图片画廊模式
<Field fieldName="attachment" /> <Field fieldName="avatar" widgetType="Image" /> <Field fieldName="photos" widgetType="MultiImage" />

表格 / 只读行为:

  • ModelTableRelationTable 会直接使用 FileInfo.url 做预览和下载渲染
  • 表格只读单元格会有意忽略面向表单的 widgetProps

结构化值类型

  • JSON / DTO:结构化对象或数组值,默认编辑器偏向 JSON
  • Filters:过滤构建器值
  • Orders:排序构建器值
<Field fieldName="config" /> <Field fieldName="filters" /> <Field fieldName="orders" />

运行时值契约

Field.defaultValue、容器级 defaultValuesform.getValues()useWatch() 使用的都是字段 UI 值,而不是原始 API payload 值。

字段类型UI / 表单值提交 / API 形态
FileFileInfo | nullfileId | null
MultiFileFileInfo[]fileId[] | null
JSON / DTO结构化对象 / 数组或 null结构化对象 / 数组或 null
FiltersFilterCondition | nullFilterCondition | null
Orders结构化排序元组或 null结构化排序元组或 null
OneToMany关联行 / 行草稿增量 patch map
ManyToManyModelReference[] 或关联行增量 patch map

说明:

  • 后端 payload 和元数据默认值可能仍以字符串形式到达;字段运行时会在加载时把它们归一化为 UI 形态
  • 传入页面 / 对话框 defaultValues 时,请直接使用上表中的 UI 形态,而不是预先字符串化
  • ManyToMany 即使使用 widgetType="TagList",提交时仍是普通增量 patch map
  • 顶层关联字段的细节请见 关联字段

FileInfo

FileMultiFile 字段在 UI 状态中使用 FileInfo 对象。

重要运行时行为:

  • 预览和下载渲染使用 FileInfo.url
  • File 只读单元格会回退为文件名链接
  • MultiFile 只读单元格显示第一个文件名,并附带 +N

布局说明

fullWidth 对这些字段渲染器有意义:

  • Text
  • RichText
  • TemplateEditor
  • Markdown
  • Code
  • OneToMany
  • ManyToMany

这些布局的默认值都是 true

只读说明

当用户仍然需要清晰读取或复制值时,优先使用 readonly,而不是禁用控件。

一般建议:

  • readonly:详情页、审计页、仅查看状态
  • disabled:受工作流、权限、前置条件或提交状态阻塞

示例

<Field fieldName="email" widgetType="Email" /> <Field fieldName="bio" widgetType="Text" fullWidth /> <Field fieldName="notes" widgetType="Markdown" widgetProps={{ mode: "preview", minHeight: 360 }} /> <Field fieldName="script" widgetType="Code" widgetProps={{ language: "sql", lineWrapping: false, minHeight: 320 }} /> <Field fieldName="avatar" widgetType="Image" widgetProps={{ aspectRatio: "1 / 1", display: "avatar", crop: { enabled: true, shape: "round" }, }} />
最后更新于