插槽

View source
通过具名插槽自定义 AutoForm 的头尾、字段控件、字段前后与布局内容。

概述

AutoForm 的插槽系统分为三个层级:

  • 表单插槽 - 自定义表单整体结构(headerfootersubmit
  • 字段插槽 - 微调每个字段的各个部分(field-labelfield-hintfield-error 等)
  • 内容插槽 - 接管对象与数组字段的渲染或在字段前后插入内容(field-contentfield-beforefield-after
字段插槽支持通用模式field-{type}命名模式field-{type}:{path},让你既能统一样式,又能精确控制特定字段。命名模式优先级高于通用模式。

示例

表单级

headerfootersubmit 接管表单的头部、尾部与提交区,均可读取 loadingerrorsstate 定制外层。

使用 submit 插槽时需设置 :submit="false" 禁用默认提交按钮,否则会重复渲染。
header 状态展示
header 在字段渲染前接收 loading,可同步说明与提交状态
校验通过本次登录
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  email: afz.email().meta({ label: '邮箱' }),
  password: afz.string({ type: 'withPasswordToggle' }).min(6).meta({ label: '密码' }),
  remember: afz.boolean({ type: 'switch', controlProps: { label: '记住登录状态' } }).default(true).meta({ label: '记住我' })
})

const form = reactive<Partial<z.output<typeof schema>>>({})

async function onSubmit() {
  await new Promise(resolve => setTimeout(resolve, 1200))
}
</script>

<template>
  <MAutoForm
    :schema="schema"
    :state="form"
    :submit="false"
    @submit="onSubmit"
  >
    <template #header="{ loading }">
      <UAlert
        color="info"
        variant="soft"
        icon="i-lucide-log-in"
        title="header 状态展示"
        description="header 在字段渲染前接收 loading,可同步说明与提交状态"
      >
        <template v-if="loading" #actions>
          <UBadge color="info" variant="subtle">
            提交中
          </UBadge>
        </template>
      </UAlert>
    </template>

    <template #footer="{ errors, state }">
      <div class="mt-3 flex flex-wrap items-center gap-2 text-sm">
        <UBadge :color="errors.length ? 'error' : 'success'" variant="subtle">
          {{ errors.length ? `${errors.length} 个错误` : '校验通过' }}
        </UBadge>
        <UBadge color="neutral" variant="subtle">
          {{ state.remember ? '保持登录' : '本次登录' }}
        </UBadge>
      </div>
    </template>

    <template #submit="{ errors, loading }">
      <div class="mt-4 flex gap-2">
        <UButton type="submit" icon="i-lucide-send" :loading="loading" :disabled="errors.length > 0">
          登录
        </UButton>
        <UButton variant="outline" color="neutral" :disabled="loading">
          创建账号
        </UButton>
      </div>
    </template>
  </MAutoForm>
</template>

通用字段

field-labelfield-descriptionfield-hintfield-error 统一覆盖所有字段的标签、描述、提示与错误区域。

至少 3 个字符

通用 hint slot

用于接收通知

通用 hint slot

通用 hint slot

<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  username: afz.string().min(3).meta({ label: '用户名', hint: '至少 3 个字符' }),
  email: afz.email().meta({ label: '邮箱', description: '用于接收通知' }),
  role: afz.enum(['admin', 'user', 'guest']).meta({ label: '角色' }),
  active: afz.boolean({ controlProps: { label: '启用账户' } }).default(true).meta({ label: '状态' })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state" :validate-on="['input', 'blur']">
    <template #field-label="{ label, path }">
      <span class="inline-flex items-center gap-2">
        <UBadge color="primary" variant="subtle" size="xs">
          {{ label }}
        </UBadge>
        <span class="text-xs text-muted">
          {{ path }}
        </span>
      </span>
    </template>

    <template #field-description="{ description }">
      <span
        v-if="description"
        class="text-xs text-toned"
      >
        {{ description }}
      </span>
    </template>

    <template #field-hint="{ hint }">
      <span class="inline-flex items-center gap-1 text-xs text-muted">
        <UIcon name="i-lucide-info" class="size-3" />
        <span>{{ hint || '通用 hint slot' }}</span>
      </span>
    </template>

    <template #field-error="{ error }">
      <UAlert
        v-if="error"
        color="error"
        variant="subtle"
        icon="i-lucide-triangle-alert"
        :description="String(error)"
        class="mt-2"
      />
    </template>
  </MAutoForm>
</template>

命名字段

field-{type}:{path} 仅替换指定字段的某个区域,未匹配的字段保持默认渲染。field-default:{path} 完全替换该字段的控件。

