懒羊羊
2024-01-31 e57a8990ae56f657a59c435a0613c5f7a8728003
提交 | 用户 | 时间
e57a89 1 <template>
2   <div class="container">
3     <div class="left-board">
4       <div class="logo-wrapper">
5         <div class="logo">
6           <img :src="logo" alt="logo"> Form Generator
7         </div>
8       </div>
9       <el-scrollbar class="left-scrollbar">
10         <div class="components-list">
11           <div class="components-title">
12             <svg-icon icon-class="component" />输入型组件
13           </div>
14           <draggable
15             class="components-draggable"
16             :list="inputComponents"
17             :group="{ name: 'componentsGroup', pull: 'clone', put: false }"
18             :clone="cloneComponent"
19             draggable=".components-item"
20             :sort="false"
21             @end="onEnd"
22           >
23             <div
24               v-for="(element, index) in inputComponents" :key="index" class="components-item"
25               @click="addComponent(element)"
26             >
27               <div class="components-body">
28                 <svg-icon :icon-class="element.tagIcon" />
29                 {{ element.label }}
30               </div>
31             </div>
32           </draggable>
33           <div class="components-title">
34             <svg-icon icon-class="component" />选择型组件
35           </div>
36           <draggable
37             class="components-draggable"
38             :list="selectComponents"
39             :group="{ name: 'componentsGroup', pull: 'clone', put: false }"
40             :clone="cloneComponent"
41             draggable=".components-item"
42             :sort="false"
43             @end="onEnd"
44           >
45             <div
46               v-for="(element, index) in selectComponents"
47               :key="index"
48               class="components-item"
49               @click="addComponent(element)"
50             >
51               <div class="components-body">
52                 <svg-icon :icon-class="element.tagIcon" />
53                 {{ element.label }}
54               </div>
55             </div>
56           </draggable>
57           <div class="components-title">
58             <svg-icon icon-class="component" /> 布局型组件
59           </div>
60           <draggable
61             class="components-draggable" :list="layoutComponents"
62             :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
63             draggable=".components-item" :sort="false" @end="onEnd"
64           >
65             <div
66               v-for="(element, index) in layoutComponents" :key="index" class="components-item"
67               @click="addComponent(element)"
68             >
69               <div class="components-body">
70                 <svg-icon :icon-class="element.tagIcon" />
71                 {{ element.label }}
72               </div>
73             </div>
74           </draggable>
75         </div>
76       </el-scrollbar>
77     </div>
78
79     <div class="center-board">
80       <div class="action-bar">
81         <el-button icon="el-icon-download" type="text" @click="download">
82           导出vue文件
83         </el-button>
84         <el-button class="copy-btn-main" icon="el-icon-document-copy" type="text" @click="copy">
85           复制代码
86         </el-button>
87         <el-button class="delete-btn" icon="el-icon-delete" type="text" @click="empty">
88           清空
89         </el-button>
90       </div>
91       <el-scrollbar class="center-scrollbar">
92         <el-row class="center-board-row" :gutter="formConf.gutter">
93           <el-form
94             :size="formConf.size"
95             :label-position="formConf.labelPosition"
96             :disabled="formConf.disabled"
97             :label-width="formConf.labelWidth + 'px'"
98           >
99             <draggable class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup">
100               <draggable-item
101                 v-for="(element, index) in drawingList"
102                 :key="element.renderKey"
103                 :drawing-list="drawingList"
104                 :element="element"
105                 :index="index"
106                 :active-id="activeId"
107                 :form-conf="formConf"
108                 @activeItem="activeFormItem"
109                 @copyItem="drawingItemCopy"
110                 @deleteItem="drawingItemDelete"
111               />
112             </draggable>
113             <div v-show="!drawingList.length" class="empty-info">
114               从左侧拖入或点选组件进行表单设计
115             </div>
116           </el-form>
117         </el-row>
118       </el-scrollbar>
119     </div>
120
121     <right-panel
122       :active-data="activeData"
123       :form-conf="formConf"
124       :show-field="!!drawingList.length"
125       @tag-change="tagChange"
126     />
127
128     <code-type-dialog
129       :visible.sync="dialogVisible"
130       title="选择生成类型"
131       :show-file-name="showFileName"
132       @confirm="generate"
133     />
134     <input id="copyNode" type="hidden">
135   </div>
136 </template>
137
138 <script>
139 import draggable from 'vuedraggable'
140 import beautifier from 'js-beautify'
141 import ClipboardJS from 'clipboard'
142 import render from '@/utils/generator/render'
143 import RightPanel from './RightPanel'
144 import { inputComponents, selectComponents, layoutComponents, formConf } from '@/utils/generator/config'
145 import { beautifierConf, titleCase } from '@/utils/index'
146 import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
147 import { makeUpJs } from '@/utils/generator/js'
148 import { makeUpCss } from '@/utils/generator/css'
149 import drawingDefault from '@/utils/generator/drawingDefault'
150 import logo from '@/assets/logo/logo.png'
151 import CodeTypeDialog from './CodeTypeDialog'
152 import DraggableItem from './DraggableItem'
153
154 let oldActiveId
155 let tempActiveData
156
157 export default {
158   components: {
159     draggable,
160     render,
161     RightPanel,
162     CodeTypeDialog,
163     DraggableItem
164   },
165   data() {
166     return {
167       logo,
168       idGlobal: 100,
169       formConf,
170       inputComponents,
171       selectComponents,
172       layoutComponents,
173       labelWidth: 100,
174       drawingList: drawingDefault,
175       drawingData: {},
176       activeId: drawingDefault[0].formId,
177       drawerVisible: false,
178       formData: {},
179       dialogVisible: false,
180       generateConf: null,
181       showFileName: false,
182       activeData: drawingDefault[0]
183     }
184   },
185   created() {
186     // 防止 firefox 下 拖拽 会新打卡一个选项卡
187     document.body.ondrop = event => {
188       event.preventDefault()
189       event.stopPropagation()
190     }
191   },
192   watch: {
193     // eslint-disable-next-line func-names
194     'activeData.label': function (val, oldVal) {
195       if (
196         this.activeData.placeholder === undefined
197         || !this.activeData.tag
198         || oldActiveId !== this.activeId
199       ) {
200         return
201       }
202       this.activeData.placeholder = this.activeData.placeholder.replace(oldVal, '') + val
203     },
204     activeId: {
205       handler(val) {
206         oldActiveId = val
207       },
208       immediate: true
209     }
210   },
211   mounted() {
212     const clipboard = new ClipboardJS('#copyNode', {
213       text: trigger => {
214         const codeStr = this.generateCode()
215         this.$notify({
216           title: '成功',
217           message: '代码已复制到剪切板,可粘贴。',
218           type: 'success'
219         })
220         return codeStr
221       }
222     })
223     clipboard.on('error', e => {
224       this.$message.error('代码复制失败')
225     })
226   },
227   methods: {
228     activeFormItem(element) {
229       this.activeData = element
230       this.activeId = element.formId
231     },
232     onEnd(obj, a) {
233       if (obj.from !== obj.to) {
234         this.activeData = tempActiveData
235         this.activeId = this.idGlobal
236       }
237     },
238     addComponent(item) {
239       const clone = this.cloneComponent(item)
240       this.drawingList.push(clone)
241       this.activeFormItem(clone)
242     },
243     cloneComponent(origin) {
244       const clone = JSON.parse(JSON.stringify(origin))
245       clone.formId = ++this.idGlobal
246       clone.span = formConf.span
247       clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
248       if (!clone.layout) clone.layout = 'colFormItem'
249       if (clone.layout === 'colFormItem') {
250         clone.vModel = `field${this.idGlobal}`
251         clone.placeholder !== undefined && (clone.placeholder += clone.label)
252         tempActiveData = clone
253       } else if (clone.layout === 'rowFormItem') {
254         delete clone.label
255         clone.componentName = `row${this.idGlobal}`
256         clone.gutter = this.formConf.gutter
257         tempActiveData = clone
258       }
259       return tempActiveData
260     },
261     AssembleFormData() {
262       this.formData = {
263         fields: JSON.parse(JSON.stringify(this.drawingList)),
264         ...this.formConf
265       }
266     },
267     generate(data) {
268       const func = this[`exec${titleCase(this.operationType)}`]
269       this.generateConf = data
270       func && func(data)
271     },
272     execRun(data) {
273       this.AssembleFormData()
274       this.drawerVisible = true
275     },
276     execDownload(data) {
277       const codeStr = this.generateCode()
278       const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
279       this.$download.saveAs(blob, data.fileName)
280     },
281     execCopy(data) {
282       document.getElementById('copyNode').click()
283     },
284     empty() {
285       this.$confirm('确定要清空所有组件吗?', '提示', { type: 'warning' }).then(
286         () => {
287           this.drawingList = []
288         }
289       )
290     },
291     drawingItemCopy(item, parent) {
292       let clone = JSON.parse(JSON.stringify(item))
293       clone = this.createIdAndKey(clone)
294       parent.push(clone)
295       this.activeFormItem(clone)
296     },
297     createIdAndKey(item) {
298       item.formId = ++this.idGlobal
299       item.renderKey = +new Date()
300       if (item.layout === 'colFormItem') {
301         item.vModel = `field${this.idGlobal}`
302       } else if (item.layout === 'rowFormItem') {
303         item.componentName = `row${this.idGlobal}`
304       }
305       if (Array.isArray(item.children)) {
306         item.children = item.children.map(childItem => this.createIdAndKey(childItem))
307       }
308       return item
309     },
310     drawingItemDelete(index, parent) {
311       parent.splice(index, 1)
312       this.$nextTick(() => {
313         const len = this.drawingList.length
314         if (len) {
315           this.activeFormItem(this.drawingList[len - 1])
316         }
317       })
318     },
319     generateCode() {
320       const { type } = this.generateConf
321       this.AssembleFormData()
322       const script = vueScript(makeUpJs(this.formData, type))
323       const html = vueTemplate(makeUpHtml(this.formData, type))
324       const css = cssStyle(makeUpCss(this.formData))
325       return beautifier.html(html + script + css, beautifierConf.html)
326     },
327     download() {
328       this.dialogVisible = true
329       this.showFileName = true
330       this.operationType = 'download'
331     },
332     run() {
333       this.dialogVisible = true
334       this.showFileName = false
335       this.operationType = 'run'
336     },
337     copy() {
338       this.dialogVisible = true
339       this.showFileName = false
340       this.operationType = 'copy'
341     },
342     tagChange(newTag) {
343       newTag = this.cloneComponent(newTag)
344       newTag.vModel = this.activeData.vModel
345       newTag.formId = this.activeId
346       newTag.span = this.activeData.span
347       delete this.activeData.tag
348       delete this.activeData.tagIcon
349       delete this.activeData.document
350       Object.keys(newTag).forEach(key => {
351         if (this.activeData[key] !== undefined
352           && typeof this.activeData[key] === typeof newTag[key]) {
353           newTag[key] = this.activeData[key]
354         }
355       })
356       this.activeData = newTag
357       this.updateDrawingList(newTag, this.drawingList)
358     },
359     updateDrawingList(newTag, list) {
360       const index = list.findIndex(item => item.formId === this.activeId)
361       if (index > -1) {
362         list.splice(index, 1, newTag)
363       } else {
364         list.forEach(item => {
365           if (Array.isArray(item.children)) this.updateDrawingList(newTag, item.children)
366         })
367       }
368     }
369   }
370 }
371 </script>
372
373 <style lang='scss'>
374 .editor-tabs{
375   background: #121315;
376   .el-tabs__header{
377     margin: 0;
378     border-bottom-color: #121315;
379     .el-tabs__nav{
380       border-color: #121315;
381     }
382   }
383   .el-tabs__item{
384     height: 32px;
385     line-height: 32px;
386     color: #888a8e;
387     border-left: 1px solid #121315 !important;
388     background: #363636;
389     margin-right: 5px;
390     user-select: none;
391   }
392   .el-tabs__item.is-active{
393     background: #1e1e1e;
394     border-bottom-color: #1e1e1e!important;
395     color: #fff;
396   }
397   .el-icon-edit{
398     color: #f1fa8c;
399   }
400   .el-icon-document{
401     color: #a95812;
402   }
403 }
404
405 // home
406 .right-scrollbar {
407   .el-scrollbar__view {
408     padding: 12px 18px 15px 15px;
409   }
410 }
411 .left-scrollbar .el-scrollbar__wrap {
412   box-sizing: border-box;
413   overflow-x: hidden !important;
414   margin-bottom: 0 !important;
415 }
416 .center-tabs{
417   .el-tabs__header{
418     margin-bottom: 0!important;
419   }
420   .el-tabs__item{
421     width: 50%;
422     text-align: center;
423   }
424   .el-tabs__nav{
425     width: 100%;
426   }
427 }
428 .reg-item{
429   padding: 12px 6px;
430   background: #f8f8f8;
431   position: relative;
432   border-radius: 4px;
433   .close-btn{
434     position: absolute;
435     right: -6px;
436     top: -6px;
437     display: block;
438     width: 16px;
439     height: 16px;
440     line-height: 16px;
441     background: rgba(0, 0, 0, 0.2);
442     border-radius: 50%;
443     color: #fff;
444     text-align: center;
445     z-index: 1;
446     cursor: pointer;
447     font-size: 12px;
448     &:hover{
449       background: rgba(210, 23, 23, 0.5)
450     }
451   }
452   & + .reg-item{
453     margin-top: 18px;
454   }
455 }
456 .action-bar{
457   & .el-button+.el-button {
458     margin-left: 15px;
459   }
460   & i {
461     font-size: 20px;
462     vertical-align: middle;
463     position: relative;
464     top: -1px;
465   }
466 }
467
468 .custom-tree-node{
469   width: 100%;
470   font-size: 14px;
471   .node-operation{
472     float: right;
473   }
474   i[class*="el-icon"] + i[class*="el-icon"]{
475     margin-left: 6px;
476   }
477   .el-icon-plus{
478     color: #409EFF;
479   }
480   .el-icon-delete{
481     color: #157a0c;
482   }
483 }
484
485 .left-scrollbar .el-scrollbar__view{
486   overflow-x: hidden;
487 }
488
489 .el-rate{
490   display: inline-block;
491   vertical-align: text-top;
492 }
493 .el-upload__tip{
494   line-height: 1.2;
495 }
496
497 $selectedColor: #f6f7ff;
498 $lighterBlue: #409EFF;
499
500 .container {
501   position: relative;
502   width: 100%;
503   height: 100%;
504 }
505
506 .components-list {
507   padding: 8px;
508   box-sizing: border-box;
509   height: 100%;
510   .components-item {
511     display: inline-block;
512     width: 48%;
513     margin: 1%;
514     transition: transform 0ms !important;
515   }
516 }
517 .components-draggable{
518   padding-bottom: 20px;
519 }
520 .components-title{
521   font-size: 14px;
522   color: #222;
523   margin: 6px 2px;
524   .svg-icon{
525     color: #666;
526     font-size: 18px;
527   }
528 }
529
530 .components-body {
531   padding: 8px 10px;
532   background: $selectedColor;
533   font-size: 12px;
534   cursor: move;
535   border: 1px dashed $selectedColor;
536   border-radius: 3px;
537   .svg-icon{
538     color: #777;
539     font-size: 15px;
540   }
541   &:hover {
542     border: 1px dashed #787be8;
543     color: #787be8;
544     .svg-icon {
545       color: #787be8;
546     }
547   }
548 }
549
550 .left-board {
551   width: 260px;
552   position: absolute;
553   left: 0;
554   top: 0;
555   height: 100vh;
556 }
557 .left-scrollbar{
558   height: calc(100vh - 42px);
559   overflow: hidden;
560 }
561 .center-scrollbar {
562   height: calc(100vh - 42px);
563   overflow: hidden;
564   border-left: 1px solid #f1e8e8;
565   border-right: 1px solid #f1e8e8;
566   box-sizing: border-box;
567 }
568 .center-board {
569   height: 100vh;
570   width: auto;
571   margin: 0 350px 0 260px;
572   box-sizing: border-box;
573 }
574 .empty-info{
575   position: absolute;
576   top: 46%;
577   left: 0;
578   right: 0;
579   text-align: center;
580   font-size: 18px;
581   color: #ccb1ea;
582   letter-spacing: 4px;
583 }
584 .action-bar{
585   position: relative;
586   height: 42px;
587   text-align: right;
588   padding: 0 15px;
589   box-sizing: border-box;;
590   border: 1px solid #f1e8e8;
591   border-top: none;
592   border-left: none;
593   .delete-btn{
594     color: #F56C6C;
595   }
596 }
597 .logo-wrapper{
598   position: relative;
599   height: 42px;
600   background: #fff;
601   border-bottom: 1px solid #f1e8e8;
602   box-sizing: border-box;
603 }
604 .logo{
605   position: absolute;
606   left: 12px;
607   top: 6px;
608   line-height: 30px;
609   color: #00afff;
610   font-weight: 600;
611   font-size: 17px;
612   white-space: nowrap;
613   > img{
614     width: 30px;
615     height: 30px;
616     vertical-align: top;
617   }
618   .github{
619     display: inline-block;
620     vertical-align: sub;
621     margin-left: 15px;
622     > img{
623       height: 22px;
624     }
625   }
626 }
627
628 .center-board-row {
629   padding: 12px 12px 15px 12px;
630   box-sizing: border-box;
631   & > .el-form {
632     // 69 = 12+15+42
633     height: calc(100vh - 69px);
634   }
635 }
636 .drawing-board {
637   height: 100%;
638   position: relative;
639   .components-body {
640     padding: 0;
641     margin: 0;
642     font-size: 0;
643   }
644   .sortable-ghost {
645     position: relative;
646     display: block;
647     overflow: hidden;
648     &::before {
649       content: " ";
650       position: absolute;
651       left: 0;
652       right: 0;
653       top: 0;
654       height: 3px;
655       background: rgb(89, 89, 223);
656       z-index: 2;
657     }
658   }
659   .components-item.sortable-ghost {
660     width: 100%;
661     height: 60px;
662     background-color: $selectedColor;
663   }
664   .active-from-item {
665     & > .el-form-item{
666       background: $selectedColor;
667       border-radius: 6px;
668     }
669     & > .drawing-item-copy, & > .drawing-item-delete{
670       display: initial;
671     }
672     & > .component-name{
673       color: $lighterBlue;
674     }
675   }
676   .el-form-item{
677     margin-bottom: 15px;
678   }
679 }
680 .drawing-item{
681   position: relative;
682   cursor: move;
683   &.unfocus-bordered:not(.activeFromItem) > div:first-child  {
684     border: 1px dashed #ccc;
685   }
686   .el-form-item{
687     padding: 12px 10px;
688   }
689 }
690 .drawing-row-item{
691   position: relative;
692   cursor: move;
693   box-sizing: border-box;
694   border: 1px dashed #ccc;
695   border-radius: 3px;
696   padding: 0 2px;
697   margin-bottom: 15px;
698   .drawing-row-item {
699     margin-bottom: 2px;
700   }
701   .el-col{
702     margin-top: 22px;
703   }
704   .el-form-item{
705     margin-bottom: 0;
706   }
707   .drag-wrapper{
708     min-height: 80px;
709   }
710   &.active-from-item{
711     border: 1px dashed $lighterBlue;
712   }
713   .component-name{
714     position: absolute;
715     top: 0;
716     left: 0;
717     font-size: 12px;
718     color: #bbb;
719     display: inline-block;
720     padding: 0 6px;
721   }
722 }
723 .drawing-item, .drawing-row-item{
724   &:hover {
725     & > .el-form-item{
726       background: $selectedColor;
727       border-radius: 6px;
728     }
729     & > .drawing-item-copy, & > .drawing-item-delete{
730       display: initial;
731     }
732   }
733   & > .drawing-item-copy, & > .drawing-item-delete{
734     display: none;
735     position: absolute;
736     top: -10px;
737     width: 22px;
738     height: 22px;
739     line-height: 22px;
740     text-align: center;
741     border-radius: 50%;
742     font-size: 12px;
743     border: 1px solid;
744     cursor: pointer;
745     z-index: 1;
746   }
747   & > .drawing-item-copy{
748     right: 56px;
749     border-color: $lighterBlue;
750     color: $lighterBlue;
751     background: #fff;
752     &:hover{
753       background: $lighterBlue;
754       color: #fff;
755     }
756   }
757   & > .drawing-item-delete{
758     right: 24px;
759     border-color: #F56C6C;
760     color: #F56C6C;
761     background: #fff;
762     &:hover{
763       background: #F56C6C;
764       color: #fff;
765     }
766   }
767 }
768 </style>