vue2实现组件拖拽案例
前言
使用
vue2.x结合draggable拖拽插件 实现组件自由拖拽!注意事项:
- 本案例中,
内置组件[折叠框、表单、图表、表格,树状下拉]这些组件都是本地封装好的,使用过程中,组件列表项需要配置自己所内置的组件!
效果预览
功能介绍
- 组件自由拖拽
- 组件效果预览
- 生成
vue模板生成
vue模板 可以根据 提供文件路径,在本地开发目录下生成对应vue文件,此方式仅支持本地开发模式,线上模式暂不支持!原因:线上模式为打包后的代码,无法在具体路径下生成对应vue 模板文件!线上模板生成: 需要借助file-saver该插件 并以保存的形式 在本地生成文件, 此方式是在本地生成文件并不是在开发者本地生成!
插件
| 插件名称 | 插件版本 | 插件描述 | 安装 |
|---|---|---|---|
vuedraggable | 2.24.3 | 拖拽插件 | npm install --save vuedraggable@2.24.3 |
file-saver | 2.0.5 | 文件保存下载至本地插件 | npm install --save file-saver@2.0.5 |
js-beautify | 1.14.7 | 代码美化插件,包含 html css js | npm install --save js-beautify@1.14.7 |
vue2-ace-editor | 0.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 | page-designer |
开发阶段
安装插件
- 拖拽插件
vuedraggable1
npm install --save vuedraggable@2.24.3
- 文件保存
file-saver1
npm install --save file-saver@2.0.5
- 代码美化
js-beautify1
npm install --save js-beautify@1.14.7
- 代码预览
vue2-ace-editor1
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 | import componentConfig from "./componentConfig"; |
1 | export let formConfigItems = [ |
1 | import { formConfigItems } from "@/components/Form/config"; |
panelMoreAttrConfig 包含 表单 表格 更多属性 面板配置!
panelMoreAttrConfig这里是 针对自定义内置组件table和form来配置的,比如column、form中的多个字段展示!
1 | const FORM_COMPONENT_TYPES = { |
创建组件项列表
component-items/index.vue
注意:
代码中用到
utils相关代码可以在这找 JS常用的工具函数1 | <template> |
1 | <script> |
1 | <style lang="scss" scoped> |
创建拖拽区域
拖拽区域主界面
draggable-area>index.vue
组件渲染组件draggable-area>RenderComponents>index.vue
index.vue
1 | <template> |
RenderComponents/index.vue
1 | <template> |
创建组件面板
setting-attr-panel/index.vuesetting-attr-panel/component/SettingFormItems.vuesetting-attr-panel/component/SettingPaginationTable.vue
index.vueindex.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>SettingFormItems.vueSettingFormItems
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 ;
}
}
}
</style>SettingPaginationTable.vueSettingPaginationTable
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文件,此文件包括,templatescriptstyle,当我们在生成代码的时候,需要将最终设置好的template和script动态内容拼接至模板当中!/template/index.js: 包含.vue的模板文件!/template/utils.js : 动态生成template和script的方法包含在内!
index.jsindex
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
106import { 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)
}utils.jsutils
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
134import {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: 针对paginationTable和m-form组件 可添加多个属性配置!
ConfigMultipleAttr.vueConfigMultipleAttr
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 ;
}
}
}
</style>PreviewCodeDialog.vuePreviewCodeDialog
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 ;
}
</style>PreviewPageDialogPreviewPageDialog
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>RenderComponentRenderComponent
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 | <template> |
创建 server 文件
src/server/index.js: 此文件作用在开发环境下,可启动此服务,通过代码生成,将文件保存至开发目录下的目标路径!
此方式,通过nodejs在本地启动服务,通过接口监听,获取参数包括,模板内容,以及保存路径参数!
server/index.js
1 | const http = require('http'); |
创建 store 状态管理
store/module/designer.js
designer.js
1 | const state = { |
结束
此功能,可能会存在一些缺陷,目前还在开发阶段,平时开发不是那么频繁,偶尔会测试下功能的可用行等,并优化相关问题!
