vue antd 自定义搜索筛选栏组件封装
这里写自定义目录标题1、背景2、分析3、实现(1)form表单组件(searchForm.vue)示例代码:(2)formItem表单项组件(formItem.vue)示例代码:4、关键点5、参数配置项解释(1)示例:(2)参数传递解析的流程:(3)特殊情况的处理6、按钮组7、使用方式示例8、完整代码(1)searchForm.vue(2)formItem.vue依赖引入的一些函数方法 tools
这里写自定义目录标题
1、背景
vue后台管理系统,会有很多表格页面,表格上方会有一些搜索选项。表格直接使用a-table即可,而搜索栏区域每次写起来都很繁琐,且多人开发情况下每个人写的样式都不相同,布局样式无法统一。
所以要考虑对搜索栏做一个封装,统一配置引用,提升开发维护效率和界面统一。
完成后的效果大概就是长这样:
2、分析
项目使用的是antd框架,搜索栏这种表单提交,首先要使用a-form-model组件来封装,而复杂点就是表单项可能有很多种,例如input输入框、select选择框、日期时间选择框、日期时间范围选择框、cascader级联选择框等,每一项的字段名prop、名称label、绑定的属性方法都不尽相同。所以不能通过普通的绑定个别属性的方式来处理,而slot插槽的方式也无法简化,最终决定通过传递一个配置项数组的形式来解析生成相应的结构。
3、实现
目前实现的方式由两部分组成,一部分是form表单组件,接受父组件传递的配置项数组,一部分是封装一些常用的表单项组件,通过v-if来控制,form表单组件里引入该表单项组件,循环遍历,根据传递的表单项类型来匹配显示具体的表单项。
(1)form表单组件(searchForm.vue)示例代码:
<div class="search-form-box">
<a-row>
<a-form-model
:model="formData"
ref="formRef"
layout="horizontal"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-col :span="6" v-for="(item, index) in formOptions" :key="newKeys[index]">
<a-form-model-item
:prop="item.prop"
:label="item.label ? item.label + ':' : ''"
:rules="item.rules"
>
<formItem v-model="formData[item.prop]" :itemOptions="item" :needParams="needParams" />
</a-form-model-item>
</a-col>
<!-- 自定义插槽,可用于特殊表单块 -->
<slot></slot>
</a-form-model>
</a-row>
<a-row type="flex" justify="end">
<!-- 提交按钮 -->
<div class="btn-box">
<a-button
v-if="btnItems.includes('search')"
type="primary"
class="btn-search"
@click="onSearch"
>搜索</a-button
>
<a-button
v-if="btnItems.includes('export')"
type="primary"
class="btn-export"
@click="onExport"
>导出</a-button
>
<a-button
v-if="btnItems.includes('reset')"
type="default"
class="btn-reset"
@click="onReset"
>重置</a-button
>
</div>
</a-row>
</div>
(2)formItem表单项组件(formItem.vue)示例代码:
<div class="form-item">
<a-input
v-if="isInput"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
/>
<a-input-number
v-if="isInputNumber"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
/>
<a-select
v-if="isSelect"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
>
<a-select-option
v-for="item in itemOptions.options"
:key="item.value"
:label="item.label"
:value="item.value"
></a-select-option>
</a-select>
<!-- datetimerange/daterange -->
<a-range-picker
v-if="isDatePickerDateRange"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
:placeholder="['开始日期', '结束日期']"
separator="至"
:default-time="['00:00:00', '23:59:59']"
valueFormat="YYYY-MM-DD"
/>
<!-- monthrange -->
<a-date-picker
v-if="isDatePickerMonthRange"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
type="monthrange"
allowClear
:placeholder="['开始日期', '结束日期']"
range-separator="至"
valueFormat="YYYY-MM"
></a-date-picker>
<!-- others -->
<a-date-picker
v-if="isDatePickerOthers"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
placeholder="请选择日期"
valueFormat="YYYY-MM-DD"
></a-date-picker>
<a-cascader
v-if="isCascader"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
:options="itemOptions.options"
allowClear
></a-cascader>
</div>
4、关键点
由于antd表单组件本身有很多配置属性,不可能把所有的属性和方法都写死封装,要想无缝支持,需要用到vue的v-bind和v-on特性,vue的v-bind和v-on支持赋值为对象类型,vue会自动遍历对象里的属性依次绑定,v2.4.0+支持。
5、参数配置项解释
(1)示例:
/**
* 表单配置
* 示例:
* [{
* label: '用户名', // label文字
* prop: 'username', // 字段名
* type: 'input', // 指定antd组件
* defaultValue: '阿黄', // 字段初始值
* placeholder: '请输入用户名', // antd组件属性
* rules: [{ required: true, message: '必填项', trigger: 'blur' }], // antd组件属性
* events: { // antd组件方法
* input (val) {
* console.log(val)
* },
* ...... // 可添加任意antd组件支持的方法
* }
* ...... // 可添加任意antd组件支持的属性
* }]
*/
具体扩展属性可参考antd官方文档(antd官方文档)
(2)参数传递解析的流程:
- 首先,searchForm.vue组件里通过props接收参数:
formOptions: {
type: Array,
required: true,
default () {
return []
}
/**
* 请求所需参数
* 示例:
* needParams: {
* // 参数名
* // 参数值
* token: "123"
* },
*/
needParams: {
type: Object,
default() {
return {};
}
},
},
- created生命周期里处理初始值:
// 添加初始值
addInitValue () {
const obj = {}
this.formOptions.forEach(v => {
if (v.initValue !== undefined) {
obj[v.prop] = v.initValue
}
})
this.formData = obj
}
- 一部分配置项绑定在a-form-model-item上,一部分传递给formItem表单项组件再绑定:
<a-form-model
:model="formData"
ref="formRef"
layout="horizontal"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-col :span="6" v-for="(item, index) in formOptions" :key="newKeys[index]">
<a-form-model-item
:prop="item.prop"
:label="item.label ? item.label + ':' : ''"
:rules="item.rules"
>
<formItem v-model="formData[item.prop]" :itemOptions="item" :needParams="needParams" />
</a-form-model-item>
</a-col>
<!-- 自定义插槽,可用于特殊表单块 -->
<slot></slot>
</a-form-model>
- formItem.vue表单项组件里props接受传参:
itemOptions: {
type: Object,
default () {
return {}
}
}
- computed里处理接收的参数itemOptions,生成要绑定的所有属性对象bindProps:
// 绑定属性
bindProps() {
let obj = { ...this.itemOptions };
// 移除冗余属性
delete obj.label;
delete obj.prop;
delete obj.type;
delete obj.defaultValue;
delete obj.rules;
delete obj.events;
if (obj.type === "select") {
delete obj.options;
}
return obj;
},
- computed里生成要绑定的所有方法对象bindEvents:
// 绑定方法
bindEvents() {
return this.itemOptions.events || {};
},
- 处理请求所需参数
// 获取请求参数
requestParams() {
let params = { ...this.needParams };
let results = {};
if (Array.isArray(this.itemOptions.dicParamsList)) {
this.itemOptions.dicParamsList.forEach(item => {
if(item.value){
results[item.key] = item.value;
}else {
results[item.key] = params[item.key];
}
});
}
return results;
},
- 最后组件上使用
<a-input
v-if="isInput"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
/>
(3)特殊情况的处理
由于antd的a-select里是通过a-select-option遍历实现的,而遍历数组options按antd官方不是绑定在a-select上的,所以针对a-select的配置项再加一个options属性,即select选择项的数据数组。
<a-select
v-if="isSelect"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
>
<a-select-option
v-for="item in itemOptions.options"
:key="item.value"
:label="item.label"
:value="item.value"
></a-select-option>
</a-select>
created中请求数据
created() {
// select 动态添加options
if (this.itemOptions.url) {
axios({
url: this.itemOptions.url,
params: this.requestParams
}).then(res => {
this.itemOptions.options = res.data && res.data[this.itemOptions.propsHttpRes];
})
}
},
6、按钮组
目前通过prop传入,三个按钮
// 提交按钮项,多个用逗号分隔(search搜索, export导出, reset重置)
btnItems: {
type: String,
default () {
return 'search'
}
}
7、使用方式示例
template
<searchForm
:formOptions="formOptions"
:needParams="needParams"
btnItems="search, export, reset"
/>
data
formOptions: [
{
label: "用户名很长是是是", // label文字
prop: "username", // 字段名
type: "input", // 指定antd组件
defaultValue: "阿黄", // 字段初始值
placeholder: "请输入用户名", // antd组件属性
rules: [{ required: true, message: "必填项", trigger: "blur" }], // antd组件属性
events: {
// antd组件方法
input(val) {
console.log(val);
},
},
},
{
label: "年龄", // label文字
prop: "age", // 字段名
type: "number", // 指定antd组件
defaultValue: 18, // 字段初始值
placeholder: "请输入年龄", // antd组件属性
rules: [{ required: true, message: "必填项", trigger: "blur" }], // antd组件属性
events: {
// antd组件方法
input(val) {
console.log(val);
},
},
},
{
label: "性别", // label文字
prop: "sex", // 字段名
type: "select", // 指定antd组件
defaultValue: "", // 字段初始值
placeholder: "请选择性别", // antd组件属性
options: [
{
label: "男",
value: "1",
},
{
label: "女",
value: "2",
},
],
events: {
// antd组件方法
change(val) {
console.log(val);
},
},
},
{
label: "下拉框", // label文字
prop: "address", // 字段名
type: "select", // 指定antd组件
defaultValue: "", // 字段初始值
placeholder: "请选择性别", // antd组件属性
options: [],
url: 'http://rap2api.taobao.org/app/mock/270426/city',
methods: 'get',
dicParamsList: [
{
key: 'token'
},
{
key: 'bussinessId',
value: '324'
}
],
filedName: {
label: 'name',
value: 'id'
},
propsHttpRes: 'data',
events: {
// antd组件方法
change(val) {
console.log(val);
},
},
},
{
label: "项目地址", // label文字
prop: "project", // 字段名
type: "cascader", // 指定antd组件
defaultValue: [], // 字段初始值
placeholder: "请选择性别", // antd组件属性
options: [],
url: 'http://rap2api.taobao.org/app/mock/270426/getCascaderList',
methods: 'get',
dicParamsList: [
{
key: 'token'
},
{
key: 'bussinessId',
value: '324'
}
],
filedName: {
label: 'name',
value: 'id'
},
propsHttpRes: 'data',
events: {
// antd组件方法
change(val) {
console.log(val);
},
},
},
{
label: "到货日期", // label文字
prop: "arrialDate", // 字段名
type: "range-picker", // 指定antd组件
defaultValue: [], // 字段初始值
placeholder: "请选择", // antd组件属性
events: {
// antd组件方法
change(val) {
console.log(val);
},
},
},
{
label: "发货日期", // label文字
prop: "delieverDate", // 字段名
type: "date-picker", // 指定antd组件
defaultValue: '', // 字段初始值
placeholder: "请选择", // antd组件属性
events: {
// antd组件方法
change(val) {
console.log(val);
},
},
},
],
8、完整代码
(1)searchForm.vue
<template>
<div class="search-form-box">
<a-row>
<a-form-model
:model="formData"
ref="formRef"
layout="horizontal"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-col :span="6" v-for="(item, index) in formOptions" :key="newKeys[index]">
<a-form-model-item
:prop="item.prop"
:label="item.label ? item.label + ':' : ''"
:rules="item.rules"
>
<formItem v-model="formData[item.prop]" :itemOptions="item" :needParams="needParams" />
</a-form-model-item>
</a-col>
<!-- 自定义插槽,可用于特殊表单块 -->
<slot></slot>
</a-form-model>
</a-row>
<a-row type="flex" justify="end">
<!-- 提交按钮 -->
<div class="btn-box">
<a-button
v-if="btnItems.includes('search')"
type="primary"
class="btn-search"
@click="onSearch"
>搜索</a-button
>
<a-button
v-if="btnItems.includes('export')"
type="primary"
class="btn-export"
@click="onExport"
>导出</a-button
>
<a-button
v-if="btnItems.includes('reset')"
type="default"
class="btn-reset"
@click="onReset"
>重置</a-button
>
</div>
</a-row>
</div>
</template>
<script>
import formItem from "./formItem";
import tools from '@/utils/tools'
export default {
name: 'searchForm',
props: {
/**
* 表单配置
* 示例:
* [{
* label: '用户名', // label文字
* prop: 'username', // 字段名
* type: 'input', // 指定antd组件
* defaultValue: '阿黄', // 字段初始值
* placeholder: '请输入用户名', // antd组件属性
* rules: [{ required: true, message: '必填项', trigger: 'blur' }], // antd组件属性
* events: { // antd组件方法
* input (val) {
* console.log(val)
* },
* ...... // 可添加任意antd组件支持的方法
* }
* ...... // 可添加任意antd组件支持的属性
* }]
*/
formOptions: {
type: Array,
required: true,
default() {
return [];
}
},
/**
* 请求所需参数
* 示例:
* needParams: {
* // 参数名
* // 参数值
* token: "123"
* },
*/
needParams: {
type: Object,
default() {
return {};
}
},
// 提交按钮项,多个用逗号分隔(search, export, reset)
btnItems: {
type: String,
default() {
return "search";
}
}
},
data() {
return {
labelCol: {
xs: { span: 24 },
sm: { span: 9 },
md: { span: 11 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 15 },
md: { span: 13 },
},
formData: {}
};
},
computed: {
// 生成新的key
newKeys() {
return this.formOptions.map(() => {
return tools.createUniqueString()
});
}
},
created() {
this.addDefaultValue();
},
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();
},
// 添加初始值
addDefaultValue() {
const obj = {};
this.formOptions.forEach(v => {
if (v.defaultValue !== undefined) {
obj[v.prop] = v.defaultValue;
}
});
this.formData = obj;
}
},
components: { formItem }
};
</script>
<style lang="scss">
.search-form-box {
margin-bottom: 15px;
.btn-box {
padding-top: 5px;
display: flex;
button {
height: 28px;
margin: 0 5px;
}
}
.ant-input-number {
width: 100% ;
}
.ant-form-item {
margin-bottom: 10px;
}
}
</style>
(2)formItem.vue
<template>
<div class="form-item">
<a-input
v-if="isInput"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
/>
<a-input-number
v-if="isInputNumber"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
/>
<a-select
v-if="isSelect"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
>
<a-select-option
v-for="item in itemOptions.options"
:key="item.value"
:label="item.label"
:value="item.value"
></a-select-option>
</a-select>
<!-- datetimerange/daterange -->
<a-range-picker
v-if="isDatePickerDateRange"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
:placeholder="['开始日期', '结束日期']"
separator="至"
:default-time="['00:00:00', '23:59:59']"
valueFormat="YYYY-MM-DD"
/>
<!-- monthrange -->
<a-date-picker
v-if="isDatePickerMonthRange"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
type="monthrange"
allowClear
:placeholder="['开始日期', '结束日期']"
range-separator="至"
valueFormat="YYYY-MM"
></a-date-picker>
<!-- others -->
<a-date-picker
v-if="isDatePickerOthers"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
allowClear
placeholder="请选择日期"
valueFormat="YYYY-MM-DD"
></a-date-picker>
<a-cascader
v-if="isCascader"
v-model="currentVal"
v-bind="bindProps"
v-on="bindEvents"
:options="itemOptions.options"
allowClear
></a-cascader>
</div>
</template>
<script>
import axios from "axios";
export default {
inheritAttrs: false,
props: {
value: {},
itemOptions: {
type: Object,
default() {
return {};
}
},
needParams: {
type: Object,
default: () => ({})
}
},
computed: {
// 双向绑定数据值
currentVal: {
get() {
return this.value;
},
set(val) {
this.$emit("input", val);
}
},
// 绑定属性
bindProps() {
let obj = { ...this.itemOptions };
// 移除冗余属性
delete obj.label;
delete obj.prop;
delete obj.type;
delete obj.defaultValue;
delete obj.rules;
delete obj.events;
if (obj.type === "select") {
delete obj.options;
}
return obj;
},
// 绑定方法
bindEvents() {
return this.itemOptions.events || {};
},
// 获取请求参数
requestParams() {
let params = { ...this.needParams };
let results = {};
if (Array.isArray(this.itemOptions.dicParamsList)) {
this.itemOptions.dicParamsList.forEach(item => {
if(item.value){
results[item.key] = item.value;
}else {
results[item.key] = params[item.key];
}
});
}
return results;
},
// a-input
isInput() {
return this.itemOptions.type === "input";
},
// a-input-number
isInputNumber() {
return this.itemOptions.type === "number";
},
// a-select
isSelect() {
return this.itemOptions.type === "select";
},
// a-date-picker (type: datetimerange/daterange)
isDatePickerDateRange() {
return this.itemOptions.type === "range-picker";
},
// a-date-picker (type: monthrange)
isDatePickerMonthRange() {
return this.itemOptions.type === "month-range";
},
// a-date-picker (type: other)
isDatePickerOthers() {
return this.itemOptions.type === "date-picker";
},
// a-cascader
isCascader() {
return this.itemOptions.type === "cascader";
},
},
created() {
// select 动态添加options
if (this.itemOptions.url) {
axios({
url: this.itemOptions.url,
params: this.requestParams
}).then(res => {
this.itemOptions.options = res.data && res.data[this.itemOptions.propsHttpRes];
})
}
},
methods: {},
components: {},
};
</script>
<style lang="less" scoped></style>
依赖引入的一些函数方法 tools.js
/**
* 创建唯一的字符串
* @return {string} ojgdvbvaua40
*/
function createUniqueString() {
const timestamp = +new Date() + "";
const randomNum = parseInt((1 + Math.random()) * 65536) + "";
return (+(randomNum + timestamp)).toString(32);
}
export default {
createUniqueString,
};
更多推荐
所有评论(0)