Vue 2.x折腾记 - (16) 基于Ant Design Vue 封装一个配置式的表单搜索组件
前言
这次的后台管理系统项目选型用了Vue
来作为主技术栈;
因为前段时间用过React
来写过项目(用了antd
),感觉棒棒的。
所以这次就排除了Element UI
,而采用了Ant Design Vue
;
在分析整个项目原型后,发现又可以抽离类似之前的React表格搜索组件
效果图
- 2019-04-10 14:50 : 修正了部分的初始化
props
及联动,新增了slot
的传递
- 2019-04-17: 我又增加了一种布局展示,内联模式,顺带修复了一些已知的问题,组件重命名为
AdvancedSearch.vue
2019-04-23: 新增
slider
组件的配置2019-04-25:若是传入的数据长度小于最大格式,默认显示为内联模式,否则为卡片模式
2019-05-12: 回调支持传入自定义函数(用于返回自己组合的数据格式)
其他特性等,具体可以看下面的思维导图.
具体业务的封装中还要复杂的多,还结合了一些自定义封装组件,展示出来代码篇幅太长。
实现思路
- 用什么来实现组件之间的通讯
昨天写第一版的时候,思维还没绕过来,用props
和自定义事件($on,$emit
)来实现,
实现出来的代码量贼多,因为每细化多一层组件,复杂度就越高。各种互相回调来实现。
仔细翻了下Ant Design Vue
的文档,发下可以类似React
的套路实现
- 怎么来实现
要实现一个结合业务可复用的东东,首先我们必须先梳理我们要实现的功能点。
props
尽量不破坏文档控件暴露的特性,而是折中去实现,拓展。
先画个思维导图梳理下功能点
遇到的问题
jsx
来实现的问题
一开始想用jsx
来实现,发现还是太天真了。各种报错,特别对Vue
指令的支持一团糟
以及函数式组件的写法也是坑挺多,没办法,乖乖的回归template
的写法
vue
官方提供了jsx
的支持,日渐完善;Github:vue/jsx
- 控件挤成一坨的问题
这个可能是antd vue
版本的样式没处理好,我仔细排查了。若没有复写他的样式,完全没法展开。
placeholder
不会自动撑开,数字控件也是很小
修正前:
修正后
- 补全当初写
react
版本一些欠缺考虑的东东(比如返回的查询对象上)
用法
就普通的引入,具体暴露的props
和change
如下
子项会覆盖全局带过来的同名特性,优先级比较高
选项 | 类型 | 解释 |
---|---|---|
responsive | 对象 | 栅栏的布局对象 |
size | 字符串 |
控件规格大小(大部分都有default,small,large )
|
gutter | 数字 | 控件的间距 |
datetimeTotimeStamp | 布尔类型 |
若是为true ,所有时间控件都会转为时间戳返回
|
searchDataSource | 数组对象 |
就是需要渲染控件的数据源,具体看源码的props
|
@change | 函数 | 就是查询的回调 |
@callbackFormat | 可选函数 | 传递会改动回调数据,不传递则忽略 |
// SearchDataSource是数据源,具体可以看props的默认值
<table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" /><table-search :SearchDataSource="SearchDataSource" @change="tableSearchChange" @callbackFormat="formatFunc"><a-button type="primary" @click="test">xxxx</a-button><template v-slot:extra><div>fasdfas</div></template>
</table-search>// 对象默认为true的,null这个特殊对象会给if直接过滤掉
methods: {tableSearchChange(searchParams) {if (searchParams) {// 执行查询} else {// 执行了重置,一般默认重新请求整个不带参数的列表}console.log('回调接受的表单数据: ', searchParams);}
}
代码实现
AdvancedSearch.vue
<template><div class="advance-search-wrapper"><a-form :form="form" @submit="handleSubmit"><template v-if="layoutMode === 'inline'"><a-card :bordered="bordered"><a-row :gutter="gutter"><template v-for="(item, index) in renderDataSource"><field-render:SearchGlobalOptions="SearchGlobalOptions":itemOptions="item":key="item.fieldName"v-show="index < SearchGlobalOptions.maxItem || (index >= SearchGlobalOptions.maxItem && collapsed)"/></template><a-col :style="{ width: collapsed ? '100%' : 'auto' }"><a-tooltip placement="bottom"><template slot="title"><span>执行查询</span></template><a-button type="primary" :size="SearchGlobalOptions.size" @click="handleSubmit" icon="search">查询</a-button></a-tooltip><a-tooltip placement="bottom"><template slot="title"><span>清空所有控件的值</span></template><a-button:size="SearchGlobalOptions.size"style="margin-left: 8px"@click="resetSearchForm"icon="border">重置</a-button></a-tooltip><template v-if="showCollapsedText"><a @click="togglecollapsed" style="margin-left: 8px"><a-tooltip placement="bottom"><template slot="title"><span>{{ collapsed ? '点击收起部分控件' : '点击展开所有控件' }}</span></template>{{ collapsed ? '收起' : '展开' }}<a-icon :type="collapsed ? 'up' : 'down'" /></a-tooltip></a></template><slot name="extra" /></a-col></a-row></a-card></template><template v-else><a-card :bordered="bordered"><template v-slot:title><span style="text-align:left;margin:0;">{{ title }}</span></template><template v-slot:extra><a-row type="flex" justify="start" align="middle"><slot><a-tooltip placement="bottom"><template slot="title"><span>执行查询</span></template><a-button type="primary" :size="SearchGlobalOptions.size" @click="handleSubmit" icon="search">查询</a-button></a-tooltip><a-tooltip placement="bottom"><template slot="title"><span>清空所有控件的值</span></template><a-button:size="SearchGlobalOptions.size"style="margin-left: 8px"@click="resetSearchForm"icon="border">重置</a-button></a-tooltip></slot><template v-if="showCollapsedText"><a @click="togglecollapsed" style="margin-left: 8px"><a-tooltip placement="bottom"><template slot="title"><span>{{ collapsed ? '点击收起部分控件' : '点击展开所有控件' }}</span></template>{{ collapsed ? '收起' : '展开' }}<a-icon :type="collapsed ? 'up' : 'down'" /></a-tooltip></a></template><slot name="extra" /></a-row></template><a-row :gutter="gutter"><template v-for="(item, index) in renderDataSource"><template v-if="item.type && item.fieldName"><field-render:SearchGlobalOptions="SearchGlobalOptions":itemOptions="item":key="item.fieldName"v-show="index < SearchGlobalOptions.maxItem || (index >= SearchGlobalOptions.maxItem && collapsed)"/></template></template></a-row></a-card></template></a-form></div>
</template><script>
import FieldRender from './FieldRender';
export default {name: 'AdvancedSearch',components: {FieldRender},computed: {showCollapsedText() {// 显示展开搜索和收缩的判定return this.renderDataSource.length > this.maxItem;},SearchGlobalOptions() {// 全局配置return {maxItem: this.maxItem,size: this.size,immediate: this.immediate,responsive: this.responsive};},renderDataSource() {// 重组传入的数据,合并全局配置,子项的配置优先全局return this.dataSource.map(item => ({ ...this.SearchGlobalOptions, ...item }));},layoutMode() {// 展示模式优化if (this.layout) return this.layout;if (this.maxItem > this.dataSource.length) {return 'inline';} else {return 'card';}}},props: {layout: {//搜索区域的布局type: String,default: ''},bordered: {// 是否显示边框type: Boolean,default: false},datetimeTotimeStamp: {// 是否把时间控件的返回值全部转为时间戳type: Boolean,default: false},maxItem: {// 超过多少个折叠type: Number,default: 4},gutter: {// 控件的间距type: Number,default: 48},size: {// 控件的尺寸type: String,default: 'default'},responsive: {type: Object,default: function() {return {xxl: 6,xl: 8,md: 12,sm: 24};}},title: {type: String,default: '搜索条件区域'},dataSource: {// 数据源type: Array,default: function() {return [{type: 'text', // 控件类型labelText: '控件名称', // 控件显示的文本fieldName: 'formField1',placeholder: '文本输入区域' // 默认控件的空值文本},{labelText: '数字输入框',type: 'number',fieldName: 'formField2',placeholder: '这只是一个数字的文本输入框'},{labelText: '单选框',type: 'radio',fieldName: 'formField3',defaultValue: '0',options: [{label: '选项1',value: '0'},{label: '选项2',value: '1'}]},{labelText: '日期选择',type: 'datetime',fieldName: 'formField4',placeholder: '选择日期'},{labelText: '日期范围',type: 'datetimeRange',fieldName: 'formField5',placeholder: ['开始日期', '选择日期']},{labelText: '下拉框',type: 'select',fieldName: 'formField7',placeholder: '下拉选择你要的',options: [{label: 'text1',value: '0'},{label: 'text2',value: '1'}]},{labelText: '联动',type: 'cascader',fieldName: 'formField6',placeholder: '级联选择',options: [{value: 'zhejiang',label: 'Zhejiang',children: [{value: 'hangzhou',label: 'Hangzhou',children: [{value: 'xihu',label: 'West Lake'},{value: 'xiasha',label: 'Xia Sha',disabled: true}]}]},{value: 'jiangsu',label: 'Jiangsu',children: [{value: 'nanjing',label: 'Nanjing',children: [{value: 'zhonghuamen',label: 'Zhong Hua men'}]}]}]}];}}},data() {return {// 高级搜索 展开/关闭collapsed: false};},beforeCreate() {this.form = this.$form.createForm(this);},methods: {togglecollapsed() {this.collapsed = !this.collapsed;},handleParams(obj) {// 判断必须为objif (!(Object.prototype.toString.call(obj) === '[object Object]')) {return {};}let tempObj = {};for (let [key, value] of Object.entries(obj)) {if (Array.isArray(value) && value.length <= 0) continue;if (Object.prototype.toString.call(value) === '[object Function]') continue;if (this.datetimeTotimeStamp) {// 若是为true,则转为时间戳if (Object.prototype.toString.call(value) === '[object Object]' && value._isAMomentObject) {// 判断momentvalue = value.valueOf();}if (Array.isArray(value) && value[0]._isAMomentObject && value[1]._isAMomentObject) {// 判断momentvalue = value.map(item => item.valueOf());}}// 若是为字符串则清除两边空格if (value && typeof value === 'string') {value = value.trim();}tempObj[key] = value;}return tempObj;},handleSubmit(e) {// 触发表单提交,也就是搜索按钮e.preventDefault();this.form.validateFields((err, values) => {if (!err) {if (this.$listeners.callBackFormat && typeof this.$listeners.callBackFormat === 'function') {let formatData = this.$listeners.callBackFormat(values);this.$emit('change', formatData);} else {const queryParams = this.handleParams(values);this.$emit('change', queryParams);}}});},resetSearchForm() {// 重置整个查询表单this.form.resetFields();this.$emit('change', null);}}
};
</script><style lang="scss">
.advance-search-wrapper {.ant-form-item {display: flex;margin-bottom: 12px !important;margin-right: 0;.ant-form-item-control-wrapper {flex: 1;display: inline-block;vertical-align: middle;}> .ant-form-item-label {line-height: 32px;padding-right: 8px;width: auto;}.ant-form-item-control {height: 32px;line-height: 32px;display: flex;justify-content: flex-start;align-items: center;.ant-form-item-children {min-width: 160px;}}}.table-page-search-submitButtons {display: block;margin-bottom: 24px;white-space: nowrap;}
}
</style>
FieldRender.vue(渲染对应控件)
<template><a-col v-bind="fieldOptions.responsive" v-if="fieldOptions.fieldName && fieldOptions.type === 'text'"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-input:size="fieldOptions.size ? fieldOptions.size : 'default'"v-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' }]":placeholder="fieldOptions.placeholder"/></a-form-item></a-col><a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-selectstyle="width: 100%"showSearch:filterOption="selectFilterOption":size="fieldOptions.size ? fieldOptions.size : 'default'"allowClearv-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined }]":placeholder="fieldOptions.placeholder"><template v-for="(item, index) in fieldOptions.options"><a-select-option :value="item.value" :key="index">{{ item.label }}</a-select-option></template></a-select></a-form-item></a-col><a-col v-else-if="fieldOptions.fieldName && fieldOptions.type === 'number'" v-bind="fieldOptions.responsive"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-input-number:size="fieldOptions.size ? fieldOptions.size : 'default'":min="fieldOptions.min ? fieldOptions.min : 1"style="width: 100%"v-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' }]":placeholder="fieldOptions.placeholder"/></a-form-item></a-col><a-colv-bind="fieldOptions.responsive"v-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-radio-group:size="fieldOptions.size ? fieldOptions.size : 'default'"buttonStyle="solid"v-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '' }]"><template v-for="(item, index) in fieldOptions.options"><a-radio-button :key="index" :value="item.value">{{ item.label }} </a-radio-button></template></a-radio-group></a-form-item></a-col><a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-date-picker:size="fieldOptions.size ? fieldOptions.size : 'default'":placeholder="fieldOptions.placeholder"v-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null }]"/></a-form-item></a-col><a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-range-picker:size="fieldOptions.size ? fieldOptions.size : 'default'"v-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null }]":placeholder="fieldOptions.placeholder"/></a-form-item></a-col><a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'cascader'"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-cascader:size="fieldOptions.size ? fieldOptions.size : 'default'":options="fieldOptions.options":showSearch="{ cascaderFilter }"v-decorator="[fieldOptions.fieldName,{ initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [] }]":placeholder="fieldOptions.placeholder"/></a-form-item></a-col><a-col v-bind="fieldOptions.responsive" v-else-if="fieldOptions.fieldName && fieldOptions.type === 'slider'"><a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="fieldOptions.labelText"><a-slider:min="1"range:marks="fieldOptions.marks":tipFormatter="e => e * (fieldOptions.baseMultiple ? fieldOptions.baseMultiple : 500)"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [0, 0]}]"/></a-form-item></a-col>
</template><script>
export default {computed: {fieldOptions() {if (this.itemOptions.baseMultiple) {return {marks: {0: 0,1: this.itemOptions.baseMultiple,100: this.itemOptions.baseMultiple * 100},...this.itemOptions};}return this.itemOptions;}},props: {itemOptions: {// 控件的基本参数type: Object,default: function() {return {type: 'text', // 控件类型defaultValue: '', // 默认值label: '控件名称', // 控件显示的文本value: '', // 控件的值responsive: {md: 8,sm: 24},size: '', // 控件大小placeholder: '' // 默认控件的空值文本};}}},data() {return {labelCol: { span: 6 },wrapperCol: { span: 18 }};},methods: {selectFilterOption(input, option) {// 下拉框过滤函数return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0;},cascaderFilter(inputValue, path) {// 级联过滤函数return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1);}}
};
</script>
总结
到这类一个中规中矩的查询组件就实现了,有什么不对之处请留言,会及时修正。
还有一些功能没有拓展进去,比如任意控件触发回调,更丰富的组件支持,类似导出功能。
具体业务具体分析,有兴趣的可以自行拓展,谢谢阅读。
Vue 2.x折腾记 - (16) 基于Ant Design Vue 封装一个配置式的表单搜索组件相关推荐
- Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件
前言 写了个类似上篇搜索的封装,但是要考虑的东西更多. 具体业务比展示的代码要复杂,篇幅太长就不引入了. 效果图 2019-04-25 添加了下拉多选的渲染,并搜索默认过滤文本而非值 简化了渲染的子组 ...
- vue将每个路由打包成html,Ant Design Vue pro 动态路由的实现和打包
Ant Design Vue pro 动态路由的实现和打包 Ant Design Vue pro 动态路由的实现和打包 配置路由权限 在config文件夹下router.config.js中配置路由权 ...
- 基于Ant Design vue框架登录demo
我们直接进入正题吧~~~ 先来看下效果图 那么前端代码呢~~~ 不着急,这就双手奉上哈~~ <a-col :span="12"><div class=" ...
- 基于Ant Design vue框架之三 删除功能细分
我们还是老规矩,先上效果图吧~~ 需要看整个页面的小盆友可以点下面这个路径哈~~ 页面路径:总页面展示 继续上干货吧~翠花,上代码~~ <a-button type="danger&q ...
- 基于Ant Design of Vue实现时长组件 duration
最近遇到一个需求,需要一个输入时长的组件,在经过一番寻找后没有合适的,最终自己动手写一个(实现了v-model双向绑定),记录一下,也给小伙伴们提供一个方便. 本示例基于ant design of v ...
- 基于 Vue3.0 和 Ant Design Vue ,高颜值管理后台UI框架vue-vben-admin运行
简介 Vue Vben Admin 是一个免费开源的中后台模版.使用了最新的vue3,vite2,TypeScript等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考. Github地址 ...
- 【Ant Design Vue】之Grid栅格和Space间距
文章目录 Grid 栅格 Space 间距 Grid 栅格 Ant Design Vue 将整个设计建议区域按照 24 等分的原则进行划分,划分之后的信息区块我们称之为『盒子』.建议横向排列的盒子数量 ...
- Element UI, Ant Design Vue
1. 对比 框架名称 组件数量 单元测试率 admin项目 维护团队 GitHub Star数(2019/10/16) 原型设计素材 Element UI 46 81% vue-element-adm ...
- ant design vue table 高度自适应_很受欢迎的vue前端UI框架
最近在逛各大网站,论坛,SegmentFault等编程问答社区,发现Vue.js异常火爆,重复性的提问和内容也很多,小编自己也趁着这个大前端的热潮,着手学习了一段时间的Vue.js,目前用它正在做自己 ...
最新文章
- 技术人生:与其鸟宿檐下,不如击翅风雨
- mysql double 20_MySQL教程20-小数类型
- Java堆和栈的区别
- IsDlgButtonChecked()
- 【Python基础】Python基础语法14个知识点大串讲
- 智能车大赛信标组_第十五届全国大学生智能汽车竞赛在南京信息工程大学圆满闭幕...
- python控制灯_Python 控制树莓派 GPIO 输出:控制 LED 灯
- 网页上无缝滚动的实现
- C++各种文件的作用
- (最新版 易卷/自动出题平台)自动阅卷系统 | 自动阅卷机 | 网络阅卷系统
- javascript监听输入框_js与jquery实时监听输入框值的oninput与onpropertychange方法
- 数字IC四大岗位分析
- OpenCV与图像算法笔记
- 特斯拉召回43万辆国产车/ 苹果头显最早明年发布/ 网易将在暴雪游戏停运后退款… 今日更多新鲜事在此...
- python实现马科维茨模型的资本市场线_均值方差模型与资本市场线
- 用户行为分析的基本概览和常用名词解释
- e2e 测试 出现的错误
- 电池极耳尺寸视觉检测系统
- 华为手机刷屏老显示服务器出错,华为手机刷机出现update exception emmc is readonly解决方法...
- 【MPC5744P】劳特巴赫调试器Trace32的使用方法