布局系统

View source
使用布局系统灵活控制表单字段的视觉呈现和组织结构

基础用法

简单容器布局

使用 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>
布局字段的 key(如 $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>
使用 Tailwind 的响应式前缀(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(),避免验证错误。

布局配置详解

AutoFormLayoutConfig

afz.layout 接受一个配置对象,包含以下属性:

属性类型说明
componentComponent | string布局容器组件(默认:'div'
classstring | (context) => stringCSS 类名,支持响应式函数
propsobject | (context) => object组件属性,支持响应式函数
slotsRecord<string, () => VNode> | (context) => ...组件插槽内容
fieldsRecord<string, ZodType>包含的字段定义
fieldSlotstring | (context) => string所有字段渲染到指定插槽
fieldSlotsRecord<string, string> | (context) => ...不同字段分配到不同插槽

响应式配置函数

classpropsslotsfieldSlotfieldSlots 都支持响应式函数,接收一个上下文对象:

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 vs fieldSlots

  • 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>
嵌套布局的深度没有限制,但过深的嵌套可能影响可读性。建议保持在 2-3 层。

数据提取机制

布局字段在数据验证时会被自动展开为其内部的实际数据字段:

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。

这意味着你可以自由调整布局结构,而不影响现有的数据结构或验证逻辑。
Copyright © 2025 - 2025 YiXuan - MIT License