针对大部分后台系统,搜索功能被大量使用。表单功能被频繁重复使用,不利于后期维护和做统一样式调整。可以将搜索功能二次封装,避免这些问题。
效果图如下
最终效果图
首先,根据form表单和业务功能需求,定义组件输入的数据
- form model 接收的数据,定义为params
// params的key,既是业务字段,也是el-form-item的prop
params: {
name: '',
}
- 定义渲染el-form-item的数据格式
我们希望通过循环el-form-item,渲染出有序的表单筛选组件,那就需要一个包含筛选组件选项的数组
// label和prop是el-form-item上的属性,label可选,prop必填且与params中的key对应
[
{ label: '组件1', prop: 'name' },
{ label: '组件2', prop: 'age' },
]
同时根据业务需要,我们用两个这样的数组代表基础和更多筛选选项
// 基础选项
baseAttribute: []
// 更多选项
moreAttribute: []
根据筛选组件的特性,我们定义组件的参数
下面以输入框组件为例
<el-input
v-if="item.type === 'input'"
v-model.trim="attrs.params[item.prop]"
:maxlength="item.maxlength"
clearable
:placeholder="item.placeholder"
></el-input>
根据el-input的属性,我们定义了如下的参数。type代表当前的组件类型,这样输入数据和html模版就能一一对应
{ type: 'input', label: '输入框', prop: 'name', placeholder: '请填写名称', maxlength: 10 }
目前已定义的组件如下,根据业务可自主扩展:
[
{ type: 'input', label: '输入框', prop: 'name', placeholder: '请填写名称', maxlength: 10 },
// multiple 是否为多选
{ type: 'select', multiple: false, filterable: false, label: '下拉选择框', prop: 'region', placeholder: '清选择活动区域', options: [{ label: '区域一', value: 1 }, { label: '区域二', value: 2 }] },
{ type: 'date-picker', label: '日期选择', prop: 'date1', placeholder: '请选择活动时间' },
{ type: 'time-picker', label: '选择时间', prop: 'date2', placeholder: '清选择时间' },
{ type: 'daterange', label: '选择时间', prop: 'date3', startPlaceholder: "开始日期", endPlaceholder: "结束日期" },
{ type: 'switch', label: '开关', prop: 'delivery', placeholder: '' },
{ type: 'radio-group', label: '单选框', prop: 'type', placeholder: '', options: [{ label: '线上品牌商赞助', value: 1 }, { label: '线下场地免费', value: 2 }] },
{ type: 'checkbox-group', label: '多选框', prop: 'activeName', placeholder: '', options: [{ label: '美食/餐厅线上活动', value: 1 }, { label: '地推活动', value: 2 }] },
{ type: 'autocomplete', label: '远程搜索输入框', prop: 'name', placeholder: '请输入进行搜索', querySearchAsync: Function, handleSelect: Function },
]
- 针对特殊场景,可能现有element基础组件不能满足业务需求。需要开发自己去封装实现一些组件,但这些组件又不是通用的,可以通过具名插槽完成
在组件中我们预留了一个特殊类型组件
<slot v-if="item.type === 'slot'" :name="item.slotName"></slot>
对应数据
{ type: 'slot', slotName: 'input1', prop: 'name1', label: '特殊名词' }
template中这些写
<Search v-bind="formData" @submitForm="submitForm">
<el-input v-slot:input1 v-model="form.params.name1" clearable placeholder="请输入特殊名词"></el-input>
</Search>
- 校验规则,key与prop值保持一致
rules: {
name: [
{ required: true, message: '请填写姓名', trigger: 'blur' }
],
}
- 预留两个操作按钮,可根据业务扩展
// 查询按钮
searchBtn: {
isShow: true, // 是否显示
text: '查询'
}
// 重置按钮
resetBtn: {
isShow: true, // 是否显示
text: '重置'
}
以上两个操作,最终都会触发表单提交,对应业务父组件中处理params数据
- 其它属性
labelWidth: 'auto', // 表单域标签的宽度,格式'100px'或auto,默认auto
labelPosition: 'left', // 表单域标签的位置 right/left/top,默认left
loading: false, // 查询按钮loading
具体封装如下
search.vue el-form 模块,定义主体样式
<template>
<el-form
class="search"
:model="attrs.params"
:rules="attrs.rules"
ref="ruleForm"
:label-width="attrs.labelWidth || 'auto'"
:label-position="attrs.labelPosition || 'left'"
>
<div class="base">
<FormItem v-bind="$attrs" attribute="baseAttribute"></FormItem>
</div>
<div class="more" v-if="isShow">
<FormItem v-bind="$attrs" attribute="moreAttribute"></FormItem>
</div>
<div class="btnRight">
<el-button
v-if="attrs.searchBtn && attrs.searchBtn.isShow"
:loading="attrs.loading"
type="primary"
:icon="Search"
@click="submitForm(ruleForm)"
>{{ attrs.searchBtn.text }}</el-button
>
<el-button
v-if="attrs.resetBtn && attrs.resetBtn.isShow"
:loading="attrs.loading"
:icon="Refresh"
@click="resetForm(ruleForm)"
>{{ attrs.resetBtn.text }}</el-button
>
<el-button
v-if="attrs.moreAttribute && attrs.moreAttribute.length != 0"
type="text"
@click="isShow = !isShow"
>{{ isShow ? "收起" : "展开" }}</el-button
>
</div>
</el-form>
</template>
<script setup>
import FormItem from './FormItem.vue';
import {
Search,
Refresh
} from '@element-plus/icons-vue'
import { ref, useAttrs } from 'vue'
const attrs = useAttrs()
const emit = defineEmits(['submitForm'])
const isShow = ref(true)
const ruleForm = ref(null)
function submitForm(formEL) {
formEL.validate((valid) => {
if (valid) {
emit('submitForm')
} else {
return false
}
});
}
function resetForm(formEL) {
formEL.resetFields()
submitForm(formEL)
}
</script>
<style scoped>
.search {
width: 100%;
overflow-x: auto;
}
.base {
display: flex;
flex-wrap: nowrap;
align-items: center;
}
.btnRight {
float: right;
}
.more {
display: flex;
flex-wrap: wrap;
}
</style>
FormItem.vue 处理el-form-item下的组件
<template>
<el-form-item
v-for="(item, index) in attrs[attrs.attribute]"
:key="index"
:label="item.label"
:class="[item.label === '' ? 'special' : '']"
:prop="item.prop"
>
<el-input
v-if="item.type === 'input'"
v-model.trim="attrs.params[item.prop]"
:maxlength="item.maxlength"
clearable
:placeholder="item.placeholder"
></el-input>
<el-select
v-if="item.type === 'select'"
v-model="attrs.params[item.prop]"
:filterable="item.filterable"
:multiple="item.multiple"
:placeholder="item.placeholder"
clearable
>
<el-option
v-for="(option, index) in item.options"
:key="index"
:label="option.label"
:value="option.value"
></el-option>
</el-select>
<el-date-picker
v-if="item.type === 'date-picker'"
type="date"
:placeholder="item.placeholder"
v-model="attrs.params[item.prop]"
clearable
></el-date-picker>
<el-time-picker
v-if="item.type === 'time-picker'"
:placeholder="item.placeholder"
v-model="attrs.params[item.prop]"
clearable
></el-time-picker>
<el-switch
v-if="item.type === 'switch'"
v-model="attrs.params[item.prop]"
:placeholder="item.placeholder"
></el-switch>
<el-checkbox-group
v-if="item.type === 'checkbox-group'"
v-model="attrs.params[item.prop]"
:placeholder="item.placeholder"
>
<el-checkbox
v-for="(option, index) in item.options"
:key="index"
:label="option.value"
>{{ option.label }}</el-checkbox
>
</el-checkbox-group>
<el-radio-group
v-if="item.type === 'radio-group'"
v-model="attrs.params[item.prop]"
>
<el-radio
v-for="(option, index) in item.options"
:key="index"
:label="option.value"
>{{ option.label }}</el-radio
>
</el-radio-group>
<el-date-picker
v-if="item.type === 'daterange'"
style="width: 250px"
v-model="attrs.params[item.prop]"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
range-separator="至"
:start-placeholder="item.startPlaceholder"
:end-placeholder="item.endPlaceholder"
clearable
>
</el-date-picker>
<el-autocomplete
v-if="item.type === 'autocomplete'"
v-model="attrs.params[item.prop]"
:fetch-suggestions="item.querySearchAsync"
:placeholder="item.placeholder"
@select="item.handleSelect"
clearable
></el-autocomplete>
<slot v-if="item.type === 'slot'" :name="item.slotName"></slot>
</el-form-item>
</template>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
<style scoped>
.el-form-item {
margin-right: 10px;
}
.el-form-item:last-child {
margin-right: 0;
}
.el-form-item__label-wrap {
margin-left: 0 !important;
}
.special .el-form-item__label-wrap {
margin-left: 0 !important;
}
.special .el-form-item__content {
margin-left: 0 !important;
}
</style>
应用
可以注册为全局组件
import Search from '@/components/search.vue'
const app = createApp(App)
app.component('Search', Search)
业务组件
<template>
<Search v-if="formData.baseAttribute.length != 0" v-bind="formData" @submitForm="submitForm"></Search>
</template>
<script setup lang="ts">
import { form } from "./form"
import { ref, reactive, onMounted } from 'vue'
interface FormData {
params: Object,
baseAttribute: Array<Object>,
moreAttribute: Array<Object>,
searchBtn: Object,
resetBtn: Object,
loading: Boolean
}
const formData = ref<FormData>({
params: {},
baseAttribute: [],
moreAttribute: [],
searchBtn: {},
resetBtn: {},
loading: false
})
const orgOptions = ref([
{ label: '组织1', value: 'org1' },
{ label: '组织2', value: 'org2' },
{ label: '组织3', value: 'org3' },
{ label: '组织4', value: 'org4' },
])
onMounted(() => {
formData.value = form({ _data: { orgOptions }, _methods: { querySearchAsync, handleSelect }})
})
// 表单提交
function submitForm() {
}
</script>