EntryGrid 表单项网格
通用布局能力:
RenderEntryGrid实例统一支持width、height、min/max、margin和槽位对齐;列定义中的固定宽度只控制轨道,不替代组件自身布局属性。详见组件通用布局属性。
DirectSurface UI 的表单项网格公共类名是 RenderEntryGrid。它用于把一组输入项、展示项或 RenderFieldPresenter 按列轨道排列成表单布局,支持 px、fr、auto 三类轨道、列间距、行间距、columnSpan、rowSpan、行级校验反馈和 Enter 跳转到下一个焦点项。
RenderEntryGrid 不是数据表格。它不维护数据源、行对象、排序、选择或单元格编辑模型。多行多列业务数据使用 GridView,简单二维展示使用 Table,对象属性查看使用 ObjectInspector。
API 总览
import { RenderEntryGrid, RenderFieldPresenter, RenderFormPanel, RenderText, RenderTextBox, auto, fieldTrack, fr, labelTrack, metaTrack, px, unitTrack, wideFieldTrack, type EntryGridChildData, type EntryGridEnterNavigation, type EntryGridTrack, type EntryGridTrackOptions, type EntryGridValidationPresentation, type FormPanelChildData, type LabeledFieldLabelOverflow, type LabeledFieldMetaTone, type LabeledFieldValidationDisplay,} from 'ds-ui'
| API | 类型 | 用途 |
|---|---|---|
RenderEntryGrid |
class | 表单项网格,负责列轨道、跨列、跨行、行级校验反馈和 Enter 导航。 |
EntryGridTrack / EntryGridTrackOptions |
type | 列轨道配置。 |
EntryGridChildData |
type | 子项跨度配置,包含 columnSpan 和 rowSpan。 |
EntryGridValidationPresentation |
type | 校验展示方式。 |
EntryGridEnterNavigation |
type | Enter 焦点移动策略。 |
RenderFormPanel |
class | 较轻量的字段面板,按字段列数排布 RenderFieldPresenter。 |
FormPanelChildData |
type | RenderFormPanel.addField() 的子项跨度配置。 |
LabeledFieldLabelOverflow |
type | 字段 label 溢出策略:expand 或 clip。 |
LabeledFieldMetaTone |
type | 字段 meta 文本语气:default、success、warning、danger。 |
LabeledFieldValidationDisplay |
type | 字段校验展示形态:compact 或 inline。 |
px() / fr() / auto() |
function | 创建固定、弹性和自适应轨道。 |
labelTrack() / fieldTrack() / wideFieldTrack() / unitTrack() / metaTrack() |
function | 常用表单轨道快捷函数。 |
最小装配顺序是:定义 columns,创建 RenderEntryGrid,再按视觉顺序 addChild(child, data?)。它只负责布局和焦点导航,字段值、校验状态和提交逻辑都由业务或子控件维护。
何时使用
- 表单录入页:多列字段、字段标签、输入框和单位。
- 查询条件区:搜索框、下拉框、日期范围、树选择。
- 详情页属性区:姓名、年龄、科室、床号等键值型字段。
- 需要某些字段跨列或跨行,例如主诉、诊断、备注、多行文本。
- 需要把字段错误/警告集中绘制到当前行下方。
- 需要 Enter 在字段之间按视觉顺序移动焦点。
不适合:
- 需要滚动、排序或选择行的数据表格。
- 需要对象树展开查看的调试工具。
- 需要自动 label/value 语义的数据结构映射。
RenderEntryGrid只负责布局,字段值和校验由业务维护。 - 父容器不给明确宽度的场景。
RenderEntryGrid的列宽计算依赖constraints.maxWidth。
最小示例
const nameInput = new RenderTextBox({ value: '', placeholder: '姓名' })const ageInput = new RenderTextBox({ value: '', placeholder: '年龄' }) const entryGrid = new RenderEntryGrid({ columns: [labelTrack(), fieldTrack(), labelTrack(), fieldTrack()], enterNavigation: 'next-focusable',}) entryGrid.addChild(new RenderText('姓名'))entryGrid.addChild(nameInput)entryGrid.addChild(new RenderText('年龄'))entryGrid.addChild(ageInput)
更常见的写法是每个字段都使用 RenderFieldPresenter,让 label、必填标记、单位、meta 和校验状态由字段壳处理:
const formGrid = new RenderEntryGrid({ columns: [fieldTrack(), fieldTrack()], columnGap: 12, rowGap: 8,}) formGrid.addChild(new RenderFieldPresenter({ label: '患者姓名', required: true, child: new RenderTextBox({ value: '', placeholder: '请输入' }),})) formGrid.addChild(new RenderFieldPresenter({ label: '联系电话', child: new RenderTextBox({ value: '', placeholder: '请输入手机号' }),}))
构造参数
new RenderEntryGrid(options) 的 options:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
columns |
EntryGridTrack[] |
必填 | 列轨道配置。 |
columnGap |
number |
主题 columnGap |
列间距。 |
rowGap |
number |
主题 rowGap |
行间距。 |
validationPresentation |
'none' | 'row-feedback' |
'none' |
校验信息展示方式。 |
enterNavigation |
'none' | 'next-focusable' |
'none' |
Enter 未被子控件处理时是否移动焦点。 |
const queryGrid = new RenderEntryGrid({ columns: [fieldTrack(), fieldTrack(), fieldTrack(), fieldTrack()], columnGap: 10, rowGap: 8, validationPresentation: 'row-feedback', enterNavigation: 'next-focusable',})
Track 类型
EntryGridTrack 有三种:
| 类型 | 创建函数 | 行为 |
|---|---|---|
px |
px(value, options?) |
固定宽度。默认 min/max 都等于该宽度。 |
fr |
fr(value, options?) |
弹性轨道。按 grow 分配剩余空间,按 shrink 收缩。 |
auto |
auto(options?) |
根据单列 child 的自然宽度计算。 |
EntryGridTrackOptions:
| 字段 | 类型 | 说明 |
|---|---|---|
min |
number |
轨道最小宽度。会同时考虑 child 的最小布局宽度提示。 |
max |
number |
轨道最大宽度。 |
grow |
number |
分配额外空间的权重。 |
shrink |
number |
宽度不足时收缩的权重。 |
const tracks: EntryGridTrack[] = [ px(72), fr(1, { min: 120, shrink: 1 }), auto({ min: 48, max: 96 }),]
Track 快捷函数
| 函数 | 等价配置 | 适用场景 |
|---|---|---|
labelTrack(options?) |
auto({ min: 56, shrink: 1, ...options }) |
独立 label 列。 |
fieldTrack(flex?, options?) |
fr(flex, { min: 96, shrink: flex, ...options }) |
普通输入字段列。 |
wideFieldTrack(flex?, options?) |
fr(flex, { min: 128, shrink: flex, ...options }) |
更宽的输入字段列。 |
unitTrack(options?) |
auto({ min: 24, max: 72, shrink: 0, ...options }) |
单位列。 |
metaTrack(options?) |
auto({ min: 40, max: 120, shrink: 1, ...options }) |
状态、meta、短提示列。 |
const vitalSignGrid = new RenderEntryGrid({ columns: [ labelTrack(), fieldTrack(), unitTrack(), labelTrack(), fieldTrack(), unitTrack(), ],})
子项和跨度
addChild(child, data?) 的 data 类型是 EntryGridChildData:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
columnSpan |
number |
1 |
横跨列数。会被限制在 1..columns.length。 |
rowSpan |
number |
1 |
横跨行数。小于 1 会规整为 1。 |
const summaryGrid = new RenderEntryGrid({ columns: [fieldTrack(), fieldTrack(), fieldTrack(), fieldTrack()],}) summaryGrid.addChild(new RenderFieldPresenter({ label: '主诉', child: new RenderTextBox({ value: '', placeholder: '请输入主诉' }),}), { columnSpan: 2 }) summaryGrid.addChild(new RenderFieldPresenter({ label: '病情摘要', child: new RenderTextBox({ value: '', placeholder: '请输入摘要' }),}), { columnSpan: 2, rowSpan: 2 })
布局采用从左到右、从上到下的 first-fit 算法。后添加的 child 会寻找第一个能放下 columnSpan/rowSpan 的空位。复杂跨行布局里,child 添加顺序会影响最终位置。
属性和方法
| API | 类型 / 返回值 | 说明 |
|---|---|---|
children |
RenderBox[] |
当前子项。 |
childData |
Map<RenderBox, EntryGridChildData> |
每个 child 的跨度配置。 |
columns |
EntryGridTrack[] |
当前列轨道。直接赋值不会自动 mark layout,建议用 setColumns()。 |
columnGap |
number | undefined |
列间距。赋值请求 layout。 |
rowGap |
number | undefined |
行间距。赋值请求 layout。 |
validationPresentation |
EntryGridValidationPresentation |
校验展示方式。赋值请求 layout 和 paint。 |
enterNavigation |
EntryGridEnterNavigation |
Enter 导航方式。 |
addChild(child, data?) |
void |
添加一个表单项。 |
clearChildren() |
void |
移除所有 children,detach 已挂载子项。 |
setColumns(columns) |
void |
替换列轨道并请求 layout。 |
handleUnhandledEnterFromDescendant(target, event) |
boolean |
子控件未处理 Enter 时,由父网格尝试移动焦点。 |
const entryGrid = new RenderEntryGrid({ columns: [fieldTrack(), fieldTrack()],}) entryGrid.setColumns([fieldTrack(), fieldTrack(), wideFieldTrack()])entryGrid.columnGap = 12entryGrid.rowGap = 8
布局规则
列宽计算流程:
px轨道以value作为自然宽度。- 单列 child 会先被 measure,用于推导
auto和fr的自然宽度、最小宽度提示。 - 每个轨道根据
min/max/type得到 base width。 - 如果容器有剩余宽度,按 grow 权重扩展。
- 如果总宽度超过容器,按 shrink 权重收缩,且不低于 min。
columnSpanchild 使用跨越列宽 + 中间 gap 的总宽度 layout。rowSpanchild 如果高度超过当前跨行高度,会把缺少高度平均分摊到覆盖的行。
const shrinkableGrid = new RenderEntryGrid({ columns: [ auto({ min: 56, shrink: 1 }), fr(1, { min: 96, shrink: 1 }), auto({ min: 56, shrink: 1 }), fr(2, { min: 128, shrink: 2 }), ],})
RenderEntryGrid 自身不滚动。高度由内容自然撑开。如果表单很长,应把它放入 ScrollView 或页面滚动容器中。
校验展示
validationPresentation:
| 值 | 行为 |
|---|---|
'none' |
不额外绘制行级反馈。RenderFieldPresenter 自己的状态和 adorners 仍按其自身规则工作。 |
'row-feedback' |
收集当前行内 RenderFieldPresenter 的 error/warning message,在该行下方绘制反馈块。 |
行级反馈只收集满足以下条件的 child:
- child 是
RenderFieldPresenter。 child.message非空。child.status是'error'或'warning'。
const validationGrid = new RenderEntryGrid({ columns: [fieldTrack(), fieldTrack()], validationPresentation: 'row-feedback',}) validationGrid.addChild(new RenderFieldPresenter({ label: '患者姓名', required: true, status: 'error', message: '患者姓名不能为空', child: new RenderTextBox({ value: '', placeholder: '请输入' }),}))
反馈块会使用字段 label 和 message 拼成文本,例如 患者姓名: 患者姓名不能为空。当同一行有多个反馈块并且横向重叠时,会自动分到下一条 lane,避免互相覆盖。
Enter 导航
enterNavigation:
| 值 | 行为 |
|---|---|
'none' |
不处理子控件冒泡上来的 Enter。 |
'next-focusable' |
普通 Enter 移动到下一个 focusable,Shift+Enter 移动到上一个。 |
handleUnhandledEnterFromDescendant(target, event) 会忽略 Ctrl、Meta、Alt 组合键。焦点移动使用 FocusManager.instance.moveFocusFrom(target, dir, { withinRoot: this, wrap: false }),因此只在当前 EntryGrid 内查找,不循环。
const keyboardGrid = new RenderEntryGrid({ columns: [fieldTrack(), fieldTrack()], enterNavigation: 'next-focusable',})
通常业务不需要直接调用 handleUnhandledEnterFromDescendant(),由输入组件和焦点系统协作触发。
动态更新
function rebuildQueryFields(grid: RenderEntryGrid): void { grid.clearChildren() grid.setColumns([fieldTrack(), fieldTrack(), fieldTrack()]) grid.addChild(new RenderFieldPresenter({ label: '关键字', child: new RenderTextBox({ value: '', placeholder: '请输入关键字' }), }), { columnSpan: 2 })}
动态修改建议:
- 替换列轨道用
setColumns()。 - 重建字段前调用
clearChildren(),让旧 child 正常 detach。 - 调整间距用
columnGap/rowGapsetter。 - 不要直接改
childData后期待自动 layout;改完应调用能触发布局的方法,或重新 add child。
主题和绘制
RenderEntryGrid 使用 deriveEntryGridStyle(theme):
- 默认
columnGap、rowGap。 - 行级反馈的 gap、padding、圆角、字号、行高。
- error/warning 反馈块背景、边框和文本色。
普通 child 的绘制由 child 自己完成。RenderEntryGrid 只在 validationPresentation === 'row-feedback' 且有可收集消息时,在 children 绘制之后额外绘制反馈块。
与相关组件的区别
| 能力 | RenderEntryGrid |
RenderFormPanel |
RenderDataGrid |
|---|---|---|---|
| 主要用途 | 表单项二维布局 | 更完整的表单面板/分组 | 数据表格 |
| 数据模型 | 无,只布局 child | 面向表单组合 | 行列数据 |
| 子内容 | 任意 RenderBox |
表单组件组合 | 单元格文本/编辑器 |
| 校验展示 | 可选 row feedback | 表单面板语义 | 单元格校验 |
| 排序/选择 | 不支持 | 不支持 | 支持表格能力 |
| 滚动 | 不内置 | 取决于组合 | 表格内部滚动 |
RenderFormPanel 的 addField(field, data?) 使用 FormPanelChildData:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
columnSpan |
number |
1 |
字段横跨的列数。 |
RenderFieldPresenter 的 label、meta 和校验显示相关 public type:
| 类型 | 可选值 | 用途 |
|---|---|---|
LabeledFieldLabelOverflow |
'expand' | 'clip' |
控制 label 过长时扩展还是裁剪。 |
LabeledFieldMetaTone |
'default' | 'success' | 'warning' | 'danger' |
控制 meta 文本语气。 |
LabeledFieldValidationDisplay |
'compact' | 'inline' |
控制校验信息的紧凑或行内展示方式。 |
常见问题
为什么 EntryGrid 的宽度不符合预期?
列宽计算依赖父级传入的 constraints.maxWidth。如果父级给了无限宽,EntryGrid 只能按有限信息计算,实际页面应把它放在有明确宽度约束的容器中。
为什么某个跨列字段位置不对?
EntryGrid 按 child 添加顺序 first-fit 排列。columnSpan 和 rowSpan 会占用网格格子,后续 child 会跳过已占用位置。复杂布局应先添加固定位置预期更强的字段。
为什么 row-feedback 没显示?
检查 child 是否是 RenderFieldPresenter,status 是否为 'error' 或 'warning',并且 message 是否非空。普通 RenderTextBox 或其他 child 不会被 row-feedback 收集。
为什么 Enter 没有跳到下一个字段?
需要设置 enterNavigation: 'next-focusable',并且当前子控件没有自己处理该 Enter。Ctrl/Meta/Alt + Enter 不会触发 EntryGrid 的焦点移动。
EntryGrid 会帮我管理字段值吗?
不会。字段值、校验规则、保存、清空、远程查询都由业务组件或页面状态维护。EntryGrid 只负责布局和少量交互辅助。