前言

这次的后台管理系统项目选型用了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版本一些欠缺考虑的东东(比如返回的查询对象上)

用法

就普通的引入,具体暴露的propschange如下

子项会覆盖全局带过来的同名特性,优先级比较高

选项 类型 解释
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 封装一个配置式的表单搜索组件相关推荐

  1. Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件

    前言 写了个类似上篇搜索的封装,但是要考虑的东西更多. 具体业务比展示的代码要复杂,篇幅太长就不引入了. 效果图 2019-04-25 添加了下拉多选的渲染,并搜索默认过滤文本而非值 简化了渲染的子组 ...

  2. vue将每个路由打包成html,Ant Design Vue pro 动态路由的实现和打包

    Ant Design Vue pro 动态路由的实现和打包 Ant Design Vue pro 动态路由的实现和打包 配置路由权限 在config文件夹下router.config.js中配置路由权 ...

  3. 基于Ant Design vue框架登录demo

    我们直接进入正题吧~~~ 先来看下效果图 那么前端代码呢~~~ 不着急,这就双手奉上哈~~ <a-col :span="12"><div class=" ...

  4. 基于Ant Design vue框架之三 删除功能细分

    我们还是老规矩,先上效果图吧~~ 需要看整个页面的小盆友可以点下面这个路径哈~~ 页面路径:总页面展示 继续上干货吧~翠花,上代码~~ <a-button type="danger&q ...

  5. 基于Ant Design of Vue实现时长组件 duration

    最近遇到一个需求,需要一个输入时长的组件,在经过一番寻找后没有合适的,最终自己动手写一个(实现了v-model双向绑定),记录一下,也给小伙伴们提供一个方便. 本示例基于ant design of v ...

  6. 基于 Vue3.0 和 Ant Design Vue ,高颜值管理后台UI框架vue-vben-admin运行

    简介 Vue Vben Admin 是一个免费开源的中后台模版.使用了最新的vue3,vite2,TypeScript等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考. Github地址 ...

  7. 【Ant Design Vue】之Grid栅格和Space间距

    文章目录 Grid 栅格 Space 间距 Grid 栅格 Ant Design Vue 将整个设计建议区域按照 24 等分的原则进行划分,划分之后的信息区块我们称之为『盒子』.建议横向排列的盒子数量 ...

  8. Element UI, Ant Design Vue

    1. 对比 框架名称 组件数量 单元测试率 admin项目 维护团队 GitHub Star数(2019/10/16) 原型设计素材 Element UI 46 81% vue-element-adm ...

  9. ant design vue table 高度自适应_很受欢迎的vue前端UI框架

    最近在逛各大网站,论坛,SegmentFault等编程问答社区,发现Vue.js异常火爆,重复性的提问和内容也很多,小编自己也趁着这个大前端的热潮,着手学习了一段时间的Vue.js,目前用它正在做自己 ...

最新文章

  1. 技术人生:与其鸟宿檐下,不如击翅风雨
  2. mysql double 20_MySQL教程20-小数类型
  3. Java堆和栈的区别
  4. IsDlgButtonChecked()
  5. 【Python基础】Python基础语法14个知识点大串讲
  6. 智能车大赛信标组_第十五届全国大学生智能汽车竞赛在南京信息工程大学圆满闭幕...
  7. python控制灯_Python 控制树莓派 GPIO 输出:控制 LED 灯
  8. 网页上无缝滚动的实现
  9. C++各种文件的作用
  10. (最新版 易卷/自动出题平台)自动阅卷系统 | 自动阅卷机 | 网络阅卷系统
  11. javascript监听输入框_js与jquery实时监听输入框值的oninput与onpropertychange方法
  12. 数字IC四大岗位分析
  13. OpenCV与图像算法笔记
  14. 特斯拉召回43万辆国产车/ 苹果头显最早明年发布/ 网易将在暴雪游戏停运后退款… 今日更多新鲜事在此...
  15. python实现马科维茨模型的资本市场线_均值方差模型与资本市场线
  16. 用户行为分析的基本概览和常用名词解释
  17. e2e 测试 出现的错误
  18. 电池极耳尺寸视觉检测系统
  19. 华为手机刷屏老显示服务器出错,华为手机刷机出现update exception emmc is readonly解决方法...
  20. 【MPC5744P】劳特巴赫调试器Trace32的使用方法

热门文章

  1. check the manual that corresponds to your MySQL server
  2. 大学生职业生涯发展与规划
  3. UltraEdit\UEStudio 的 SSHTelnet 功能教程
  4. openstack-M版,学习笔记六
  5. 产品运营:如何激活沉默用户
  6. HTML5及CSS3基础知识(持续更新)
  7. linux fq队列,理解fq_codel之概述
  8. 嵌入式菜单LCD简单版
  9. 续2:股票交易一点感悟和程序化交易实战
  10. 公交IC卡刷卡数据分析