# @flowertip/search-form

基于 Vue 3 + Element Plus 的可配置搜索表单组件。通过 `fields` 配置数组动态生成查询表单，支持十三种字段类型、响应式栅格布局、远程数据源、字段联动、展开/折叠等功能。CSS 样式自动注入，无需额外引入样式文件。

## 安装

```bash
npm install @flowertip/search-form
```

本组件依赖以下 peerDependencies，请确保项目中已安装：

```bash
npm install vue@^3.4 element-plus@^2.14 @element-plus/icons-vue@^2.3
```

## 快速开始

> 组件 CSS 已内联注入，`import { SearchForm }` 即可获得完整样式，无需手动引入 CSS 文件。

```js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { SearchForm } from '@flowertip/search-form'

const app = createApp(App)
app.use(ElementPlus)
app.component('SearchForm', SearchForm)
app.mount('#app')
```

```vue
<template>
  <div class="search-form-demo">
    <SearchForm
      ref="searchFormRef"
      :fields="formFields"
      :label-width="'110px'"
      label-suffix="："
      :cols="3"
      :gutter="20"
      submit-text="查询"
      reset-text="清空"
      @submit="onSubmit"
      @reset="onReset"
      @toggle-collapse="onToggleCollapse"
      @field-change="onFieldChange"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { SearchForm } from '@flowertip/search-form'
import { InfoFilled } from '@element-plus/icons-vue'

const searchFormRef = ref()
const result = ref(null)

/** 表单字段配置 */
const formFields = ref([
  // ========== 1. 基础输入框 ==========
  {
    type: 'input',
    label: '用户名',
    field: 'username',
    placeholder: '请输入用户名',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    labelTip: '请输入系统注册用户名，支持中英文及数字组合',
  },

  // ========== 2. 本地数据源下拉框 ==========
  {
    type: 'select',
    label: '本地状态',
    field: 'localStatus',
    placeholder: '请选择状态（本地数据）',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    options: [
      { label: '草稿', value: 'draft' },
      { label: '已发布', value: 'published' },
      { label: '已归档', value: 'archived' }
    ]
  },

  // ========== 3. 远程数据源下拉框（GitHub API 演示）==========
  {
    type: 'select',
    label: '远程状态',
    field: 'remoteStatus',
    placeholder: '请选择（远程数据）',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    remoteOptions: {
      url: 'https://api.github.com/repos/vuejs/core/issues',
      filterable: true,
      queryKey: 'page',
      transform: (response) => {
        return response.slice(0, 5).map((item, index) => ({
          label: `#${index + 1} ${(item.title || '').substring(0, 20)}`,
          value: item.number || index
        }))
      }
    }
  },

  // ========== 4. 日期选择器 ==========
  {
    type: 'date',
    label: '开始日期',
    field: 'startDate',
    placeholder: '请选择日期',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    dateFormat: 'YYYY-MM-DD',
    dateValueFormat: 'YYYY-MM-DD'
  },

  // ========== 5. 日期时间选择器 ==========
  {
    type: 'datetime',
    label: '创建时间',
    field: 'createTime',
    placeholder: '请选择日期时间',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    dateFormat: 'YYYY-MM-DD HH:mm:ss',
    dateValueFormat: 'YYYY-MM-DD HH:mm:ss'
  },

  // ========== 6. 级联选择器 ==========
  {
    type: 'cascader',
    label: '所属地区',
    field: 'region',
    placeholder: '请选择地区',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    options: [
      {
        label: '广东',
        value: 'guangdong',
        children: [
          { label: '深圳', value: 'shenzhen' },
          { label: '广州', value: 'guangzhou' }
        ]
      },
      {
        label: '浙江',
        value: 'zhejiang',
        children: [
          { label: '杭州', value: 'hangzhou' },
          { label: '宁波', value: 'ningbo' }
        ]
      }
    ],
    cascaderProps: { expandTrigger: 'hover' }
  },

  // ========== 7. 数值输入框 ==========
  {
    type: 'input-number',
    label: '年龄',
    field: 'age',
    placeholder: '请输入年龄',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    labelTip: '请输入有效年龄，范围 0-120 岁',
    labelIcon: InfoFilled,
    attrs: { min: 0, max: 120, step: 1 }
  },

  // ========== 8. 复合型输入框：select 嵌套在 el-input 的 #prepend 中 ==========
  {
    type: 'composite',
    label: '金额',
    field: 'amount',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    labelTip: '选择币种并输入金额数值',
    composite: {
      prefix: {
        field: 'amountUnit',
        defaultValue: 'CNY',
        placeholder: '币种',
        options: [
          { label: 'CNY', value: 'CNY' },
          { label: 'USD', value: 'USD' },
          { label: 'EUR', value: 'EUR' }
        ]
      },
      suffix: {
        field: 'amountValue',
        placeholder: '请输入金额'
      }
    }
  },

  // ========== 9. 额外字段：演示折叠 — 默认隐藏，展开后可见 ==========
  {
    type: 'input',
    label: '邮箱',
    field: 'email',
    placeholder: '请输入邮箱',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    rules: [{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }]
  },
  {
    type: 'select',
    label: '主类别',
    field: 'mainCategory',
    placeholder: '请选择主类别',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    labelTip: '选择主类别后，子类别选项自动联动更新',
    linkage: {
      targets: ['subCategory'],
      handler: (newVal, targets, formModel, helpers) => {
        // 根据主类别动态设置子类别选项
        const subOptionsMap = {
          electronics: [
            { label: '手机', value: 'phone' },
            { label: '电脑', value: 'computer' },
            { label: '耳机', value: 'headphone' }
          ],
          clothing: [
            { label: '上衣', value: 'top' },
            { label: '裤子', value: 'pants' },
            { label: '鞋子', value: 'shoes' }
          ],
          food: [
            { label: '零食', value: 'snack' },
            { label: '饮料', value: 'drink' },
            { label: '生鲜', value: 'fresh' }
          ]
        }
        // 更新子类别字段的 options（通过查找字段并直接修改）
        const subField = formFields.value.find(f => f.field === 'subCategory')
        if (subField) {
          subField.options = subOptionsMap[newVal] || []
        }
      }
    },
    options: [
      { label: '电子产品', value: 'electronics' },
      { label: '服装', value: 'clothing' },
      { label: '食品', value: 'food' }
    ]
  },
  {
    type: 'select',
    label: '子类别',
    field: 'subCategory',
    placeholder: '先选择主类别',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    labelTip: '根据主类别动态变化',
    dependsOn: 'mainCategory',
    disabled: false,
    options: []  // 由 linkage handler 动态填充
  },
  {
    type: 'input-number',
    label: '排序权重',
    field: 'weight',
    placeholder: '请输入权重',
    span: { xs: 24, sm: 12, md: 8, lg: 6 },
    attrs: { min: 0, max: 100 }
  }
])

