上一篇分析了设计器的页面布局,今天更近一步研学表单设计器核心:组件、容器实现原理
低代码研学笔记之 VForm(四) WidgetPanel 左边栏的可拖拽组件
低代码研学笔记之 VForm(五) VFormWidget 设计器容器组件
低代码研学笔记之 VForm(六) SettingPanel 组件属性和表单属性设计组件
低代码研学笔记之 VForm(七) ToolbarPanel 设计器顶部的工具栏组件
基础知识储备:
vue框架、elementui框架、vuedraggable插件。
学习关键字
vuedraggable、components、props、inject
Vue.Draggable是一款基于Sortable.js实现的vue拖拽插件。支持移动设备、拖拽和选择文本、智能滚动,可以在不同列表间拖拽、不依赖jQuery为基础、vue 2过渡动画兼容、支持撤销操作,总之是一款非常优秀的vue拖拽组件。本篇将介绍如何搭建环境及简单的例子,使用起来特别简单对被拖拽元素也没有CSS样式的特殊要求。
components: vue组件引用
props: 父级组件给子级组件传参定义
inject: 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
组件布局分析
<template>
<el-scrollbar class="side-scroll-bar" :style="{height: scrollerHeight}">
<div class="panel-container">
<!-- el标签页布局,划分组件库和模板库-->
<el-tabs v-model="firstTab" class="no-bottom-margin indent-left-margin">
<!-- 组件库-->
<el-tab-pane name="componentLib">
<span slot="label"><i class="el-icon-set-up"></i> {{i18nt('designer.componentLib')}}</span>
<!-- el布局组件手风琴-->
<el-collapse v-model="activeNames" class="widget-collapse">
<el-collapse-item name="1" :title="i18nt('designer.containerTitle')">
<!-- 初始化容器组件-->
<draggable tag="ul" :list="containers" :group="{name: 'dragGroup', pull: 'clone', put: false}"
:clone="handleContainerWidgetClone" ghost-class="ghost" :sort="false"
:move="checkContainerMove" @end="onContainerDragEnd">
<li v-for="(ctn, index) in containers" :key="index" class="container-widget-item" :title="ctn.displayName"
@dblclick="addContainerByDbClick(ctn)">
<span><svg-icon :icon-class="ctn.icon" class-name="color-svg-icon" />{{i18n2t(`designer.widgetLabel.${ctn.type}`, `extension.widgetLabel.${ctn.type}`)}}</span>
</li>
</draggable>
</el-collapse-item>
<!-- 初始化基础表单元素组件,input, selet、radio等组件-->
<el-collapse-item name="2" :title="i18nt('designer.basicFieldTitle')">
<draggable tag="ul" :list="basicFields" :group="{name: 'dragGroup', pull: 'clone', put: false}"
:move="checkFieldMove"
:clone="handleFieldWidgetClone" ghost-class="ghost" :sort="false">
<li v-for="(fld, index) in basicFields" :key="index" class="field-widget-item" :title="fld.displayName"
@dblclick="addFieldByDbClick(fld)">
<span><svg-icon :icon-class="fld.icon" class-name="color-svg-icon" />{{i18n2t(`designer.widgetLabel.${fld.type}`, `extension.widgetLabel.${fld.type}`)}}</span>
</li>
</draggable>
</el-collapse-item>
<!-- 初始化高级表单组件,如上传、图片展示等组件-->
<el-collapse-item name="3" :title="i18nt('designer.advancedFieldTitle')">
<draggable tag="ul" :list="advancedFields" :group="{name: 'dragGroup', pull: 'clone', put: false}"
:move="checkFieldMove"
:clone="handleFieldWidgetClone" ghost-class="ghost" :sort="false">
<li v-for="(fld, index) in advancedFields" :key="index" class="field-widget-item" :title="fld.displayName"
@dblclick="addFieldByDbClick(fld)">
<span><svg-icon :icon-class="fld.icon" class-name="color-svg-icon" />{{i18n2t(`designer.widgetLabel.${fld.type}`, `extension.widgetLabel.${fld.type}`)}}</span>
</li>
</draggable>
</el-collapse-item>
<!-- 初始化用户自定义组件 -->
<el-collapse-item name="4" :title="i18nt('designer.customFieldTitle')">
<draggable tag="ul" :list="customFields" :group="{name: 'dragGroup', pull: 'clone', put: false}"
:move="checkFieldMove"
:clone="handleFieldWidgetClone" ghost-class="ghost" :sort="false">
<li v-for="(fld, index) in customFields" :key="index" class="field-widget-item" :title="fld.displayName"
@dblclick="addFieldByDbClick(fld)">
<span>
<svg-icon :icon-class="fld.icon" class-name="color-svg-icon" />{{i18n2t(`designer.widgetLabel.${fld.type}`, `extension.widgetLabel.${fld.type}`)}}</span>
</li>
</draggable>
</el-collapse-item>
</el-collapse>
</el-tab-pane>
<!-- 模板库-->
<el-tab-pane v-if="showFormTemplates()" name="formLib" style="padding: 8px">
<span slot="label"><i class="el-icon-c-scale-to-original"></i> {{i18nt('designer.formLib')}}</span>
<template v-for="(ft, idx) in formTemplates">
<el-card :key="idx" :bord-style="{ padding: '0' }" shadow="hover" class="ft-card">
<el-popover placement="right" trigger="hover">
<img slot="reference" :src="ft.imgUrl" style="width: 200px">
<img :src="ft.imgUrl" style="height: 600px;width: 720px">
</el-popover>
<div class="bottom clear-fix">
<span class="ft-title">#{{idx+1}} {{ft.title}}</span>
<el-button type="text" class="right-button" @click="loadFormTemplate(ft.jsonUrl)">
{{i18nt('designer.hint.loadFormTemplate')}}</el-button>
</div>
</el-card>
</template>
</el-tab-pane>
</el-tabs>
</div>
</el-scrollbar>
</template>
依赖库分析
vue拖拽组件库;容器、表单组件、自定义组件初始化定义;新增窗口缩放处理方法;多语言库;异步请求处理库;Svg图标库;
<script>
import Draggable from 'vuedraggable'
//组件定义的核心配置文件
import {containers, basicFields, advancedFields, customFields} from "./widgetsConfig"
import {formTemplates} from './templatesConfig'
import {addWindowResizeHandler} from "@/utils/util"
import i18n from "@/utils/i18n"
import axios from "axios"
import SvgIcon from '@/components/svg-icon'
</script>
组件定义的核心配置文件,为什么是核心呢?因为左边栏的组件列表均是基于此配置数据来渲染的,与数据相对应的还有element-ui组件;vant-ui组件等;
import {containers, basicFields, advancedFields, customFields} from "./widgetsConfig"
src/components/form-designer/widget-panel/widgetsConfig.js
[
{
type: 'grid', //组件类型,宫格布局
category: 'container', //组件类别,容器
icon: 'grid', //图标
cols: [], //列
options: { // >>> 组件属性设置,是要复制属性应用到新组件的。
name: '', //组件名称,即变量名
hidden: false, //是否隐藏
gutter: 12, //栅格间隔
colHeight: null, //栅格列统一高度属性,用于解决栅格列设置响应式布局浮动后被挂住的问题!!
customClass: '', //自定义css类名
}
},
...此处省略5个
]
表单设计器属性和方法定义
export default {
name: "FieldPanel",
mixins: [i18n],
components: {
Draggable,
SvgIcon,
},
props: {
designer: Object,
},
inject: ['getBannedWidgets', 'getDesignerConfig'],
data() {
return {
designerConfig: this.getDesignerConfig(),
firstTab: 'componentLib',
scrollerHeight: 0,
activeNames: ['1', '2', '3', '4'],
containers,
basicFields,
advancedFields,
customFields,
formTemplates: formTemplates,
// ftImages: [
// {imgUrl: ftImg1},
// {imgUrl: ftImg2},
// {imgUrl: ftImg3},
// {imgUrl: ftImg4},
// {imgUrl: ftImg5},
// {imgUrl: ftImg6},
// {imgUrl: ftImg7},
// {imgUrl: ftImg8},
// ]
}
},
computed: {
//
},
mounted() {
this.loadWidgets()
this.scrollerHeight = window.innerHeight - 56 + 'px'
addWindowResizeHandler(() => {
this.$nextTick(() => {
this.scrollerHeight = window.innerHeight - 56 + 'px'
//console.log(this.scrollerHeight)
})
})
},
methods: {
//此处省略10个方法
/**
* 初始化加载组件
*/
loadWidgets() {
this.containers = this.containers.map(con => {
return {
...con,
displayName: this.i18n2t(`designer.widgetLabel.${con.type}`, `extension.widgetLabel.${con.type}`)
}
}).filter(con => {
return !con.internal && !this.isBanned(con.type)
})
this.basicFields = this.basicFields.map(fld => {
return {
...fld,
displayName: this.i18n2t(`designer.widgetLabel.${fld.type}`, `extension.widgetLabel.${fld.type}`)
}
}).filter(fld => {
return !this.isBanned(fld.type)
})
this.advancedFields = this.advancedFields.map(fld => {
return {
...fld,
displayName: this.i18n2t(`designer.widgetLabel.${fld.type}`, `extension.widgetLabel.${fld.type}`)
}
}).filter(fld => {
return !this.isBanned(fld.type)
})
this.customFields = this.customFields.map(fld => {
return {
...fld,
displayName: this.i18n2t(`designer.widgetLabel.${fld.type}`, `extension.widgetLabel.${fld.type}`)
}
}).filter(fld => {
return !this.isBanned(fld.type)
})
},
}
}