在第一篇文章中提到过表单的定制性很强,需要能扩展自己的组件form-generator作者提供了二次扩展的文章,文章很全面;本文的目的快速理解form-generator组件的扩展方式.可以先看本文,如果解决不了就看下作者的文档.
本文字数较多,可以先快速预览再细看
扩展组件前必须了解组件内部的数据是如何流动的,vue程序第一步都是数据优先
每个组件都对应一个config配置项,以单行文本框为例
{
// 1. 组件配置信息
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
defaultValue: undefined,
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
// 正则校验规则
regList: []
},
// 2. 组件的插槽属性
__slot__: {
prepend: '',
append: ''
},
// 3. 直接赋值给组件的属性
placeholder: '请输入',
style: { width: '100%' },
clearable: true,
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false
},
每个表单配置项有三个部分
1和3的区别在于3上面的属性会赋值
上而1上的属性不会
让我们再看下生成后的表单项(不用细看)
{
"fields": [{
"__config__": {
"label": "单行文本",
"labelWidth": null,
"showLabel": true,
"changeTag": true,
"tag": "el-input",
"tagIcon": "input",
"defaultValue": "你好",
"required": true,
"layout": "colFormItem",
"span": 24,
"document": "https://element.eleme.cn/#/zh-CN/component/input",
"regList": [],
"formId": 101,
"renderKey": "1011693530948107"
},
"__slot__": {
"prepend": "",
"append": ""
},
"placeholder": "请输入单行文本",
"style": {
"width": "100%"
},
"clearable": true,
"prefix-icon": "",
"suffix-icon": "",
"maxlength": null,
"show-word-limit": false,
"readonly": false,
"disabled": false,
"__vModel__": "field101"
}],
"formRef": "elForm",
"formModel": "formData",
"size": "medium",
"labelPosition": "right",
"labelWidth": 100,
"formRules": "rules",
"gutter": 15,
"disabled": false,
"span": 24,
"formBtns": true
}
请注意这几个属性
{
"fields": [{
"__config__": {
// 双向绑定的值
"defaultValue": "你好",
// 绑定到组件上的key
"renderKey": "1011693530948107"
},
// 字段名
"__vModel__": "field101"
}]
}
通过上面一进一出我们知道了,form-generator在中间做的是
从上图我们知道,
首先通过点击或者拖拽的方式将config.js中的配置项转化成了唯一的表单配置项,实现了批量生产。
在修改配置项时通过两个不同的表单,渲染表单用来展示组件和修改值,编辑表单用来修改属性
这个组件是用来操配置项的属性的
<template v-if="['EditTable'].includes(activeData.__config__.tag)">
<el-divider>表格属性el-divider>
<el-form-item label-width="100px" label="表格尺寸">
<el-radio-group v-model="activeData.size" size="mini">
<el-radio-button label="medium">
默认
el-radio-button>
<el-radio-button label="small">
小号
el-radio-button>
<el-radio-button label="mini">
迷你
el-radio-button>
el-radio-group>
el-form-item>
<el-form-item label-width="100px" label="纵向边框">
<el-switch
v-model="activeData.border" size="small"
/>
el-form-item>
template>
这个组件是用来
显示组件
和操作值的
export default {
props: {
conf: {
type: Object,
required: true
}
},
components: {
EditTable
},
mounted() {
// 动态请求数据
catchData.call(this, this.conf)
},
render(h) {
const dataObject = makeDataObject()
const confClone = deepClone(this.conf)
const children = this.$slots.default || []
// 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码
mountSlotFiles.call(this, h, confClone, children)
// 将字符串类型的事件,发送为消息
emitEvents.call(this, confClone)
// 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)”
buildDataObject.call(this, confClone, dataObject)
return h(this.conf.__config__.tag, dataObject, children)
}
}
我们可以看到render.js是一个vue组件,不过不是vue文件而是通过render函数和h函数来返回虚拟DOM
h函数的具体可以看渲染函数,简单理解就是h( 标签名,标签属性,子元素 )
使用h函数根据__config__.tag
返回特定的组件
标签属性就是绑定了诸如 style、attribute、on、slot等信息的对象。这里我们主要注意on上面会绑定一个input事件
我们就是通过它来更新数据的。
假如我需要一个这样的照片墙组件
该组件和正常组件一样,只不过通过value和input更新数据
代码
<template>
<div class="pw-list">
<div v-for="(item, index) in imageList" :key="index" class="pw-item" :style="{width:width+'px',height:height+'px'}">
<el-image
:src="item"
fit="cover"
:style="{width:width+'px',height:height+'px'}"
:preview-src-list="imageList"
/>
<div class="mask">
<i class="el-icon-search" @click="(e)=>{handlePreview(index,e)}" />
<i class="el-icon-delete" @click="handleDelete(index)" />
</div>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: [Array, String],
default: () => []
},
width: {
type: [Number, String],
default: 300
},
height: {
type: [Number, String],
default: 168
}
},
emits: ['input'],
data() {
return {
}
},
computed: {
imageList() {
if (this.value instanceof Array) {
return JSON.parse(JSON.stringify(this.value))
}
return this.value.split(',')
}
},
created() {
},
methods: {
// 点击预览--该功能依赖于preview-src-list
handlePreview(index, e) {
// 获取父节点
const { parentNode } = e.target.parentNode
// 获取图片
const imgNode = parentNode.querySelector('img')
// 触发图片点击事件
imgNode.dispatchEvent(new Event('click'))
},
// 点击删除
handleDelete(index) {
this.imageList.splice(index, 1)
if (this.value instanceof Array) {
this.$emit('input', JSON.parse(JSON.stringify(this.imageList)))
} else {
this.$emit('input', this.imageList.join(','))
}
}
}
}
</script>
<style lang="scss" scoped>
.pw-list {
display: flex;
gap: 5px;
padding: 5px 5px;
flex-wrap: wrap;
border: 1px solid #eee;
}
.pw-item {
position:relative;
border-radius: 8px;
overflow:hidden;
.mask{
display:none;
position:absolute;
top:0;left:0;
width:100%;
height:100%;
background:rgba(0,0,0,.3);
color:white;
font-size:20px;
z-index:10;
}
&:hover .mask{
display:flex;
align-items: center;
justify-content: center;
gap: 15px;
}
i{
cursor:pointer;
}
}
</style>
{
__config__: {
// 标签名
label: '照片墙',
// 标签
tag: 'PhotoWall',
// icon
tagIcon: 'component',
// 默认数据
defaultValue: [
'https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832_640.jpg',
'https://cdn.pixabay.com/photo/2018/08/21/23/29/forest-3622519_640.jpg',
'https://cdn.pixabay.com/photo/2013/10/02/23/03/mountains-190055_640.jpg',
'https://cdn.pixabay.com/photo/2017/12/15/13/51/polynesia-3021072_640.jpg'
],
// 布局方式
layout: 'colFormItem',
// 布局宽度
span: 24,
// 是否显示label
showLabel: true,
// label宽度
labelWidth: null
},
// 图片宽度
width: 300,
// 图片高度
height: 168
}
如果组件非全局组件必须注册,否则无法正常渲染
import PhotoWall from '@/components/customer/PhotoWall.vue'
components: {
PhotoWall
},
这里需要修改RightPanel.vue文件了
<template v-if="['PhotoWall'].includes(activeData.__config__.tag)">
<el-form-item label="图片宽度">
<el-input v-model="activeData.width" type="number" placeholder="请输入图片宽度" size="mini" />
el-form-item>
<el-form-item label="图片高度">
<el-input v-model="activeData.height" type="number" placeholder="请输入图片高度" size="mini" />
el-form-item>
template>
到这里就结束了,如果对你有帮助就点个赞鼓励一下把~