通过具名插槽自定义 AutoForm 的头尾、字段控件、字段前后与布局内容。
概述
AutoForm 的插槽系统分为三个层级:
- 表单插槽 - 自定义表单整体结构(
header、footer、submit) - 字段插槽 - 微调每个字段的各个部分(
field-label、field-hint、field-error等) - 内容插槽 - 接管对象与数组字段的渲染或在字段前后插入内容(
field-content、field-before、field-after)
字段插槽支持通用模式
field-{type} 与命名模式field-{type}:{path},让你既能统一样式,又能精确控制特定字段。命名模式优先级高于通用模式。示例
表单级
header、footer、submit 接管表单的头部、尾部与提交区,均可读取 loading、errors、state 定制外层。
使用
submit 插槽时需设置 :submit="false" 禁用默认提交按钮,否则会重复渲染。<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-label、field-description、field-hint、field-error 统一覆盖所有字段的标签、描述、提示与错误区域。
<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-label 与 field-label:username 时,username 字段采用后者。<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} 在默认字段内容前后追加内容,不接管子字段的渲染与交互。
<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 更新子字段。
<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
表单级插槽
| Name | Description |
|---|---|
header | 字段区域上方,适合标题、说明或状态提示 |
footer | 字段区域下方、提交按钮上方 |
submit | 接管提交区,需配合 :submit="false" |
字段插槽
| Slot | Parameter | Type | Description |
|---|---|---|---|
field-label | label | string | 标签文本 |
field-hint | hint | string | 提示文本 |
field-description | description | string | 描述文本 |
field-help | help | string | 帮助文本 |
field-error | error | string | boolean | 错误信息 |
field-default | error | string | boolean | 接管基础类型字段控件的渲染(附带字段上下文) |
field-default 仅用于自定义基础类型字段(string、number、boolean 等)的控件。对象与数组字段请使用 field-content。内容插槽
内容插槽仅适用于对象字段与数组字段,对基础类型字段不生效。
| Name | Description |
|---|---|
field-content:{path} | 完全接管对象/数组字段的渲染 |
field-before:{path} | 在默认内容前插入,不接管子字段 |
field-after:{path} | 在默认内容后插入,不接管子字段 |
setValue 签名
setValue 支持以下重载形式:
| Signature | Return | Description |
|---|---|---|
setValue(value) | void | 设置整个字段值 |
setValue(relativePath, value) | void | 按相对路径更新子字段:对象用 key(自动推断),数组用 [index].key |
// 直接设置当前字段的值
setValue(newValue)
<template #field-content:profile="{ value, setValue }">
<!-- 设置整个对象 -->
<UButton @click="setValue({ name: '张三', email: 'test@example.com' })">
填充默认值
</UButton>
<!-- 使用相对路径设置子字段 -->
<UInput
:model-value="value?.name"
@update:model-value="setValue('name', $event)"
/>
</template>
<template #field-content:tasks="{ value, setValue }">
<div v-for="(task, index) in value" :key="index">
<!-- 设置数组元素的属性 -->
<UInput
:model-value="task?.title"
@update:model-value="setValue(`[${index}].title`, $event)"
/>
<!-- 删除数组元素 -->
<UButton
@click="setValue(value.filter((_, i) => i !== index))"
icon="i-lucide-trash-2"
/>
</div>
<!-- 添加新元素 -->
<UButton
@click="setValue([...value, { title: '', completed: false }])"
icon="i-lucide-plus"
/>
</template>
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
}