Skip to content

AtForm ​

沈孟平
最后更新于 2026-10-30 11:40:07

一个声明式的支持自动处理联动依赖关系的表单封装。

  • 完备的类型提示
  • 嵌套表单,支持对象或者对象数组的形式,可选择是以表格还是块状形式渲染
  • 校验错误可以自动滚动定位到第一个错误项,通过 scrollToFirstError 属性控制
  • 通过 apiFn 配置,支持自动获取下拉数据
  • 支持自动处理表单项之间依赖和外部依赖,仅需声明依赖和依赖处理函数即可
  • 支持不可编辑状态,通过 editable 属性控制 v0.12.35
  • 支持 可编辑表格形式渲染 嵌套表单 objArrayTable
  • 支持 按分块模式渲染 嵌套表单 objArrayBlock

详细场景见 AtForm 复杂用法。

心智模型 ​

理解 AtForm 最重要的是把它看成一个“声明式字段渲染引擎”,而不是普通表单容器:

  1. model 负责承载数据。
  2. configs 负责描述字段如何渲染、何时隐藏、依赖谁、怎么联动。
  3. type + props + children 决定单个字段或容器字段的渲染方式。
  4. deps + listener 让字段之间的动态关系变成配置,而不是散落在模板里的手写监听。

什么时候适合用它 ​

  • 表单字段较多,且未来会持续增长。
  • 表单项之间存在联动、显隐、异步选项加载。
  • 同一套配置需要在普通表单、弹窗表单、搜索表单之间复用。
  • 需要对象块、数组块、数组表格等复杂嵌套结构。

和其它组件的关系 ​

  • AtSearch 本质上就是基于 AtForm 的搜索壳。
  • AtModal 常和 AtForm 组合,用于新增、编辑、查看详情。
  • AtTreeFilter、AtUpload、AtCronInput、AtMonacoEditor 等也都可以作为 AtForm 字段类型的一部分。

使用建议 ​

  • 静态字段使用普通配置数组;动态配置优先 deps + listener,把依赖和变化放在字段配置旁。
  • 字段之间依赖用字段名,外部状态依赖用 ref;不默认用 computed 重建整份配置,也不重复添加页面 watch。
  • 简单表单在本组件内组织 model/configs 与打开、关闭、提交;复杂后按职责拆局部 hook 或子组件。
  • listener 负责字段联动;提交、成功事件和关闭由表单容器处理。复杂请求或数据转换不持续堆入字段回调。
  • 提交时将草稿转换成接口 DTO,不要把编辑中的数组等数据原地序列化;失败后应能继续编辑和重试。
  • 接入联动前核对当前组件版本的隐藏字段、校验、远程选项行为;示例中的业务条件需要按实际需求替换。

代码演示 ​

支持的所有表单类型 ​

支持的类型(包括插槽):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,包括外部 Ref 依赖。以下 computed 示例保留为整体替换配置的备选方式,不作为普通字段显隐或禁用的默认写法;不要混用配置整体重建与 deps / listener 的更新职责。

显隐控制 ​

如果表单要控制某一项的显示隐藏,则可以通过依赖声明,并在 listener 中通过参数 config 控制即可。

可编辑表格 ​

  1. 可以通过 tableProps.maxHeight 来启用表格内滚动条,每次新增会自动定位到最新一条。
  2. 嵌套表单中,config 参数中会多一个 index 属性,表示当前表单项的索引,还有一个 parentModel 属性,表示当前表单项的父级 model。

外部表单项控制表格表单渲染 ​

API ​

TIP

AtFormItemConfig 继承自 NaiveUI FormItem Props

AtForm Props ​

名称类型默认值说明版本
configsarray必填项表单配置项
modelobject必填项表单数据对象
init-valueobjectundefined默认数据(设置后,重置表单会恢复到绑定值状态)
layoutGridProps{ xGap: 12 }栅格配置(v0.13.0 起由 NRow 迁移至 NGrid)v0.13.0
scroll-to-first-errorbooleantrue是否滚动到第一个出现错误的位置
n-form-propsFormPropsundefinedNaiveUI Form Props

