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-upload
v-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-input
v-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-select
v-else-if="fieldOptions.fieldName && fieldOptions.type === 'select'"
style="width: 100%"
showSearch
:options="fieldOptions.options"
:filterOption="selectFilterOption"
:size="fieldOptions.size ? fieldOptions.size : 'default'"
allowClear
v-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-number
v-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-group
v-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-picker
v-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-picker
v-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-cascader
v-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-picker
v-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-select
mode="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) {
// 判断必须为obj
if (!(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) {
// 判断moment
value = value.valueOf();
}
if (Array.isArray(value) && value[0]._isAMomentObject && value[1]._isAMomentObject) {
// 判断moment
value = 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
实现的机子不一致导致;
不管怎么说,不考虑极端情况下,目前这个库用起来感觉还好;
至少是可用状态,后续若有修正,会继续更新文章,谢谢阅读
转载于:https://juejin.im/post/5cb5d89af265da03a85ab689
Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件的更多相关文章
- 基于Ant Design Vue封装一个表单控件
开源代码 https://github.com/naturefwvue/nf-vue3-ant 有缺点本来是写在最后的,但是博文写的似乎有点太长了,估计大家没时间往下看,于是就把有缺点写在前面了,不喜 ...
- 文档驱动 —— 表单组件(五):基于Ant Design Vue 的表单控件的demo,再也不需要写代码了。
源码 https://github.com/naturefwvue/nf-vue3-ant 特点 只需要更改meta,既可以切换表单 可以统一修改样式,统一升级,以最小的代价,应对UI的升级.切换,应 ...
- Ant Design Vue select下拉列表设置默认值
在项目中需要为Ant Design Vue 的 select 组件设置一个默认值,如下图所示的状态下拉选择框,默认选择全部 代码如下: <a-select v-model="query ...
- Ant Design Vue Pro 项目实战-项目初始化(一)
写在前面 时间真快,转眼又是新的一年.随着前后端技术的不断更新迭代,尤其是前端,在目前前后端分离开发模式这样的一个大环境下,交互性.兼容性等传统的开发模式已经显得有些吃力.之前一直用的是react,随 ...
- 使用ant design vue的日历组件,实现一个简单交易日与非交易日的切换
使用ant design vue的日历组件,实现一个简单交易日与非交易日的切换 需求: 日历区分交易日.非交易日 可以切换面板查看整年交易日信息 可以在手动调整交易日.非交易日 演示实例 序--使用软 ...
- 使用npm安装 Ant Design Vue 时报错—ant-design-vue@latest(sha1-qsf / gCIFcRYxyGmOKgx7TmHf1z4 =)seems to be corrupted.
安装 Ant Design Vue 时报错: npm install ant-design-vue --save ant-design-vue @ latest(sha1-qsf / gCIFcRYx ...
- Vue3学习(二)之集成Ant Design Vue
一.集成Ant Design Vue npm install ant-design-vue@2.0.0-rc.3 --save 兼容性 Ant Design Vue 2.x 支持所有的现代浏览器. 如 ...
- 从后端到前端之Vue(六)表单组件
表单组件 做项目的时候会遇到一个比较头疼的问题,一个大表单里面有好多控件,一个一个做设置太麻烦,更头疼的是,需求还总在变化,一会多选.一会单选.一会下拉的,变来变去的烦死宝宝了. 那么怎么解决这个问题 ...
- 封装Vue Element的form表单组件
前两天封装了一个基于vue和Element的table表格组件,阅读的人还是很多的,看来大家都是很认同组件化.高复用这种开发模式的,毕竟开发效率高,代码优雅,逼格高嘛.虽然这两天我的心情很糟糕,就像& ...
随机推荐
- Html 慕课园编程练习10-1
23:10:25 2019-08-14 自己写的这个好丑.... 题目:利用之前我们学过的JavaScript知识,实现选项卡切换的效果. 效果图: (另外 这个动图是怎么插入的 用url就行 复制就 ...
- python学习要点(二)
我的博客:https://www.luozhiyun.com/archives/269 '==' VS 'is' '=='操作符比较对象之间的值是否相等. 'is'操作符比较的是对象的身份标识是否相等 ...
- Golang入门(2):一天学完GO的基本语法
摘要 在配置好环境之后,要研究的就是这个语言的语法了.在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆.因为这篇文章只是入门Golang的第二 ...
- JavaScript中||和&&的运算
一般来讲 && 运算和 | | 运算得到的结果都是 true 和 false ,但是 js 中的有点不太一样.当进行 a&&b 和 a| |b (如 1&&am ...
- 世界疫情app柱形图显示
访问云服务器的mysql实现数据的获取.最后通过柱形图的形式将数据显示在页面上: 遇到的主要困难时对于云服务器的mysql连接本地的navicat之间事情,最后通过网上的各种解决办法完成了相关的内容. ...
- Python爬虫利器 cURL你用过吗?
hello,小伙伴们,今天给大家分享的开源项目是一个python爬虫利器,感兴趣的小伙伴看完这篇文章不妨去尝试一下,这个开源项目就是curlconverter,不知道小伙伴们分析完整个网站后去code ...
- Python设计模式(2)-策略模式
# 策略模式和简单工厂模式相比,少了使用switch case 做判断,然后去实例化相应的 # 对象,比简单工厂模式更灵活. 它们代码的区别就在于此处使用了抽象类代替工厂类 # coding=utf- ...
- Django-rest-framework 是个什么鬼?
作者:HelloGitHub-追梦人物 我们首先来回顾一下传统的基于模板引擎的 django 开发工作流: 绑定 URL 和视图函数.当用户访问某个 URL 时,调用绑定的视图函数进行处理. 编写视图 ...
- java异常处理:finally中不要return
java异常处理:finally中不要return 复制代码 public class Ex1 { public static void main(String[] args) { System.ou ...
- 视频图文教学 - 用最快的速度把 DotNet Core Blazor 程序安装到 树莓派中 并且用网页控制 GPIO 闪灯
前言 dotnet core 在3.0时代已经发展得很好. 尤其是在跨平台方面更已经是达到了很实用的阶段. 作为 dotnet 程序员, 应该对 Linux 有充分的了解, 也可以在业余时间玩玩硬件, ...