SearchForm
简介
MSearchForm 是一个 Schema 驱动的搜索表单组件,内置网格布局、搜索 / 重置按钮和折叠行为。当搜索项较多时,超出可见行数的字段会自动折叠,用户可点击展开 / 收起按钮查看全部搜索项。
用法
按 AutoForm schema 渲染字段,cols 控制栅格,内置搜索与重置按钮;点「搜索」触发校验并发出 @submit:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入姓名' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入部门' } }).meta({ label: '部门' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入关键词' } }).meta({ label: '关键词' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入邮箱' } }).meta({ label: '邮箱' }).optional()
})
const state = ref<Partial<z.output<typeof schema>>>({})
const result = ref('')
function handleSearch(event: FormSubmitEvent<Record<string, unknown>>) {
result.value = JSON.stringify(event.data, null, 2)
}
function handleReset() {
result.value = ''
}
</script>
<template>
<div class="space-y-4">
<MSearchForm
v-model="state"
:schema="schema"
@submit="handleSearch"
@reset="handleReset"
/>
<pre v-if="result" class="text-sm bg-muted p-3 rounded-(--ui-radius)">{{ result }}</pre>
</div>
</template>
v-model 绑定
v-model 双向绑定表单数据,传入的初始值会被记录为重置基准:点「重置」恢复到该初始值而非清空。
{
"name": "张三",
"status": "启用"
}<script setup lang="ts">
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入姓名' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入关键词' } }).meta({ label: '关键词' }).optional()
})
const state = ref<Partial<z.output<typeof schema>>>({
name: '张三',
status: '启用'
})
</script>
<template>
<div class="space-y-4">
<MSearchForm v-model="state" :schema="schema" />
<pre class="text-sm bg-muted p-3 rounded-(--ui-radius)">{{ JSON.stringify(state, null, 2) }}</pre>
<UButton
size="sm"
color="neutral"
variant="outline"
@click="state = { name: '李四', status: '禁用', keyword: '测试' }"
>
外部设值
</UButton>
</div>
</template>
cols 网格列数
cols 控制网格列数:传数字固定列数,传断点对象则列数随窗口宽度在 sm、md、lg、xl 间切换。
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" />
</template>
调整浏览器窗口宽度查看效果:sm=1, md=2, lg=3, xl=4
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
const params = ref({})
</script>
<template>
<div class="space-y-2">
<p class="text-sm text-muted">
调整浏览器窗口宽度查看效果:sm=1, md=2, lg=3, xl=4
</p>
<MSearchForm
v-model="params"
:schema="schema"
:cols="{ sm: 1, md: 2, lg: 3, xl: 4 }"
/>
</div>
</template>
visibleRows 折叠行为
visibleRows 控制可见行数,超出的字段折叠到展开区。下方拖动 cols、visibleRows 实时改变折叠阈值。
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
phone: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '手机号' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" />
</template>
expanded 受控展开
v-model:expanded 接管展开状态,优先级高于 defaultExpanded,可由外部按钮或逻辑驱动。
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
phone: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '手机号' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
const expanded = ref(false)
</script>
<template>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm text-muted">
<UButton size="sm" color="neutral" variant="outline" @click="expanded = !expanded">
外部{{ expanded ? '收起' : '展开' }}
</UButton>
<span>expanded = {{ expanded }}</span>
</div>
<MSearchForm v-model:expanded="expanded" :schema="schema" />
</div>
</template>
expandText / collapseText / icon 展开按钮
expandText、collapseText 自定义展开 / 收起文案,icon 切换按钮图标,collapseButtonProps 透传按钮属性。
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
phone: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '手机号' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" :collapse-button-props="{ color: 'primary', variant: 'soft' }" />
</template>
actions 操作按钮
actions 数组扩展或裁剪按钮:
- 内置
key: search自动绑定提交,key: reset自动绑定重置 - 自定义
key需提供onClick(ctx),ctx含state、errors、search、reset、clear、toggle、loading、expanded - 传
actions: []关闭所有内置按钮,配合actionsslot 完全自定义
仅保留搜索按钮(actions 只传 search)
关闭全部内置按钮(actions: []),通过 actions slot 完全自定义
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '姓名' })
.optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
department: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '部门' })
.optional(),
keyword: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '关键词' })
.optional()
})
</script>
<template>
<div class="space-y-6">
<div class="space-y-2">
<p class="text-sm text-muted">仅保留搜索按钮(actions 只传 search)</p>
<MSearchForm
:schema="schema"
:actions="[{ key: 'search', label: '搜索', icon: 'i-lucide-search', type: 'submit' }]"
/>
</div>
<div class="space-y-2">
<p class="text-sm text-muted">
关闭全部内置按钮(actions: []),通过 actions slot 完全自定义
</p>
<MSearchForm :schema="schema" :actions="[]">
<template #actions="{ search, reset }">
<div class="flex items-end gap-2 justify-end">
<UButton color="primary" variant="solid" icon="i-lucide-filter" @click="search">
筛选
</UButton>
<UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="reset">
清空
</UButton>
</div>
</template>
</MSearchForm>
</div>
</div>
</template>
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '姓名' })
.optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
keyword: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '关键词' })
.optional(),
department: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '部门' })
.optional()
})
function onExport() {
console.log('导出')
}
</script>
<template>
<MSearchForm
:schema="schema"
:actions="[
{
key: 'search',
label: '查询',
icon: 'i-lucide-search',
type: 'submit',
color: 'primary',
variant: 'solid'
},
{
key: 'reset',
label: '清空',
icon: 'i-lucide-rotate-ccw',
color: 'error',
variant: 'outline'
},
{
key: 'export',
label: '导出',
icon: 'i-lucide-download',
color: 'primary',
variant: 'soft',
onClick: onExport
}
]"
/>
</template>
示例
接管操作区
#actions 插槽暴露 search、clear、loading,可用自定义按钮替换默认操作区:
<script setup lang="ts">
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '姓名' })
.optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional()
})
const state = ref<Partial<z.output<typeof schema>>>({})
</script>
<template>
<MSearchForm v-model="state" :schema="schema" :cols="3">
<template #actions="{ search, clear, loading }">
<div class="flex items-end gap-2">
<UButton
color="primary"
variant="solid"
icon="i-lucide-filter"
:loading="loading"
@click="search"
>
筛选
</UButton>
<UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="clear"> 清空 </UButton>
</div>
</template>
</MSearchForm>
</template>
扩展布局区域
header、footer、extraActions 插入辅助内容,slot props 随展开与表单值更新:
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" :cols="3">
<template #header="{ expanded }">
<div class="rounded border border-dashed border-primary/40 bg-primary/5 px-3 py-2 text-xs text-primary">
#header · expanded={{ expanded }}
</div>
</template>
<template #extraActions>
<UButton size="sm" color="neutral" variant="outline" icon="i-lucide-save">
保存方案
</UButton>
</template>
<template #footer="{ state: formState }">
<div class="mt-2 rounded border border-dashed border-success/40 bg-success/5 px-3 py-2 text-xs text-success">
#footer · 当前关键词: {{ (formState as Record<string, unknown>).keyword ?? '—' }}
</div>
</template>
</MSearchForm>
</template>
异步提交与校验
loading 控制按钮加载态,@error 返回 Zod 校验失败的错误列表:
<script setup lang="ts">
import type { FormErrorEvent, FormSubmitEvent } from '@nuxt/ui'
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }),
email: afz
.email({ controlProps: { placeholder: '请输入合法邮箱' } })
.meta({ label: '邮箱' })
.optional()
})
type Schema = z.output<typeof schema>
const state = ref<Partial<Schema>>({})
const loading = ref(false)
const toast = useToast()
function onSearch(event: FormSubmitEvent<Schema>) {
loading.value = true
setTimeout(() => {
loading.value = false
toast.add({ title: '查询完成', description: JSON.stringify(event.data), color: 'success' })
}, 1500)
}
function onError(event: FormErrorEvent) {
toast.add({
title: '校验失败',
description: `共 ${event.errors?.length ?? 0} 项错误`,
color: 'error'
})
}
</script>
<template>
<MSearchForm
v-model="state"
:schema="schema"
:loading="loading"
:validate-on="['blur']"
@submit="onSearch"
@error="onError"
/>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
schema | SZod 对象 schema,定义表单字段 | |
cols | 3 | number | { sm?: number; md?: number; lg?: number; xl?: number; }网格列数 |
visibleRows | 1 | number可见行数(折叠时显示的行数) |
actions | [{ key: 'search', ... }, { key: 'reset', ... }] | SearchFormAction[]动作按钮配置;不传时使用默认 search, reset;传 则关闭所有内置按钮 |
collapseButtonProps | ButtonProps收起按钮属性 | |
icon | 'i-lucide-chevron-down' | any展开/收起按钮图标 |
expandText | '展开' | string展开按钮文本 |
collapseText | '收起' | string收起按钮文本 |
controls | AutoFormControls自定义控件映射 | |
globalMeta | ZodAutoFormFieldMeta全局字段元数据配置 | |
validateOn | [] | FormInputEvents[]表单验证时机,详见 UForm 的 validateOn 属性 |
id | string | number | |
validate | (state: Partial<InferInput<S>>) => FormError<string>[] | Promise<FormError<string>[]>Custom validation function to validate the form state. | |
name | stringThe | |
validateOnInputDelay | `300` | numberDelay in milliseconds before validating the form on input events. |
transform | `true` | trueIf true, applies schema transformations on submit. |
nested | `false` | falseIf true, this form will attach to its parent Form and validate at the same time. |
onSubmit | (): void | Promise<void> | (event: FormSubmitEvent<InferOutput<S>>): void | Promise<void> | |
acceptcharset | string | |
action | string | |
autocomplete | string | |
enctype | string | |
method | string | |
novalidate | false | true | "true" | "false" | |
target | string | |
modelValue | {} | Partial<InferInput<S>> |
loading | boolean搜索按钮加载状态(作用于 type==='submit' 或 key==='search' 的按钮) | |
expanded | boolean受控展开状态;优先级高于 defaultExpanded | |
defaultExpanded | false | boolean默认展开状态 |
loadingAuto | true | boolean是否启用自动 loading 功能。 |
disabled | booleanDisable all inputs inside the form. | |
ui | Record<string, ClassNameValue> & { root?: SlotClass; form?: SlotClass; visible?: SlotClass; grid?: SlotClass; header?: SlotClass; footer?: SlotClass; actions?: SlotClass; toggleWrapper?: SlotClass; toggle?: SlotClass; toggleIcon?: SlotClass; collapsed?: SlotClass; } |
Emits
| Event | Type |
|---|---|
reset | [state: Partial<InferInput<S>>] |
clear | [state: Partial<InferInput<S>>] |
expand | [expanded: boolean] |
update:expanded | [expanded: boolean] |
error | [event: FormErrorEvent] |
update:modelValue | [value: Partial<InferInput<S>>] |
@submit 转发自底层 UForm,未列入上表。校验通过后触发,返回 FormSubmitEvent,搜索条件位于 event.data——这是接收查询参数的主要出口。Slots
| Slot | Type |
|---|---|
header | SearchFormSlotProps<S> |
footer | SearchFormSlotProps<S> |
actions | SearchFormSlotProps<S> |
extraActions | SearchFormSlotProps<S> |
field-label | { label?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-hint | { hint?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-description | { description?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-help | { help?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-error | { error?: string | boolean; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-default | { error?: string | boolean; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
Expose
您可以通过 useTemplateRef 访问该类型化组件实例。
| Name | Type |
|---|---|
formRef | Ref<InstanceType<typeof UForm>> UForm 组件引用 |
submit() | void 程序化触发表单提交(等价于点击 search 按钮) |
reset() | void 恢复到 baseline(首次挂载的 v-model 快照),并触发 |
clear() | void 清空表单所有字段为空值,并触发 |
setBaseline(value?) | void 设置 |
expanded | ComputedRef<boolean> 当前展开 / 收起状态 |
toggle() | void 切换展开 / 收起,并 emit |
Theme
export default defineAppConfig({
ui: {
searchForm: {
slots: {
root: 'group/search',
form: 'space-y-4',
visible: 'relative',
grid: 'grid gap-4 [grid-template-columns:repeat(var(--m-search-cols,1),minmax(0,1fr))] sm:[grid-template-columns:repeat(var(--m-search-cols-sm,var(--m-search-cols)),minmax(0,1fr))] md:[grid-template-columns:repeat(var(--m-search-cols-md,var(--m-search-cols-sm,var(--m-search-cols))),minmax(0,1fr))] lg:[grid-template-columns:repeat(var(--m-search-cols-lg,var(--m-search-cols-md,var(--m-search-cols-sm,var(--m-search-cols)))),minmax(0,1fr))] xl:[grid-template-columns:repeat(var(--m-search-cols-xl,var(--m-search-cols-lg,var(--m-search-cols-md,var(--m-search-cols-sm,var(--m-search-cols))))),minmax(0,1fr))]',
header: '',
footer: '',
actions: 'flex items-end gap-2 justify-end',
toggleWrapper: 'flex justify-center pt-2',
toggle: 'group opacity-75 hover:opacity-100 active:opacity-90 transition-all duration-200',
toggleIcon: 'size-3.5 transition-all duration-200 opacity-80 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100',
collapsed: 'mt-4'
}
}
}
})