---
title: "SearchForm"
description: "Schema 驱动的可折叠搜索表单组件。"
seo_title: "SearchForm"
seo_description: "A schema-driven, collapsible search form with grid layout, search/reset actions and auto row collapsing."
canonical_url: "https://nuxt.mhaibaraai.cn/docs/components/search-form"
---
# SearchForm

> Schema 驱动的可折叠搜索表单组件。

## 简介

`MSearchForm` 是一个 Schema 驱动的搜索表单组件，内置网格布局、搜索 / 重置按钮和折叠行为。当搜索项较多时，超出可见行数的字段会自动折叠，用户可点击展开 / 收起按钮查看全部搜索项。

> \[\!NOTE\]
> See: /docs/auto-form/quickstart
> 
> 复用 AutoForm 基础设施（schema 内省、控件映射、字段渲染器），通过 Zod schema 定义搜索字段。

## 用法

按 AutoForm schema 渲染字段，`cols` 控制栅格，内置搜索与重置按钮；点「搜索」触发校验并发出 `@submit`：

```vue [ComponentsSearchFormBasicExample.vue]
<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` 双向绑定表单数据，传入的初始值会被记录为重置基准：点「重置」恢复到该初始值而非清空。

```vue [ComponentsSearchFormModelExample.vue]
<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` 间切换。

```vue [ComponentsSearchFormColsExample.vue]
<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>
```

```vue [ComponentsSearchFormResponsiveExample.vue]
<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` 实时改变折叠阈值。

```vue [ComponentsSearchFormExpandExample.vue]
<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`，可由外部按钮或逻辑驱动。

```vue [ComponentsSearchFormExpandedControlledExample.vue]
<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` 透传按钮属性。

```vue [ComponentsSearchFormToggleButtonExample.vue]
<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: []` 关闭所有内置按钮，配合 `actions` slot 完全自定义

```vue [ComponentsSearchFormHideButtonsExample.vue]
<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>
```

```vue [ComponentsSearchFormCustomExample.vue]
<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`，可用自定义按钮替换默认操作区：

```vue [ComponentsSearchFormActionsSlotExample.vue]
<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 随展开与表单值更新：

```vue [ComponentsSearchFormLayoutSlotsExample.vue]
<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 校验失败的错误列表：

```vue [ComponentsSearchFormAsyncExample.vue]
<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

```ts
/**
 * Props for the MSearchForm component
 */