命名插槽优先级高于通用插槽:同时存在 field-labelfield-label:username 时,username 字段采用后者。
field-help:password 只替换 password 字段的帮助说明,并保留字段默认控件
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  username: afz.string().min(2).meta({ label: '用户名' }),
  password: afz.string({ type: 'withPasswordToggle' }).min(8).meta({ label: '密码' }),
  bio: afz.string({ type: 'textarea' }).default('一个简单的自我介绍').meta({ label: '简介' })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-label:username="{ label }">
      <UBadge icon="i-lucide-user" color="success" variant="subtle" size="xs">
        {{ label }}
      </UBadge>
    </template>

    <template #field-help:password>
      <UAlert
        color="warning"
        variant="subtle"
        icon="i-lucide-shield-check"
        description="field-help:password 只替换 password 字段的帮助说明,并保留字段默认控件"
        class="mt-2"
      />
    </template>

    <template #field-default:bio="{ setValue, value }">
      <UTextarea
        :model-value="value"
        :rows="5"
        placeholder="field-default:bio 完全替换默认 textarea"
        class="ring-2 ring-primary/30 rounded w-full"
        @update:model-value="setValue"
      />
    </template>
  </MAutoForm>
</template>

字段前后

field-before:{path}field-after:{path} 在默认字段内容前后追加内容,不接管子字段的渲染与交互。

field-before 插入点
field-before:settings 不接管子字段,只在默认内容前追加说明。
通知关闭未选择摘要频率 field-after 保留默认字段交互,只追加汇总状态。
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  settings: afz.object({
    notifications: afz.boolean({ type: 'switch', controlProps: { label: '接收通知' } }).default(true).meta({ label: '通知' }),
    digest: afz.enum(['daily', 'weekly', 'monthly']).default('weekly').meta({ label: '摘要频率' }),
    notes: afz.string({ type: 'textarea' }).default('保留默认字段渲染,只在前后插入 slot 内容。').meta({ label: '备注' })
  }).default({
    notifications: true,
    digest: 'weekly',
    notes: '保留默认字段渲染,只在前后插入 slot 内容。'
  }).meta({ label: '通知设置', collapsible: { defaultOpen: true } })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-before:settings="{ path }">
      <UAlert
        color="neutral"
        variant="subtle"
        icon="i-lucide-settings"
        title="field-before 插入点"
        :description="`field-before:${path} 不接管子字段,只在默认内容前追加说明。`"
        class="mb-4"
      />
    </template>

    <template #field-after:settings="{ value }">
      <div class="mt-4 flex flex-wrap items-center gap-2">
        <UBadge :color="value?.notifications ? 'success' : 'neutral'" variant="subtle">
          {{ value?.notifications ? '通知开启' : '通知关闭' }}
        </UBadge>
        <UBadge color="info" variant="subtle">
          {{ value?.digest || '未选择摘要频率' }}
        </UBadge>
        <span class="text-xs text-muted">
          field-after 保留默认字段交互,只追加汇总状态。
        </span>
      </div>
    </template>
  </MAutoForm>
</template>

对象内容

field-content:{path} 完全替换对象字段的渲染,通过 setValue('key', value) 以相对 key 更新子字段。

自定义个人资料

每个控件都通过相对 key 更新 profile 子字段

object
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  profile: afz.object({
    displayName: afz.string().meta({ label: '显示名称' }),
    website: afz.url().meta({ label: '个人网站' }),
    bio: afz.string({ type: 'textarea' }).meta({ label: '简介' })
  }).default({
    displayName: 'Movk User',
    website: 'https://movk.dev',
    bio: '使用 field-content:profile 接管对象字段渲染。'
  }).meta({ label: '个人资料', collapsible: { defaultOpen: true } })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-content:profile="{ path, value, setValue }">
      <div class="space-y-4 rounded-md border border-default p-4">
        <div class="flex items-center justify-between">
          <div>
            <p class="font-medium">
              自定义个人资料
            </p>
            <p class="text-sm text-muted">
              每个控件都通过相对 key 更新 profile 子字段
            </p>
          </div>
          <UBadge color="info" variant="subtle">
            object
          </UBadge>
        </div>

        <UFormField label="显示名称" :name="`${path}.displayName`" required>
          <UInput
            :model-value="value?.displayName"
            icon="i-lucide-user"
            placeholder="显示名称"
            @update:model-value="setValue('displayName', $event)"
          />
        </UFormField>

        <UFormField label="个人网站">
          <UInput
            :model-value="value?.website"
            icon="i-lucide-link"
            placeholder="https://movk.dev"
            @update:model-value="setValue('website', $event)"
          />
        </UFormField>

        <UFormField label="简介">
          <UTextarea
            :model-value="value?.bio"
            :rows="4"
            placeholder="简介"
            @update:model-value="setValue('bio', $event)"
          />
        </UFormField>
      </div>
    </template>
  </MAutoForm>
</template>

数组内容

field-content:{path} 替换数组字段的渲染,setValue 既可写入整个列表,也可用 [index].key 路径更新单项。

<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  todos: afz.array(
    afz.object({
      title: afz.string().min(1).meta({ label: '标题' }),
      priority: afz.enum(['low', 'medium', 'high']).default('medium').meta({ label: '优先级' }),
      done: afz.boolean().default(false).meta({ label: '完成' })
    })
  ).default([
    { title: '整理 slot 示例', priority: 'high', done: false },
    { title: '梳理 setValue 路径', priority: 'medium', done: false }
  ]).meta({ label: '待办列表', collapsible: { defaultOpen: true } })
})

