@flowertip/search-form
v0.2.2
Published
基于 Vue 3 + Element Plus 的可配置搜索表单组件,支持十三种字段类型、响应式布局、远程数据、字段联动、展开折叠、自定义插槽
Maintainers
Readme
@flowertip/search-form
基于 Vue 3 + Element Plus 的可配置搜索表单组件。通过 fields 配置数组动态生成查询表单,支持十三种字段类型、响应式栅格布局、远程数据源、字段联动、展开/折叠等功能。CSS 样式自动注入,无需额外引入样式文件。
安装
npm install @flowertip/search-form本组件依赖以下 peerDependencies,请确保项目中已安装:
npm install vue@^3.4 element-plus@^2.14 @element-plus/icons-vue@^2.3快速开始
组件 CSS 已内联注入,
import { SearchForm }即可获得完整样式,无需手动引入 CSS 文件。
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')<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(远程数据源)
{
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 插槽中,适合「前缀选择 + 后缀输入」的场景(如币种 + 金额):
{
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 控件:
<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(字段联动)
联动有两种写法:
简写(数组):
{ field: 'province', linkage: ['city', 'district'] }完整写法(对象):
{
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(组件实例方法)
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
