1、背景
vue后台管理系统,会有很多表格页面,表格上方会有一些搜索选项。表格直接使用el-table即可,而搜索栏区域每次写起来都很繁琐,且多人开发情况下每个人写的样式都不相同,布局样式无法统一。
所以要考虑对搜索栏做一个封装,统一配置引用,提升开发维护效率和界面统一。
完成后的效果大概就是长这样:

2、分析
项目使用的是elementui框架,搜索栏这种表单提交,首先要使用el-form组件来封装,而复杂点就是表单项可能有很多种,例如input输入框、select选择框、日期时间选择框、日期时间范围选择框、cascader级联选择框等,每一项的字段名prop、名称label、绑定的属性方法都不尽相同。所以不能通过普通的绑定个别属性的方式来处理,而slot插槽的方式也无法简化,最终决定通过传递一个配置项数组的形式来解析生成相应的结构。
3、实现
目前实现的方式由两部分组成,一部分是form表单组件,接受父组件传递的配置项数组,一部分是封装一些常用的表单项组件,通过v-if来控制,form表单组件里引入该表单项组件,循环遍历,根据传递的表单项类型来匹配显示具体的表单项。
(1)form表单组件(searchForm.vue)示例代码:
<el-form:model="formData"ref="formRef":inline="true"
><el-form-itemv-for="(item, index) in formOptions":key="newKeys[index]":prop="item.prop":label="item.label ? (item.label + ':') : ''":rules="item.rules"><formItemv-model="formData[item.prop]":itemOptions="item"/></el-form-item>
</el-form>
(2)formItem表单项组件(formItem.vue)示例代码:
<el-inputv-if="isInput"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"size="mini"
></el-input><el-selectv-if="isSelect"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"size="mini"clearable><el-optionv-for="item in itemOptions.options":key="item.value":label="item.label":value="item.value"></el-option>
</el-select>
4、关键点
由于elementui表单组件本身有很多配置属性,不可能把所有的属性和方法都写死封装,要想无缝支持,需要用到vue的v-bind和v-on特性,vue的v-bind和v-on支持赋值为对象类型,vue会自动遍历对象里的属性依次绑定,v2.4.0+支持。
5、参数配置项解释
(1)示例:
[{label: '用户名', // label文字prop: 'username', // 字段名element: 'el-input', // 指定elementui组件initValue: '阿黄', // 字段初始值placeholder: '请输入用户名', // elementui组件属性rules: [{ required: true, message: '必填项', trigger: 'blur' }], // elementui组件属性events: { // elementui组件方法input (val) {console.log(val)},}
}]
- label 用于绑定给el-form-item上的label,表单项标题
- prop 用于绑定给el-form-item上的prop,字段名,必填
- element 指定elementui表单项的组件名,必填
- initValue 表单项的初始值,可选
- events 对象,对象里加方法,js原生方法或者elementui表单项组件支持的方法都可以加进去,通过v-on遍历绑定
- … 其他elementui表单项组件支持的属性或者html原生属性都可以添加,常用的例如rules表单校验、placeholder提示,通过v-bind遍历绑定
(2)参数传递解析的流程:
- 首先,searchForm.vue组件里通过props接收参数:
formOptions: {type: Array,required: true,default () {return []}
},
- created生命周期里处理初始值:
// 添加初始值
addInitValue () {const obj = {}this.formOptions.forEach(v => {if (v.initValue !== undefined) {obj[v.prop] = v.initValue}})this.formData = obj
}
- 一部分配置项绑定在el-form-item上,一部分传递给formItem表单项组件再绑定:
<el-form-itemv-for="(item, index) in formOptions":key="newKeys[index]":prop="item.prop":label="item.label ? (item.label + ':') : ''":rules="item.rules"
><formItemv-model="formData[item.prop]":itemOptions="item"/>
</el-form-item>
- formItem.vue表单项组件里props接受传参:
itemOptions: {type: Object,default () {return {}}
}
- computed里处理接收的参数itemOptions,生成要绑定的所有属性对象bindProps:
// 绑定属性
bindProps () {let obj = { ...this.itemOptions }// 移除已使用的或不相关的冗余属性delete obj.labeldelete obj.propdelete obj.elementdelete obj.initValuedelete obj.rulesdelete obj.eventsif (obj.element === 'el-select') {delete obj.options}return obj
},
- computed里生成要绑定的所有方法对象bindEvents:
// 绑定方法
bindEvents () {return this.itemOptions.events || {}
},
- 最后dom里使用这些数据绑定:
<el-inputv-if="isInput"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"
></el-input>
(3)特殊情况的处理
由于elementui的el-select里是通过el-option遍历实现的,而遍历数组options按elementui官方不是绑定在el-select上的,所以针对el-select的配置项再加一个options属性,即select选择项的数据数组。
<el-selectv-if="isSelect"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"size="mini"clearable
><el-optionv-for="item in itemOptions.options":key="item.value":label="item.label":value="item.value"></el-option>
</el-select>
elementui的日期时间选择器分了很多种,根据业务需要分别处理一下,我这里是根据type划分成了三种分开处理,最常用的是datetimerange日期时间范围选择器,作为默认项,还有一种monthrange,其余的都划为一种。(具体处理见文章末尾的完整代码)
6、按钮组
按钮其实就那么几个,没必要做太多的封装,根据业务有哪些按钮就封装进去,目前我这里就封装了三个按钮。
通过props接受一个字符串标识按钮组:
// 提交按钮项,多个用逗号分隔(search搜索, export导出, reset重置)
btnItems: {type: String,default () {return 'search'}
}
7、使用方式示例
- dom里:
<!-- 搜索 -->
<searchForm :formOptions="formOptions" @onSearch="onSearch"/>
- vue data里:
data () {return {formOptions: [{label: '意见内容',prop: 'content',element: 'el-input'},{label: '类型',prop: 'type',element: 'el-select',options: [{ label: '给点意见', value: '1' },{ label: '售后问题', value: '2' }]},{label: '状态',prop: 'status',element: 'el-select',options: getFeedbackStatus()},{label: '提交时间',prop: 'timeRange',element: 'el-date-picker'}],}
}
- vue methods里:
methods: {// 获取搜索表单提交的数据onSearch (val) {console.log(val)}
}
8、完整代码
(1)searchForm.vue
/*** 搜索栏公共组件*/
<template><div class="search-form-box"><el-form:model="formData"ref="formRef":inline="true"><el-form-itemv-for="(item, index) in formOptions":key="newKeys[index]":prop="item.prop":label="item.label ? (item.label + ':') : ''":rules="item.rules"><formItemv-model="formData[item.prop]":itemOptions="item"/></el-form-item><!-- 自定义插槽,可用于特殊表单块 --><slot></slot></el-form><!-- 提交按钮 --><div class="btn-box"><el-buttonv-if="btnItems.includes('search')"size="mini"type="primary"class="btn-search"@click="onSearch">搜索</el-button><el-buttonv-if="btnItems.includes('export')"size="mini"type="primary"class="btn-export"@click="onExport">导出</el-button><el-buttonv-if="btnItems.includes('reset')"size="mini"type="default"class="btn-reset"@click="onReset">重置</el-button></div></div>
</template><script>
import formItem from './formItem'
import tools from '@/utils/tools'export default {props: {/*** 表单配置* 示例:* [{* label: '用户名', // label文字* prop: 'username', // 字段名* element: 'el-input', // 指定elementui组件* initValue: '阿黄', // 字段初始值* placeholder: '请输入用户名', // elementui组件属性* rules: [{ required: true, message: '必填项', trigger: 'blur' }], // elementui组件属性* events: { // elementui组件方法* input (val) {* console.log(val)* },* ...... // 可添加任意elementui组件支持的方法* }* ...... // 可添加任意elementui组件支持的属性* }]*/formOptions: {type: Array,required: true,default () {return []}},// 提交按钮项,多个用逗号分隔(search, export, reset)btnItems: {type: String,default () {return 'search'}}},data () {return {formData: {}}},computed: {newKeys () {return this.formOptions.map(v => {return tools.createUniqueString()})}},created () {this.addInitValue()},methods: {// 校验onValidate (callback) {this.$refs.formRef.validate(valid => {if (valid) {console.log('提交成功')console.log(this.formData)callback()}})},// 搜索onSearch () {this.onValidate(() => {this.$emit('onSearch', this.formData)})},// 导出onExport () {this.onValidate(() => {this.$emit('onExport', this.formData)})},onReset () {this.$refs.formRef.resetFields()},// 添加初始值addInitValue () {const obj = {}this.formOptions.forEach(v => {if (v.initValue !== undefined) {obj[v.prop] = v.initValue}})this.formData = obj}},components: { formItem }
}
</script><style lang='less' scoped>
.search-form-box {display: flex;margin-bottom: 15px;.btn-box {padding-top: 5px;display: flex;button {height: 28px;}}.el-form {/deep/ .el-form-item__label {padding-right: 0;}.el-form-item {margin-bottom: 0;&.is-error {margin-bottom: 22px;}}// el-input宽度/deep/ .form-item {> .el-input:not(.el-date-editor) {width: 120px;}}/deep/ .el-select {width: 120px;}/deep/ .el-cascader {width: 200px;}}
}</style>
(2)formItem.vue
/*** 表单匹配项*/
<template><div class='form-item'><el-inputv-if="isInput"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"size="mini"></el-input><el-input-numberv-if="isInputNumber"v-model="currentVal"v-bind="bindProps"v-on="bindEvents":controls-position="itemOptions['controls-position'] || 'right'"size="mini"></el-input-number><el-selectv-if="isSelect"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"size="mini"clearable><el-optionv-for="item in itemOptions.options":key="item.value":label="item.label":value="item.value"></el-option></el-select><!-- datetimerange/daterange --><el-date-pickerv-if="isDatePickerDateRange"v-model="currentVal"v-bind="bindProps"v-on="bindEvents":type="itemOptions.type || 'datetimerange'"size="mini"clearable:picker-options="pickerOptionsRange"start-placeholder="开始日期"range-separator="至"end-placeholder="结束日期":default-time="['00:00:00', '23:59:59']"value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker><!-- monthrange --><el-date-pickerv-if="isDatePickerMonthRange"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"type="monthrange"size="mini"clearable:picker-options="pickerOptionsRangeMonth"start-placeholder="开始日期"range-separator="至"end-placeholder="结束日期"value-format="yyyy-MM"></el-date-picker><!-- others --><el-date-pickerv-if="isDatePickerOthers"v-model="currentVal"v-bind="bindProps"v-on="bindEvents":type="itemOptions.type"size="mini"clearableplaceholder="请选择日期"></el-date-picker><el-cascaderv-if="isCascader"v-model="currentVal"v-bind="bindProps"v-on="bindEvents"size="mini"clearable></el-cascader></div>
</template><script>
import tools from '@/utils/tools'export default {inheritAttrs: false,props: {value: {},itemOptions: {type: Object,default () {return {}}}},data () {return {pickerOptionsRange: tools.pickerOptionsRange,pickerOptionsRangeMonth: tools.pickerOptionsRangeMonth}},computed: {// 双向绑定数据值currentVal: {get () {return this.value},set (val) {this.$emit('input', val)}},// 绑定属性bindProps () {let obj = { ...this.itemOptions }// 移除冗余属性delete obj.labeldelete obj.propdelete obj.elementdelete obj.initValuedelete obj.rulesdelete obj.eventsif (obj.element === 'el-select') {delete obj.options}return obj},// 绑定方法bindEvents () {return this.itemOptions.events || {}},// el-inputisInput () {return this.itemOptions.element === 'el-input'},// el-input-numberisInputNumber () {return this.itemOptions.element === 'el-input-number'},// el-selectisSelect () {return this.itemOptions.element === 'el-select'},// el-date-picker (type: datetimerange/daterange)isDatePickerDateRange () {const isDatePicker = this.itemOptions.element === 'el-date-picker'const isDateRange = !this.itemOptions.type ||this.itemOptions.type === 'datetimerange' ||this.itemOptions.type === 'daterange'return isDatePicker && isDateRange},// el-date-picker (type: monthrange)isDatePickerMonthRange () {const isDatePicker = this.itemOptions.element === 'el-date-picker'const isMonthRange = this.itemOptions.type === 'monthrange'return isDatePicker && isMonthRange},// el-date-picker (type: other)isDatePickerOthers () {const isDatePicker = this.itemOptions.element === 'el-date-picker'return isDatePicker && !this.isDatePickerDateRange && !this.isDatePickerMonthRange},// el-cascaderisCascader () {return this.itemOptions.element === 'el-cascader'}},created () {},methods: {},components: {}
}
</script><style lang='less' scoped></style>
(3)依赖引入的一些函数方法 tools.js
/*** 创建唯一的字符串* @return {string} ojgdvbvaua40*/
function createUniqueString () {const timestamp = +new Date() + ''const randomNum = parseInt((1 + Math.random()) * 65536) + ''return (+(randomNum + timestamp)).toString(32)
}// elementui日期时间范围 快捷选项
const pickerOptionsRange = {shortcuts: [{text: '今天',onClick (picker) {const end = new Date()const start = new Date(new Date().toDateString())start.setTime(start.getTime())picker.$emit('pick', [start, end])}}, {text: '最近一周',onClick (picker) {const end = new Date()const start = new Date()start.setTime(end.getTime() - 3600 * 1000 * 24 * 7)picker.$emit('pick', [start, end])}}, {text: '最近一个月',onClick (picker) {const end = new Date()const start = new Date()start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)picker.$emit('pick', [start, end])}}, {text: '最近三个月',onClick (picker) {const end = new Date()const start = new Date()start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)picker.$emit('pick', [start, end])}}]
}// elementui月份范围 快捷选项
const pickerOptionsRangeMonth = {shortcuts: [{text: '今年至今',onClick (picker) {const end = new Date()const start = new Date(new Date().getFullYear(), 0)picker.$emit('pick', [start, end])}},{text: '最近半年',onClick (picker) {const end = new Date()const start = new Date()start.setMonth(start.getMonth() - 6)picker.$emit('pick', [start, end])}},{text: '最近一年',onClick (picker) {const end = new Date()const start = new Date()start.setMonth(start.getMonth() - 12)picker.$emit('pick', [start, end])}}]
}
(4)一些elmentui全局样式的修改
// el-input-number (controls-position="right")
.el-input-number.is-controls-right {.el-input-number__decrease {display: none;}.el-input-number__increase {display: none;top: 2px; // fix style bug}&:hover {.el-input-number__decrease {display: inline-block;}.el-input-number__increase {display: inline-block;}}.el-input__inner {text-align: left;padding-left: 5px;padding-right: 40px;}
}// el-date-picker datetimerange
.el-date-editor.el-date-editor--datetimerange {.el-range-separator {width: 24px;color: #999;padding: 0;}.el-range__icon {margin-left: 0;}&.el-input__inner {vertical-align: middle;padding: 3px 5px;}&.el-range-editor--medium {width: 380px;.el-range-separator {line-height: 30px;}}&.el-range-editor--mini {width: 330px;.el-range-separator {line-height: 22px;}}
}// el-date-picker not datetimerange
.el-date-editor {.el-input__prefix {left: 0;top: 1px;}.el-input__suffix {right: 0;top: 1px;}.el-input__inner {padding: 0 25px;}&.el-input--mini {width: 175px;}&.el-input--medium {width: 195px;}
}// input padding
.el-input__inner {padding: 0 5px;
}
9、vue3兼容指南
https://blog.csdn.net/u010059669/article/details/111269498