const state = reactive<Partial<z.output<typeof schema>>>({})

const priorityItems = [
  { label: '', value: 'low' },
  { label: '', value: 'medium' },
  { label: '', value: 'high' }
]

function createTodo() {
  return { title: '', priority: 'medium' as const, done: false }
}
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-content:todos="{ value, setValue }">
      <div class="space-y-3">
        <div
          v-for="(todo, index) in value || []"
          :key="index"
          class="rounded-md border border-default p-4 space-y-3"
        >
          <div class="flex items-center justify-between gap-3">
            <UBadge color="neutral" variant="subtle">
              #{{ index + 1 }}
            </UBadge>
            <UButton
              icon="i-lucide-trash-2"
              color="error"
              variant="ghost"
              size="xs"
              @click="setValue((value || []).filter((_, current) => current !== index))"
            />
          </div>

          <UFormField label="标题">
            <UInput
              :model-value="todo?.title"
              placeholder="任务标题"
              @update:model-value="setValue(`[${index}].title`, $event)"
            />
          </UFormField>

          <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
            <UFormField label="优先级">
              <USelect
                :model-value="todo?.priority"
                :items="priorityItems"
                @update:model-value="setValue(`[${index}].priority`, $event)"
              />
            </UFormField>

            <UFormField label="完成状态">
              <USwitch
                :model-value="todo?.done"
                :label="todo?.done ? '已完成' : '进行中'"
                @update:model-value="setValue(`[${index}].done`, $event)"
              />
            </UFormField>
          </div>
        </div>

        <UButton
          icon="i-lucide-plus"
          color="info"
          variant="soft"
          size="sm"
          @click="setValue([...(value || []), createTodo()])"
        >
          添加任务
        </UButton>
      </div>
    </template>
  </MAutoForm>
</template>

API

表单级插槽

NameDescription
header字段区域上方,适合标题、说明或状态提示
footer字段区域下方、提交按钮上方
submit接管提交区,需配合 :submit="false"

字段插槽

SlotParameterTypeDescription
field-labellabelstring标签文本
field-hinthintstring提示文本
field-descriptiondescriptionstring描述文本
field-helphelpstring帮助文本
field-errorerrorstring | boolean错误信息
field-defaulterrorstring | boolean接管基础类型字段控件的渲染(附带字段上下文)
field-default 仅用于自定义基础类型字段(string、number、boolean 等)的控件。对象与数组字段请使用 field-content

内容插槽

内容插槽仅适用于对象字段与数组字段,对基础类型字段不生效。
NameDescription
field-content:{path}完全接管对象/数组字段的渲染
field-before:{path}在默认内容前插入,不接管子字段
field-after:{path}在默认内容后插入,不接管子字段

setValue 签名

setValue 支持以下重载形式:

SignatureReturnDescription
setValue(value)void设置整个字段值
setValue(relativePath, value)void按相对路径更新子字段:对象用 key(自动推断),数组用 [index].key
// 直接设置当前字段的值
setValue(newValue)

AutoFormFieldContext

字段上下文类型,在插槽和响应式函数中可用:

export interface AutoFormFieldContext<S = any, P extends string = string> {
  /** 表单数据 */
  readonly state: S
  /** 字段路径 */
  readonly path: P
  /** 字段值(可能为 undefined) */
  readonly value: FieldValueType<S, P>
  /**
   * 设置字段值的回调函数
   * @description 支持多种调用方式:
   * 1. setValue(value) - 设置当前字段的整个值
   * 2. setValue(key, value) - 设置子字段值(对象)或元素属性(数组)
   * 3. setValue(path, value) - 设置任意路径的值(字符串回退)
   * @example
   * // 对象字段 profile: { name, email, bio }
   * setValue({ name: '张三', email: 'test@example.com' })  // 设置整个对象
   * setValue('name', '张三')  // key 自动推断为 'name' | 'email' | 'bio'
   *
   * // 数组字段 tasks: [{ title, priority, completed }]
   * setValue([...tasks, newTask])  // 设置整个数组
   * setValue('[0].title', '新标题')  // 使用索引路径字符串
   *
   * // 或者在遍历中直接使用属性名(推荐)
   * v-for="(task, index) in tasks"
   * setValue(`[${index}].title`, value)  // 动态索引
   */
  setValue: {
    (value: FieldValueType<S, P>): void
    <K extends RelativePath<NonUndefinedFieldValue<S, P>>>(
      relativePath: K extends never ? string : K,
      value: any
    ): void
    (relativePath: string, value: any): void
  }
  /** 字段错误列表 */
  readonly errors: unknown[]
  /** 表单提交加载状态 */
  readonly loading: boolean
  /** 字段折叠状态 */
  readonly open?: boolean
  /** 数组元素下标 */
  readonly count?: number
}
Copyright © 2025 - 2026 YiXuan - MIT License