Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件
前言
写了个类似上篇搜索的封装,但是要考虑的东西更多。
具体业务比展示的代码要复杂,篇幅太长就不引入了。
效果图
2019-04-25
添加了下拉多选的渲染,并搜索默认过滤文本而非值
简化了渲染的子组件的代码
2019-04-28
- 增加了对
input type
的控制
- 增加了对
实现思路和功能
基础的功能直接配置上来渲染,而上传组件就不大合适了;
所以选择了slot
来实现,如何保证传入的form-item
的布局一致,则是拿slot-scope
我这边选型用的是vue 2.6 +
的版本,所以直接用的是最新的写法
而且作为表单组件,校验这些肯定需要考虑,所以数据的构造改造了下,
对于校验规则这些走的是antd form
用的那套,所以在传递的时候把对应的属性拍平了,
到里面再进行数据结构调整,目前部分控件样式依旧需要自己修正!!!
演示的代码用法
<form-list @change="onFormListChange"><template #field="{options}"><a-form-item label="Upload" v-bind="options"><a-uploadv-decorator="['upload',{valuePropName: 'fileList',getValueFromEvent: normFile}]"name="logo"action="/upload.do"list-type="picture"><a-button> <a-icon type="upload" /> Click to upload </a-button></a-upload></a-form-item></template></form-list>
代码
- FieldRender.vue
<template>
<a-form-item:label="fieldOptions.labelText":label-col="fieldOptions.labelCol":wrapper-col="fieldOptions.wrapperCol"
><a-inputv-if="fieldOptions.fieldName && fieldOptions.type === 'text'":size="fieldOptions.size ? fieldOptions.size : 'default'"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]":placeholder="fieldOptions.placeholder"/><a-selectv-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'"style="width: 100%"showSearch:options="fieldOptions.options":filterOption="selectFilterOption":size="fieldOptions.size ? fieldOptions.size : 'default'"allowClearv-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : undefined,rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]":placeholder="fieldOptions.placeholder"/><a-input-numberv-else-if="fieldOptions.fieldName && fieldOptions.type === '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 : '',rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]":placeholder="fieldOptions.placeholder"/><a-radio-groupv-else-if="fieldOptions.fieldName && fieldOptions.type === 'radio' && Array.isArray(fieldOptions.options)":size="fieldOptions.size ? fieldOptions.size : 'default'"buttonStyle="solid"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : '',rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]"><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-date-pickerv-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetime'":size="fieldOptions.size ? fieldOptions.size : 'default'":placeholder="fieldOptions.placeholder"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]"/><a-range-pickerv-else-if="fieldOptions.fieldName && fieldOptions.type === 'datetimeRange'":size="fieldOptions.size ? fieldOptions.size : 'default'"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]":placeholder="fieldOptions.placeholder"/><a-cascaderv-else-if="fieldOptions.fieldName && fieldOptions.type === '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-time-pickerv-else-if="fieldOptions.fieldName && fieldOptions.type === 'timepicker'"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]":size="fieldOptions.size ? fieldOptions.size : 'default'"/><a-textarea:placeholder="fieldOptions.placeholder"v-else-if="fieldOptions.fieldName && fieldOptions.type === 'textarea'"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : null,rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]":autosize="{ minRows: 6, maxRows: 24 }"/><a-selectmode="multiple":size="fieldOptions.size ? fieldOptions.size : 'default'"optionFilterProp="children"v-else-if="fieldOptions.fieldName && fieldOptions.type === 'multiple'":placeholder="fieldOptions.placeholder"style="width: 100%":options="fieldOptions.options"v-decorator="[fieldOptions.fieldName,{initialValue: fieldOptions.defaultValue ? fieldOptions.defaultValue : [],rules: Array.isArray(fieldOptions.rules) && fieldOptions.rules.length > 0 ? fieldOptions.rules : []}]"/>
</a-form-item>
</template><script>
export default {props: {fieldOptions: {// 待渲染的对象type: Object,default: function() {return {};}}
},
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>
- FormList.vue
<template><div class="form-list-wrapper"><a-form :layout="formLayout" :form="form"><template v-for="(item, index) in renderDataSource"><template v-if="item.type && item.fieldName"><field-render :fieldOptions="item" :key="item.fieldName" /></template></template><slot name="field" :options="GlobalOptions" /><a-form-item :wrapper-col="buttonItemLayout.wrapperCol"><a-tooltip placement="bottom"><template slot="title"><span>提交表单</span></template><a-button type="primary" :size="size" @click="handleSubmit">提交</a-button></a-tooltip><a-tooltip placement="bottom"><template slot="title"><span>清空所有控件的值</span></template><a-button :size="size" style="margin-left: 8px" @click="resetSearchForm">重置</a-button></a-tooltip></a-form-item></a-form></div>
</template><script>
import FieldRender from './FieldRender';
export default {name: 'FormList',components: {FieldRender},props: {formLayout: {// 表单布局type: String, // 'horizontal'|'vertical'|'inline'default: 'horizontal'},datetimeTotimeStamp: {// 是否把时间控件的返回值全部转为时间戳type: Boolean,default: false},size: {// 全局控件大小type: String,default: 'default'},responsive: {// 表单项的响应布局type: Object,default: function() {return {labelCol: { span: 5 },wrapperCol: { span: 16 }};}},dataSource: {type: Array,default: function() {return [{type: 'text', // 控件类型labelText: '控件名称', // 控件显示的文本fieldName: 'formField1',placeholder: '文本输入区域', // 默认控件的空值文本rules: [{required: true,message: '必填'}]},{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: 'timepicker',fieldName: 'formField8',placeholder: '请选择时刻(时间)'},{labelText: '文本区域',type: 'textarea',fieldName: 'formField9',placeholder: '请输入文本了内容'},{type: 'multiple',labelText: '角色',fieldName: 'role',defaultValue: [],rules: [{required: true,message: '必须选择一种角色'}],options: [{label: '系统管理员',value: '0'},{label: '风控管理员',value: '1'},{label: '催收管理员',value: '2'},{label: '催收员',value: '3'},{label: '审核员',value: '4'},{label: '财务',value: '5'}]},{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'}]}]}]}];}}},beforeCreate() {this.form = this.$form.createForm(this);},computed: {GlobalOptions() {// 全局配置return {size: this.size,...this.formItemLayout};},renderDataSource() {// 重组传入的数据,合并全局配置,子项的配置优先全局return this.dataSource.map(item => ({ ...this.GlobalOptions, ...item }));},formItemLayout() {// 更改布局项目的尺寸const { formLayout } = this;if (formLayout === 'horizontal') {return this.responsive;} else {return {};}},buttonItemLayout() {// 提交按钮布局const { formLayout } = this;return formLayout === 'horizontal'? {wrapperCol: { span: 14, offset: 4 }}: {};}},methods: {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) {console.log('处理前的表单数据', values);const queryParams = this.handleParams(values);this.$emit('change', queryParams);}});},resetSearchForm() {// 重置整个查询表单this.form.resetFields();this.$emit('change', null);}}
};
</script><style lang="scss">
.form-list-wrapper {.ant-form-inline {.ant-form-item {display: flex;margin-bottom: 12px;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>
问题
暴露的方法和搜索组件一样,@change
回来表单数据;
问题:
操作父的props
会造成死循环(在有slot
的情况下,因slot-scope
拿的是父props
经过computed
后的值)。
解决方案:
已经改用其他实现姿势,抽离成独立组件,再联动数据。
总结
antd vue
版本目前的功能复现上,还是有所欠缺,可能vue
和react
实现的机子不一致导致;
不管怎么说,不考虑极端情况下,目前这个库用起来感觉还好;
至少是可用状态,后续若有修正,会继续更新文章,谢谢阅读
Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件相关推荐
- Vue 2.x折腾记 - (16) 基于Ant Design Vue 封装一个配置式的表单搜索组件
前言 这次的后台管理系统项目选型用了Vue来作为主技术栈: 因为前段时间用过React来写过项目(用了antd),感觉棒棒的. 所以这次就排除了Element UI,而采用了Ant Design Vu ...
- 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 input change_ElementUI 不维护了?供我们选择的 Vue 组件库还有很多!
1 ElementUI 近况 根据我最近的观察,得知一些关于 ElementUI 维护人员都退去的消息,这意味着什么? 这意味着后期 ElementUI 将无人维护,就算 Vue3.0 正式版出来 E ...
- 【Ant Design Vue】之Grid栅格和Space间距
文章目录 Grid 栅格 Space 间距 Grid 栅格 Ant Design Vue 将整个设计建议区域按照 24 等分的原则进行划分,划分之后的信息区块我们称之为『盒子』.建议横向排列的盒子数量 ...
- ant design vue table 高度自适应_很受欢迎的vue前端UI框架
最近在逛各大网站,论坛,SegmentFault等编程问答社区,发现Vue.js异常火爆,重复性的提问和内容也很多,小编自己也趁着这个大前端的热潮,着手学习了一段时间的Vue.js,目前用它正在做自己 ...
最新文章
- Java 中 Comparable 和 Comparator 比较
- 预处理_不锈钢锻件预处理的必要性
- 时间序列数据库——索引用ES、聚合分析时加载数据用什么?docvalues的列存储貌似更优优势一些...
- Delphi常见的运行期Access Violation错误分析
- python通讯录管理程序的用户可行性_通讯录管理系统项目可行性分析
- jQuery 效果 - 动画
- PAT_B_1055_Java(25分)
- powerCat进行常规tcp端口转发
- 单链表的应用(电话本)
- 钉钉老版本下载3.31_钉钉3.3.1老版本官方下载-钉钉3.3.1旧版本PC版官方版-东坡下载...
- VMWare NSX安全生产和DMZ用例的详细设计指南
- SQL纯手写创建数据库到表内内容
- 【论文解读】ICLR2021 知识建模与信息抽取
- cpc专利电子申请客户端安装教程以及常见错误
- Android 使用Vector XML文件创建矢量图片资源
- PHP navicat数据搭建,navicat怎么建表
- 高精度信号链电路精密模拟器件双轨供电方案
- Android 9.0 http无法访问网络问题
- 12个开源的后台端管理系统
- 课后自主练习(递归)1059. Fj haozi medium《编程思维与实践》个人学习笔记