前言

使用 vue2.x 结合 draggable 拖拽插件 实现组件自由拖拽!

注意事项:

  1. 本案例中,内置组件[折叠框、表单、图表、表格,树状下拉]这些组件都是本地封装好的,使用过程中,组件列表项需要配置自己所内置的组件!

效果预览

功能介绍

  1. 组件自由拖拽
  2. 组件效果预览
  3. 生成vue模板

    生成vue模板 可以根据 提供文件路径,在本地开发目录下生成对应vue文件,此方式仅支持本地开发模式,线上模式暂不支持!
    原因: 线上模式为打包后的代码,无法在具体路径下生成对应 vue 模板文件!
    线上模板生成: 需要借助 file-saver该插件 并以保存的形式 在本地生成文件, 此方式是在本地生成文件并不是在开发者本地生成!

插件

插件名称插件版本插件描述安装
vuedraggable2.24.3拖拽插件npm install --save vuedraggable@2.24.3
file-saver2.0.5文件保存下载至本地插件npm install --save file-saver@2.0.5
js-beautify1.14.7代码美化插件,包含 html css jsnpm install --save js-beautify@1.14.7
vue2-ace-editor0.0.15代码预览插件npm install --save vue2-ace-editor@0.0.15

目录结构

文件目录功能描述
page-designer为 总目录, 可以理解为 @/views/page-designer
component-items左侧 组件列表项
draggable-area中测 组件拖拽区域
setting-attr-panel右测 组件面板属性设置
component该组件内部,提供页面设计器所需的公用组件,例如,组件面板,设置多个属性时用到的ConfigMultipleAttr组件!
template包含vue 模板 以及 组合 模板 中使用到的 utils 工具模块!
config包含 组件 配置项, 这里 可以 配置你的内置组件,以及 其他可被 is 属性所渲染 的组件项!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
page-designer
├── component
│ ├── ConfigMultipleAttr.vue
│ └── dialog
│ ├── PreviewCodeDialog.vue
│ ├── PreviewPageDialog.vue
│ └── RenderComponent.vue
├── component-items
│ └── index.vue
├── config
│ ├── componentConfig.js
│ ├── index.js
│ └── panelMoreAttrConfig.js
├── draggable-area
│ ├── index.vue
│ └── RenderComponents
│ └── index.vue
├── index.vue
├── setting-attr-panel
│ ├── components
│ │ ├── SettingFormItems.vue
│ │ └── SettingPaginationTable.vue
│ └── index.vue
└── template
├── index.js
└── utils.js

开发阶段

安装插件

  1. 拖拽插件 vuedraggable
    1
    npm install --save vuedraggable@2.24.3
  2. 文件保存 file-saver
    1
    npm install --save file-saver@2.0.5
  3. 代码美化 js-beautify
    1
    npm install --save js-beautify@1.14.7
  4. 代码预览 vue2-ace-editor
    1
    npm install --save vue2-ace-editor@0.0.15

创建组件配置文件

@/views/page-designer/config 文件夹,其内容包含如下:
componentConfig.js : 代码内容包含了组件列表项配置,包括每个组件中所绑定的面板属性!
panelMoreAttrConfig.js : 此文件内容,是针对封装后paginationTable 以及 m-form组件设置的,包含表格配置多列,表单包含多个表单项!
index.js : 此文件 是将两个 js 作为总站 导出的index.js文件!

1
2
3
4
5
6
7
8
import componentConfig from "./componentConfig";
import formConfig from './panelMoreAttrConfig'


