使用 afz.layout 创建一个基础的容器布局,通过 class 属性控制样式:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod/v4'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$layout: afz.layout({
class: 'space-y-4',
fields: {
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' }),
email: afz.email().meta({ label: '邮箱地址' })
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<UCard>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</UCard>
</template>
$layout)以 $ 开头是惯例写法,但不是必需的。你可以使用任何字段名,只要其值为 afz.layout() 即可。使用 CSS Grid 创建多列布局,通过字段级别的 class 控制跨列:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod/v4'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$grid: afz.layout({
class: 'grid grid-cols-2 gap-4',
fields: {
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' }),
email: afz.email().meta({
class: 'col-span-2',
label: '邮箱地址',
description: '此字段占据两列宽度'
}),
phone: afz.string().meta({ label: '电话' }),
age: afz.number().int().min(0).meta({ label: '年龄' })
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<UCard>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</UCard>
</template>
利用 Tailwind 的响应式前缀,创建自适应不同屏幕尺寸的布局:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod/v4'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$responsive: afz.layout({
// 移动端 1 列,平板 2 列,桌面端 3 列
class: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4',
fields: {
title: afz.string().meta({
class: 'col-span-full', // 所有屏幕尺寸都占满整行
label: '标题'
}),
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' }),
email: afz.email().meta({ label: '邮箱' }),
phone: afz.string().meta({ label: '电话' }),
age: afz.number().int().min(0).meta({ label: '年龄' }),
description: afz.string({ type: 'textarea' }).meta({
class: 'col-span-full',
label: '描述'
})
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<UCard>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</UCard>
</template>
md:、lg:)可以轻松实现跨设备的自适应布局。字段可以通过 col-span-full 占据整行。使用 Nuxt UI 的 UAccordion 组件,将字段组织在可折叠的面板中:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod/v4'
import { UAccordion } from '#components'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$accordion: afz.layout({
component: UAccordion,
props: {
type: 'multiple',
ui: {
content: 'space-y-4'
},
items: [
{ label: '基本信息', icon: 'i-lucide-user', slot: 'item-0' },
{ label: '详细信息', icon: 'i-lucide-square-pen', slot: 'item-1' }
]
},
fieldSlots: {
name: 'item-0',
email: 'item-0',
bio: 'item-1'
},
fields: {
name: afz.string().meta({ label: '姓名' }),
email: afz.email().meta({ label: '邮箱' }),
bio: afz.string({ type: 'textarea' }).meta({ label: '个人简介' }).optional()
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<UCard>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</UCard>
</template>
fieldSlots 用于将不同字段分配到布局组件的不同插槽中。插槽名称需要与 items 中定义的 slot 对应。使用 UTabs 组件创建分页式表单,支持动态响应式配置:
<script setup lang="ts">
import { UTabs } from '#components'
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod/v4'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
userType: afz.enum(['personal', 'company'])
.default('personal')
.meta({ label: '用户类型' }),
$tabs: afz.layout({
component: UTabs,
props: ({ state }) => {
const isCompany = state?.userType === 'company'
return {
ui: { content: 'space-y-4' },
items: isCompany
? [
{ label: '公司信息', icon: 'i-lucide-building', slot: 'tab-0' },
{ label: '联系方式', icon: 'i-lucide-phone', slot: 'tab-1' }
]
: [
{ label: '个人信息', icon: 'i-lucide-user', slot: 'tab-0' },
{ label: '联系方式', icon: 'i-lucide-phone', slot: 'tab-1' }
]
}
},
fieldSlots: ({ state }) => {
if (state?.userType === 'company') {
return {
companyName: 'tab-0',
registrationNumber: 'tab-0',
phone: 'tab-1',
email: 'tab-1',
address: 'tab-1'
}
}
return {
username: 'tab-0',
age: 'tab-0',
phone: 'tab-1',
email: 'tab-1',
address: 'tab-1'
}
},
fields: {
username: afz.string().meta({
label: '姓名',
if: ({ state }) => state?.userType === 'personal'
}).optional(),
age: afz.number().int().min(0).meta({
label: '年龄',
if: ({ state }) => state?.userType === 'personal'
}).optional(),
companyName: afz.string().meta({
label: '公司名称',
if: ({ state }) => state?.userType === 'company'
}).optional(),
registrationNumber: afz.string().meta({
label: '注册号',
if: ({ state }) => state?.userType === 'company'
}).optional(),
phone: afz.string().meta({ label: '联系电话' }),
email: afz.email().meta({ label: '邮箱地址' }),
address: afz.string().meta({ label: '地址' })
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<UCard>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</UCard>
</template>
if)时,建议将字段标记为 .optional(),避免验证错误。afz.layout 接受一个配置对象,包含以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
component | Component | string | 布局容器组件(默认:'div') |
class | string | (context) => string | CSS 类名,支持响应式函数 |
props | object | (context) => object | 组件属性,支持响应式函数 |
slots | Record<string, () => VNode> | (context) => ... | 组件插槽内容 |
fields | Record<string, ZodType> | 包含的字段定义 |
fieldSlot | string | (context) => string | 将所有字段渲染到指定插槽 |
fieldSlots | Record<string, string> | (context) => ... | 将不同字段分配到不同插槽 |
class、props、slots、fieldSlot 和 fieldSlots 都支持响应式函数,接收一个上下文对象:
interface LayoutContext {
state: FormState // 表单当前状态
path: string // 布局字段的路径
value: any // 布局字段的值(通常为空)
setValue: (value) => void // 设置值的函数
}
示例:
afz.layout({
class: ({ state }) => state?.mode === 'advanced'
? 'grid grid-cols-3 gap-4'
: 'space-y-4',
props: ({ state }) => ({
disabled: state?.readOnly === true
})
})
fieldSlot - 将所有字段渲染到同一个插槽,适用于简单场景:afz.layout({
component: UAccordion,
fieldSlot: 'content', // 所有字段都渲染到 'content' 插槽
fields: { /* ... */ }
})
fieldSlots - 精确控制每个字段的插槽位置,适用于分组场景:afz.layout({
component: UAccordion,
fieldSlots: {
name: 'panel-1',
email: 'panel-1',
bio: 'panel-2'
},
fields: { /* ... */ }
})
布局字段可以无限嵌套,构建复杂的层级结构:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod/v4'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$personalInfo: afz.layout({
class: 'grid grid-cols-2 gap-4',
fields: {
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' })
}
}),
$contactInfo: afz.layout({
class: 'space-y-4 mt-4',
fields: {
email: afz.email().meta({ label: '邮箱地址' }),
$phoneLayout: afz.layout({
class: 'grid grid-cols-3 gap-2',
fields: {
countryCode: afz.string().default('+86').meta({ label: '国家代码' }),
phoneNumber: afz.string().meta({
class: 'col-span-2',
label: '电话号码',
hint: '此字段占据两列宽度'
})
}
})
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<UCard>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</UCard>
</template>
布局字段在数据验证时会被自动展开为其内部的实际数据字段:
const schema = afz.object({
$layout: afz.layout({
fields: {
name: afz.string(),
email: afz.email()
}
})
})
// 表单数据结构(不包含 $layout)
const formData = {
name: 'John',
email: 'john@example.com'
}
这个机制由 extractPureSchema 函数实现,它会递归遍历 schema,提取所有布局字段中的实际数据字段,生成一个“纯净”的验证 schema。