function onSubmit(formData) {
  console.log('查询数据:', formData)
  result.value = formData
}

function onFieldChange({ field, value }) {
  console.log(`字段 [${field}] 变更为:`, value)
}

function onReset(formData) {
  console.log('表单已重置:', formData)
  result.value = null
}

function onToggleCollapse(collapsed) {
  console.log('切换折叠状态:', collapsed ? '已折叠' : '已展开')
}
</script>

<style scoped>
.search-form-demo {
  margin: 20px auto 0;
  width: 90%;
  padding: 10px;
  box-sizing: border-box;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid var(--el-border-color);
  box-shadow: var(--el-box-shadow-lighter);
}
</style>
```

## Props

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| fields | Array | — (必填) | 字段配置数组 |
| initialValue | Object | {} | 初始表单值 |
| labelWidth | String | '100px' | 标签宽度 |
| labelSuffix | String | '' | 标签后缀 |
| cols | Number | 4 | 默认每行字段数 |
| gutter | Number | 20 | 栅格间距 |
| size | String | 'default' | 表单尺寸：large / default / small |
| disabled | Boolean | false | 是否全局禁用表单 |
| showActions | Boolean | true | 是否显示操作按钮 |
| submitText | String | '查询' | 提交按钮文本 |
| resetText | String | '重置' | 重置按钮文本 |
| submitIcon | Component | '' | 提交按钮图标 |
| resetIcon | Component | '' | 重置按钮图标 |
| containerWidth | Number | 0 | 容器宽度（用于计算折叠行数），不传则自动获取 |

## FieldConfig（字段配置）

每个字段支持以下属性：

### 通用属性

| 属性 | 类型 | 说明 |
|------|------|------|
| type | String | 字段类型，见下方 |
| label | String | 标签文本 |
| field | String | 绑定字段名 |
| placeholder | String | 占位文本 |
| span | Number / Object | 栅格列宽。数字表示固定值；对象支持 `{ xs, sm, md, lg, xl }` 响应式 |
| visible | Boolean | 是否可见，默认 true |
| disabled | Boolean | 是否禁用 |
| defaultValue | Any | 字段默认值 |
| rules | Array | el-form 校验规则 |
| clearable | Boolean | 是否可清空，默认 true |
| attrs | Object | 透传给控件的额外属性 |
| labelIcon | Component | label 旁图标，默认 QuestionFilled |
| labelTip | String | 图标 hover 提示文字 |
| linkage | Array / Object | 联动配置 |
| dependsOn | String / Array | 依赖字段名 |

### type 类型

支持以下十三种字段类型：

| type | 控件 | 额外属性 |
|------|------|----------|
| `input` | el-input | — |
| `select` | el-select | `options`, `remoteOptions` |
| `date` | el-date-picker | `dateFormat`, `dateValueFormat` |
| `datetime` | el-date-picker | `dateFormat`, `dateValueFormat` |
| `week` | el-date-picker | `dateFormat`, `dateValueFormat` |
| `month` | el-date-picker | `dateFormat`, `dateValueFormat` |
| `year` | el-date-picker | `dateFormat`, `dateValueFormat` |
| `daterange` | el-date-picker | `startPlaceholder`, `endPlaceholder`, `rangeSeparator`, `dateFormat`, `dateValueFormat` |
| `datetimerange` | el-date-picker | `startPlaceholder`, `endPlaceholder`, `rangeSeparator`, `dateFormat`, `dateValueFormat` |
| `cascader` | el-cascader | `options`, `cascaderProps` |
| `input-number` | el-input-number | `attrs` 传入 min/max/step |
| `composite` | el-input(prepend) | `composite: { prefix: {...}, suffix: {...} }` |
| `custom` | 作用域插槽 | 通过 `#field-{field}` 插槽自由渲染 |

