<template> <div class="container"> <div class="left-board"> <div class="logo-wrapper"> <div class="logo"> <img :src="logo" alt="logo"> Form Generator </div> </div> <el-scrollbar class="left-scrollbar"> <div class="components-list"> <div class="components-title"> <svg-icon icon-class="component" />输入型组件 </div> <draggable class="components-draggable" :list="inputComponents" :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent" draggable=".components-item" :sort="false" @end="onEnd" > <div v-for="(element, index) in inputComponents" :key="index" class="components-item" @click="addComponent(element)" > <div class="components-body"> <svg-icon :icon-class="element.tagIcon" /> {{ element.label }} </div> </div> </draggable> <div class="components-title"> <svg-icon icon-class="component" />选择型组件 </div> <draggable class="components-draggable" :list="selectComponents" :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent" draggable=".components-item" :sort="false" @end="onEnd" > <div v-for="(element, index) in selectComponents" :key="index" class="components-item" @click="addComponent(element)" > <div class="components-body"> <svg-icon :icon-class="element.tagIcon" /> {{ element.label }} </div> </div> </draggable> <div class="components-title"> <svg-icon icon-class="component" /> 布局型组件 </div> <draggable class="components-draggable" :list="layoutComponents" :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent" draggable=".components-item" :sort="false" @end="onEnd" > <div v-for="(element, index) in layoutComponents" :key="index" class="components-item" @click="addComponent(element)" > <div class="components-body"> <svg-icon :icon-class="element.tagIcon" /> {{ element.label }} </div> </div> </draggable> </div> </el-scrollbar> </div> <div class="center-board"> <div class="action-bar"> <el-button icon="el-icon-download" type="text" @click="download"> 导出vue文件 </el-button> <el-button class="copy-btn-main" icon="el-icon-document-copy" type="text" @click="copy"> 复制代码 </el-button> <el-button class="delete-btn" icon="el-icon-delete" type="text" @click="empty"> 清空 </el-button> </div> <el-scrollbar class="center-scrollbar"> <el-row class="center-board-row" :gutter="formConf.gutter"> <el-form :size="formConf.size" :label-position="formConf.labelPosition" :disabled="formConf.disabled" :label-width="formConf.labelWidth + 'px'" > <draggable class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup"> <draggable-item v-for="(element, index) in drawingList" :key="element.renderKey" :drawing-list="drawingList" :element="element" :index="index" :active-id="activeId" :form-conf="formConf" @activeItem="activeFormItem" @copyItem="drawingItemCopy" @deleteItem="drawingItemDelete" /> </draggable> <div v-show="!drawingList.length" class="empty-info"> 从左侧拖入或点选组件进行表单设计 </div> </el-form> </el-row> </el-scrollbar> </div> <right-panel :active-data="activeData" :form-conf="formConf" :show-field="!!drawingList.length" @tag-change="tagChange" /> <code-type-dialog :visible.sync="dialogVisible" title="选择生成类型" :show-file-name="showFileName" @confirm="generate" /> <input id="copyNode" type="hidden"> </div> </template> <script> import draggable from 'vuedraggable' import { saveAs } from 'file-saver' import beautifier from 'js-beautify' import ClipboardJS from 'clipboard' import render from '@/utils/generator/render' import RightPanel from './RightPanel' import { inputComponents, selectComponents, layoutComponents, formConf } from '@/utils/generator/config' import { exportDefault, beautifierConf, isNumberStr, titleCase } from '@/utils/index' import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html' import { makeUpJs } from '@/utils/generator/js' import { makeUpCss } from '@/utils/generator/css' import drawingDefalut from '@/utils/generator/drawingDefalut' import logo from '@/assets/logo/logo.png' import CodeTypeDialog from './CodeTypeDialog' import DraggableItem from './DraggableItem' const emptyActiveData = { style: {}, autosize: {} } let oldActiveId let tempActiveData export default { components: { draggable, render, RightPanel, CodeTypeDialog, DraggableItem }, data() { return { logo, idGlobal: 100, formConf, inputComponents, selectComponents, layoutComponents, labelWidth: 100, drawingList: drawingDefalut, drawingData: {}, activeId: drawingDefalut[0].formId, drawerVisible: false, formData: {}, dialogVisible: false, generateConf: null, showFileName: false, activeData: drawingDefalut[0] } }, created() { // 防止 firefox 下 拖拽 会新打卡一个选项卡 document.body.ondrop = event => { event.preventDefault() event.stopPropagation() } }, watch: { // eslint-disable-next-line func-names 'activeData.label': function (val, oldVal) { if ( this.activeData.placeholder === undefined || !this.activeData.tag || oldActiveId !== this.activeId ) { return } this.activeData.placeholder = this.activeData.placeholder.replace(oldVal, '') + val }, activeId: { handler(val) { oldActiveId = val }, immediate: true } }, mounted() { const clipboard = new ClipboardJS('#copyNode', { text: trigger => { const codeStr = this.generateCode() this.$notify({ title: '成功', message: '代码已复制到剪切板,可粘贴。', type: 'success' }) return codeStr } }) clipboard.on('error', e => { this.$message.error('代码复制失败') }) }, methods: { activeFormItem(element) { this.activeData = element this.activeId = element.formId }, onEnd(obj, a) { if (obj.from !== obj.to) { this.activeData = tempActiveData this.activeId = this.idGlobal } }, addComponent(item) { const clone = this.cloneComponent(item) this.drawingList.push(clone) this.activeFormItem(clone) }, cloneComponent(origin) { const clone = JSON.parse(JSON.stringify(origin)) clone.formId = ++this.idGlobal clone.span = formConf.span clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件 if (!clone.layout) clone.layout = 'colFormItem' if (clone.layout === 'colFormItem') { clone.vModel = `field${this.idGlobal}` clone.placeholder !== undefined && (clone.placeholder += clone.label) tempActiveData = clone } else if (clone.layout === 'rowFormItem') { delete clone.label clone.componentName = `row${this.idGlobal}` clone.gutter = this.formConf.gutter tempActiveData = clone } return tempActiveData }, AssembleFormData() { this.formData = { fields: JSON.parse(JSON.stringify(this.drawingList)), ...this.formConf } }, generate(data) { const func = this[`exec${titleCase(this.operationType)}`] this.generateConf = data func && func(data) }, execRun(data) { this.AssembleFormData() this.drawerVisible = true }, execDownload(data) { const codeStr = this.generateCode() const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' }) saveAs(blob, data.fileName) }, execCopy(data) { document.getElementById('copyNode').click() }, empty() { this.$confirm('确定要清空所有组件吗?', '提示', { type: 'warning' }).then( () => { this.drawingList = [] } ) }, drawingItemCopy(item, parent) { let clone = JSON.parse(JSON.stringify(item)) clone = this.createIdAndKey(clone) parent.push(clone) this.activeFormItem(clone) }, createIdAndKey(item) { item.formId = ++this.idGlobal item.renderKey = +new Date() if (item.layout === 'colFormItem') { item.vModel = `field${this.idGlobal}` } else if (item.layout === 'rowFormItem') { item.componentName = `row${this.idGlobal}` } if (Array.isArray(item.children)) { item.children = item.children.map(childItem => this.createIdAndKey(childItem)) } return item }, drawingItemDelete(index, parent) { parent.splice(index, 1) this.$nextTick(() => { const len = this.drawingList.length if (len) { this.activeFormItem(this.drawingList[len - 1]) } }) }, generateCode() { const { type } = this.generateConf this.AssembleFormData() const script = vueScript(makeUpJs(this.formData, type)) const html = vueTemplate(makeUpHtml(this.formData, type)) const css = cssStyle(makeUpCss(this.formData)) return beautifier.html(html + script + css, beautifierConf.html) }, download() { this.dialogVisible = true this.showFileName = true this.operationType = 'download' }, run() { this.dialogVisible = true this.showFileName = false this.operationType = 'run' }, copy() { this.dialogVisible = true this.showFileName = false this.operationType = 'copy' }, tagChange(newTag) { newTag = this.cloneComponent(newTag) newTag.vModel = this.activeData.vModel newTag.formId = this.activeId newTag.span = this.activeData.span delete this.activeData.tag delete this.activeData.tagIcon delete this.activeData.document Object.keys(newTag).forEach(key => { if (this.activeData[key] !== undefined && typeof this.activeData[key] === typeof newTag[key]) { newTag[key] = this.activeData[key] } }) this.activeData = newTag this.updateDrawingList(newTag, this.drawingList) }, updateDrawingList(newTag, list) { const index = list.findIndex(item => item.formId === this.activeId) if (index > -1) { list.splice(index, 1, newTag) } else { list.forEach(item => { if (Array.isArray(item.children)) this.updateDrawingList(newTag, item.children) }) } } } } </script> <style lang='scss'> body, html{ margin: 0; padding: 0; background: #fff; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; } input, textarea{ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; } .editor-tabs{ background: #121315; .el-tabs__header{ margin: 0; border-bottom-color: #121315; .el-tabs__nav{ border-color: #121315; } } .el-tabs__item{ height: 32px; line-height: 32px; color: #888a8e; border-left: 1px solid #121315 !important; background: #363636; margin-right: 5px; user-select: none; } .el-tabs__item.is-active{ background: #1e1e1e; border-bottom-color: #1e1e1e!important; color: #fff; } .el-icon-edit{ color: #f1fa8c; } .el-icon-document{ color: #a95812; } } // home .right-scrollbar { .el-scrollbar__view { padding: 12px 18px 15px 15px; } } .left-scrollbar .el-scrollbar__wrap { box-sizing: border-box; overflow-x: hidden !important; margin-bottom: 0 !important; } .center-tabs{ .el-tabs__header{ margin-bottom: 0!important; } .el-tabs__item{ width: 50%; text-align: center; } .el-tabs__nav{ width: 100%; } } .reg-item{ padding: 12px 6px; background: #f8f8f8; position: relative; border-radius: 4px; .close-btn{ position: absolute; right: -6px; top: -6px; display: block; width: 16px; height: 16px; line-height: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 50%; color: #fff; text-align: center; z-index: 1; cursor: pointer; font-size: 12px; &:hover{ background: rgba(210, 23, 23, 0.5) } } & + .reg-item{ margin-top: 18px; } } .action-bar{ & .el-button+.el-button { margin-left: 15px; } & i { font-size: 20px; vertical-align: middle; position: relative; top: -1px; } } .custom-tree-node{ width: 100%; font-size: 14px; .node-operation{ float: right; } i[class*="el-icon"] + i[class*="el-icon"]{ margin-left: 6px; } .el-icon-plus{ color: #409EFF; } .el-icon-delete{ color: #157a0c; } } .left-scrollbar .el-scrollbar__view{ overflow-x: hidden; } .el-rate{ display: inline-block; vertical-align: text-top; } .el-upload__tip{ line-height: 1.2; } $selectedColor: #f6f7ff; $lighterBlue: #409EFF; .container { position: relative; width: 100%; height: 100%; } .components-list { padding: 8px; box-sizing: border-box; height: 100%; .components-item { display: inline-block; width: 48%; margin: 1%; transition: transform 0ms !important; } } .components-draggable{ padding-bottom: 20px; } .components-title{ font-size: 14px; color: #222; margin: 6px 2px; .svg-icon{ color: #666; font-size: 18px; } } .components-body { padding: 8px 10px; background: $selectedColor; font-size: 12px; cursor: move; border: 1px dashed $selectedColor; border-radius: 3px; .svg-icon{ color: #777; font-size: 15px; } &:hover { border: 1px dashed #787be8; color: #787be8; .svg-icon { color: #787be8; } } } .left-board { width: 260px; position: absolute; left: 0; top: 0; height: 100vh; } .left-scrollbar{ height: calc(100vh - 42px); overflow: hidden; } .center-scrollbar { height: calc(100vh - 42px); overflow: hidden; border-left: 1px solid #f1e8e8; border-right: 1px solid #f1e8e8; box-sizing: border-box; } .center-board { height: 100vh; width: auto; margin: 0 350px 0 260px; box-sizing: border-box; } .empty-info{ position: absolute; top: 46%; left: 0; right: 0; text-align: center; font-size: 18px; color: #ccb1ea; letter-spacing: 4px; } .action-bar{ position: relative; height: 42px; text-align: right; padding: 0 15px; box-sizing: border-box;; border: 1px solid #f1e8e8; border-top: none; border-left: none; .delete-btn{ color: #F56C6C; } } .logo-wrapper{ position: relative; height: 42px; background: #fff; border-bottom: 1px solid #f1e8e8; box-sizing: border-box; } .logo{ position: absolute; left: 12px; top: 6px; line-height: 30px; color: #00afff; font-weight: 600; font-size: 17px; white-space: nowrap; > img{ width: 30px; height: 30px; vertical-align: top; } .github{ display: inline-block; vertical-align: sub; margin-left: 15px; > img{ height: 22px; } } } .center-board-row { padding: 12px 12px 15px 12px; box-sizing: border-box; & > .el-form { // 69 = 12+15+42 height: calc(100vh - 69px); } } .drawing-board { height: 100%; position: relative; .components-body { padding: 0; margin: 0; font-size: 0; } .sortable-ghost { position: relative; display: block; overflow: hidden; &::before { content: " "; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: rgb(89, 89, 223); z-index: 2; } } .components-item.sortable-ghost { width: 100%; height: 60px; background-color: $selectedColor; } .active-from-item { & > .el-form-item{ background: $selectedColor; border-radius: 6px; } & > .drawing-item-copy, & > .drawing-item-delete{ display: initial; } & > .component-name{ color: $lighterBlue; } } .el-form-item{ margin-bottom: 15px; } } .drawing-item{ position: relative; cursor: move; &.unfocus-bordered:not(.activeFromItem) > div:first-child { border: 1px dashed #ccc; } .el-form-item{ padding: 12px 10px; } } .drawing-row-item{ position: relative; cursor: move; box-sizing: border-box; border: 1px dashed #ccc; border-radius: 3px; padding: 0 2px; margin-bottom: 15px; .drawing-row-item { margin-bottom: 2px; } .el-col{ margin-top: 22px; } .el-form-item{ margin-bottom: 0; } .drag-wrapper{ min-height: 80px; } &.active-from-item{ border: 1px dashed $lighterBlue; } .component-name{ position: absolute; top: 0; left: 0; font-size: 12px; color: #bbb; display: inline-block; padding: 0 6px; } } .drawing-item, .drawing-row-item{ &:hover { & > .el-form-item{ background: $selectedColor; border-radius: 6px; } & > .drawing-item-copy, & > .drawing-item-delete{ display: initial; } } & > .drawing-item-copy, & > .drawing-item-delete{ display: none; position: absolute; top: -10px; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; font-size: 12px; border: 1px solid; cursor: pointer; z-index: 1; } & > .drawing-item-copy{ right: 56px; border-color: $lighterBlue; color: $lighterBlue; background: #fff; &:hover{ background: $lighterBlue; color: #fff; } } & > .drawing-item-delete{ right: 24px; border-color: #F56C6C; color: #F56C6C; background: #fff; &:hover{ background: #F56C6C; color: #fff; } } } </style>