# QyqForm 动态表单

> QyqForm 基于 Vue 3 与 Element Plus，通过字段配置构建查询和录入表单，提供接入示例、校验与重置说明、日期金额和自定义插槽用法。

原始页面：[QyqForm 动态表单](http://docs.quyongqing.cn/components/qyq-form.html)

## 说明

`QyqForm` 是基于 Vue 3 与 Element Plus 的动态表单组件，通过 `formOptions` 配置字段、通过 `v-model:modelValue` 绑定数据，适合后台查询筛选、客户录入和合同编辑。组件负责字段展示与校验，查询请求、保存接口和提交状态由调用方维护。

## 快速接入

当前组件通过仓库源码接入，尚未发布独立 npm 包。文档工程使用 Vue 3.5、Element Plus 2、`@element-plus/icons-vue` 和 dayjs；复制到其他项目时，需要保留整个 `src/components/qyq-form/` 目录与 `src/components/CardTitle.vue`，并注册 Element Plus、引入对应样式。下方示例假设 `@` 指向 `src`。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import QyqForm from '@/components/qyq-form/index.vue'
import type { FormItemProp } from '@/components/qyq-form/types'

const filters = ref({ keyword: '', status: '' })
const submittedFilters = ref({ ...filters.value })

const formOptions: FormItemProp[] = [
  { prop: 'keyword', label: '关键词', type: 'input', clearable: true },
  {
    prop: 'status',
    label: '状态',
    type: 'select',
    options: [
      { label: '待处理', value: 'pending' },
      { label: '已完成', value: 'done' },
    ],
  },
]

function handleSearch() {
  // 可在这里使用 filters.value 请求列表接口，并处理加载与错误状态。
  submittedFilters.value = { ...filters.value }
}
</script>

<template>
  <QyqForm
    v-model:modelValue="filters"
    :form-options="formOptions"
    :columns="2"
    @search="handleSearch"
    @reset="handleSearch"
  />
  <pre>{{ submittedFilters }}</pre>
</template>
```

内置查询按钮先校验，通过后触发无参数的 `search` 事件，调用方从绑定的数据中读取条件。内置重置按钮重置后触发无参数的 `reset` 事件；上面的示例会在重置后同步查询条件。

## 适用场景与边界

| 场景 | 建议用法 |
| --- | --- |
| 列表查询 | 配置 `formOptions`，监听 `search` 与 `reset`，把条件传给 [QyqTable](http://docs.quyongqing.cn/components/qyq-table.html) 所在页面的数据请求 |
| 录入与编辑 | 使用 `required` 或 `rules`，在保存前调用 `validate()` |
| 特殊字段 | 使用 `type: 'custom'` 和以字段 `prop` 命名的插槽，自行绑定字段值 |
| 自定义操作区 | 使用 `elgsFormBtn` 插槽，并保持 `showBtn` 为 `true` |

该组件依赖 Element Plus 的表单行为。需要保留任意复杂布局或特殊业务流程时，可以用局部插槽或直接组合 Element Plus 表单项。

## 功能概览

查询筛选

适合列表页条件栏，支持列数、标签位置、查询重置与轻量搜索。

内置校验

支持 required、格式校验、自定义 rules 与暴露的验证方法。

日期金额

支持日期范围、快捷项、禁用日期、金额与金额区间格式化。

组合扩展

支持 selectInput、标题区 slot、按钮 slot、自定义字段 slot。

复杂录入

可组合 searchSelect、autocomplete、moneyrange 等能力构建产品级表单。

## 源码位置

`src/components/qyq-form/index.vue`

## 1\. 基础配置

这一段偏查询筛选场景，用来展示一行几列、标签位置、标签宽度、按钮位置、禁用态等基础能力。

**基础查询示例**

```vue
<script setup lang="ts">
import { ref } from 'vue'
import QyqForm from '@/components/qyq-form/index.vue'

const formData = ref({
  keyword: '',
  status: '',
  createdAt: [],
})

const formOptions = [
  { prop: 'keyword', label: '关键词', type: 'input', clearable: true },
  {
    prop: 'status',
    label: '状态',
    type: 'select',
    options: [
      { label: '全部', value: '' },
      { label: '待处理', value: 'pending' },
      { label: '已完成', value: 'done' },
    ],
  },
  {
    prop: 'createdAt',
    label: '创建时间',
    type: 'daterange',
    dateValidateData: { isBefore: true, isShowShortcuts: true },
  },
]
</script>

<template>
  <QyqForm
    v-model:modelValue="formData"
    :form-options="formOptions"
    :columns="3"
    :show-btn="true"
    :form-label-info="{ labelPosition: 'top', labelWidth: '110px', showColon: false }"
    title="业务查询表单"
  />
</template>
```

前半部分文档都偏查询筛选场景，重点体现 `columns`、`formLabelInfo`、`showBtn`、`btnPosition` 和轻量检索能力。

## 2\. 内置校验

这一段偏录入编辑场景，重点展示 `required`、`rules`、文本格式校验以及组件暴露出来的校验方法。

**内置校验示例**

```vue
<script setup lang="ts">
import { ref } from 'vue'
import type { FormRules } from 'element-plus'
import QyqForm from '@/components/qyq-form/index.vue'

const formData = ref({
  customerName: '',
  phone: '',
  email: '',
})

const formOptions = [
  { prop: 'customerName', label: '客户名称', type: 'input', required: true },
  { prop: 'phone', label: '手机号', type: 'input' },
  { prop: 'email', label: '邮箱', type: 'input' },
]

const rules: FormRules = {
  phone: [
    { required: true, message: '请输入手机号', trigger: ['blur', 'change'] },
    {
      trigger: 'blur',
      validator: (_rule, value, callback) => {
        /^1\d{10}$/.test(String(value || ''))
          ? callback()
          : callback(new Error('请输入正确的手机号'))
      },
    },
  ],
}
</script>

<template>
  <QyqForm
    v-model:modelValue="formData"
    :form-options="formOptions"
    :rules="rules"
    :columns="2"
    title="客户录入校验"
  />
</template>
```

## 3\. 日期与金额逻辑

这一段专门展示组件的日期和金额封装能力，包括快捷时间、范围限制、禁用日期、金额自动格式化与金额区间；当前示例里，服务周期可选去年到明年，且起止最大跨度为 2 个月。

**日期与金额示例**

```vue
<script setup lang="ts">
import { ref } from 'vue'
import QyqForm from '@/components/qyq-form/index.vue'

const formData = ref({
  signDate: '',
  servicePeriod: [],
  budgetAmount: '',
  taxMin: '',
  taxMax: '',
})

const formOptions = [
  {
    prop: 'servicePeriod',
    label: '服务周期',
    type: 'daterange',
    required: true,
    dateValidateData: {
      isLimitRange: false,
      isBefore: false,
      validateType: 'months',
      validateNumber: 2,
      limitRangeArr: [
        { number: 1, type: 'year' },
        { number: 1, type: 'year' },
      ],
    },
    hint: {
      text: '可选去年到明年，且起止最大跨度为 2 个月。',
    },
  },
]
</script>

<template>
  <QyqForm
    v-model:modelValue="formData"
    :form-options="formOptions"
    :columns="2"
    title="日期与金额示例"
  />
</template>
```

## 4\. 组合输入与 Slot

这一段展示 `selectInput`、标题区插槽、字段提示插槽和按钮区扩展，同时也演示如何通过 `item.style` 让字段横跨多列布局。

**组合输入与 Slot 模板片段**

```vue
<template>
  <QyqForm
    v-model:modelValue="formData"
    :form-options="formOptions"
    :columns="2"
    title="组合输入与插槽"
  >
    <template #cardTitleInfo>
      <span>支持在标题右侧补充业务状态</span>
    </template>

    <template #addressTip>
      <span>此处可接地图选点或地址解析能力</span>
    </template>

    <template #previewAction>
      <el-button type="primary" plain>预览</el-button>
    </template>
  </QyqForm>
</template>
```

组合输入更适合“区号 + 电话”“币种 + 金额”这类高频业务字段；如果后面还有需要，依然可以继续通过 `item.style` 做 Grid 跨列布局。

## 5\. 综合复杂表单

这一段把查询和录入里常用的能力集中到一个正式业务案例里，采用“合同录入 / 编辑”场景，包含 `searchSelect`、`autocomplete`、日期、金额、组合输入、校验和自定义按钮。

**综合复杂表单模板片段**

```vue
<template>
  <QyqForm
    ref="formRef"
    v-model:modelValue="formData"
    :form-options="formOptions"
    :rules="rules"
    :columns="3"
    :show-btn="true"
    title="合同录入表单"
  >
    <template #elgsFormBtn>
      <el-button>取消</el-button>
      <el-button type="primary">保存草稿</el-button>
      <el-button type="success">提交审批</el-button>
    </template>
  </QyqForm>
</template>
```

## 6\. 暴露方法

`QyqForm` 暴露了常用的表单实例方法，适合在父组件里配合按钮、步骤条、抽屉页脚等外部操作一起使用。

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `validate(callback?)` | `FormInstance["validate"]` | `-` | 触发整表校验；不传回调时使用 Promise，校验失败需要捕获拒绝。 |
| `resetFields(props?)` | `(string \| string[]) => void` | `-` | 重置整表或指定字段；普通字段恢复为表单项挂载时的初始值，复合字段见下方限制。 |
| `validateField(prop)` | `FormInstance["validateField"]` | `-` | 只校验指定字段，返回 Promise；校验失败需要捕获拒绝。 |
| `clearValidate(prop?)` | `(string \| string[]) => void` | `-` | 清理校验错误提示，适合回填数据、切换步骤或重置局部状态后调用。 |

**暴露方法示例**

```vue
<script setup lang="ts">
import { ref } from 'vue'
import QyqForm from '@/components/qyq-form/index.vue'

const formRef = ref<InstanceType<typeof QyqForm> | null>(null)
const formData = ref({ keyword: '', phone: '' })
const validationMessage = ref('')

const formOptions = [
  { prop: 'keyword', label: '关键词', type: 'input' },
  { prop: 'phone', label: '手机号', type: 'input', required: true },
]

async function handleValidate() {
  if (!formRef.value) return

  try {
    await formRef.value.validate()
    validationMessage.value = '校验通过'
  } catch {
    validationMessage.value = '请修正表单中的校验项'
  }
}

function handleResetKeyword() {
  formRef.value?.resetFields('keyword')
}

function handleClearPhoneError() {
  formRef.value?.clearValidate('phone')
}
</script>

<template>
  <QyqForm ref="formRef" v-model:modelValue="formData" :form-options="formOptions" />
  <button type="button" @click="handleValidate">校验</button>
  <button type="button" @click="handleResetKeyword">重置关键词</button>
  <button type="button" @click="handleClearPhoneError">清理手机号校验提示</button>
  <p role="status">{{ validationMessage }}</p>
</template>
```

## 参数

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `object` | `{}` | 表单数据，使用 v-model:modelValue 绑定。 |
| `formOptions` | `array` | `[]` | 表单项配置。 |
| `rules` | `object` | `{}` | 表单校验规则。 |
| `formLabelInfo` | `object` | `{ labelPosition: 'top' }` | 标签位置、宽度、星号位置和冒号展示配置。 |
| `showBtn` | `boolean` | `true` | 是否显示查询和重置按钮。 |
| `btnPosition` | `string` | `'right'` | 默认按钮区域靠左或靠右。 |
| `columns` | `number` | `4` | 每行展示列数。 |
| `validateOnRuleChange` | `boolean` | `false` | 规则变更后是否立即触发表单校验。 |
| `disabled` | `boolean` | `false` | 是否禁用整张表单。 |
| `title` | `string` | `''` | 表单标题。 |
| `titleTip` | `string` | `''` | 标题右侧提示文案。 |
| `titleFontWeight` | `string \| number` | `'bold'` | 表单标题字重。 |
| `titleSize` | `number` | `16` | 表单标题字号。 |
| `selectOptions` | `object` | `{}` | 下拉选项映射，key 对应表单 prop。 |

## 常见问题

### 如何在保存前校验表单？

通过组件 `ref` 调用 `validate()`。不传回调时，校验通过返回成功结果，失败会拒绝 Promise；捕获校验失败后应停止保存。下面是保存按钮的处理片段，表单需已绑定 `ref="formRef"`：

```ts
async function handleSave() {
  if (!formRef.value) return

  try {
    await formRef.value.validate()
  } catch {
    return
  }

  // 在此调用保存接口，并单独处理接口失败。
}
```

`required: true` 用于声明必填，复杂规则通过 `rules` 传入。字段的 `prop`、数据对象的键和规则对象的键需要一致。

### 重置为什么没有把所有值清空？

普通字段沿用 Element Plus 的 `resetFields()` 行为，恢复为表单项挂载时记录的初始值。需要“清空为指定值”或“恢复最近一次接口回填”时，应由调用方设置数据，再调用 `clearValidate()` 清理提示。

复合字段另有配对值清理逻辑，当前对字段数组的处理与单字段不完全一致。重置金额区间或组合输入时，建议显式恢复两端的数据，不依赖 `resetFields(['字段一', '字段二'])` 自动同步配对值。直接调用 `resetFields()` 不会触发组件的 `reset` 事件，该事件由内置重置按钮触发。

### 为什么自定义按钮或字段没有显示？

按钮插槽 `elgsFormBtn` 位于 `showBtn` 控制的区域内，设置 `showBtn: false` 会连同插槽一起隐藏。自定义字段使用 `type: 'custom'`，插槽名要与 `formOptions` 中的 `prop` 一致，字段内部的 `v-model` 由调用方绑定。

### 输入内容可以包含空格吗？

普通文本输入默认去除首尾空格，并对输入中的空格进行校验。需要保留首尾空格时设置 `isNotTrim: true`；需要允许文本输入中的空格时设置 `allowSpecialChars: true`。两个配置分别控制输入修剪与校验，可按业务需要组合使用。

## 配置字典补充

-   `formOptions[].type`：当前文档已覆盖 `input`、`textarea`、`select`、`radioGroup`、`date`、`daterange`、`money`、`moneyrange`、`selectInput`、`searchSelect`、`autocomplete` 和自定义 slot。
-   `formOptions[].required`：适合快速声明必填；复杂格式校验建议配合 `rules` 使用。
-   `formOptions[].tip / hint`：适合补充字段说明、帮助文本和风险提示，既支持纯文本也支持 slot。
-   `formOptions[].propOption`：用于 `selectInput` 组合输入，适合区号 + 电话、币种 + 金额等复合场景。
-   `formOptions[].style`：会透传到字段容器上，适合做 `gridColumn`、`gridRow` 等跨列跨行布局。
-   `formLabelInfo`：统一控制标签布局，查询表单更常用 `top`，录入表单可根据字段密度切到 `left`。