interface MSearchFormProps {
  /**
   * Zod 对象 schema，定义表单字段
   */
  schema: S;
  /**
   * 网格列数
   * @default "3"
   */
  cols?: number | { sm?: number | undefined; md?: number | undefined; lg?: number | undefined; xl?: number | undefined; } | undefined;
  /**
   * 可见行数（折叠时显示的行数）
   * @default "1"
   */
  visibleRows?: number | undefined;
  /**
   * 动作按钮配置；不传时使用默认 [search, reset]；传 [] 则关闭所有内置按钮
   */
  actions?: SearchFormAction[] | undefined;
  /**
   * 搜索按钮加载状态（作用于 type==='submit' 或 key==='search' 的按钮）
   */
  loading?: boolean | undefined;
  /**
   * 收起按钮属性
   */
  collapseButtonProps?: ButtonProps | undefined;
  /**
   * 展开/收起按钮图标
   * @default "\"i-lucide-chevron-down\""
   */
  icon?: any;
  /**
   * 展开按钮文本
   * @default "\"展开\""
   */
  expandText?: string | undefined;
  /**
   * 收起按钮文本
   * @default "\"收起\""
   */
  collapseText?: string | undefined;
  /**
   * 受控展开状态；优先级高于 defaultExpanded
   */
  expanded?: boolean | undefined;
  /**
   * 默认展开状态
   * @default "false"
   */
  defaultExpanded?: boolean | undefined;
  /**
   * 自定义控件映射
   */
  controls?: AutoFormControls | undefined;
  /**
   * 全局字段元数据配置
   */
  globalMeta?: ZodAutoFormFieldMeta | undefined;
  /**
   * 是否启用自动 loading 功能。
   * @default "true"
   */
  loadingAuto?: boolean | undefined;
  /**
   * 表单验证时机，详见 UForm 的 validateOn 属性
   * @default "[]"
   */
  validateOn?: FormInputEvents[] | undefined;
  ui?: (Record<string, C> & { root?: SlotClass; form?: SlotClass; visible?: SlotClass; grid?: SlotClass; header?: SlotClass; footer?: SlotClass; actions?: SlotClass; toggleWrapper?: SlotClass; toggle?: SlotClass; toggleIcon?: SlotClass; collapsed?: SlotClass; }) | undefined;
  id?: string | number | undefined;
  /**
   * Custom validation function to validate the form state.
   */
  validate?: ((state: Partial<InferInput<S>>) => FormError<string>[] | Promise<FormError<string>[]>) | undefined;
  /**
   * Disable all inputs inside the form.
   */
  disabled?: boolean | undefined;
  /**
   * The `name` attribute of the form element.
   * For nested forms (`nested` is true), this is also used as the path of the form's state within its parent form.
   */
  name?: string | undefined;
  /**
   * Delay in milliseconds before validating the form on input events.
   */
  validateOnInputDelay?: number | undefined;
  /**
   * If true, applies schema transformations on submit.
   */
  transform?: true | undefined;
  /**
   * If true, this form will attach to its parent Form and validate at the same time.
   */
  nested?: false | undefined;
  onSubmit?: (() => void) | ((event: FormSubmitEvent<InferOutput<S>>) => void) | undefined;
  acceptcharset?: string | undefined;
  action?: string | undefined;
  autocomplete?: string | undefined;
  enctype?: string | undefined;
  method?: string | undefined;
  novalidate?: Booleanish | undefined;
  target?: string | undefined;
  /**
   * @default "{}"
   */
  modelValue?: Partial<InferInput<S>> | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the MSearchForm component
 */
interface MSearchFormEmits {
  reset: (payload: [state: Partial<InferInput<S>>]) => void;
  clear: (payload: [state: Partial<InferInput<S>>]) => void;
  expand: (payload: [expanded: boolean]) => void;
  update:expanded: (payload: [expanded: boolean]) => void;
  error: (payload: [event: FormErrorEvent]) => void;
  update:expanded: (payload: [value: boolean | undefined]) => void;
  update:modelValue: (payload: [value: Partial<InferInput<S>>]) => void;
}
```

> \[\!NOTE\]
> 
> @submit
> 
>  转发自底层 
> 
> UForm
> 
> ，未列入上表。校验通过后触发，返回 
> 
> FormSubmitEvent
> 
> ，搜索条件位于 
> 
> event.data
> 
> ——这是接收查询参数的主要出口。

### Slots

```ts
/**
 * Slots for the MSearchForm component
 */
interface MSearchFormSlots {
  header(): any;
  footer(): any;
  actions(): any;
  extraActions(): any;
  field-label(): any;
  field-hint(): any;
  field-description(): any;
  field-help(): any;
  field-error(): any;
  field-default(): any;
}
```

### Expose

您可以通过 [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref){rel="[\"nofollow\"]"} 访问该类型化组件实例。

| Name                  | Type                                                     |
| --------------------- | -------------------------------------------------------- |
| `formRef`             | `Ref<InstanceType<typeof UForm>>`   
 UForm 组件引用         |
| `submit()`            | `void`   
 程序化触发表单提交（等价于点击 search 按钮）                    |
| `reset()`             | `void`   
 恢复到 baseline（首次挂载的 v-model 快照），并触发 `reset` 事件 |
| `clear()`             | `void`   
 清空表单所有字段为空值，并触发 `clear` 事件                    |
| `setBaseline(value?)` | `void`   
 设置 `reset()` 的恢复基准；不传参时使用当前 v-model 值         |
| `expanded`            | `ComputedRef<boolean>`   
 当前展开 / 收起状态                   |
| `toggle()`            | `void`   
 切换展开 / 收起，并 emit `expand` 与 `update:expanded` |

## Theme

<component-theme></component-theme>## Changelog

See commit history for [src/runtime/components/SearchForm.vue](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/components/SearchForm.vue).


## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
