Appearance
AtForm
沈孟平
最后更新于 2026-04-23 19:53:53
一个声明式的支持自动处理联动依赖关系的表单封装。
- 完备的类型提示
- 嵌套表单,支持对象或者对象数组的形式,可选择是以表格还是块状形式渲染
- 校验错误可以自动滚动定位到第一个错误项,通过
scrollToFirstError属性控制 - 通过
apiFn配置,支持自动获取下拉数据 - 支持自动处理表单项之间依赖和外部依赖,仅需声明依赖和依赖处理函数即可
- 支持不可编辑状态,通过
editable属性控制 v0.12.35 - 支持 可编辑表格形式渲染 嵌套表单
objArrayTable - 支持 按分块模式渲染 嵌套表单
objArrayBlock
心智模型
理解 AtForm 最重要的是把它看成一个“声明式字段渲染引擎”,而不是普通表单容器:
model负责承载数据。configs负责描述字段如何渲染、何时隐藏、依赖谁、怎么联动。type + props + children决定单个字段或容器字段的渲染方式。deps + listener让字段之间的动态关系变成配置,而不是散落在模板里的手写监听。
什么时候适合用它
- 表单字段较多,且未来会持续增长。
- 表单项之间存在联动、显隐、异步选项加载。
- 同一套配置需要在普通表单、弹窗表单、搜索表单之间复用。
- 需要对象块、数组块、数组表格等复杂嵌套结构。
和其它组件的关系
- AtSearch 本质上就是基于
AtForm的搜索壳。 - AtModal 常和
AtForm组合,用于新增、编辑、查看详情。 AtTreeFilter、AtUpload、AtCronInput、AtMonacoEditor等也都可以作为AtForm字段类型的一部分。
代码演示
支持的所有表单类型
支持的类型(包括插槽):input、switch、inputNumber、select、cascader、datePicker、timePicker、treeSelect、radio、upload、colorPicker、checkbox、cronInput、titleBar、objArrayBlock、objArrayTable、basicArray、objBlock、monacoEditor、custom、title v0.12.37
三级联动
该例子展示了如何使用声明依赖的方式deps + listener来进行表单之间的联动控制。
异步级联或者异步树形表单组件值回显
注意 确保自己的数据是响应式的
ts
// 表单项配置文件
const orgConfigs: AtFormItemConfig[] = [
// ...
{
label: '机构位置',
field: 'area_code1',
type: 'cascader',
props: {
labelField: 'name',
valueField: 'code',
},
onChange({value, ...rest}) {
// const option = rest[0]
const pathValues = rest[1]
orgModel.area_code = pathValues.map((item: any) => item.code)
},
deps: [areaCodeOptions, loadingAreaCodeOptions],
listener({ config }) {
config.props = {
options: areaCodeOptions.value,
loading: loadingAreaCodeOptions.value,
labelField: 'name',
valueField: 'code',
remote: true,
checkStrategy: 'child',
onLoad: async (option: any) => {
// 这里演示如何异步加载
// 注: 如果这里你使用的是`useRequest`的返回数据, 会发现响应式丢失, 因为默认是**shallowRef**, 需要将useRequest的options.shallow 设置为 `false`, 返回深层响应式数据
const { data } = await ARC_MANAGE_API.areaList({ level: +option.level + 1, code: option.code })
option.children = data.data.map((item: any) => ({ ...item, isLeaf: +option.level > 3 }))
},
}
},
},
// ...
]ts
// 演示如何回显
async function getAreaCodeOptions(params: any) {
const { data } = await ARC_MANAGE_API.areaList(params)
return data.data.map((item: any) => ({ ...item, isLeaf: params.level > 3 })) || []
}
const loadingAreaCodeOptions = ref(false)
// 级联回显函数
async function cascadeEcho(areaCodes: string[]): Promise<any[]> {
loadingAreaCodeOptions.value = true
// 如果没有需要回显的区域代码,直接返回顶层选项
if (!areaCodes.length) {
const data = await getAreaCodeOptions({})
loadingAreaCodeOptions.value = false
return data
}
async function loadChildren(options: any[], codes: string[], currentIndex: number): Promise<void> {
if (currentIndex >= codes.length)
return
const currentCode = codes[currentIndex]
for (const option of options) {
if (option.code === currentCode) {
// 获取下一级选项
const level = +option.level + 1
const children = await getAreaCodeOptions({ code: currentCode, level })
option.children = children
// 递归加载下一级
await loadChildren(children, codes, currentIndex + 1)
break
}
}
}
const result = await getAreaCodeOptions({})
await loadChildren(result, areaCodes, 0)
loadingAreaCodeOptions.value = false
console.log('loadingAreaCodeOptions: ', loadingAreaCodeOptions.value)
return result
}响应式表单(不使用 deps|listener 方式)
如果你的表单不涉及到比较复杂的动态控制,推荐使用这个,写起来更简单一些。使用 computed 包裹后,deps|listener 方式会失效。
显隐控制
如果表单要控制某一项的显示隐藏,则可以通过依赖声明,并在 listener 中通过参数 config 控制即可。
可编辑表格
- 可以通过
tableProps.maxHeight来启用表格内滚动条,每次新增会自动定位到最新一条。 - 嵌套表单中,
config参数中会多一个index属性,表示当前表单项的索引,还有一个parentModel属性,表示当前表单项的父级 model。
外部表单项控制表格表单渲染
API
TIP
AtFormItemConfig 继承自 NaiveUI FormItem Props
AtForm Props
| 名称 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| configs | array | 必填项 | 表单配置项 | |
| model | object | 必填项 | 表单数据对象 | |
| init-value | object | undefined | 默认数据(设置后,重置表单会恢复到绑定值状态) | |
| layout | GridProps | { xGap: 12 } | 栅格配置(v0.13.0 起由 NRow 迁移至 NGrid) | v0.13.0 |
| scroll-to-first-error | boolean | true | 是否滚动到第一个出现错误的位置 | |
| n-form-props | FormProps | undefined | NaiveUI Form Props |
AtFormItemConfig Properties
| 名称 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| type | string | 必填项 | 支持的所有表单类型 | |
| field | string | 必填项 | 表单项的key | |
| label | string | Component | undefined | 标签信息 | |
| auto-required | boolean | RuleType | undefined | 是否必填校验 | v0.12.36 |
| props | 根据type获取对应的类型 | undefined | 控件对应的Props | |
| on-change | function | undefined | 表单项变化回调, 类型参考 类型声明 | |
| call-change-on-mounted | boolean | undefined | 控件挂载的时候是否调用 onChange | |
| span | number | ResponsiveDescription | 24 | 栅格占据的列数 | |
| hide | boolean | false | 是否隐藏 | |
| api-fn | ApiFn | undefined | 需要自动发起 api 请求 | |
| refresh-api-when-show | boolean | undefined | 当hide 属性变为 true 时,是否重新调用 apiFn 函数 | |
| component | Component | undefined | 当type为custom时的组件 | |
| deep | boolean | undefined | 是否在对依赖处理的时候需要深度监听 | |
| deps | (string | Ref<any>)[] | undefined | 声明监听依赖 | |
| listener | function | undefined | 当依赖变化时调用 | |
| no-call-listener-on-mounted | boolean | undefined | 控件挂载的时候是否调用 listener | |
| observe-parent-model | boolean | undefined | 当存在多层级表单时,是否监听父级表单的 | |
| children | array | undefined | 嵌套表单项 | |
| editable | boolean | true | 是否可编辑 | v0.12.35 |
| slots | object | undefined | 控件对应的插槽 | v0.12.39 |
| multiple-config | multipleConfig Properties | undefined | 针对混合表单(数组) | |
| basic-array | Omit<AtFormItemConfig, 'field' | 'children' | 'multipleConfig'> | undefined | 针对基础数组表单 | |
| table-props | DataTableProps & { operationConfig?: object } | undefined | 针对表格表单 |
multipleConfig Properties
| 名称 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| default-item | Array<any> | object | undefined | 默认取数组第一行作为初始值,如果是 arrayBlock 则必须传 | |
| editable | boolean | true | 是否可编辑 | |
| limit | number | undefined | 表示数组至少要有几项 | |
| hide-num-prefix | boolean | false | 是否隐藏行号 | |
| num-prefix | string | function | undefined | 嵌套表单的数字前缀 | |
| hide-add-btn | boolean | false | 是否隐藏添加按钮 | |
| hide-remove-btn | boolean | false | 是否隐藏删除按钮 | |
| add-btn-text | string | undefined | 添加一行的按钮文本 | |
| add-btn-props | AtIconButtonProps | undefined | 添加一行的按钮 props | |
| on-add-button-click | function | undefined | 点击添加一行按钮执行的回调函数 | |
| on-remove-button-click | function | undefined | 点击每一项的删除按钮触发的回调函数 | |
| item-class | string | undefined | 每一项的 class |
容器型表单项的 props.layout v0.13.0
objBlock / objArrayBlock / basicArray 这三种容器型表单项内部使用 NGrid 渲染子字段,可通过 props.layout 透传 GridProps 自定义内层栅格行为(如调整 xGap / yGap / cols 等)。
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| layout | GridProps | basicArray 为 { xGap: 8, yGap: 8 }objBlock / objArrayBlock 为 { xGap: 8 } | 透传给内层 NGrid 的栅格配置,会覆盖上述默认值 |
TIP
容器内部默认带 layout-shift-disabled,以兼容 field 组件作为 NGrid 直接子节点的场景(NGrid 在响应模式下会过滤非 NGi 的直接子节点)。如果显式传 { layoutShiftDisabled: false, ... },子字段会被全部隐藏,请勿这样使用。
示例:
ts
const configs: AtFormItemConfig[] = [
{
type: 'objBlock',
field: 'profile',
label: '个人信息',
props: {
showBorder: true,
layout: { xGap: 16, yGap: 8 },
},
children: [
{ type: 'input', field: 'name', label: '姓名', span: 12 },
{ type: 'input', field: 'phone', label: '电话', span: 12 },
],
},
{
type: 'objArrayBlock',
field: 'addresses',
props: { layout: { xGap: 12 } },
children: [
{ type: 'input', field: 'city', label: '城市', span: 8 },
{ type: 'input', field: 'street', label: '街道', span: 16 },
],
},
{
type: 'basicArray',
field: 'tags',
props: { layout: { xGap: 4, yGap: 4 } },
basicArray: { type: 'input', span: 8 },
},
]AtForm Expose
| 名称 | 类型 | 说明 | 版本 |
|---|---|---|---|
| validate | (...args: any[]) => Promise<any> | 手动触发校验表单 | |
| reset-value | () => void | 重置表单数据为初始化值 | |
| set-value | (newValue: any) => void | 手动设置表单数据 | |
| restore-validation | () => void | 还原到未校验的状态 | |
| get-el | () => HTMLElement | 获取表单dom | |
| get-final-configs | () => AtFormItemConfig[] | 获取动态变化后的表单项配置 | v0.12.38 |
类型声明
显示类型声明
Typescript
interface ListenerOptions<P> {
refreshApi?: () => Promise<any>
config: AtFormItemConfig<P>
model: Recordable
index?: number
parentModel?: any
}
interface OnChangeOptions<P> {
/** 当前表单项的 apiFn */
refreshApi?: () => Promise<any>
/** 当前表单项的配置,支持直接修改 */
config: AtFormItemConfig<P>
/** 当前 model,嵌套表单为父级 */
model: Recordable
/** 嵌套表单时对应的数组下标 */
index?: number
/** 嵌套表单时对应的父级 model */
parentModel?: any
/** 当前表单项变化后的值 */
value: any
/** 其他 naive表单变化时传的参数,如 option|pathValue等,打印出来通过下标访问(剩余参数收集) */
[key: string]: any
}
type ApiFn = (apiFnOptions: { config?: AtFormItemConfig, model?: any }) => Promise<any[]>破坏性变更 v0.13.0
Caution
v0.13.0 起 AtForm 内部由 NRow / NCol 全量迁移至 NGrid / NGi,对外 API 不再兼容旧写法。升级时请按下面的对照做改动。
1. AtForm 的 layout 属性类型调整
- 类型:
RowProps→GridProps - 默认值:
{ gutter: 12 }→{ xGap: 12 } NRow.gutter/NCol.span的 API 在NGrid侧没有完全等价字段,请按 naive-ui 官方字段名调整。
常用字段对照:
v0.12.x(NRow) | v0.13.0(NGrid) | 说明 |
|---|---|---|
gutter: N | xGap: N | 水平间距 |
gutter: [x, y] | xGap: x, yGap: y | 水平 / 垂直间距 |
| — | cols | 栅格总列数,默认 24 |
| — | layoutShiftDisabled | 内部默认开启,详见下方“注意事项” |
迁移示例:
diff
- <AtForm :layout="{ gutter: 8 }" :model="model" :configs="configs" />
+ <AtForm :layout="{ xGap: 8 }" :model="model" :configs="configs" />2. 容器型表单项新增 props.layout
objBlock / objArrayBlock / basicArray 的 props 新增可选的 layout?: GridProps,用于自定义内层 NGrid,详见 容器型表单项的 props.layout。
3. 直接依赖 NRow / NCol 的外部代码需同步迁移
如果你在自己的模板里直接使用过 <NRow> / <NCol> 且其 props 来自 AtFormProps['layout'] / AtFormItemConfig['span'],请改用 <NGrid> / <NGi>。AtFormItemConfig.span 的类型已由 ColProps['span'] 改为 GridItemProps['span'],实际取值(number | ResponsiveDescription)保持不变,一般无需改动。
注意事项
Caution
- 注意:新版本若使用
computed包裹表单配置项,deps|listener方式去修改当前config会失效。 - 也就是说如果使用
computed包裹表单配置项,则不要使用deps|listener方式去修改当前config。 - 当涉及到表单间联动的时候推荐可以采用
deps声明依赖,使用listener进行依赖变更后处理,使用方法看这里 或者 看那里 - 注意
deps监听字符串只能针对同级表单生效,如果需要监听非平级表单,则需要包裹computed进行监听,例如model.a.b监听model.a的值, 则需要包裹computed进行监听,例如computed(() => model.a)。注意 - 表单验证出现
value是number或其他类型的时候,在配置项中的rule需要指定type为number或者指定类型, 否则会报错。 autoRequired开始支持指定类型简单校验。v0.12.36- 注意!!!
objArrayTable与objArrayBlock在超多行多列下会表现卡顿,推荐设置editable为false去自行处理编辑。v0.12.30
Warning
- 注意当使用
apiFn的时候,props中的options会覆盖apiFn返回的options。 - 注意当使用
apiFn的时候,它返回的值采用的是 shallowRef 包裹,如果你使用例如cascader组件,并且需要使用异步加载(onLoad和remote),则需要设置apiFn的返回值为reactive(options)。 - 当使用自定义组件,也就是
type为custom的时候,需要使用markRaw包裹该组件。