export default {
componentConfig,
formConfig
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
export let formConfigItems = [
{
name: '表单',
componentName: 'el-form',
children: [],
attributePanes: [
{ model: {}, propModel: 'model' },
{ disabled: false, panelLabel: '禁用表单', componentType: 'switch', propModel: 'disabled' },
{
labelPosition: '',
panelLabel: '表单域标签的位置',
componentType: 'select',
propModel: 'labelPosition',
childrenComponentName: 'el-option',
options: [
{ label: '右', value: 'right' },
{ label: '左', value: 'left' },
{ label: '上', value: 'top' },
]
},
{ statusIcon: false, panelLabel: '是否在输入框中显示校验结果反馈图标', componentType: 'switch', propModel: 'statusIcon' },
{ labelWidth: '120px', panelLabel: '表单域标签的宽度', componentType: 'input', propModel: 'labelWidth' },
{ labelSuffix: ':', panelLabel: '表单域标签的后缀', componentType: 'input', propModel: 'labelSuffix' }
/*{ labelWidth: '120px', label: 'labelWidth', type: 'input', propModel: 'labelWidth' },
{ labelSuffix: ':', label: 'labelSuffix', type: 'input', propModel: 'labelSuffix' },
{ disabled: false, label: '禁用表单', type: 'switch', propModel: 'disabled' },
{ formConfigItems: [], propModel: 'formConfigItems' },
{ formData: {}, propModel: 'formData' },*/
]
},
{
name: '表单项',
componentName: 'el-form-item',
children: [],
attributePanes: [
{ prop: '', panelLabel: '校验表单属性', componentType: 'input', propModel: 'prop' },
{ label: '', panelLabel: '表单项前缀', componentType: 'input', propModel: 'label' },
/*{ labelWidth: '120px', label: 'labelWidth', type: 'input', propModel: 'labelWidth' },
{ labelSuffix: ':', label: 'labelSuffix', type: 'input', propModel: 'labelSuffix' },
{ disabled: false, label: '禁用表单', type: 'switch', propModel: 'disabled' },
{ formConfigItems: [], propModel: 'formConfigItems' },
{ formData: {}, propModel: 'formData' },*/
]
},
{
span: 6,
labelName: '输入框',
componentName: 'el-input',
attributePanes: [
{ prop: '', propModel: 'prop' },
{ placeholder: '请输入', panelLabel: 'placeholder', componentType: 'input', propModel: 'placeholder' },
{ disabled: false, panelLabel: '禁用', componentType: 'switch', propModel: 'disabled' },
{ clearable: false, panelLabel: '是否清空', componentType: 'switch', propModel: 'clearable' },
{ showPassword: false, panelLabel: '是否显示密码', componentType: 'switch', propModel: 'showPassword' },
{ suffixIcon: '', panelLabel: '后缀图标', componentType: 'input', propModel: 'suffixIcon' },
{ prefixIcon: '', panelLabel: '前缀图标', componentType: 'input', propModel: 'prefixIcon' },
],
/*attributes: {
placeholder: '',
disabled: false,
clearable: true,
showPassword: false,
suffixIcon: "",
prefixIcon: "",
type: "",
rows: '', // number
autosize: { minRows: '', maxRows: '' }, // number
event: { eventType: '', eventName: '' }
}*/
},
{
span: 6,
labelName: '日期',
componentName: 'el-date-picker',
prop: '',
attributes: {
type: 'date',
valueFormat: '',
format: '',
rangeSeparator: "",
startPlaceholder: "",
endPlaceholder: "",
clearable: true,
pickerOptions: {
disabledDate(time) {
// console.log('time', time)
return '';
},
},
event: { eventType: '', eventName: '' }
}
},
{
span: 6,
labelName: '下拉框',
componentName: 'el-select',
prop: '',
attributes: {
clearable: true,
filterable: true,
childrenComponentName: 'el-option',
options: [
{ label: '测试1', value: '1'},
{ label: '测试2', value: '2'},
],
event: { eventType: '', eventName: '' }
}
},
{
span: 6,
labelName: '单选按钮',
componentName: 'el-radio',
prop: '',
attributes: {
label: ''
}
},
{
span: 6,
labelName: '单选按钮组',
componentName: 'el-radio-group',
prop: 'radioGroup',
attributes: {
childrenComponentName: 'el-radio-button',
options: [
{ label: '1', value: '单选1' },
{ label: '2', value: '单选2' }
]
}
},
{
span: 6,
labelName: '复选框',
componentName: 'el-checkbox',
prop: '',
attributes: {
label: ''
}
},
{
span: 6,
labelName: '复选框组',
componentName: 'el-checkbox-group',
prop: '',
attributes: {
childrenComponentName: 'el-checkbox',
options: [{ label: '' }]
}
},
{
span: 6,
labelName: '开关',
componentName: 'el-switch',
prop: '',
attributes: {
disabled: false,
activeColor: '', // 激活背景色
inactiveColor: '', // 关闭背景色
activeValue: '', // 激活的默认值
inactiveValue: '' // 关闭的默认值
}
},
{
span: 6,
labelName: '树状下拉',
componentName: 'tree-select',
prop: '',
asyncOptions: () => [],
attributes: {
disabled: false,
options: []
}
}
]

export const getFormDefaultConfigItems = ( requireItems = []) => {
if ( !requireItems.length ) return formConfigItems
return formConfigItems.filter(component => requireItems.includes(component.componentName.split('-')[1]))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { formConfigItems } from "@/components/Form/config";
/*
* children 表示 各个组件
* attributePanes 表示 各个组件 所需要 绑定的 prop 属性值, 以及 右侧 所展示的 属性面板
* 例如:
* { headerTitle: '', label: '标题', type: 'input', propModel: 'headerTitle' }
* headerTitle : 该 属性 所属与 collapse 内置组件所需要的 prop 属性值,也就是 显示 title 标题的 地方, 该属性值 可以 设置默认值
* label: 该属性为 属性面板 右侧所需的配置项 , 如 标题
* type: 该属性为 属性面板 右侧所需的配置项 , 如 你所期望 通过 什么方式[input, select, input-number...] 去修改 预览区域内所对应的组件属性
* propModel: 该属性 用于绑定 想要 修改预览区域内 对应组件中的 prop 属性,如果不设置该属性,则右侧 预览面板 设置 不会响应对应到组件属性身上
* { formData: 'formData', propModel: 'formData' }, propModel 定义的 value 与 formData属性相对应的,这样在属性面板中修改属性后,
* 才能影响到 formData 所需要绑定到组件上的属性值
* returnType: 该属性是 组件上所绑定的 属性值的类型,如 el-col中的 :span属性,所需要的类型为 Number, 但属性面板中 当修改了 span属性值后,
* 该属性类型 为 String ,则需要将其转换到 Number类型
* */
export default {
componentItems: [
{
classifyCode: 'formControl',
classifyName: '表单组件',
children: formConfigItems,
},
{
classifyCode: 'elementUi',
classifyName: 'element-ui 内置组件',
children: [
{
name: '按钮',
componentName: 'el-button',
attributePanes: [
{
type: 'primary',
panelLabel: '主题风格',
componentType: 'select',
propModel: 'type',
childrenComponentName: 'el-option',
options: [
{ value: 'primary', label: '蓝色'},
{ value: 'warning', label: '黄色'},
{ value: 'danger', label: '红色'},
{ value: 'success', label: '绿色'},
{ value: 'info', label: '灰色'},
{ value: 'plain', label: '朴素'},
]
},
{
size: 'small',
panelLabel: '大小',
componentType: 'select',
propModel: 'size',
childrenComponentName: 'el-option',
options: [
{ value: 'medium', label: '中等按钮'},
{ value: 'small', label: '小型按钮'},
{ value: 'mini', label: '超小按钮'},
]
},
{
round: false,
panelLabel: '是否圆角',
componentType: 'switch',
propModel: 'round',
},
{
text: '按钮',
panelLabel: '按钮文本',
componentType: 'input',
propModel: 'text',
}
]
}
]
},
{
classifyCode: 'builtIn',
classifyName: '内置组件',
children: [
{
name: '表单',
componentName: 'm-form',
attributePanes: [
{ labelWidth: '120px', panelLabel: 'labelWidth', componentType: 'input', propModel: 'labelWidth' },
{ labelSuffix: ':', panelLabel: 'labelSuffix', componentType: 'input', propModel: 'labelSuffix' },
{ disabled: false, panelLabel: '禁用表单', componentType: 'switch', propModel: 'disabled' },
{ formConfigItems: [], propModel: 'formConfigItems' },
{ formData: {}, propModel: 'formData' },
{ panelLabel: '批量新增表单项', componentType: 'button', eventType: 'click', btnName: '批量生成表单项', attributes: { type: 'primary' }},
]
},
{
name: '折叠框',
componentName: 'collapse',
children: [],
attributePanes: [
{ headerTitle: '', panelLabel: '标题', componentType: 'input', propModel: 'headerTitle' }
]
},
{
name: '表格',
componentName: 'pagination-table',
attributePanes: [
{ data: [], propModel: 'data' },
{ columns: [], propModel: 'columns' },
{ actions: [], propModel: 'actions' },
{ height: '300px', panelLabel: '表格高度', componentType: 'input', propModel: 'height'},
{ pagination: true, panelLabel: '是否分页', componentType: 'switch', propModel: 'pagination'},
{ multiSelectable: false, panelLabel: '是否多选', componentType: 'switch', propModel: 'multiSelectable'},
{ showIndex: false, panelLabel: '是否显示序号', componentType: 'switch', propModel: 'showIndex'},
]
},
{
name: '图表',
componentName: 'echarts',
attributePanes: [
{ enableThemeTransfer: false, panelLabel: '是否切换主题', componentType: 'switch', propModel: 'enableThemeTransfer' },
{ title: 'echart title', panelLabel: '标题', componentType: 'input', propModel: 'title' },
{ onClickChart: (value)=>{}, propModel: 'onClickChart'},
{
chartOptions: [{ type: 'bar'}],
componentType: 'select',
panelLabel: '图表类型',
propModel: 'chartOptions',
childrenComponentName: 'el-option',
options: [
{ value: 'bar', label: '柱状图', type: 'bar'},
{ value: 'pie', label: '饼图', type: 'pie'},
{ value: 'line', label: '线型图', type: 'line'},
{ value: 'gauge', label: '仪表盘', type: 'gauge'},
],
// el select value-key 属性,用于绑定 对象, 选择后 返回 对象 例如 选择 'bar' 返回 { value: 'bar', label: '柱状图', type: 'bar'},
attributes: {
valueKey: 'type'
},
returnType: 'Array',
},
{ panelLabel: '', componentType: 'button', eventType: 'click', btnName: '配置Options', attributes: { type: 'primary' }},
]
}
]
},

{
classifyCode: 'layout',
classifyName: '栅格布局',
children: [
{
name: '行',
componentName: 'el-row',
children: [],
attributePanes: [
{ gutter: 20 , panelLabel: '行', componentType: 'input', propModel: 'gutter', returnType: 'Number'},
]
},
{
name: '列',
componentName: 'el-col',
children: [],
attributePanes: [
{ span: 6 , panelLabel: '列', componentType: 'input-number', propModel: 'span', returnType: 'Number' }
]
}
]
}
]
}

panelMoreAttrConfig 包含 表单 表格 更多属性 面板配置!

panelMoreAttrConfig 这里是 针对自定义内置组件 tableform 来配置的,比如columnform 中的 多个字段展示!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
const FORM_COMPONENT_TYPES = {
'el-input': '输入框',
'el-select': '下拉框',
'el-checkbox': '复选框',
'el-radio': '单选框',
'el-radio-group': '单选框组',
'el-date-picker': '日期',
'el-checkbox-group': '复选框组',
'el-switch': '开关',
'tree-select': '树状下拉',
}
/*
* 内置封装 m-form 表单组件
* 配置更多的表单项
* */
const formConfigItems = [
{
span: 24,
componentName: 'el-input',
labelName: 'span',
prop: 'span',
},
{
span: 24,
componentName: 'el-input',
labelName: 'Label',
prop: 'labelName',
},
{
span: 24,
componentName: 'el-input',
labelName: 'model',
prop: 'prop',
},
{
span: 24,
componentName: 'el-select',
labelName: '组件类型',
prop: 'componentName',
attributes: {
childrenComponentName: 'el-option',
options: [
{ value: 'el-input', label: '文本输入框' },
{ value: 'el-select', label: '下拉框' },
{ value: 'el-switch', label: '开关' },
{ value: 'el-date-picker', label: '日期' },
{ value: 'tree-select', label: '树状下拉' },
],
event: { eventType: 'change', eventName: 'changeComponent' }
}
}
]

/*
* 内置组件 paginationTable 表格组件
* 配置更多的列 columns
* */
const paginationTable = {
columns: [
{
span: 24,
componentName: 'el-input',
labelName: '列名称',
prop: 'label',
attributes: {
event: { eventType: 'blur', eventName: 'handleBlur' },
}
},
{
span: 24,
componentName: 'el-input',
labelName: 'prop',
prop: 'prop',
attributes: {
event: { eventType: 'blur', eventName: 'handleBlur' },
}
},
{
span: 24,
componentName: 'el-input',
labelName: '列的宽度',
prop: 'width',
attributes: {
event: { eventType: 'blur', eventName: 'handleBlur' },
}
},
{
span: 24,
componentName: 'el-select',
labelName: '位置排列',
prop: 'align',
attributes: {
childrenComponentName: 'el-option',
options: [
{ value : 'left', label: '居左'},
{ value : 'center', label: '居中'},
{ value : 'right', label: '居右'},
],
event: { eventType: 'change', eventName: 'handleBlur' },
}
},
{
span: 24,
componentName: 'el-switch',
labelName: '是否编辑',
prop: 'editable',
attributes: {
event: { eventType: 'change', eventName: 'handleBlur' },
}
},
],
actions: [
{
span: 24,
componentName: 'el-input',
labelName: '事件名称',
prop: 'action',
attributes: {
event: { eventType: 'blur', eventName: 'handleBlur' },
}
},
{
span: 24,
componentName: 'el-input',
labelName: '按钮名称',
prop: 'name',
attributes: {
event: { eventType: 'blur', eventName: 'handleBlur' },
}
},
{
span: 24,
componentName: 'el-input',
labelName: '禁用标识',
prop: 'disabledFlag',
attributes: {
event: { eventType: 'blur', eventName: 'handleBlur' },
}
}
]
}
export default {
FORM_COMPONENT_TYPES,
formConfigItems,
paginationTable
}

创建组件项列表

component-items/index.vue

注意:


代码中用到utils相关代码可以在这找 JS常用的工具函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class = "left-container">
<!-- <el-divider content-position="component-items">表单组件</el-divider>-->
<div v-for="(item, index) in schemas.componentItems" :key="`${item}-${index}`">
<div class = "form-component-list">
<el-divider content-position="left"> {{ item.classifyName }} </el-divider>
<draggable v-model="item.children"
:forceFallback="true"
animation="300"
drag-class="dragClass"
ghost-class="ghost"
chosen-class="chosen"
:options="{ group:{name: 'site',pull: 'clone', put: false }, sort: false}"
@start="onStart"
@end="onEnd"
:move="onMove"
:clone="cloneData"
>
<transition-group>
<div v-for="(component, index) in item.children" :key="`${component}-${index}`" class="component-item">
{{ componentName(component) }}
</div>
</transition-group>
</draggable>
</div>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<script>
import { formConfigItems } from "@/components/Form/config";
import draggable from 'vuedraggable'
import configItems from '../config'
import { clone, getUUID } from '@/utils'
import { findComponentByName } from '../template/utils'
import {mapGetters} from "vuex/dist/vuex.esm.browser";
const { FORM_COMPONENT_TYPES } = configItems.formConfig
export default {
name: "index",
props: ['selectComponent', 'draggableObject'],
components: { draggable },
computed: {
componentName(){
return ( component ) => {
return FORM_COMPONENT_TYPES[component.componentName] || component.name
}
},
...mapGetters(['designer'])
},
data() {
return {
controlOnStart: true,
formConfigItems: formConfigItems,
schemas: clone(configItems.componentConfig),
isCanDraggable: true
}
},
methods: {
// 拖拽开始
onStart({ originalEvent }) {
// 只能 让 el-form-item 存在与 el-form 子集下,其他情况下不允许拖拽!
this.controlOnStart = originalEvent.ctrlKey
let { _underlying_vm_: currentDragItem } = originalEvent.target
let { componentName } = currentDragItem
if ( componentName === 'el-form-item') this.isCanDraggable = this.validateElFormItem( componentName )
if ( componentName === 'el-col') this.isCanDraggable = this.validateElCol( componentName )
!this.isCanDraggable && componentName === 'el-form-item' && this.$message.info('表单项[el-form-item],必须有表单[el-form]作为父组件!')
!this.isCanDraggable && componentName === 'el-col' && this.$message.info('列[el-col],必须有行[el-row]作为父组件!')
},
// 拖拽结束
onEnd(e){
this.isCanDraggable = true
},
/*
* draggedContext: 被拖拽的元素
index: 被拖拽的元素的序号
element: 被拖拽的元素对应的对象
futureIndex: 预期位置、目标位置
relatedContext: 将停靠的对象
index: 目标停靠对象的序号
element: 目标的元素对应的对象
list: 目标数组
component: 将停靠的vue组件对象
* */
onMove( e, originalEvent ){
let { draggedContext, relatedContext } = e
let { component } = relatedContext
// 只能 让 el-form-item 存在与 el-form 子集下,其他情况下不允许拖拽!
let targetComponent = component.getChildrenNodes() && Array.from(component.getChildrenNodes())[0].key
// console.log('draggedContext', draggedContext, relatedContext, originalEvent, )
// console.log('component.getChildrenNodes()', component.getChildrenNodes())
console.log('draggedContext', draggedContext)
let { element: { componentName } } = draggedContext
if ( componentName === 'el-form-item' && this.isCanDraggable ) {
// console.log('draggedContext', componentName, targetComponent)
return (targetComponent && targetComponent.indexOf('el-form') === -1);
}
if ( componentName === 'el-col' && this.isCanDraggable ) {
// console.log('draggedContext', this.isCanDraggable, componentName, targetComponent)
return (targetComponent && targetComponent.indexOf('el-row') === -1);
}
// console.log(draggedContext, relatedContext, componentName, this.isCanDraggable )
this.$emit('update:draggableObject', { draggedContext, relatedContext })
return this.isCanDraggable
},
// validate el-form-item
// 只能 让 el-form-item 存在与 el-form 子集下,其他情况下不允许拖拽!
validateElFormItem(componentName = ''){
if ( componentName === 'el-form-item' ) {
let { configItems } = this.designer.schemas
let elForm = findComponentByName(configItems, 'el-form', [])
// console.log('currentComponent', elForm)
if ( !elForm.length ) {
// this.$message.warning('表单项[el-form-item],必须有表单[el-form]作为父组件!')
return false;
}
// let elFormItem = findComponentByName(configItems, 'el-form-item', [])
// console.log('elFormItem', elFormItem)
return true;
}
return true;
},
// validate el-col 是否出现在 el-row子集下
validateElCol(componentName = ''){
console.log('componentName', componentName)
if ( componentName === 'el-col' ) {
let { configItems } = this.designer.schemas
let elRow = findComponentByName(configItems, 'el-row', [])
// console.log('currentComponent', elRow)
if ( !elRow.length ) {
// this.$message.warning('表单项[el-form-item],必须有表单[el-form]作为父组件!')
return false;
}
// let elFormItem = findComponentByName(configItems, 'el-form-item', [])
// console.log('elFormItem', elFormItem)
return true;
}
return true;
},
cloneData( component ){
let componentData = {
...clone(component),
id: getUUID()
}
this.$emit('update:selectComponent', componentData)
return componentData
},
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<style lang="scss" scoped>
.left-container{
//width: 23%;
height: 100vh;
margin-right: 5px;
border-radius: 5px;
//background-color: #fff;
overflow: scroll;
.form-component-list{
margin: 0;
padding: 0;
//list-style: none;
/deep/.dragClass {
background-color: blue !important;
}

.chosen {
background-color: #000 !important;
color: #fff;
}

.ghost {
background-color: red !important;
}
span{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
div.component-item {
width: 65px;
line-height: 25px;
margin: 5px 5px;
text-align: center;
background-color: #f1f2f3;
border-radius: 10px;
font-size: 12px;
&:hover {
background-color: #979797;
color: #fff;
cursor: move;
}
}
}
}
}
</style>

创建拖拽区域

拖拽区域主界面 draggable-area > index.vue
组件渲染组件 draggable-area > RenderComponents > index.vue

index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
<template>
<div class = "center-container">
<div class = "center-top">
<div class = "left-operator"></div>
<div class = "right-operator">
<el-button type="danger" icon="el-icon-delete" @click="clearPanel">清空</el-button>
<el-button type="success" icon="el-icon-view" @click="handlePreview">预览</el-button>
<el-button type="primary" icon="el-icon-view" @click="handleViewCode">代码</el-button>
<!-- <el-button type="primary" :loading="saveLoading" @click="save">保存</el-button>-->
</div>
</div>

<!-- 拖拽区域 -->
<div class = "center-bottom">
<render-components :configItems.sync="schemas.configItems" ref="renderComponentRef" />
</div>

<!-- 预览 -->
<preview-dialog ref="previewDialogRef" :schemas="schemas"/>

<!-- 代码 -->
<preview-code-dialog ref="previewCodeDialogRef" />
</div>
</template>

<script>
import draggable from 'vuedraggable'
import PreviewCodeDialog from '../component/dialog/PreviewCodeDialog'
import PreviewDialog from '../component/dialog/PreviewPageDialog'
import RenderComponents from './RenderComponents'
import { mapGetters } from "vuex/dist/vuex.esm.browser";

export default {
name: "index",
props: ['selectComponent', 'draggableObject'],
components: { RenderComponents, draggable, PreviewDialog, PreviewCodeDialog },
data() {
return {
saveLoading: false,
currentItem: {},
// 拖拽组件的集合配置项
schemas: {
configItems: []
},
}
},
computed: {
...mapGetters(['designer'])
},
watch: {
'designer.selectComponent': {
handler(val){
if (!val || !val.attributes) return
this.settingAttribute(val)
},
deep: true
},
'schemas': {
handler(val){
if ( !val.configItems.length ) return
this.$store.commit('SETTING_SCHEMAS', this.schemas.configItems)
},
deep: true
}
},
methods: {
onAdd( e ){
/*
* element 拖拽的元素
* index 被拖拽的 index
* futureIndex 预期要拖拽的位置
* */
let { element, futureIndex, index } = this.draggableObject.draggedContext
this.schemas.configItems[futureIndex].children = []
},
clearPanel(){
this.schemas.configItems = []
this.$store.commit('SETTING_SCHEMAS', [])
this.$store.commit('HAS_REMOVE_CURRENT_ITEM', true)
this.$refs['renderComponentRef'].clearPanel()
this.$emit('clearNullRightPanel')
},
// 预览界面
handlePreview(){
this.$nextTick(() => this.$refs['previewDialogRef'].open())
},
// 预览代码
handleViewCode(){
this.$refs['previewCodeDialogRef'].open()
},
// 保存
save(){
this.saveLoading = true
setTimeout(() =>{
this.$message.success('保存成功!')
this.saveLoading = false
}, 1000)
},
settingAttribute( { attributes, id } ) {
let currentComponent = this.findComponentById( this.schemas.configItems, id, [] )[0]
if ( currentComponent && currentComponent.attributes ) {
currentComponent.attributes = attributes
}
},
findComponentById( configItems, id, result ) {
if ( !id ) return null
function findComponent(configItems, id){
let components = configItems.filter(item => {
if ( item.id === id ) {
return true
}
if ( item.children && item.children.length ) {
findComponent( item.children, id)
}
})
if ( components.length ) result = components
}
findComponent(configItems, id)
return result
}
},
mounted() {
this.clearPanel()
}
}
</script>

<style lang="scss" scoped>
/deep/.el-col{
min-height: 50px;
}
.center-container{
//width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
.center-top{
display: flex;
justify-content: space-between;
line-height: 45px;
margin-bottom: 5px;
//background-color: #fff;
.left-operator{

}
.right-operator{
text-align: right;
}
}
.center-bottom{
position: relative;
height: 100vh;
overflow-y: scroll;
span{
position: absolute;
display: block;
width: 100%;
div.config-item{
position: relative;
border: 2px dotted #979797;
padding: 15px;
&:active{
border: 2px dotted #1890FF;
}
.label{
position: absolute;
top: 0;
left: 0;
padding: 5px;
background-color: rgba( 245, 222, 179, .5);
font-size: 12px;
font-weight: bold;
}
.operator{
position: absolute;
bottom: 0;
right: 0;
.el-button{
margin-left: 2px;
}
/deep/.el-button--mini, .el-button--mini.is-round{
padding: 2px 8px;
}
}
}
div.isActive{
border: 2px dotted #1890FF;
}
}
}
.center-top,.center-bottom{
padding: 2px 3px;
//background-color: #fff;
border-radius: 5px;
//overflow-y: scroll;
}
}
</style>
RenderComponents/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
<template>
<div class = "render-component-wrap">
<draggable v-model="componentItems"
:forceFallback="true"
:group="{ name: 'site' }"
:options="{ disabled: false }"
animation="300"
dragClass="dragClass"
ghost-class="ghost"
chosen-class="chosen"
@add="dragAdd"
@choose="dragChoose"

>
<transition-group>
<div v-for="(item,index) in componentItems"
:key="`${item.componentName}-${index}`"
:data-id="item.id"
:class = "{ isActive: designer.selectComponent && designer.selectComponent.id === item.id && item.componentName !== 'el-col', 'config-item': item.componentName !== 'el-col' }"
@click.stop="handleClick(item, index)"
>
<div class = "label" v-if="designer.selectComponent && designer.selectComponent.id === item.id && item.componentName !== 'el-col'"> {{ item.componentName }} </div>
<!-- 打印测试 -->
<!-- <h2>{{ item.attributes }} {{ item.attributes.childrenComponentName }}</h2>-->

<div
:class = "{
isActive: designer.selectComponent && designer.selectComponent.id === item.id && item.componentName === 'el-col',
'child-component-item': item.componentName === 'el-col'
}"
:data-id="item.id"
:is="item.componentName"
v-bind="getComponentAttr(item.attributePanes || item.attributes)"
v-model="item.prop || ''"
>
<div class = "label" v-if="designer.selectComponent && designer.selectComponent.id === item.id && item.componentName === 'el-col'">
{{ item.componentName }}
</div>

{{ item.componentName === 'el-button' && getButtonText(item.attributePanes) || ''}}
<!-- <template v-if="item.componentName === 'collapse'" slot="button">
<el-button type="plain">collapseBtn</el-button>
</template>-->

<!-- 处理表单子组件 如 select下的 option radioGroup 下的 radio -->
<template v-if="item.attributes && item.attributes.childrenComponentName">
<component v-for="(childItem, index) in item.attributes.options"
:key="`${childItem.label}-${index}`"
:label="childItem.label"
:value="childItem.value"
:is="item.attributes.childrenComponentName"
>
</component>
</template>

<render-components
v-if="item.children"
:configItems.sync="item.children"
/>

<div class = "operator" v-if="designer.selectComponent && designer.selectComponent.id === item.id && item.componentName === 'el-col'">
<el-button type="danger" icon="el-icon-delete" size="mini" @click.stop="removeCurrentComponent( item, index )"></el-button>
<el-button type="warning" icon="el-icon-document-copy" size="mini" @click.stop="copyCurrentComponent( item, index )"></el-button>
</div>
</div>

<div class = "operator" v-if="designer.selectComponent && designer.selectComponent.id === item.id && item.componentName !== 'el-col'">
<el-button type="danger" icon="el-icon-delete" size="mini" @click.stop="removeCurrentComponent( item, index )"></el-button>
<el-button type="warning" icon="el-icon-document-copy" size="mini" @click.stop="copyCurrentComponent( item, index )"></el-button>
</div>
</div>
</transition-group>
</draggable>
</div>
</template>

<script>
import draggable from 'vuedraggable'
import { mapGetters } from "vuex";
import {clone, getUUID, isFunction, isString} from "@/utils";
export default {
name: "RenderComponents",
components: { draggable },
props: {
configItems: Array
},
data() {
return {
selectComponent: {},
formData: {},
componentItems: this.configItems,
currentAddParentId: ''
}
},
watch: {
'componentItems': {
handler( val ){
this.$emit('update:configItems', val)
},
deep: true
},
},
computed: {
...mapGetters(['designer']),
getButtonText(){
return ( attr ) => {
return attr.find(item => item.panelLabel === '按钮文本').text
}
},
// 过滤组件需要绑定的属性
getComponentAttr(){
return (attributePanes) => {
// console.log('attributePanes', attributePanes)
if ( !attributePanes || !attributePanes.length ) return []
let filterAttr = ['componentType', 'propModel', 'returnType', 'panelLabel', 'btnName', 'options']
// console.log('attributePanes', attributePanes)
let attributes = clone(attributePanes).filter(attr => attr.propModel)
let result = []
attributes.map(item => {
let filterAfter = Object.keys(item).filter(attr => !filterAttr.includes(attr))
filterAfter.map(key => {
if ( item[key] ) {
result.push({ [key]: item[key] })
}
if ( isString(item[key]) && item[key].indexOf('function') !== -1) {
// console.log('filter', key, item[key])
result.push({ [key]: new Function(item[key]) })
}
})
})
// console.log('filter result', result)
return result
}
}
},
methods: {
dragAdd(e){
let { target: { offsetParent: { dataset } } } = e
// console.log('dragAdd', e, dataset)
// this.settingParentId( relatedContext, originalEvent )
this.currentAddParentId = dataset && dataset.id
// console.log('this.currentAddParentId', this.currentAddParentId)
this.$store.commit('HAS_REMOVE_CURRENT_ITEM', false)
},
dragChoose(e){
let { target: { offsetParent: { dataset } } } = e
this.currentAddParentId = dataset && dataset.id
// console.log('dragChoose',e, this.currentAddParentId)
},
// 点击的当前组件
handleClick(currentItem, index){
// console.log('currentItem', currentItem)
// this.$emit('update:selectComponent', currentItem);
this.$store.commit('SETTING_CURRENT_COMPONENT', currentItem)
this.$store.commit('HAS_REMOVE_CURRENT_ITEM', false)
this.selectComponent = this.designer.selectComponent
},
// 移除当前组件
removeCurrentComponent( currentComponent, index ) {
this.configItems.map((parent, index) => {
if ( parent.id === currentComponent.id ) {
this.configItems.splice(index, 1)
this.$store.commit('HAS_REMOVE_CURRENT_ITEM', true)
return
}
if ( parent.children && parent.children.length ) {
parent.children.map((item, index) => {
if ( item.id === currentComponent.id ) {
parent.children.splice(index, 1)
this.$store.commit('HAS_REMOVE_CURRENT_ITEM', true)
}
})
}
})
if ( !this.configItems.length ) this.$emit('clearNullRightPanel')
},
// 复制当前组件 待开发
copyCurrentComponent( item, index ) {
this.findParentComponent(item)
},
// 寻找父级组件
findParentComponent( childrenComponent ){
let parentComponentId = this.currentAddParentId
console.log('findParentComponent parentComponentId', parentComponentId)
if (!parentComponentId) return;
let { configItems } = this.designer.schemas
if ( !configItems.length ) return
let parentComponent = null
function findParentItem(configItems, parentComponentId) {
configItems.map( item => {
if ( item.id === parentComponentId ){
parentComponent = item
}
if ( item.id !== parentComponentId && item.children && item.children.length) {
findParentItem(item.children, parentComponentId)
}
})
}
findParentItem(configItems, parentComponentId)
if ( parentComponent ) {
let { children } = parentComponent
children.length && children.push(
{
...childrenComponent,
id: getUUID()
}
)
}
console.log('parentComponent', parentComponent)
},
// 清除面板
clearPanel(){
this.componentItems = []
}
}
}
</script>

<style lang="scss" scoped>
/deep/.el-col{
min-height: 50px;
}
.center-container{
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
.center-top{
display: flex;
justify-content: space-between;
line-height: 45px;
margin-bottom: 5px;
background-color: #fff;
.left-operator{

}
.right-operator{
text-align: right;
}
}

.center-bottom{
position: relative;
height: 100vh;
.config-item{
background-color: #f1f1f1;
}
span{
display: inline-block;
width: 100%;
min-height: 38px;
//border: 1px solid red;
/deep/.component-item {
background-color: #ccc !important;
padding: 5px 20px;
display: inline-block;
border-radius: 10px;
}
.dragClass {
background-color: #1890FF !important;
}
/*.chosen {
background-color: #ccc !important;
}
.ghost {
background-color: #f1f2f3 !important;
}
.component-item{
background-color: #1890FF;
}*/
div.child-component-item,.config-item{
position: relative;
border: 2px dotted #979797;
padding: 5px 12px;
min-height: 34px;
&:active{
border: 2px dotted #1890FF;
}
.label{
position: absolute;
top: 0;
left: 0;
padding: 3px;
font-size: 12px;
font-weight: bold;
text-align: right;
background-color: rgba( 245, 222, 179, .5);
z-index: 999;
}
.operator{
position: absolute;
bottom: 0;
right: 0;
.el-button{
margin-left: 2px;
}
/deep/.el-button--mini, .el-button--mini.is-round{
padding: 2px 8px;
}
}
}
div.isActive{
border: 2px dotted #1890FF;
}
}
}

.center-top,.center-bottom{
padding: 2px 3px;
background-color: #fff;
border-radius: 5px;
//overflow-y: scroll;
}
}
</style>

创建组件面板

setting-attr-panel/index.vue
setting-attr-panel/component/SettingFormItems.vue
setting-attr-panel/component/SettingPaginationTable.vue

  1. index.vue

    index.vue
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    <template>
    <div class = "right-container">
    <el-divider content-position="left">属性面板</el-divider>
    <template v-if="componentAttr && componentAttr.length">
    <el-form ref="formRef" label-suffix=":">
    <div v-for="(attr, key) in componentAttr" :key="key">
    <el-form-item :label="attr.panelLabel" v-if="attr.componentType">
    <component
    :is="`el-${attr.componentType}`"
    v-model.trim="attr[attr.propModel || '']"
    v-on:[eventType(attr.eventType)]="handleEvent(attr)"
    v-bind="attr.attributes"
    @change="updateAttribute" clearable>
    {{ attr.btnName ? attr.btnName : '' }}

    <template v-if="attr.options && attr.options.length">
    <component v-for="(item, index) in attr.options"
    :key="`${item.label}-${index}`"
    :label="item.label"
    :value="attr.returnType !== 'Array' && item.value || item"
    :is="attr.childrenComponentName"
    >
    </component>
    </template>
    </component>
    </el-form-item>
    </div>
    </el-form>

    <!-- 配置表单项面板 -->
    <setting-form-items v-if="selectComponent && selectComponent.componentName === 'm-form'"
    ref="settingFormItemsRef"
    @generalNewFormItems="generalNewFormItems"
    />

    <!-- 配置 paginationTable属性 -->
    <setting-pagination-table v-if="selectComponent && selectComponent.componentName === 'pagination-table'"
    @generalTableAttributes="generalTableAttributes"
    />
    </template>

    <!-- <code-view ref="codeViewRef" :JsonCodeModel="formConfigItems" @onJsonSave="onJsonSave"></code-view>-->
    </div>
    </template>

    <script>
    import {clone, isObject} from '@/utils'
    import { mapGetters } from 'vuex';
    export default {
    name: "index",
    props: ['schemas'],
    components: {
    SettingFormItems: () => import(/*webpackChunkName: 'SettingFormItems'*/ './components/SettingFormItems'),
    SettingPaginationTable: () => import(/*webpackChunkName: 'SettingFormItems'*/ './components/SettingPaginationTable'),
    },
    data() {
    return {
    componentAttr: [],
    formConfigItems: [],
    updateTimer: null,
    selectComponent: this.designer && this.designer.selectComponent || null
    }
    },
    computed: {
    ...mapGetters(['designer']),
    eventType(){
    return ( type ) => type && type || ''
    }
    },
    watch: {
    // 监听当前组件选择的变化
    'designer.selectComponent': {
    handler( val ) {
    // 如果清空了拖拽面板,并且属性面板 仍有展示属性显示,则清空属性面板
    if ( !val && this.componentAttr && this.componentAttr.length ) {
    this.componentAttr = []
    console.log('designer.selectComponent is not change', val)
    return
    }
    if ( val ){
    console.log('designer.selectComponent is change', val)
    this.componentAttr = val.attributePanes ? clone( val.attributePanes ) : []
    this.selectComponent = val
    }
    },
    deep: true
    },
    'designer.hasRemoveCurrentItem': {
    handler(val) {
    if ( val ) this.componentAttr = []
    },
    deep: true
    }
    },
    methods: {
    // 同步当前组件修改属性后绑定到看板组件
    updateAttribute( val ){
    clearTimeout(this.updateTimer);
    this.updateTimer = setTimeout(() => {
    let disposeAfterAttrs = this.disposeAttrValue(this.componentAttr, [])
    // console.log('disposeAfterAttrs', this.componentAttr, val)
    // this.designer.selectComponent.attributePanes = disposeAfterAttrs
    // console.log('this.componentAttr', this.componentAttr)
    this.$set(this.designer.selectComponent, 'attributePanes', disposeAfterAttrs)
    // console.log('disposeAfterAttrs', this.designer.selectComponent, disposeAfterAttrs)
    this.$store.commit('SETTING_CURRENT_COMPONENT', this.designer.selectComponent)
    this.$emit('update', { componentAttr: disposeAfterAttrs, id: this.designer.selectComponent.id })
    }, 100);
    },
    // 处理 组件 接受 prop 绑定的 value 值的类型, 避免特殊绑定值类型不一致报错 [String, Number, Object, Array]
    disposeAttrValue( attrs = [], newAttrs = []){
    if ( !attrs && !attrs.length) return attrs
    attrs.map(item => {
    if ( item.returnType && item.returnType === 'Number' && !isNaN(item[item.propModel]) ) {
    item[item.propModel] = +item[item.propModel]
    // newAttrs.push(item)
    }
    if ( item.returnType && ['Object', 'Array'].includes(item.returnType)) {
    // item[item.propModel] = item[item.propModel]
    console.log('item[item.propModel]', item, item[item.propModel])
    // newAttrs.push(item)
    // console.log('item[item.propModel] ', item, item[item.propModel], item.propModel )
    }
    newAttrs.push(item)
    })
    console.log('newAttrs', newAttrs)
    return newAttrs.length && newAttrs || attrs
    },
    handleEvent( attr ){

    },
    /*settingFormItems( attr ){
    let { formConfigItems } = attr
    if ( !formConfigItems || !formConfigItems.length ) return []
    this.formConfigItems = formConfigItems
    this.$refs['codeViewRef'].isShowDialog = true
    },*/
    /*onJsonSave(val){
    this.formConfigItems = val
    let { attributePanes } = this.designer.selectComponent
    let findFormConfigItems = attributePanes.find(item => !!item['formConfigItems'])
    if ( findFormConfigItems ) findFormConfigItems.formConfigItems = val
    },*/
    // 生成新的表单项
    generalNewFormItems( newFormConfigItems ){
    this.formConfigItems = newFormConfigItems
    // console.log('newFormConfigItems', newFormConfigItems)
    let { attributePanes } = this.designer.selectComponent
    let findFormConfigItems = attributePanes.find(item => !!item['formConfigItems'])
    if ( findFormConfigItems ) findFormConfigItems.formConfigItems = newFormConfigItems
    // console.log('designer.selectComponent', this.designer.selectComponent)
    },
    generalTableAttributes({ attrName, attr }) {
    let { attributePanes } = this.designer.selectComponent
    let findOldAttr = attributePanes.find(item => Object.keys(item).includes(attrName))
    if ( findOldAttr ) findOldAttr[attrName] = attr
    // console.log('generalTableAttributes', findOldAttr,attrName, attr)
    }
    }
    }
    </script>

    <style lang="scss" scoped>
    .right-container{
    //width: 23%;
    height: 100vh;
    margin-left: 5px;
    padding: 5px;
    //background-color: #fff;
    border-radius: 5px;
    overflow-y: scroll;
    }
    </style>

  2. SettingFormItems.vue

    SettingFormItems
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    <template>
    <!-- 设置表单项 -->
    <div class="setting-form-item">
    <el-collapse v-model="activeNames" accordion @change="handleChangeCollapse">
    <el-collapse-item v-for="(item, key) in configItems"
    :key="`${item.formData.componentName}-${key}`"
    :name="key"
    >
    <template slot="title">
    <div class = "collapse-title">
    <div class = "title">
    <i class="el-icon-s-operation"></i>
    {{ formComponentTypes[item.formData.componentName] || '表单项' }}
    </div>
    <div class = "button">
    <el-button type="text" icon="el-icon-delete" size="mini" @click.stop="removeDomain(item, key)">删除</el-button>
    </div>
    </div>
    </template>
    <m-form :formData="item.formData"
    :formConfigItems="item.formConfigItems"
    :labelWidth="'80px'"
    @changeComponent="changeComponent"
    >
    </m-form>
    </el-collapse-item>
    </el-collapse>

    <div class = "operator" style="text-align: center">
    <el-button type="primary" icon="el-icon-plus" @click="addFormItem">添加表单项</el-button>
    </div>
    </div>
    </template>

    <script>
    import config from '../../config'
    import {getUUID} from "@/utils";
    const { FORM_COMPONENT_TYPES, formConfigItems } = config.formConfig
    export default {
    name: "SettingFormItems",
    props: {
    formItemAttr: {
    type: Array,
    default: () => []
    }
    },
    data() {
    return {
    componentName: '',
    activeNames: 0,
    configItems: [
    {
    id: getUUID(),
    formData: { span: 6 },
    formConfigItems: formConfigItems
    }
    ],
    formComponentTypes: FORM_COMPONENT_TYPES,
    // 组件对应的属性
    componentRelativeAttr: {
    'el-select': { childrenComponentName: 'el-option', options: [{ value: '01', label: '测试1'}, { value: '02', label: '测试2'}] },
    'tree-select': { options: [] },
    'el-date-picker': { type: 'date', valueFormat: 'yyyy-MM-dd', pickerOptions: {
    disabledDate(time) {
    return '';
    },
    },
    },
    }
    }
    },
    watch: {
    configItems: {
    handler( configItems ) {
    if ( !configItems.length ) {
    this.$emit('generalNewFormItems', [])
    return
    }
    let hasComponent = configItems.every(item => item.formData.componentName)
    if ( !hasComponent ) return;
    let items = this.generalFormItems( configItems )
    this.$emit('generalNewFormItems', items)
    },
    deep: true
    }
    },
    methods: {
    getUUID: getUUID,
    addFormItem(){
    this.activeNames = this.configItems.length
    this.configItems.push({
    id: getUUID(),
    formData: { span: 6 },
    formConfigItems: formConfigItems
    })
    },
    removeDomain( item, index ){
    this.configItems.splice(index, 1)
    console.log('item', this.configItems, item)
    },
    generalFormItems(){
    return this.configItems.map(item => {
    let componentAttr = this.componentRelativeAttr[item.formData.componentName] || {}
    return item.formData.attributes = { ...item.formData, span: Number(item.formData.span) || 6, attributes: componentAttr }
    })
    },
    handleChangeCollapse( item ){},
    changeComponent( selectComponent ){},
    }
    }
    </script>

    <style lang="scss" scoped>
    .operator{
    margin-top:5px;
    text-align: center
    }
    .collapse-title{
    display: flex;
    width: 100%;
    justify-content: space-between;
    .title,.button{
    margin-right: 5px;
    }
    .title{
    font-weight: bold;
    color: #43a8e8;
    }
    .button{
    .el-button--text{
    color: #d7195d !important;
    }
    }
    }
    </style>

  3. SettingPaginationTable.vue

    SettingPaginationTable
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    <template>
    <div class = "setting-pagination-table-box">
    <config-multiple-attr title="配置列"
    collapse-title="label"
    button-name="添加列"
    :config-items="columnsConfigs"
    @add-attr="addAttrConfig(columnsConfigs, 'columns')"
    @remove-attr="({ item, index }) => this.removeColumns(item, index, columnsConfigs)"
    @handleInputBlur="columnsChange"
    />
    <config-multiple-attr title="配置操作按钮"
    collapse-title="name"
    button-name="添加操作按钮"
    :config-items="actionsConfigs"
    @add-attr="addAttrConfig(actionsConfigs, 'actions')"
    @remove-attr="({ item, index }) => this.removeColumns(item, index, actionsConfigs)"
    @handleInputBlur="actionsChange"
    />
    </div>
    </template>

    <script>
    import { getUUID } from "@/utils";
    import ConfigMultipleAttr from "../../component/ConfigMultipleAttr";
    import config from '../../config'
    const { paginationTable: tableOptions } = config.formConfig
    export default {
    name: "SettingPaginationTable",
    components: { ConfigMultipleAttr },
    data() {
    return {
    columnsConfigs: [
    {
    formData: { prop: '' },
    formConfigItems: tableOptions.columns
    }
    ],
    actionsConfigs: [
    {
    formData: {},
    formConfigItems: tableOptions.actions
    }
    ],
    }
    },
    methods: {
    handleChangeCollapse(){},
    addAttrConfig( configs, attrOptions ){
    let configOptions = {
    id: getUUID(),
    formData: {},
    formConfigItems: tableOptions[attrOptions]
    }
    // paginationTable 内部 prop 属性不可缺少,即时没有,也要给空,否则内置组件报错
    attrOptions === 'columns' ? configOptions.formData.prop = '' : ''
    configs.push(configOptions)
    },
    removeColumns(item, index, configs){
    configs.splice(index, 1)
    },
    generalAttributes( configs ){
    return configs.map(item => { return { ...item.formData } })
    },
    columnsChange(e){
    let val = e.target && e.target.value || e
    if ( !val ) return
    let columnsAttr = this.generalAttributes(this.columnsConfigs)
    this.$emit('generalTableAttributes', { attrName: 'columns', attr: columnsAttr })
    },
    actionsChange(e){
    let val = e.target && e.target.value || e
    if ( !val ) return
    let actionsAttr = this.generalAttributes(this.actionsConfigs)
    this.$emit('generalTableAttributes', { attrName: 'actions', attr: actionsAttr })
    },
    }
    }
    </script>

    <style scoped>

    </style>

创建vue模板文件

/page-designer/template

这里用来作为默认的.vue文件,此文件包括,template script style,当我们在生成代码的时候,需要将最终设置好的templatescript 动态内容拼接至模板当中!
/template/index.js : 包含 .vue 的模板文件!
/template/utils.js : 动态生成 templatescript 的方法包含在内!

  1. index.js
    index
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    import { generateTemplateTag, generateVueAttrAndMethod } from './utils'
    // 代码格式化
    // const beautify = require("beautify")

    // const { html: beautifyHtml, js: beautifyJs } = require('js-beautify')
    const beautify_js = require('js-beautify');
    // const beautify_css = require('js-beautify').css;
    const beautify_html = require('js-beautify').html;

    /*
    * compTabs: 组件标签
    * responsiveData: 响应式数据,该参数会被映射到模板 data 属性当中
    * */
    const template = ( compTabs = null, responsiveData = null, responsiveMethod = null ) => `
    <template>
    <div class = "wrap-box app-container">
    ${ compTabs || '' }
    </div>
    </template>

    <script>
    import {mapGetters} from "vuex";
    import PublicModel from '@/public'

    const { businessConfig: { MeterDevice }, dictionaryTable: { enumCode }} = PublicModel
    export default {
    components: {},
    data() {
    return {
    ${ responsiveData || '' }
    }
    },
    computed: {
    ...mapGetters(['userInfo'])
    },
    methods: {
    ${ responsiveMethod || '' }
    },
    //生命周期 - 创建完成(访问当前this实例)
    created() {

    },
    //生命周期 - 挂载完成(访问DOM元素)
    mounted() {

    }
    }
    </script>
    <style lang="scss" scoped>

    </style>
    `

    // console.log('template', template)

    export const generateTemplate = ( configItems = null ) => {
    // const componentTabs = generateTemplateTag( configItems )
    // const formConfigItems = getFormConfigItems( configItems )
    // const componentTabs = beautify(generateTemplateTag( configItems ), { format: 'html' })
    console.log('configItems', configItems)
    if ( !configItems.length ) return template(componentTabs, null, null)
    const componentTabs = beautify_html(generateTemplateTag( configItems ), {
    "indent_size": "3",
    "indent_char": "\t",
    "max_preserve_newlines": "-1",
    "preserve_newlines": false,
    "keep_array_indentation": false,
    "break_chained_methods": false,
    "indent_scripts": "keep",
    "brace_style": "none",
    "space_before_conditional": true,
    "unescape_strings": false,
    "jslint_happy": false,
    "end_with_newline": false,
    "wrap_line_length": "40",
    "indent_inner_html": false,
    "comma_first": false,
    "e4x": false,
    "indent_empty_lines": false
    })

    // const responsiveData = beautify(generateVueAttrAndMethod( configItems, 'data' ), { format: 'js' })
    const responsiveData = beautify_js(generateVueAttrAndMethod( configItems, 'data' ), {
    "indent_size": "2",
    "indent_char": "\t",
    "max_preserve_newlines": "10",
    "preserve_newlines": true,
    "keep_array_indentation": false,
    "break_chained_methods": false,
    "indent_scripts": "keep",
    "brace_style": "none",
    "space_before_conditional": true,
    "unescape_strings": false,
    "jslint_happy": false,
    "end_with_newline": false,
    "wrap_line_length": "0",
    "indent_inner_html": false,
    "comma_first": false,
    "e4x": false,
    "indent_empty_lines": false
    })
    const responsiveMethod = beautify_js(generateVueAttrAndMethod( configItems, 'method' ))

    return template(componentTabs, responsiveData, responsiveMethod)
    }

  2. utils.js
    utils
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    import {isArray, isString, isObject} from '@/utils'

    // 生成 data 和 method 方法
    function generateData(data) {
    return data.map((item, index) => {
    for (const key in item) {
    // return `${key}: ${JSON.stringify(item[key], null, 2)},\n`
    return `${key}: ${JSON.stringify(item[key], null, 8)},\n\t\t`
    }
    }).join('')
    }

    // 生成模板标签
    export function generateTemplateTag(configItems) {
    const tmpl = (components) => `
    ${components.map(item => {
    if (item.componentName !== 'm-form' && item.children && item.children.length) {
    return `
    <${item.componentName} ${generateComponentAttribute(item.attributePanes)}>
    ${item.children && item.children.length ? tmpl(item.children) : ''}
    </${item.componentName}>
    `
    } else if (item.componentName === 'm-form') {
    return `
    <${item.componentName}
    ${generateComponentAttribute(item.attributePanes)}
    />
    `
    } else {
    return `
    <${item.componentName} ${generateComponentAttribute(item.attributePanes)}></${item.componentName}>
    `
    }
    }).join('')}
    `;
    return configItems && configItems.length ? tmpl(configItems) : ''
    }

    // 生成组件属性
    function generateComponentAttribute(componentAttribute = []) {
    if (!componentAttribute.length) return ''
    return componentAttribute.map(item => {
    if (item.propModel) {
    if (typeof item[item.propModel] === 'string' && item[item.propModel].indexOf('function')) return `${item.propModel}="${item[item.propModel]}"`
    if (
    isArray(item[item.propModel]) ||
    isObject(item[item.propModel]) ||
    (isString(item[item.propModel]) &&
    item[item.propModel].indexOf('function') !== -1)
    ) {
    console.log('isFunction', item, item.propModel, item[item.propModel])
    return `:${item.propModel}="${item.propModel}"`
    } else {
    return `:${item.propModel}="${item[item.propModel]}"`
    }
    }
    }).join(' \n')
    }

    // 将数组类型的数据绑定到 data 当中
    export function generateVueAttrAndMethod( configItems, transformType = 'data' ) {
    if (!configItems || !configItems.length) return []
    let data = []
    let method = []
    function findFormConfigItem(configItemList) {
    configItemList.map(item => {
    if ( item.attributePanes && item.attributePanes.length ) {
    item.attributePanes.map( attr => {
    if (attr.propModel && (isArray(attr[attr.propModel]) || isObject(attr[attr.propModel]))) {
    data.push( { [attr.propModel]: attr[attr.propModel]} )
    } else if (attr.propModel && isString(attr[attr.propModel]) && attr[attr.propModel].indexOf('function') !== -1) {
    method.push( { [attr.propModel]: attr[attr.propModel]} )
    console.log('attr.propModel', attr, attr.propModel, attr[attr.propModel],eval(attr[attr.propModel]), method)
    }
    })
    }
    if ( item.children ) findFormConfigItem(item.children)
    })
    }
    findFormConfigItem(configItems)
    /*return data.map((item, index) => {
    for (const key in item) {
    // return `${key}: ${JSON.stringify(item[key], null, 2)},\n`
    return `${key}: ${JSON.stringify(item[key], null, 8)},\n`
    }
    }).join('')*/
    // console.log('transformType', transformType, data, method)
    return transformType === 'data' && generateData(data) || generateData(method)
    }

    // 获取 m-form 配置项
    export function getFormConfigItems(configItems) {
    // console.log('configItems', configItems)
    if (!configItems || !configItems.length) return []
    let formConfigItems = []

    function findFormConfigItem(configItemList) {
    configItemList.map(item => {
    if (item.componentName === 'm-form') {
    if (item.attributePanes && item.attributePanes.length) {
    let findFormConfigItems = item.attributePanes.find(formItem => !!formItem['formConfigItems'])
    formConfigItems = findFormConfigItems && findFormConfigItems.formConfigItems
    // console.log('formConfigItemsa', formConfigItems)
    }
    } else {
    if (item.children && item.children.length) {
    findFormConfigItem(item.children)
    }
    }
    })
    }

    findFormConfigItem(configItems)
    // console.log('formConfigItemsaa', formConfigItems)
    return formConfigItems
    }

    export const findComponentByName = ( configItems, name, result ) =>{
    if ( !name ) return null
    function findComponent(configItems, name){
    let components = configItems.filter(item => {
    if ( item.componentName === name ) {
    return true
    }
    if ( item.children && item.children.length ) {
    findComponent( item.children, name)
    }
    })
    if ( components.length ) result = components
    }
    findComponent(configItems, name)
    return result
    }

创建公用组件

page-designer/component

/component/dialog/PreviewCodeDialog.vue: 代码预览
/component/dialog/PreviewPageDialog.vue: 界面预览
/component/dialog/RenderComponent.vue: 在 PreviewPageDialog 该组件中有引用,作用,就是用来渲染拖拽结果后的组件!
/component/ConfigMultipleAttr.vue : 针对 paginationTablem-form 组件 可添加多个属性配置!

  1. ConfigMultipleAttr.vue

    ConfigMultipleAttr
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    <template>
    <div class = "config-multiple-attr">
    <el-divider content-position="left"> {{ title || '配置项' }}</el-divider>
    <el-collapse v-model="activeNames" accordion @change="handleChangeCollapse">
    <el-collapse-item v-for="(item, key) in configItems"
    :key="item.id"
    :name="key"
    >
    <template slot="title">
    <div class = "collapse-title">
    <div class = "title">
    <i class="el-icon-s-operation"></i>
    {{ `${item.formData[collapseTitle] || '配置项'}-${key}`}}
    </div>
    <div class = "button">
    <el-button type="text" icon="el-icon-delete" size="mini" @click.stop="removeDomain(item, key)">删除</el-button>
    </div>
    </div>
    </template>
    <m-form :formData="item.formData"
    :formConfigItems="item.formConfigItems"
    :labelWidth="'80px'"
    @handleBlur="handleBlur"
    >
    </m-form>
    </el-collapse-item>
    </el-collapse>

    <div class = "operator" style="text-align: center">
    <el-button type="primary" icon="el-icon-plus" @click="configMoreAttr">{{ buttonName || '添加' }}</el-button>
    </div>
    </div>
    </template>

    <script>
    export default {
    name: "MultipleColumnsAttrConfig",
    props: {
    configItems: {
    type: Array,
    default: () => []
    },
    title: String,
    collapseTitle: String,
    buttonName: String,
    },
    data() {
    return {
    activeNames: 0,
    }
    },
    methods: {
    handleChangeCollapse(){},
    handleBlur(val){
    this.$emit('handleInputBlur', val)
    },
    configMoreAttr(){
    this.activeNames = this.configItems.length
    this.$emit('add-attr')
    },
    removeDomain( item, index ){
    this.$emit('remove-attr', { item, index })
    },
    }
    }
    </script>

    <style lang="scss" scoped>
    .operator{
    margin-top:5px;
    text-align: center
    }
    .collapse-title{
    display: flex;
    width: 100%;
    justify-content: space-between;
    .title,.button{
    margin-right: 5px;
    }
    .title{
    font-weight: bold;
    color: #43a8e8;
    }
    .button{
    .el-button--text{
    color: #d7195d !important;
    }
    }
    }
    </style>

  2. PreviewCodeDialog.vue

    PreviewCodeDialog
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    <template>
    <el-dialog
    title="代码预览"
    width="85%"
    top="3vh"
    :close-on-click-modal="false"
    :visible.sync="isShowDialog"
    append-to-body
    >
    <div class = "operator" style = "text-align:right; margin-bottom: 10px">
    <el-button type="primary" @click="downloadCode">生成vue文件到本地</el-button>
    </div>

    <code-editor v-model="codeSnippet"></code-editor>
    </el-dialog>
    </template>

    <script>
    import { generateTemplate } from '../../template'
    import { mapGetters } from "vuex";
    import { generateFile } from "@/api/test"
    const beautify_html = require("js-beautify").html
    // import { generateFile } from "@/utils";
    import { saveAs } from 'file-saver';
    export default {
    name: "PreviewCodeDialog",
    data() {
    return {
    isShowDialog: false,
    codeSnippet: null,
    sock: null
    }
    },
    computed: {
    ...mapGetters(['designer'])
    },
    methods: {
    open() {
    let { configItems } = this.designer.schemas
    let a = generateTemplate(configItems)
    this.codeSnippet = beautify_html(generateTemplate(configItems),{
    "indent_size": "3",
    "indent_char": "\t",
    "max_preserve_newlines": "1",
    "preserve_newlines": false,
    "keep_array_indentation": false,
    "break_chained_methods": false,
    "indent_scripts": "keep",
    "brace_style": "none",
    "space_before_conditional": true,
    "unescape_strings": false,
    "jslint_happy": false,
    "end_with_newline": false,
    "wrap_line_length": "40",
    "indent_inner_html": false,
    "comma_first": false,
    "e4x": false,
    "indent_empty_lines": false
    })
    this.isShowDialog = true
    },
    downloadCode() {
    this.$prompt('请输入要保存的文件路径', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    }).then( async ({ value }) => {
    /*let blob = new Blob([this.codeSnippet], {type: "text/javascript;charset=utf-8"})
    saveAs(blob, `${value}.vue`)*/
    console.log('value', value)
    let params = {
    templateContent: this.codeSnippet,
    fileUrl: value
    }
    let { success, data } = await generateFile( params )
    if ( success ) {
    this.$notify.success({
    title: '提示!',
    message: `操作成功,文件路径已保存在${ data.fullUrl }目录下!`
    })
    this.isShowDialog = false
    }
    }).catch((e) => {
    console.warn('保存代码至本地代码环境下,需要本地开发环境支持,并需要启动 server/index node 服务后在进行操作!')
    console.warn('此处保存,仅能保存文件至本地后,需要自己将文件放到对应的开发目录下!')
    let blob = new Blob([this.codeSnippet], {type: "text/javascript;charset=utf-8"})
    saveAs(blob, `${value}.vue`)
    });
    }
    },
    }
    </script>

    <style lang="scss" scoped>
    /deep/.el-dialog__body{
    padding: 32px 20px 50px 20px !important;
    }
    </style>

  3. PreviewPageDialog

    PreviewPageDialog
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    <template>
    <!-- 预览窗口 -->
    <el-dialog
    v-if="isShowDialog"
    class = "preview-wrapper"
    title="界面预览"
    width="75%"
    top="5vh"
    append-to-body
    :visible.sync="isShowDialog"
    :close-on-click-modal="false"
    >
    <template v-if="schemas && schemas.configItems && schemas.configItems.length">
    <render-components
    :configItems="schemas.configItems"
    />
    </template>
    </el-dialog>
    </template>

    <script>
    import RenderComponents from './RenderComponent'
    export default {
    name: "PreviewDialog",
    props: [ 'schemas' ],
    components: { RenderComponents },
    data() {
    return {
    isShowDialog: false
    }
    },
    methods: {
    open(){
    this.isShowDialog = true
    }
    }
    }
    </script>

    <style scoped>

    </style>

  4. RenderComponent

    RenderComponent
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    <template>
    <div>
    <div
    v-for="(component, key) in configItems"
    :key="component.id"
    >
    <div
    :is="component.componentName"
    :formData="formData"
    v-bind="getComponentAttr(component.attributePanes)"
    v-model="component.prop || ''"
    >
    <render-component v-if="component.children && component.children.length" :configItems="component.children" />
    </div>

    </div>
    </div>
    </template>

    <script>
    import {clone, isString} from "@/utils";

    export default {
    name: "RenderComponent",
    props: {
    configItems: {
    type: Array,
    default: () => []
    }
    },
    data() {
    return {
    formData: {}
    }
    },
    computed: {
    // 过滤组件需要绑定的属性
    getComponentAttr(){
    return (attributePanes) => {
    // console.log('attributePanes', attributePanes)
    if ( !attributePanes || !attributePanes.length ) return []
    let filterAttr = ['componentType', 'propModel', 'returnType', 'panelLabel', 'btnName', 'options']
    // console.log('attributePanes', attributePanes)
    let attributes = clone(attributePanes).filter(attr => attr.propModel)
    let result = []
    attributes.map(item => {
    let filterAfter = Object.keys(item).filter(attr => !filterAttr.includes(attr))
    filterAfter.map(key => {
    if ( item[key] ) {
    result.push({ [key]: item[key] })
    }
    if ( isString(item[key]) && item[key].indexOf('function') !== -1) {
    // console.log('filter', key, item[key])
    result.push({ [key]: new Function(item[key]) })
    }
    })
    })
    // console.log('filter result', result)
    return result
    }
    }
    }
    }
    </script>

    <style lang="scss" scoped>
    span{
    width: 100%;
    height: 100%;
    display: block;
    div.child-component-item{
    position: relative;
    border: 2px dotted #979797;
    padding: 20px;
    &:active{
    border: 2px dotted #1890FF;
    }
    .label{
    position: absolute;
    top: 0;
    left: 0;
    padding: 5px;
    font-size: 12px;
    font-weight: bold;
    text-align: right;
    background-color: rgba( 245, 222, 179, .5);
    z-index: 1;
    }
    .operator{
    position: absolute;
    bottom: 0;
    right: 0;
    .el-button{
    margin-left: 2px;
    }
    /deep/.el-button--mini, .el-button--mini.is-round{
    padding: 2px 8px;
    }
    }
    }
    div.isActive{
    border: 2px dotted #1890FF;
    }
    }
    </style>

创建 主界面

pager-designer/index.vue

index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<div class = "pager-designer">
<!-- 组件项 -->
<component-items ref="leftContainerRef"
:selectComponent.sync="selectComponent"
:draggableObject.sync="draggableObject"
/>
<!-- 拖拽区域 -->
<draggable-area ref="centerContainerRef"
:selectComponent="selectComponent"
:draggableObject="draggableObject"
:currentComponent.sync="currentComponent"
@clearNullRightPanel="clearNullRightPanel"
/>
<!-- 设置属性面板 -->
<setting-attr-panel ref="settingAttributeRef" />
<!-- <fc-designer ref="designer"/>-->
</div>
</template>

<script>
export default {
name: "index",
components: {
ComponentItems: () => import(/* webpackChunkName: 'ComponentItems*/ './component-items'),
DraggableArea: () => import(/* webpackChunkName: 'DraggableArea'*/ './draggable-area'),
SettingAttrPanel: () => import(/* webpackChunkName: 'SettingAttrPanel'*/ './setting-attr-panel')
},
data() {
return {
selectComponent: null,
draggableObject: null,
currentComponent: null,
}
},
methods: {
clearNullRightPanel(){
this.$nextTick(() => {
this.$refs['rightContainerRef'] ? this.$refs['rightContainerRef'].componentAttr = [] : ''
})
}
}
}
</script>

<style lang="scss" scoped>
.pager-designer{
display: flex;
justify-content: space-between;
height: 100vh;
.left-container, .right-container {
flex: 1;
}
.center-container{
flex: 4;
}
}
/*/deep/.el-container, /deep/.el-main{
background-color: #fff;
}*/
/*/deep/.el-aside{
width: 100%;
}*/
/deep/._fc-l-item{
min-width: 0;
}
.el-dialog{
width: 85%!important;
}
</style>

创建 server 文件

src/server/index.js: 此文件作用在开发环境下,可启动此服务,通过代码生成,将文件保存至开发目录下的目标路径!
此方式,通过nodejs在本地启动服务,通过接口监听,获取参数包括,模板内容,以及保存路径参数!

server/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const http = require('http');
const fs = require('fs');
const app = http.createServer()
const path = require('path');
const BASE_URL = `${path.dirname( __dirname)}/views/fileGenerateDir`
let fullUrl = ''
let filePath = ''
// 监听请求
app.on( 'request', (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
res.setHeader("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.setHeader('Content-type','application/json');
// 监听post 请求
req.on('data', ( postData )=>{
let requestData = JSON.parse(postData.toString())
// 注意 postData 是一个Buffer类型的数据,也就是post请求的数据存到了缓冲区
let { fileUrl, templateContent } = requestData
let url = fileUrl.substring(0, fileUrl.lastIndexOf('/')) || ''
let fileName = fileUrl.substring(fileUrl.lastIndexOf('/')+1, fileUrl.length)
// console.log('fileUrl url file', fileUrl, url, fileName)
fullUrl = path.join(BASE_URL, url)
let directory = generateDir( fullUrl, fileName, templateContent )
filePath = `${directory}${fileName}.vue`
console.log('filePath', filePath )
fs.writeFileSync(filePath, templateContent, {
encoding: 'utf8',
})
})

// 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
req.on('end',() => {
let result = JSON.stringify({ success: true, data: { fullUrl: filePath } })
res.end(result)
})
})

app.listen(3000, error => {
console.log(`server running http://localhost:3000/`);
});

// 处理文件内容
function generateDir( fileUrl ){
return fileUrl.split('\\').reduce((directories, directory) => {
directories += `${directory}\\`
if (!fs.existsSync(directories)) {
fs.mkdirSync(directories)
}
return directories
}, '')
}

创建 store 状态管理

store/module/designer.js

designer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const state = {
schemas: {},
selectComponent: {
configItems: []
},
hasRemoveCurrentItem: false
}
const mutations = {
SETTING_SCHEMAS(state, schemas) {
state.schemas.configItems = schemas
},
SETTING_CURRENT_COMPONENT(state, currentComponent) {
state.selectComponent = currentComponent
},
HAS_REMOVE_CURRENT_ITEM(state, hasRemoveCurrentItem) {
state.hasRemoveCurrentItem = hasRemoveCurrentItem
}
}
export default {
state,
mutations,
}

结束

此功能,可能会存在一些缺陷,目前还在开发阶段,平时开发不是那么频繁,偶尔会测试下功能的可用行等,并优化相关问题!