### remoteOptions（远程数据源）

```js
{
  type: 'select',
  field: 'city',
  remoteOptions: {
    url: '/api/cities',                     // 或 (formModel) => `/api/cities/${formModel.province}`
    params: (formModel) => ({ pid: formModel.province }),  // 动态查询参数
    filterable: true,                        // 是否可搜索
    queryKey: 'keyword',                     // 搜索参数键名
    transform: (response) => response.map(...)  // 响应转换函数
  },
  dependsOn: 'province'
}
```

### composite（复合输入框）

`composite` 类型将 `el-select` 嵌套在 `el-input` 的 `#prepend` 插槽中，适合「前缀选择 + 后缀输入」的场景（如币种 + 金额）：

```js
{
  type: 'composite',
  label: '金额',
  field: 'amount',
  composite: {
    prefix: {
      field: 'amountUnit',      // 前缀绑定的字段名
      defaultValue: 'CNY',       // 前缀默认值
      placeholder: '币种',
      options: [
        { label: 'CNY', value: 'CNY' },
        { label: 'USD', value: 'USD' }
      ]
    },
    suffix: {
      field: 'amountValue',      // 后缀绑定的字段名
      placeholder: '请输入金额'
    }
  }
}
```

### custom（自定义控件）

`custom` 类型通过作用域插槽 `#field-{fieldName}` 让使用者自由渲染任意 Element Plus 控件：

```vue
<template>
  <SearchForm :fields="fields">
    <!-- 插槽名必须为 field-{field} -->
    <template #field-themeColor="{ modelValue, update, disabled }">
      <el-color-picker
        :model-value="modelValue"
        :disabled="disabled"
        @update:model-value="update"
      />
    </template>
  </SearchForm>
</template>

<script setup>
const fields = [
  { type: 'custom', label: '主题颜色', field: 'themeColor', defaultValue: '#409EFF' }
]
</script>
```

作用域插槽参数：

| 参数 | 说明 |
|------|------|
| field | 字段配置对象 |
| modelValue | 当前字段值 |
| disabled | 是否禁用 |
| update | 更新值的函数 `(val) => void` |

### linkage（字段联动）

联动有两种写法：

**简写（数组）**：
```js
{ field: 'province', linkage: ['city', 'district'] }
```

**完整写法（对象）**：
```js
{
  field: 'category',
  linkage: {
    targets: ['subCategory'],
    handler: (newVal, targets, formModel, helpers) => {
      helpers.setFieldValue('otherField', 'defaultValue')
      helpers.reloadField('subCategory')
    }
  }
}
```

## Events

| 事件 | 参数 | 说明 |
|------|------|------|
| submit | formData | 表单验证通过后触发 |
| reset | formData | 重置后触发 |
| toggle-collapse | collapsed | 折叠/展开切换 |
| field-change | { field, value, linkedFields, formData } | 联动字段变化时触发 |

## Expose（组件实例方法）

```js
const formRef = ref()

formRef.value.validate()        // 返回 Promise，验证表单
formRef.value.resetFields()     // 重置表单
formRef.value.getFormData()     // 获取表单数据
formRef.value.setFormData({})   // 动态设置表单数据
formRef.value.toggleCollapse()  // 切换折叠
formRef.value.isCollapsed       // 当前折叠状态
formRef.value.remoteLoading     // { [field]: true/false } 远程加载状态
```

## License

MIT