AtFormItemConfig Properties ​

名称类型默认值说明版本
typestring必填项支持的所有表单类型
fieldstring必填项表单项的key
labelstring | Componentundefined标签信息
auto-requiredboolean | RuleTypeundefined是否必填校验v0.12.36
props根据type获取对应的类型undefined控件对应的Props
on-changefunctionundefined表单项变化回调, 类型参考 类型声明
call-change-on-mountedbooleanundefined控件挂载的时候是否调用 onChange
spannumber | ResponsiveDescription24栅格占据的列数
hidebooleanfalse是否隐藏
api-fnApiFnundefined自动获取选项;挂载时 hide 为 true 则跳过初始化请求
refresh-api-when-showbooleanundefined表单项显示时是否重新调用 apiFn,适用于 select、treeSelect、cascader
componentComponentundefined当type为custom时的组件
deepbooleanundefined是否在对依赖处理的时候需要深度监听
deps(string | Ref<any>)[]undefined声明监听依赖
listenerfunctionundefined当依赖变化时调用
no-call-listener-on-mountedbooleanundefined控件挂载的时候是否调用 listener
observe-parent-modelbooleanundefined当存在多层级表单时,是否监听父级表单的
childrenarrayundefined嵌套表单项
editablebooleantrue是否可编辑v0.12.35
slotsobjectundefined控件对应的插槽v0.12.39
multiple-configmultipleConfig Propertiesundefined针对混合表单(数组)
basic-arrayOmit<AtFormItemConfig, 'field' | 'children' | 'multipleConfig'>undefined针对基础数组表单
table-propsDataTableProps & { operationConfig?: object }undefined针对表格表单

multipleConfig Properties ​

名称类型默认值说明版本
default-itemArray<any> | objectundefined默认取数组第一行作为初始值,如果是 arrayBlock 则必须传
editablebooleantrue是否可编辑
limitnumberundefined表示数组至少要有几项
hide-num-prefixbooleanfalse是否隐藏行号
num-prefixstring | functionundefined嵌套表单的数字前缀
hide-add-btnbooleanfalse是否隐藏添加按钮
hide-remove-btnbooleanfalse是否隐藏删除按钮
add-btn-textstringundefined添加一行的按钮文本
add-btn-propsAtIconButtonPropsundefined添加一行的按钮 props
on-add-button-clickfunctionundefined点击添加一行按钮执行的回调函数
on-remove-button-clickfunctionundefined点击每一项的删除按钮触发的回调函数
item-classstringundefined每一项的 class

容器型表单项的 props.layout v0.13.0 ​

objBlock / objArrayBlock / basicArray 这三种容器型表单项内部使用 NGrid 渲染子字段,可通过 props.layout 透传 GridProps 自定义内层栅格行为(如调整 xGap / yGap / cols 等)。

名称类型默认值说明
layoutGridPropsbasicArray 为 { 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: NxGap: 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 包裹该组件。

changelog ​

521d5 - feat(skills): 改进提取脚本并补充 use-modal、use-table-list-api on 2026/10/15
af4d1 - feat(AtForm): 将`NRow`迁移到`NGrid` on 2026/4/22
a4d32 - feat(components): 组件`AtForm`所有类型组件支持插槽 on 2025/10/24
be9a6 - feat(components): 增加`AtForm`提供获取真实`configs`配置方法 on 2025/10/24
eaf16 - fix(components): 修复`AtForm`中`resetValue`处理对象问题 on 2025/10/24
333b1 - feat(components): `AtForm`新增支持类型`title` on 2025/10/16
d6f21 - feat(components): `autoRequired`支持指定类型简单校验 on 2025/10/15
ddd9a - feat(components): `AtForm`全面支持`editable`设置不可编辑状态 on 2025/9/25
08c23 - fix(components): 移除`useDeps`函数中的`editable`参数 on 2025/9/22
783c2 - feat(components): 添加可编辑属性`editable`支持,优化表单字段组件的编辑逻辑 on 2025/9/22

贡献者 ​

Last updated: