大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说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://daima100.com/13399.html