1
0
wenjianzhang 5 жил өмнө
parent
commit
af04d63792
60 өөрчлөгдсөн 4641 нэмэгдсэн , 2054 устгасан
  1. 1 1
      package.json
  2. 1 0
      public/index.html
  3. 38 0
      public/preview.html
  4. 18 7
      src/components/SvgIcon/index.vue
  5. 3 0
      src/components/Tinymce/README.md
  6. 8 0
      src/components/Tinymce/config.js
  7. 38 0
      src/components/Tinymce/example/Index.vue
  8. 3 0
      src/components/Tinymce/index.js
  9. 46 194
      src/components/Tinymce/index.vue
  10. 28 0
      src/components/Tinymce/package.json
  11. 420 0
      src/components/Tinymce/zh_CN.js
  12. 545 0
      src/components/generator/config.js
  13. 3 3
      src/components/generator/css.js
  14. 37 0
      src/components/generator/drawingDefalut.js
  15. 399 0
      src/components/generator/html.js
  16. 253 0
      src/components/generator/js.js
  17. 17 0
      src/components/generator/ruleTrigger.js
  18. 186 0
      src/components/parser/Parser.vue
  19. 17 0
      src/components/parser/README.md
  20. 324 0
      src/components/parser/example/Index.vue
  21. 3 0
      src/components/parser/index.js
  22. 25 0
      src/components/parser/package.json
  23. 19 0
      src/components/render/package.json
  24. 104 0
      src/components/render/render.js
  25. 5 0
      src/components/render/slots/el-button.js
  26. 13 0
      src/components/render/slots/el-checkbox-group.js
  27. 8 0
      src/components/render/slots/el-input.js
  28. 13 0
      src/components/render/slots/el-radio-group.js
  29. 9 0
      src/components/render/slots/el-select.js
  30. 17 0
      src/components/render/slots/el-upload.js
  31. 0 0
      src/icons/svg/button.svg
  32. 0 0
      src/icons/svg/rich-text.svg
  33. 12 0
      src/main.js
  34. 271 0
      src/styles/generator.scss
  35. 271 0
      src/styles/generator/home.scss
  36. 137 0
      src/styles/generator/index.scss
  37. 33 0
      src/styles/generator/mixin.scss
  38. 0 423
      src/utils/generator/config.js
  39. 54 0
      src/utils/generator/db.js
  40. 0 29
      src/utils/generator/drawingDefalut.js
  41. 0 339
      src/utils/generator/html.js
  42. 41 12
      src/utils/generator/index.js
  43. 0 236
      src/utils/generator/js.js
  44. 26 0
      src/utils/generator/loadBeautifier.js
  45. 42 0
      src/utils/generator/loadMonaco.js
  46. 60 0
      src/utils/generator/loadScript.js
  47. 26 0
      src/utils/generator/loadTinymce.js
  48. 0 121
      src/utils/generator/render.js
  49. 4 1
      src/views/tools/build/CodeTypeDialog.vue
  50. 39 28
      src/views/tools/build/DraggableItem.vue
  51. 332 0
      src/views/tools/build/FormDrawer.vue
  52. 0 1
      src/views/tools/build/IconsDialog.vue
  53. 144 0
      src/views/tools/build/JsonDrawer.vue
  54. 116 0
      src/views/tools/build/ResourceDialog.vue
  55. 178 114
      src/views/tools/build/RightPanel.vue
  56. 13 3
      src/views/tools/build/TreeNodeDialog.vue
  57. 170 542
      src/views/tools/build/index.vue
  58. 0 0
      src/views/tools/preview/index.vue
  59. 60 0
      src/views/tools/preview/main.js
  60. 11 0
      vue.config.js

+ 1 - 1
package.json

@@ -98,7 +98,7 @@
     "lint-staged": "8.1.5",
     "mockjs": "1.0.1-beta3",
     "monaco-editor-webpack-plugin": "^1.9.0",
-    "node-sass": "^4.13.1",
+    "node-sass": "^4.14.1",
     "plop": "2.3.0",
     "runjs": "^4.3.2",
     "sass-loader": "^7.1.0",

+ 1 - 0
public/index.html

@@ -8,6 +8,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
   <link rel="icon" href="<%= BASE_URL %>favicon.ico">
   <title><%= webpackConfig.name %> - go-admin</title>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.20.0/min/vs/editor/editor.main.css" rel="stylesheet">
   <meta name="keywords" content="go-admin,gin,权限管理系统,gin-admin,gin-vue-admin,go">
   <meta name="description" content="基于Gin + Vue + Element UI的前后端分离权限管理系统,初始化极度简单,只需要配置文件中,修改数据库连接,系统启动后会自动初始化数据库信息以及必须的基础数据">
   <style>

+ 38 - 0
public/preview.html

@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="zh">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>form-generator-preview</title>
+    <link href="https://lib.baomitu.com/element-ui/2.13.2/theme-chalk/index.css" rel="stylesheet">
+    <script src="https://lib.baomitu.com/vue/2.6.11/vue.min.js"></script>
+    <script src="https://lib.baomitu.com/vue-router/3.1.3/vue-router.min.js"></script>
+    <!-- <script src="https://lib.baomitu.com/axios/0.19.2/axios.min.js"></script> -->
+    <script src="https://lib.baomitu.com/element-ui/2.13.2/index.js"></script>
+    <style>
+      body{
+        margin: 0;
+        padding: 0;
+        overflow-x: hidden;
+        -moz-osx-font-smoothing: grayscale;
+        -webkit-font-smoothing: antialiased;
+        text-rendering: optimizeLegibility;
+        height: calc(100vh - 33px);
+        padding: 12px;
+        box-sizing: border-box;
+        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;
+      }
+    </style>
+  </head>
+  <body>
+    <noscript>
+      <strong>抱歉,javascript被禁用,请开启后重试。</strong>
+    </noscript>
+    <div id="previewApp"></div>
+  </body>
+</html>

+ 18 - 7
src/components/SvgIcon/index.vue

@@ -1,13 +1,25 @@
 <template>
-  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
-  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
-    <use :href="iconName" />
+  <div
+    v-if="isExternal"
+    :style="styleExternalIcon"
+    class="svg-external-icon svg-icon"
+    v-on="$listeners"
+  />
+  <svg
+    v-else
+    :class="svgClass"
+    aria-hidden="true"
+    v-on="$listeners"
+  >
+    <use :xlink:href="iconName" />
   </svg>
 </template>
 
 <script>
 // doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
-import { isExternal } from '@/utils/validate'
+function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
 
 export default {
   name: 'SvgIcon',
@@ -30,10 +42,9 @@ export default {
     },
     svgClass() {
       if (this.className) {
-        return 'svg-icon ' + this.className
-      } else {
-        return 'svg-icon'
+        return `svg-icon ${this.className}`
       }
+      return 'svg-icon'
     },
     styleExternalIcon() {
       return {

+ 3 - 0
src/components/Tinymce/README.md

@@ -0,0 +1,3 @@
+## 简介
+富文本编辑器tinymce的一个vue版本封装。使用cdn动态脚本引入的方式加载。
+

+ 8 - 0
src/components/Tinymce/config.js

@@ -0,0 +1,8 @@
+/* eslint-disable max-len */
+
+export const plugins = [
+  'advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount'
+]
+export const toolbar = [
+  'code searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote removeformat subscript superscript codesample hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'
+]

+ 38 - 0
src/components/Tinymce/example/Index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div>
+    <Tinymce v-model="defaultValue" :height="300" placeholder="在这里输入文字" />
+  </div>
+</template>
+
+<script>
+import Tinymce from '../index.vue'
+
+export default {
+  components: {
+    Tinymce
+  },
+  props: {
+
+  },
+  data() {
+    return {
+      defaultValue: '<p>配置文档参阅:http://tinymce.ax-z.cn</p>'
+    }
+  },
+  computed: {
+
+  },
+  watch: {
+
+  },
+  created() {
+
+  },
+  mounted() {
+
+  },
+  methods: {
+
+  }
+}
+</script>

+ 3 - 0
src/components/Tinymce/index.js

@@ -0,0 +1,3 @@
+import Index from './index.vue'
+
+export default Index

+ 46 - 194
src/components/Tinymce/index.vue

@@ -1,237 +1,89 @@
 <template>
-  <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
-    <textarea :id="tinymceId" class="tinymce-textarea" />
-    <div class="editor-custom-btn-container">
-      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
-    </div>
-  </div>
+  <textarea :id="tinymceId" style="visibility: hidden" />
 </template>
 
 <script>
-/**
- * docs:
- * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
- */
-import editorImage from './components/EditorImage'
-import plugins from './plugins'
-import toolbar from './toolbar'
-import load from './dynamicLoadScript'
+import loadTinymce from '@/utils/generator/loadTinymce'
+import { plugins, toolbar } from './config'
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { debounce } from 'throttle-debounce'
 
-// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
-const tinymceCDN = 'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js'
+let num = 1
 
 export default {
-  name: 'Tinymce',
-  components: { editorImage },
   props: {
     id: {
       type: String,
-      default: function() {
-        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      default: () => {
+        num === 10000 && (num = 1)
+        return `tinymce${+new Date()}${num++}`
       }
     },
     value: {
-      type: String,
       default: ''
-    },
-    toolbar: {
-      type: Array,
-      required: false,
-      default() {
-        return []
-      }
-    },
-    menubar: {
-      type: String,
-      default: 'file edit insert view format table'
-    },
-    height: {
-      type: [Number, String],
-      required: false,
-      default: 360
-    },
-    width: {
-      type: [Number, String],
-      required: false,
-      default: 'auto'
     }
   },
   data() {
     return {
-      hasChange: false,
-      hasInit: false,
-      tinymceId: this.id,
-      fullscreen: false,
-      languageTypeList: {
-        'en': 'en',
-        'zh': 'zh_CN',
-        'es': 'es_MX',
-        'ja': 'ja'
-      }
-    }
-  },
-  computed: {
-    containerWidth() {
-      const width = this.width
-      if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
-        return `${width}px`
-      }
-      return width
-    }
-  },
-  watch: {
-    value(val) {
-      if (!this.hasChange && this.hasInit) {
-        this.$nextTick(() =>
-          window.tinymce.get(this.tinymceId).setContent(val || ''))
-      }
+      tinymceId: this.id
     }
   },
   mounted() {
-    this.init()
-  },
-  activated() {
-    if (window.tinymce) {
-      this.initTinymce()
-    }
-  },
-  deactivated() {
-    this.destroyTinymce()
-  },
-  destroyed() {
-    this.destroyTinymce()
-  },
-  methods: {
-    init() {
-      // dynamic load tinymce from cdn
-      load(tinymceCDN, (err) => {
-        if (err) {
-          this.$message.error(err.message)
-          return
-        }
-        this.initTinymce()
-      })
-    },
-    initTinymce() {
-      const _this = this
-      window.tinymce.init({
+    loadTinymce(tinymce => {
+      // eslint-disable-next-line global-require
+      require('./zh_CN')
+      let conf = {
         selector: `#${this.tinymceId}`,
-        language: this.languageTypeList['en'],
-        height: this.height,
-        body_class: 'panel-body ',
+        language: 'zh_CN',
+        menubar: 'file edit insert view format table',
+        plugins,
+        toolbar,
+        height: 300,
+        branding: false,
         object_resizing: false,
-        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
-        menubar: this.menubar,
-        plugins: plugins,
         end_container_on_empty_block: true,
         powerpaste_word_import: 'clean',
         code_dialog_height: 450,
         code_dialog_width: 1000,
         advlist_bullet_styles: 'square',
         advlist_number_styles: 'default',
-        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
         default_link_target: '_blank',
         link_title: false,
-        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
-        init_instance_callback: editor => {
-          if (_this.value) {
-            editor.setContent(_this.value)
-          }
-          _this.hasInit = true
-          editor.on('NodeChange Change KeyUp SetContent', () => {
-            this.hasChange = true
-            this.$emit('input', editor.getContent())
-          })
-        },
-        setup(editor) {
-          editor.on('FullscreenStateChanged', (e) => {
-            _this.fullscreen = e.state
-          })
+        nonbreaking_force_tab: true
+      }
+      conf = Object.assign(conf, this.$attrs)
+      conf.init_instance_callback = editor => {
+        if (this.value) editor.setContent(this.value)
+        this.vModel(editor)
+      }
+      tinymce.init(conf)
+    })
+  },
+  destroyed() {
+    this.destroyTinymce()
+  },
+  methods: {
+    vModel(editor) {
+      // 控制连续写入时setContent的触发频率
+      const debounceSetContent = debounce(250, editor.setContent)
+      this.$watch('value', (val, prevVal) => {
+        if (editor && val !== prevVal && val !== editor.getContent()) {
+          if (typeof val !== 'string') val = val.toString()
+          debounceSetContent.call(editor, val)
         }
-        // 整合七牛上传
-        // images_dataimg_filter(img) {
-        //   setTimeout(() => {
-        //     const $image = $(img);
-        //     $image.removeAttr('width');
-        //     $image.removeAttr('height');
-        //     if ($image[0].height && $image[0].width) {
-        //       $image.attr('data-wscntype', 'image');
-        //       $image.attr('data-wscnh', $image[0].height);
-        //       $image.attr('data-wscnw', $image[0].width);
-        //       $image.addClass('wscnph');
-        //     }
-        //   }, 0);
-        //   return img
-        // },
-        // images_upload_handler(blobInfo, success, failure, progress) {
-        //   progress(0);
-        //   const token = _this.$store.getters.token;
-        //   getToken(token).then(response => {
-        //     const url = response.data.qiniu_url;
-        //     const formData = new FormData();
-        //     formData.append('token', response.data.qiniu_token);
-        //     formData.append('key', response.data.qiniu_key);
-        //     formData.append('file', blobInfo.blob(), url);
-        //     upload(formData).then(() => {
-        //       success(url);
-        //       progress(100);
-        //     })
-        //   }).catch(err => {
-        //     failure('出现未知问题,刷新页面,或者联系程序员')
-        //     console.log(err);
-        //   });
-        // },
+      })
+
+      editor.on('change keyup undo redo', () => {
+        this.$emit('input', editor.getContent())
       })
     },
     destroyTinymce() {
+      if (!window.tinymce) return
       const tinymce = window.tinymce.get(this.tinymceId)
-      if (this.fullscreen) {
-        tinymce.execCommand('mceFullScreen')
-      }
-
       if (tinymce) {
         tinymce.destroy()
       }
-    },
-    setContent(value) {
-      window.tinymce.get(this.tinymceId).setContent(value)
-    },
-    getContent() {
-      window.tinymce.get(this.tinymceId).getContent()
-    },
-    imageSuccessCBK(arr) {
-      const _this = this
-      arr.forEach(v => {
-        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
-      })
     }
   }
 }
 </script>
-
-<style scoped>
-.tinymce-container {
-  position: relative;
-  line-height: normal;
-}
-.tinymce-container>>>.mce-fullscreen {
-  z-index: 10000;
-}
-.tinymce-textarea {
-  visibility: hidden;
-  z-index: -1;
-}
-.editor-custom-btn-container {
-  position: absolute;
-  right: 4px;
-  top: 4px;
-  /*z-index: 2005;*/
-}
-.fullscreen .editor-custom-btn-container {
-  z-index: 10000;
-  position: fixed;
-}
-.editor-upload-btn {
-  display: inline-block;
-}
-</style>

+ 28 - 0
src/components/Tinymce/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "form-gen-tinymce",
+  "version": "1.0.0",
+  "description": "富文本编辑器tinymce的一个vue版本封装。使用cdn动态脚本引入的方式加载。",
+  "main": "lib/form-gen-tinymce.umd.js",
+  "directories": {
+    "example": "example"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/JakHuang/form-generator.git"
+  },
+  "keywords": [
+    "tinymce-vue"
+  ],
+  "dependencies": {
+    "throttle-debounce": "^2.1.0"
+  },
+  "author": "jakHuang",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/JakHuang/form-generator/issues"
+  },
+  "homepage": "https://github.com/JakHuang/form-generator/blob/dev/src/components/tinymce"
+}

+ 420 - 0
src/components/Tinymce/zh_CN.js

@@ -0,0 +1,420 @@
+/* eslint-disable */
+tinymce.addI18n('zh_CN',{
+"Redo": "\u91cd\u505a",
+"Undo": "\u64a4\u9500",
+"Cut": "\u526a\u5207",
+"Copy": "\u590d\u5236",
+"Paste": "\u7c98\u8d34",
+"Select all": "\u5168\u9009",
+"New document": "\u65b0\u6587\u4ef6",
+"Ok": "\u786e\u5b9a",
+"Cancel": "\u53d6\u6d88",
+"Visual aids": "\u7f51\u683c\u7ebf",
+"Bold": "\u7c97\u4f53",
+"Italic": "\u659c\u4f53",
+"Underline": "\u4e0b\u5212\u7ebf",
+"Strikethrough": "\u5220\u9664\u7ebf",
+"Superscript": "\u4e0a\u6807",
+"Subscript": "\u4e0b\u6807",
+"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
+"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
+"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
+"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
+"Justify": "\u4e24\u7aef\u5bf9\u9f50",
+"Bullet list": "\u9879\u76ee\u7b26\u53f7",
+"Numbered list": "\u7f16\u53f7\u5217\u8868",
+"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
+"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
+"Close": "\u5173\u95ed",
+"Formats": "\u683c\u5f0f",
+"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
+"Headers": "\u6807\u9898",
+"Header 1": "\u6807\u98981",
+"Header 2": "\u6807\u98982",
+"Header 3": "\u6807\u98983",
+"Header 4": "\u6807\u98984",
+"Header 5": "\u6807\u98985",
+"Header 6": "\u6807\u98986",
+"Headings": "\u6807\u9898",
+"Heading 1": "\u6807\u98981",
+"Heading 2": "\u6807\u98982",
+"Heading 3": "\u6807\u98983",
+"Heading 4": "\u6807\u98984",
+"Heading 5": "\u6807\u98985",
+"Heading 6": "\u6807\u98986",
+"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
+"Div": "Div",
+"Pre": "Pre",
+"Code": "\u4ee3\u7801",
+"Paragraph": "\u6bb5\u843d",
+"Blockquote": "\u5f15\u6587\u533a\u5757",
+"Inline": "\u6587\u672c",
+"Blocks": "\u57fa\u5757",
+"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
+"Fonts": "\u5b57\u4f53",
+"Font Sizes": "\u5b57\u53f7",
+"Class": "\u7c7b\u578b",
+"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
+"OR": "\u6216",
+"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
+"Upload": "\u4e0a\u4f20",
+"Block": "\u5757",
+"Align": "\u5bf9\u9f50",
+"Default": "\u9ed8\u8ba4",
+"Circle": "\u7a7a\u5fc3\u5706",
+"Disc": "\u5b9e\u5fc3\u5706",
+"Square": "\u65b9\u5757",
+"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
+"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
+"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
+"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
+"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
+"Anchor...": "\u951a\u70b9...",
+"Name": "\u540d\u79f0",
+"Id": "\u6807\u8bc6\u7b26",
+"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
+"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
+"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
+"Special character...": "\u7279\u6b8a\u5b57\u7b26...",
+"Source code": "\u6e90\u4ee3\u7801",
+"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
+"Language": "\u8bed\u8a00",
+"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
+"Color Picker": "\u9009\u8272\u5668",
+"R": "R",
+"G": "G",
+"B": "B",
+"Left to right": "\u4ece\u5de6\u5230\u53f3",
+"Right to left": "\u4ece\u53f3\u5230\u5de6",
+"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
+"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
+"Title": "\u6807\u9898",
+"Keywords": "\u5173\u952e\u8bcd",
+"Description": "\u63cf\u8ff0",
+"Robots": "\u673a\u5668\u4eba",
+"Author": "\u4f5c\u8005",
+"Encoding": "\u7f16\u7801",
+"Fullscreen": "\u5168\u5c4f",
+"Action": "\u64cd\u4f5c",
+"Shortcut": "\u5feb\u6377\u952e",
+"Help": "\u5e2e\u52a9",
+"Address": "\u5730\u5740",
+"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
+"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
+"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
+"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
+"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
+"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
+"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
+"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
+"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
+"Plugins": "\u63d2\u4ef6",
+"Handy Shortcuts": "\u5feb\u6377\u952e",
+"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
+"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
+"Image description": "\u56fe\u7247\u63cf\u8ff0",
+"Source": "\u5730\u5740",
+"Dimensions": "\u5927\u5c0f",
+"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
+"General": "\u666e\u901a",
+"Advanced": "\u9ad8\u7ea7",
+"Style": "\u6837\u5f0f",
+"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
+"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
+"Border": "\u8fb9\u6846",
+"Insert image": "\u63d2\u5165\u56fe\u7247",
+"Image...": "\u56fe\u7247...",
+"Image list": "\u56fe\u7247\u5217\u8868",
+"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
+"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
+"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
+"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
+"Edit image": "\u7f16\u8f91\u56fe\u7247",
+"Image options": "\u56fe\u7247\u9009\u9879",
+"Zoom in": "\u653e\u5927",
+"Zoom out": "\u7f29\u5c0f",
+"Crop": "\u88c1\u526a",
+"Resize": "\u8c03\u6574\u5927\u5c0f",
+"Orientation": "\u65b9\u5411",
+"Brightness": "\u4eae\u5ea6",
+"Sharpen": "\u9510\u5316",
+"Contrast": "\u5bf9\u6bd4\u5ea6",
+"Color levels": "\u989c\u8272\u5c42\u6b21",
+"Gamma": "\u4f3d\u9a6c\u503c",
+"Invert": "\u53cd\u8f6c",
+"Apply": "\u5e94\u7528",
+"Back": "\u540e\u9000",
+"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
+"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
+"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
+"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
+"Text to display": "\u663e\u793a\u6587\u5b57",
+"Url": "\u5730\u5740",
+"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
+"Current window": "\u5f53\u524d\u7a97\u53e3",
+"None": "\u65e0",
+"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
+"Remove link": "\u5220\u9664\u94fe\u63a5",
+"Anchors": "\u951a\u70b9",
+"Link...": "\u94fe\u63a5...",
+"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
+"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
+"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
+"Link list": "\u94fe\u63a5\u5217\u8868",
+"Insert video": "\u63d2\u5165\u89c6\u9891",
+"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
+"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
+"Alternative source": "\u955c\u50cf",
+"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
+"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
+"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
+"Embed": "\u5185\u5d4c",
+"Media...": "\u591a\u5a92\u4f53...",
+"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
+"Page break": "\u5206\u9875\u7b26",
+"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
+"Preview": "\u9884\u89c8",
+"Print...": "\u6253\u5370...",
+"Save": "\u4fdd\u5b58",
+"Find": "\u67e5\u627e",
+"Replace with": "\u66ff\u6362\u4e3a",
+"Replace": "\u66ff\u6362",
+"Replace all": "\u5168\u90e8\u66ff\u6362",
+"Previous": "\u4e0a\u4e00\u4e2a",
+"Next": "\u4e0b\u4e00\u4e2a",
+"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
+"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
+"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
+"Find whole words only": "\u5168\u5b57\u5339\u914d",
+"Spell check": "\u62fc\u5199\u68c0\u67e5",
+"Ignore": "\u5ffd\u7565",
+"Ignore all": "\u5168\u90e8\u5ffd\u7565",
+"Finish": "\u5b8c\u6210",
+"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
+"Insert table": "\u63d2\u5165\u8868\u683c",
+"Table properties": "\u8868\u683c\u5c5e\u6027",
+"Delete table": "\u5220\u9664\u8868\u683c",
+"Cell": "\u5355\u5143\u683c",
+"Row": "\u884c",
+"Column": "\u5217",
+"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
+"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
+"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
+"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
+"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
+"Delete row": "\u5220\u9664\u884c",
+"Row properties": "\u884c\u5c5e\u6027",
+"Cut row": "\u526a\u5207\u884c",
+"Copy row": "\u590d\u5236\u884c",
+"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
+"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
+"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
+"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
+"Delete column": "\u5220\u9664\u5217",
+"Cols": "\u5217",
+"Rows": "\u884c",
+"Width": "\u5bbd",
+"Height": "\u9ad8",
+"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
+"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
+"Show caption": "\u663e\u793a\u6807\u9898",
+"Left": "\u5de6\u5bf9\u9f50",
+"Center": "\u5c45\u4e2d",
+"Right": "\u53f3\u5bf9\u9f50",
+"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
+"Scope": "\u8303\u56f4",
+"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
+"H Align": "\u6c34\u5e73\u5bf9\u9f50",
+"V Align": "\u5782\u76f4\u5bf9\u9f50",
+"Top": "\u9876\u90e8\u5bf9\u9f50",
+"Middle": "\u5782\u76f4\u5c45\u4e2d",
+"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
+"Header cell": "\u8868\u5934\u5355\u5143\u683c",
+"Row group": "\u884c\u7ec4",
+"Column group": "\u5217\u7ec4",
+"Row type": "\u884c\u7c7b\u578b",
+"Header": "\u8868\u5934",
+"Body": "\u8868\u4f53",
+"Footer": "\u8868\u5c3e",
+"Border color": "\u8fb9\u6846\u989c\u8272",
+"Insert template...": "\u63d2\u5165\u6a21\u677f...",
+"Templates": "\u6a21\u677f",
+"Template": "\u6a21\u677f",
+"Text color": "\u6587\u5b57\u989c\u8272",
+"Background color": "\u80cc\u666f\u8272",
+"Custom...": "\u81ea\u5b9a\u4e49...",
+"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
+"No color": "\u65e0",
+"Remove color": "\u79fb\u9664\u989c\u8272",
+"Table of Contents": "\u5185\u5bb9\u5217\u8868",
+"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
+"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
+"Word count": "\u5b57\u6570",
+"Count": "\u8ba1\u6570",
+"Document": "\u6587\u6863",
+"Selection": "\u9009\u62e9",
+"Words": "\u5355\u8bcd",
+"Words: {0}": "\u5b57\u6570\uff1a{0}",
+"{0} words": "{0} \u5b57",
+"File": "\u6587\u4ef6",
+"Edit": "\u7f16\u8f91",
+"Insert": "\u63d2\u5165",
+"View": "\u89c6\u56fe",
+"Format": "\u683c\u5f0f",
+"Table": "\u8868\u683c",
+"Tools": "\u5de5\u5177",
+"Powered by {0}": "\u7531{0}\u9a71\u52a8",
+"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
+"Image title": "\u56fe\u7247\u6807\u9898",
+"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
+"Border style": "\u8fb9\u6846\u6837\u5f0f",
+"Error": "\u9519\u8bef",
+"Warn": "\u8b66\u544a",
+"Valid": "\u6709\u6548",
+"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
+"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
+"System Font": "\u7cfb\u7edf\u5b57\u4f53",
+"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
+"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
+"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
+"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
+"example": "\u793a\u4f8b",
+"Search": "\u641c\u7d22",
+"All": "\u5168\u90e8",
+"Currency": "\u8d27\u5e01",
+"Text": "\u6587\u5b57",
+"Quotations": "\u5f15\u7528",
+"Mathematical": "\u6570\u5b66",
+"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
+"Symbols": "\u7b26\u53f7",
+"Arrows": "\u7bad\u5934",
+"User Defined": "\u81ea\u5b9a\u4e49",
+"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
+"currency sign": "\u8d27\u5e01\u7b26\u53f7",
+"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
+"colon sign": "\u5192\u53f7",
+"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
+"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
+"lira sign": "\u91cc\u62c9\u7b26\u53f7",
+"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
+"naira sign": "\u5948\u62c9\u7b26\u53f7",
+"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
+"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
+"won sign": "\u97e9\u5143\u7b26\u53f7",
+"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
+"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
+"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
+"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
+"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
+"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
+"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
+"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
+"austral sign": "\u6fb3\u5143\u7b26\u53f7",
+"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
+"cedi sign": "\u585e\u5730\u7b26\u53f7",
+"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
+"spesmilo sign": "spesmilo\u7b26\u53f7",
+"tenge sign": "\u575a\u6208\u7b26\u53f7",
+"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
+"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
+"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
+"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
+"ruble sign": "\u5362\u5e03\u7b26\u53f7",
+"yen character": "\u65e5\u5143\u5b57\u6837",
+"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
+"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
+"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
+"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
+"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
+"People": "\u4eba\u7c7b",
+"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
+"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
+"Activity": "\u6d3b\u52a8",
+"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
+"Objects": "\u7269\u4ef6",
+"Flags": "\u65d7\u5e1c",
+"Characters": "\u5b57\u7b26",
+"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
+"{0} characters": "{0} \u4e2a\u5b57\u7b26",
+"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
+"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
+"Update": "\u66f4\u65b0",
+"Color swatch": "\u989c\u8272\u6837\u672c",
+"Turquoise": "\u9752\u7eff\u8272",
+"Green": "\u7eff\u8272",
+"Blue": "\u84dd\u8272",
+"Purple": "\u7d2b\u8272",
+"Navy Blue": "\u6d77\u519b\u84dd",
+"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
+"Dark Green": "\u6df1\u7eff\u8272",
+"Medium Blue": "\u4e2d\u84dd\u8272",
+"Medium Purple": "\u4e2d\u7d2b\u8272",
+"Midnight Blue": "\u6df1\u84dd\u8272",
+"Yellow": "\u9ec4\u8272",
+"Orange": "\u6a59\u8272",
+"Red": "\u7ea2\u8272",
+"Light Gray": "\u6d45\u7070\u8272",
+"Gray": "\u7070\u8272",
+"Dark Yellow": "\u6697\u9ec4\u8272",
+"Dark Orange": "\u6df1\u6a59\u8272",
+"Dark Red": "\u6df1\u7ea2\u8272",
+"Medium Gray": "\u4e2d\u7070\u8272",
+"Dark Gray": "\u6df1\u7070\u8272",
+"Light Green": "\u6d45\u7eff\u8272",
+"Light Yellow": "\u6d45\u9ec4\u8272",
+"Light Red": "\u6d45\u7ea2\u8272",
+"Light Purple": "\u6d45\u7d2b\u8272",
+"Light Blue": "\u6d45\u84dd\u8272",
+"Dark Purple": "\u6df1\u7d2b\u8272",
+"Dark Blue": "\u6df1\u84dd\u8272",
+"Black": "\u9ed1\u8272",
+"White": "\u767d\u8272",
+"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
+"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
+"history": "\u5386\u53f2",
+"styles": "\u6837\u5f0f",
+"formatting": "\u683c\u5f0f\u5316",
+"alignment": "\u5bf9\u9f50",
+"indentation": "\u7f29\u8fdb",
+"permanent pen": "\u8bb0\u53f7\u7b14",
+"comments": "\u5907\u6ce8",
+"Format Painter": "\u683c\u5f0f\u5237",
+"Insert\/edit iframe": "\u63d2\u5165\/\u7f16\u8f91\u6846\u67b6",
+"Capitalization": "\u5927\u5199",
+"lowercase": "\u5c0f\u5199",
+"UPPERCASE": "\u5927\u5199",
+"Title Case": "\u9996\u5b57\u6bcd\u5927\u5199",
+"Permanent Pen Properties": "\u6c38\u4e45\u7b14\u5c5e\u6027",
+"Permanent pen properties...": "\u6c38\u4e45\u7b14\u5c5e\u6027...",
+"Font": "\u5b57\u4f53",
+"Size": "\u5b57\u53f7",
+"More...": "\u66f4\u591a...",
+"Spellcheck Language": "\u62fc\u5199\u68c0\u67e5\u8bed\u8a00",
+"Select...": "\u9009\u62e9...",
+"Preferences": "\u9996\u9009\u9879",
+"Yes": "\u662f",
+"No": "\u5426",
+"Keyboard Navigation": "\u952e\u76d8\u6307\u5f15",
+"Version": "\u7248\u672c",
+"Anchor": "\u951a\u70b9",
+"Special character": "\u7279\u6b8a\u7b26\u53f7",
+"Code sample": "\u4ee3\u7801\u793a\u4f8b",
+"Color": "\u989c\u8272",
+"Emoticons": "\u8868\u60c5",
+"Document properties": "\u6587\u6863\u5c5e\u6027",
+"Image": "\u56fe\u7247",
+"Insert link": "\u63d2\u5165\u94fe\u63a5",
+"Target": "\u6253\u5f00\u65b9\u5f0f",
+"Link": "\u94fe\u63a5",
+"Poster": "\u5c01\u9762",
+"Media": "\u5a92\u4f53",
+"Print": "\u6253\u5370",
+"Prev": "\u4e0a\u4e00\u4e2a",
+"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
+"Whole words": "\u5168\u5b57\u5339\u914d",
+"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
+"Caption": "\u6807\u9898",
+"Insert template": "\u63d2\u5165\u6a21\u677f"
+});

+ 545 - 0
src/components/generator/config.js

@@ -0,0 +1,545 @@
+// 表单属性【右面板】
+export const formConf = {
+  formRef: 'elForm',
+  formModel: 'formData',
+  size: 'medium',
+  labelPosition: 'right',
+  labelWidth: 100,
+  formRules: 'rules',
+  gutter: 15,
+  disabled: false,
+  span: 24,
+  formBtns: true
+}
+
+// 输入型组件 【左面板】
+export const inputComponents = [
+  {
+    // 组件的自定义配置
+    __config__: {
+      label: '单行文本',
+      labelWidth: null,
+      showLabel: true,
+      changeTag: true,
+      tag: 'el-input',
+      tagIcon: 'input',
+      defaultValue: undefined,
+      required: true,
+      layout: 'colFormItem',
+      span: 24,
+      document: 'https://element.eleme.cn/#/zh-CN/component/input',
+      // 正则校验规则
+      regList: []
+    },
+    // 组件的插槽属性
+    __slot__: {
+      prepend: '',
+      append: ''
+    },
+    // 其余的为可直接写在组件标签上的属性
+    placeholder: '请输入',
+    style: { width: '100%' },
+    clearable: true,
+    'prefix-icon': '',
+    'suffix-icon': '',
+    maxlength: null,
+    'show-word-limit': false,
+    readonly: false,
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '多行文本',
+      labelWidth: null,
+      showLabel: true,
+      tag: 'el-input',
+      tagIcon: 'textarea',
+      defaultValue: undefined,
+      required: true,
+      layout: 'colFormItem',
+      span: 24,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/input'
+    },
+    type: 'textarea',
+    placeholder: '请输入',
+    autosize: {
+      minRows: 4,
+      maxRows: 4
+    },
+    style: { width: '100%' },
+    maxlength: null,
+    'show-word-limit': false,
+    readonly: false,
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '密码',
+      showLabel: true,
+      labelWidth: null,
+      changeTag: true,
+      tag: 'el-input',
+      tagIcon: 'password',
+      defaultValue: undefined,
+      layout: 'colFormItem',
+      span: 24,
+      required: true,
+      regList: [],
+      document: 'https://element.eleme.cn/#/zh-CN/component/input'
+    },
+    __slot__: {
+      prepend: '',
+      append: ''
+    },
+    placeholder: '请输入',
+    'show-password': true,
+    style: { width: '100%' },
+    clearable: true,
+    'prefix-icon': '',
+    'suffix-icon': '',
+    maxlength: null,
+    'show-word-limit': false,
+    readonly: false,
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '计数器',
+      showLabel: true,
+      changeTag: true,
+      labelWidth: null,
+      tag: 'el-input-number',
+      tagIcon: 'number',
+      defaultValue: undefined,
+      span: 24,
+      layout: 'colFormItem',
+      required: true,
+      regList: [],
+      document: 'https://element.eleme.cn/#/zh-CN/component/input-number'
+    },
+    placeholder: '',
+    min: undefined,
+    max: undefined,
+    step: 1,
+    'step-strictly': false,
+    precision: undefined,
+    'controls-position': '',
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '编辑器',
+      showLabel: true,
+      changeTag: true,
+      labelWidth: null,
+      tag: 'tinymce',
+      tagIcon: 'rich-text',
+      defaultValue: null,
+      span: 24,
+      layout: 'colFormItem',
+      required: true,
+      regList: [],
+      document: 'http://tinymce.ax-z.cn'
+    },
+    placeholder: '请输入',
+    height: 300, // 编辑器高度
+    branding: false // 隐藏右下角品牌烙印
+  }
+]
+
+// 选择型组件 【左面板】
+export const selectComponents = [
+  {
+    __config__: {
+      label: '下拉选择',
+      showLabel: true,
+      labelWidth: null,
+      tag: 'el-select',
+      tagIcon: 'select',
+      layout: 'colFormItem',
+      span: 24,
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/select'
+    },
+    __slot__: {
+      options: [{
+        label: '选项一',
+        value: 1
+      }, {
+        label: '选项二',
+        value: 2
+      }]
+    },
+    placeholder: '请选择',
+    style: { width: '100%' },
+    clearable: true,
+    disabled: false,
+    filterable: false,
+    multiple: false
+  },
+  {
+    __config__: {
+      label: '级联选择',
+      showLabel: true,
+      labelWidth: null,
+      tag: 'el-cascader',
+      tagIcon: 'cascader',
+      layout: 'colFormItem',
+      defaultValue: [],
+      dataType: 'dynamic',
+      span: 24,
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/cascader'
+    },
+    options: [{
+      id: 1,
+      value: 1,
+      label: '选项1',
+      children: [{
+        id: 2,
+        value: 2,
+        label: '选项1-1'
+      }]
+    }],
+    placeholder: '请选择',
+    style: { width: '100%' },
+    props: {
+      props: {
+        multiple: false,
+        label: 'label',
+        value: 'value',
+        children: 'children'
+      }
+    },
+    'show-all-levels': true,
+    disabled: false,
+    clearable: true,
+    filterable: false,
+    separator: '/'
+  },
+  {
+    __config__: {
+      label: '单选框组',
+      labelWidth: null,
+      showLabel: true,
+      tag: 'el-radio-group',
+      tagIcon: 'radio',
+      changeTag: true,
+      defaultValue: undefined,
+      layout: 'colFormItem',
+      span: 24,
+      optionType: 'default',
+      regList: [],
+      required: true,
+      border: false,
+      document: 'https://element.eleme.cn/#/zh-CN/component/radio'
+    },
+    __slot__: {
+      options: [{
+        label: '选项一',
+        value: 1
+      }, {
+        label: '选项二',
+        value: 2
+      }]
+    },
+    style: {},
+    size: 'medium',
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '多选框组',
+      tag: 'el-checkbox-group',
+      tagIcon: 'checkbox',
+      defaultValue: [],
+      span: 24,
+      showLabel: true,
+      labelWidth: null,
+      layout: 'colFormItem',
+      optionType: 'default',
+      required: true,
+      regList: [],
+      changeTag: true,
+      border: false,
+      document: 'https://element.eleme.cn/#/zh-CN/component/checkbox'
+    },
+    __slot__: {
+      options: [{
+        label: '选项一',
+        value: 1
+      }, {
+        label: '选项二',
+        value: 2
+      }]
+    },
+    style: {},
+    size: 'medium',
+    min: null,
+    max: null,
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '开关',
+      tag: 'el-switch',
+      tagIcon: 'switch',
+      defaultValue: false,
+      span: 24,
+      showLabel: true,
+      labelWidth: null,
+      layout: 'colFormItem',
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/switch'
+    },
+    style: {},
+    disabled: false,
+    'active-text': '',
+    'inactive-text': '',
+    'active-color': null,
+    'inactive-color': null,
+    'active-value': true,
+    'inactive-value': false
+  },
+  {
+    __config__: {
+      label: '滑块',
+      tag: 'el-slider',
+      tagIcon: 'slider',
+      defaultValue: null,
+      span: 24,
+      showLabel: true,
+      layout: 'colFormItem',
+      labelWidth: null,
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/slider'
+    },
+    disabled: false,
+    min: 0,
+    max: 100,
+    step: 1,
+    'show-stops': false,
+    range: false
+  },
+  {
+    __config__: {
+      label: '时间选择',
+      tag: 'el-time-picker',
+      tagIcon: 'time',
+      defaultValue: null,
+      span: 24,
+      showLabel: true,
+      layout: 'colFormItem',
+      labelWidth: null,
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
+    },
+    placeholder: '请选择',
+    style: { width: '100%' },
+    disabled: false,
+    clearable: true,
+    'picker-options': {
+      selectableRange: '00:00:00-23:59:59'
+    },
+    format: 'HH:mm:ss',
+    'value-format': 'HH:mm:ss'
+  },
+  {
+    __config__: {
+      label: '时间范围',
+      tag: 'el-time-picker',
+      tagIcon: 'time-range',
+      span: 24,
+      showLabel: true,
+      labelWidth: null,
+      layout: 'colFormItem',
+      defaultValue: null,
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
+    },
+    style: { width: '100%' },
+    disabled: false,
+    clearable: true,
+    'is-range': true,
+    'range-separator': '至',
+    'start-placeholder': '开始时间',
+    'end-placeholder': '结束时间',
+    format: 'HH:mm:ss',
+    'value-format': 'HH:mm:ss'
+  },
+  {
+    __config__: {
+      label: '日期选择',
+      tag: 'el-date-picker',
+      tagIcon: 'date',
+      defaultValue: null,
+      showLabel: true,
+      labelWidth: null,
+      span: 24,
+      layout: 'colFormItem',
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/date-picker'
+    },
+    placeholder: '请选择',
+    type: 'date',
+    style: { width: '100%' },
+    disabled: false,
+    clearable: true,
+    format: 'yyyy-MM-dd',
+    'value-format': 'yyyy-MM-dd',
+    readonly: false
+  },
+  {
+    __config__: {
+      label: '日期范围',
+      tag: 'el-date-picker',
+      tagIcon: 'date-range',
+      defaultValue: null,
+      span: 24,
+      showLabel: true,
+      labelWidth: null,
+      required: true,
+      layout: 'colFormItem',
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/date-picker'
+    },
+    style: { width: '100%' },
+    type: 'daterange',
+    'range-separator': '至',
+    'start-placeholder': '开始日期',
+    'end-placeholder': '结束日期',
+    disabled: false,
+    clearable: true,
+    format: 'yyyy-MM-dd',
+    'value-format': 'yyyy-MM-dd',
+    readonly: false
+  },
+  {
+    __config__: {
+      label: '评分',
+      tag: 'el-rate',
+      tagIcon: 'rate',
+      defaultValue: 0,
+      span: 24,
+      showLabel: true,
+      labelWidth: null,
+      layout: 'colFormItem',
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/rate'
+    },
+    style: {},
+    max: 5,
+    'allow-half': false,
+    'show-text': false,
+    'show-score': false,
+    disabled: false
+  },
+  {
+    __config__: {
+      label: '颜色选择',
+      tag: 'el-color-picker',
+      tagIcon: 'color',
+      span: 24,
+      defaultValue: null,
+      showLabel: true,
+      labelWidth: null,
+      layout: 'colFormItem',
+      required: true,
+      regList: [],
+      changeTag: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/color-picker'
+    },
+    'show-alpha': false,
+    'color-format': '',
+    disabled: false,
+    size: 'medium'
+  },
+  {
+    __config__: {
+      label: '上传',
+      tag: 'el-upload',
+      tagIcon: 'upload',
+      layout: 'colFormItem',
+      defaultValue: null,
+      showLabel: true,
+      labelWidth: null,
+      required: true,
+      span: 24,
+      showTip: false,
+      buttonText: '点击上传',
+      regList: [],
+      changeTag: true,
+      fileSize: 2,
+      sizeUnit: 'MB',
+      document: 'https://element.eleme.cn/#/zh-CN/component/upload'
+    },
+    __slot__: {
+      'list-type': true
+    },
+    action: 'https://jsonplaceholder.typicode.com/posts/',
+    disabled: false,
+    accept: '',
+    name: 'file',
+    'auto-upload': true,
+    'list-type': 'text',
+    multiple: false
+  }
+]
+
+// 布局型组件 【左面板】
+export const layoutComponents = [
+  {
+    __config__: {
+      layout: 'rowFormItem',
+      tagIcon: 'row',
+      label: '行容器',
+      layoutTree: true,
+      document: 'https://element.eleme.cn/#/zh-CN/component/layout#row-attributes'
+    },
+    type: 'default',
+    justify: 'start',
+    align: 'top'
+  },
+  {
+    __config__: {
+      label: '按钮',
+      showLabel: true,
+      changeTag: true,
+      labelWidth: null,
+      tag: 'el-button',
+      tagIcon: 'button',
+      span: 24,
+      layout: 'colFormItem',
+      document: 'https://element.eleme.cn/#/zh-CN/component/button'
+    },
+    __slot__: {
+      default: '主要按钮'
+    },
+    type: 'primary',
+    icon: 'el-icon-search',
+    round: false,
+    size: 'medium',
+    plain: false,
+    circle: false,
+    disabled: false
+  }
+]

+ 3 - 3
src/utils/generator/css.js → src/components/generator/css.js

@@ -4,10 +4,10 @@ const styles = {
 }
 
 function addCss(cssList, el) {
-  const css = styles[el.tag]
+  const css = styles[el.__config__.tag]
   css && cssList.indexOf(css) === -1 && cssList.push(css)
-  if (el.children) {
-    el.children.forEach(el2 => addCss(cssList, el2))
+  if (el.__config__.children) {
+    el.__config__.children.forEach(el2 => addCss(cssList, el2))
   }
 }
 

+ 37 - 0
src/components/generator/drawingDefalut.js

@@ -0,0 +1,37 @@
+export default [
+  {
+    __config__: {
+      label: '单行文本',
+      labelWidth: null,
+      showLabel: true,
+      changeTag: true,
+      tag: 'el-input',
+      tagIcon: 'input',
+      defaultValue: undefined,
+      required: true,
+      layout: 'colFormItem',
+      span: 24,
+      document: 'https://element.eleme.cn/#/zh-CN/component/input',
+      // 正则校验规则
+      regList: [{
+        pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
+        message: '手机号格式错误'
+      }]
+    },
+    // 组件的插槽属性
+    __slot__: {
+      prepend: '',
+      append: ''
+    },
+    __vModel__: 'mobile',
+    placeholder: '请输入手机号',
+    style: { width: '100%' },
+    clearable: true,
+    'prefix-icon': 'el-icon-mobile',
+    'suffix-icon': '',
+    maxlength: 11,
+    'show-word-limit': true,
+    readonly: false,
+    disabled: false
+  }
+]

+ 399 - 0
src/components/generator/html.js

@@ -0,0 +1,399 @@
+/* eslint-disable max-len */
+import ruleTrigger from './ruleTrigger'
+
+let confGlobal
+let someSpanIsNot24
+
+export function dialogWrapper(str) {
+  return `<el-dialog v-bind="$attrs" v-on="$listeners" @open="onOpen" @close="onClose" title="Dialog Titile">
+    ${str}
+    <div slot="footer">
+      <el-button @click="close">取消</el-button>
+      <el-button type="primary" @click="handelConfirm">确定</el-button>
+    </div>
+  </el-dialog>`
+}
+
+export function vueTemplate(str) {
+  return `<template>
+    <div>
+      ${str}
+    </div>
+  </template>`
+}
+
+export function vueScript(str) {
+  return `<script>
+    ${str}
+  </script>`
+}
+
+export function cssStyle(cssStr) {
+  return `<style>
+    ${cssStr}
+  </style>`
+}
+
+function buildFormTemplate(scheme, child, type) {
+  let labelPosition = ''
+  if (scheme.labelPosition !== 'right') {
+    labelPosition = `label-position="${scheme.labelPosition}"`
+  }
+  const disabled = scheme.disabled ? `:disabled="${scheme.disabled}"` : ''
+  let str = `<el-form ref="${scheme.formRef}" :model="${scheme.formModel}" :rules="${scheme.formRules}" size="${scheme.size}" ${disabled} label-width="${scheme.labelWidth}px" ${labelPosition}>
+      ${child}
+      ${buildFromBtns(scheme, type)}
+    </el-form>`
+  if (someSpanIsNot24) {
+    str = `<el-row :gutter="${scheme.gutter}">
+        ${str}
+      </el-row>`
+  }
+  return str
+}
+
+function buildFromBtns(scheme, type) {
+  let str = ''
+  if (scheme.formBtns && type === 'file') {
+    str = `<el-form-item size="large">
+          <el-button type="primary" @click="submitForm">提交</el-button>
+          <el-button @click="resetForm">重置</el-button>
+        </el-form-item>`
+    if (someSpanIsNot24) {
+      str = `<el-col :span="24">
+          ${str}
+        </el-col>`
+    }
+  }
+  return str
+}
+
+// span不为24的用el-col包裹
+function colWrapper(scheme, str) {
+  if (someSpanIsNot24 || scheme.__config__.span !== 24) {
+    return `<el-col :span="${scheme.__config__.span}">
+      ${str}
+    </el-col>`
+  }
+  return str
+}
+
+const layouts = {
+  colFormItem(scheme) {
+    const config = scheme.__config__
+    let labelWidth = ''
+    let label = `label="${config.label}"`
+    if (config.labelWidth && config.labelWidth !== confGlobal.labelWidth) {
+      labelWidth = `label-width="${config.labelWidth}px"`
+    }
+    if (config.showLabel === false) {
+      labelWidth = 'label-width="0"'
+      label = ''
+    }
+    const required = !ruleTrigger[config.tag] && config.required ? 'required' : ''
+    const tagDom = tags[config.tag] ? tags[config.tag](scheme) : null
+    let str = `<el-form-item ${labelWidth} ${label} prop="${scheme.__vModel__}" ${required}>
+        ${tagDom}
+      </el-form-item>`
+    str = colWrapper(scheme, str)
+    return str
+  },
+  rowFormItem(scheme) {
+    const config = scheme.__config__
+    const type = scheme.type === 'default' ? '' : `type="${scheme.type}"`
+    const justify = scheme.type === 'default' ? '' : `justify="${scheme.justify}"`
+    const align = scheme.type === 'default' ? '' : `align="${scheme.align}"`
+    const gutter = scheme.gutter ? `:gutter="${scheme.gutter}"` : ''
+    const children = config.children.map(el => layouts[el.__config__.layout](el))
+    let str = `<el-row ${type} ${justify} ${align} ${gutter}>
+      ${children.join('\n')}
+    </el-row>`
+    str = colWrapper(scheme, str)
+    return str
+  }
+}
+
+const tags = {
+  'el-button': el => {
+    const {
+      tag, disabled
+    } = attrBuilder(el)
+    const type = el.type ? `type="${el.type}"` : ''
+    const icon = el.icon ? `icon="${el.icon}"` : ''
+    const round = el.round ? 'round' : ''
+    const size = el.size ? `size="${el.size}"` : ''
+    const plain = el.plain ? 'plain' : ''
+    const circle = el.circle ? 'circle' : ''
+    let child = buildElButtonChild(el)
+
+    if (child) child = `\n${child}\n` // 换行
+    return `<${tag} ${type} ${icon} ${round} ${size} ${plain} ${disabled} ${circle}>${child}</${tag}>`
+  },
+  'el-input': el => {
+    const {
+      tag, disabled, vModel, clearable, placeholder, width
+    } = attrBuilder(el)
+    const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
+    const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
+    const readonly = el.readonly ? 'readonly' : ''
+    const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
+    const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
+    const showPassword = el['show-password'] ? 'show-password' : ''
+    const type = el.type ? `type="${el.type}"` : ''
+    const autosize = el.autosize && el.autosize.minRows
+      ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
+      : ''
+    let child = buildElInputChild(el)
+
+    if (child) child = `\n${child}\n` // 换行
+    return `<${tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${tag}>`
+  },
+  'el-input-number': el => {
+    const {
+      tag, disabled, vModel, placeholder
+    } = attrBuilder(el)
+    const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
+    const min = el.min ? `:min='${el.min}'` : ''
+    const max = el.max ? `:max='${el.max}'` : ''
+    const step = el.step ? `:step='${el.step}'` : ''
+    const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
+    const precision = el.precision ? `:precision='${el.precision}'` : ''
+
+    return `<${tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}></${tag}>`
+  },
+  'el-select': el => {
+    const {
+      tag, disabled, vModel, clearable, placeholder, width
+    } = attrBuilder(el)
+    const filterable = el.filterable ? 'filterable' : ''
+    const multiple = el.multiple ? 'multiple' : ''
+    let child = buildElSelectChild(el)
+
+    if (child) child = `\n${child}\n` // 换行
+    return `<${tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}</${tag}>`
+  },
+  'el-radio-group': el => {
+    const { tag, disabled, vModel } = attrBuilder(el)
+    const size = `size="${el.size}"`
+    let child = buildElRadioGroupChild(el)
+
+    if (child) child = `\n${child}\n` // 换行
+    return `<${tag} ${vModel} ${size} ${disabled}>${child}</${tag}>`
+  },
+  'el-checkbox-group': el => {
+    const { tag, disabled, vModel } = attrBuilder(el)
+    const size = `size="${el.size}"`
+    const min = el.min ? `:min="${el.min}"` : ''
+    const max = el.max ? `:max="${el.max}"` : ''
+    let child = buildElCheckboxGroupChild(el)
+
+    if (child) child = `\n${child}\n` // 换行
+    return `<${tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}</${tag}>`
+  },
+  'el-switch': el => {
+    const { tag, disabled, vModel } = attrBuilder(el)
+    const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
+    const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
+    const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
+    const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
+    const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
+    const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
+
+    return `<${tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}></${tag}>`
+  },
+  'el-cascader': el => {
+    const {
+      tag, disabled, vModel, clearable, placeholder, width
+    } = attrBuilder(el)
+    const options = el.options ? `:options="${el.__vModel__}Options"` : ''
+    const props = el.props ? `:props="${el.__vModel__}Props"` : ''
+    const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
+    const filterable = el.filterable ? 'filterable' : ''
+    const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
+
+    return `<${tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}></${tag}>`
+  },
+  'el-slider': el => {
+    const { tag, disabled, vModel } = attrBuilder(el)
+    const min = el.min ? `:min='${el.min}'` : ''
+    const max = el.max ? `:max='${el.max}'` : ''
+    const step = el.step ? `:step='${el.step}'` : ''
+    const range = el.range ? 'range' : ''
+    const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
+
+    return `<${tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}></${tag}>`
+  },
+  'el-time-picker': el => {
+    const {
+      tag, disabled, vModel, clearable, placeholder, width
+    } = attrBuilder(el)
+    const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
+    const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
+    const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
+    const isRange = el['is-range'] ? 'is-range' : ''
+    const format = el.format ? `format="${el.format}"` : ''
+    const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
+    const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
+
+    return `<${tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}></${tag}>`
+  },
+  'el-date-picker': el => {
+    const {
+      tag, disabled, vModel, clearable, placeholder, width
+    } = attrBuilder(el)
+    const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
+    const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
+    const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
+    const format = el.format ? `format="${el.format}"` : ''
+    const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
+    const type = el.type === 'date' ? '' : `type="${el.type}"`
+    const readonly = el.readonly ? 'readonly' : ''
+
+    return `<${tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}></${tag}>`
+  },
+  'el-rate': el => {
+    const { tag, disabled, vModel } = attrBuilder(el)
+    const max = el.max ? `:max='${el.max}'` : ''
+    const allowHalf = el['allow-half'] ? 'allow-half' : ''
+    const showText = el['show-text'] ? 'show-text' : ''
+    const showScore = el['show-score'] ? 'show-score' : ''
+
+    return `<${tag} ${vModel} ${max} ${allowHalf} ${showText} ${showScore} ${disabled}></${tag}>`
+  },
+  'el-color-picker': el => {
+    const { tag, disabled, vModel } = attrBuilder(el)
+    const size = `size="${el.size}"`
+    const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
+    const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
+
+    return `<${tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}></${tag}>`
+  },
+  'el-upload': el => {
+    const { tag } = el.__config__
+    const disabled = el.disabled ? ':disabled=\'true\'' : ''
+    const action = el.action ? `:action="${el.__vModel__}Action"` : ''
+    const multiple = el.multiple ? 'multiple' : ''
+    const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
+    const accept = el.accept ? `accept="${el.accept}"` : ''
+    const name = el.name !== 'file' ? `name="${el.name}"` : ''
+    const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
+    const beforeUpload = `:before-upload="${el.__vModel__}BeforeUpload"`
+    const fileList = `:file-list="${el.__vModel__}fileList"`
+    const ref = `ref="${el.__vModel__}"`
+    let child = buildElUploadChild(el)
+
+    if (child) child = `\n${child}\n` // 换行
+    return `<${tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}</${tag}>`
+  },
+  tinymce: el => {
+    const { tag, vModel, placeholder } = attrBuilder(el)
+    const height = el.height ? `:height="${el.height}"` : ''
+    const branding = el.branding ? `:branding="${el.branding}"` : ''
+    return `<${tag} ${vModel} ${placeholder} ${height} ${branding}></${tag}>`
+  }
+}
+
+function attrBuilder(el) {
+  return {
+    tag: el.__config__.tag,
+    vModel: `v-model="${confGlobal.formModel}.${el.__vModel__}"`,
+    clearable: el.clearable ? 'clearable' : '',
+    placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
+    width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
+    disabled: el.disabled ? ':disabled=\'true\'' : ''
+  }
+}
+
+// el-buttin 子级
+function buildElButtonChild(scheme) {
+  const children = []
+  const slot = scheme.__slot__ || {}
+  if (slot.default) {
+    children.push(slot.default)
+  }
+  return children.join('\n')
+}
+
+// el-input 子级
+function buildElInputChild(scheme) {
+  const children = []
+  const slot = scheme.__slot__
+  if (slot && slot.prepend) {
+    children.push(`<template slot="prepend">${slot.prepend}</template>`)
+  }
+  if (slot && slot.append) {
+    children.push(`<template slot="append">${slot.append}</template>`)
+  }
+  return children.join('\n')
+}
+
+// el-select 子级
+function buildElSelectChild(scheme) {
+  const children = []
+  const slot = scheme.__slot__
+  if (slot && slot.options && slot.options.length) {
+    children.push(`<el-option v-for="(item, index) in ${scheme.__vModel__}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>`)
+  }
+  return children.join('\n')
+}
+
+// el-radio-group 子级
+function buildElRadioGroupChild(scheme) {
+  const children = []
+  const slot = scheme.__slot__
+  const config = scheme.__config__
+  if (slot && slot.options && slot.options.length) {
+    const tag = config.optionType === 'button' ? 'el-radio-button' : 'el-radio'
+    const border = config.border ? 'border' : ''
+    children.push(`<${tag} v-for="(item, index) in ${scheme.__vModel__}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
+  }
+  return children.join('\n')
+}
+
+// el-checkbox-group 子级
+function buildElCheckboxGroupChild(scheme) {
+  const children = []
+  const slot = scheme.__slot__
+  const config = scheme.__config__
+  if (slot && slot.options && slot.options.length) {
+    const tag = config.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
+    const border = config.border ? 'border' : ''
+    children.push(`<${tag} v-for="(item, index) in ${scheme.__vModel__}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
+  }
+  return children.join('\n')
+}
+
+// el-upload 子级
+function buildElUploadChild(scheme) {
+  const list = []
+  const config = scheme.__config__
+  if (scheme['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
+  else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${config.buttonText}</el-button>`)
+  if (config.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${config.fileSize}${config.sizeUnit} 的${scheme.accept}文件</div>`)
+  return list.join('\n')
+}
+
+/**
+ * 组装html代码。【入口函数】
+ * @param {Object} formConfig 整个表单配置
+ * @param {String} type 生成类型,文件或弹窗等
+ */
+export function makeUpHtml(formConfig, type) {
+  const htmlList = []
+  confGlobal = formConfig
+  // 判断布局是否都沾满了24个栅格,以备后续简化代码结构
+  someSpanIsNot24 = formConfig.fields.some(item => item.__config__.span !== 24)
+  // 遍历渲染每个组件成html
+  formConfig.fields.forEach(el => {
+    htmlList.push(layouts[el.__config__.layout](el))
+  })
+  const htmlStr = htmlList.join('\n')
+  // 将组件代码放进form标签
+  let temp = buildFormTemplate(formConfig, htmlStr, type)
+  // dialog标签包裹代码
+  if (type === 'dialog') {
+    temp = dialogWrapper(temp)
+  }
+  confGlobal = null
+  return temp
+}

+ 253 - 0
src/components/generator/js.js

@@ -0,0 +1,253 @@
+import { isArray } from 'util'
+import { exportDefault, titleCase, deepClone } from '@/utils/generator/index'
+import ruleTrigger from './ruleTrigger'
+
+const units = {
+  KB: '1024',
+  MB: '1024 / 1024',
+  GB: '1024 / 1024 / 1024'
+}
+let confGlobal
+const inheritAttrs = {
+  file: '',
+  dialog: 'inheritAttrs: false,'
+}
+
+/**
+ * 组装js 【入口函数】
+ * @param {Object} formConfig 整个表单配置
+ * @param {String} type 生成类型,文件或弹窗等
+ */
+export function makeUpJs(formConfig, type) {
+  confGlobal = formConfig = deepClone(formConfig)
+  const dataList = []
+  const ruleList = []
+  const optionsList = []
+  const propsList = []
+  const methodList = mixinMethod(type)
+  const uploadVarList = []
+
+  formConfig.fields.forEach(el => {
+    buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
+  })
+
+  const script = buildexport(
+    formConfig,
+    type,
+    dataList.join('\n'),
+    ruleList.join('\n'),
+    optionsList.join('\n'),
+    uploadVarList.join('\n'),
+    propsList.join('\n'),
+    methodList.join('\n')
+  )
+  confGlobal = null
+  return script
+}
+
+// 构建组件属性
+function buildAttributes(scheme, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) {
+  const config = scheme.__config__
+  const slot = scheme.__slot__
+  buildData(scheme, dataList)
+  buildRules(scheme, ruleList)
+
+  // 特殊处理options属性
+  if (scheme.options || (slot && slot.options && slot.options.length)) {
+    buildOptions(scheme, optionsList)
+    if (config.dataType === 'dynamic') {
+      const model = `${scheme.__vModel__}Options`
+      const options = titleCase(model)
+      buildOptionMethod(`get${options}`, model, methodList)
+    }
+  }
+
+  // 处理props
+  if (scheme.props && scheme.props.props) {
+    buildProps(scheme, propsList)
+  }
+
+  // 处理el-upload的action
+  if (scheme.action && config.tag === 'el-upload') {
+    uploadVarList.push(
+      `${scheme.__vModel__}Action: '${scheme.action}',
+      ${scheme.__vModel__}fileList: [],`
+    )
+    methodList.push(buildBeforeUpload(scheme))
+    // 非自动上传时,生成手动上传的函数
+    if (!scheme['auto-upload']) {
+      methodList.push(buildSubmitUpload(scheme))
+    }
+  }
+
+  // 构建子级组件属性
+  if (config.children) {
+    config.children.forEach(item => {
+      buildAttributes(item, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
+    })
+  }
+}
+
+// 混入处理函数
+function mixinMethod(type) {
+  const list = []; const
+    minxins = {
+      file: confGlobal.formBtns ? {
+        submitForm: `submitForm() {
+        this.$refs['${confGlobal.formRef}'].validate(valid => {
+          if(!valid) return
+          // TODO 提交表单
+        })
+      },`,
+        resetForm: `resetForm() {
+        this.$refs['${confGlobal.formRef}'].resetFields()
+      },`
+      } : null,
+      dialog: {
+        onOpen: 'onOpen() {},',
+        onClose: `onClose() {
+        this.$refs['${confGlobal.formRef}'].resetFields()
+      },`,
+        close: `close() {
+        this.$emit('update:visible', false)
+      },`,
+        handelConfirm: `handelConfirm() {
+        this.$refs['${confGlobal.formRef}'].validate(valid => {
+          if(!valid) return
+          this.close()
+        })
+      },`
+      }
+    }
+
+  const methods = minxins[type]
+  if (methods) {
+    Object.keys(methods).forEach(key => {
+      list.push(methods[key])
+    })
+  }
+
+  return list
+}
+
+// 构建data
+function buildData(scheme, dataList) {
+  const config = scheme.__config__
+  if (scheme.__vModel__ === undefined) return
+  const defaultValue = JSON.stringify(config.defaultValue)
+  dataList.push(`${scheme.__vModel__}: ${defaultValue},`)
+}
+
+// 构建校验规则
+function buildRules(scheme, ruleList) {
+  const config = scheme.__config__
+  if (scheme.__vModel__ === undefined) return
+  const rules = []
+  if (ruleTrigger[config.tag]) {
+    if (config.required) {
+      const type = isArray(config.defaultValue) ? 'type: \'array\',' : ''
+      let message = isArray(config.defaultValue) ? `请至少选择一个${config.label}` : scheme.placeholder
+      if (message === undefined) message = `${config.label}不能为空`
+      rules.push(`{ required: true, ${type} message: '${message}', trigger: '${ruleTrigger[config.tag]}' }`)
+    }
+    if (config.regList && isArray(config.regList)) {
+      config.regList.forEach(item => {
+        if (item.pattern) {
+          rules.push(
+            `{ pattern: ${eval(item.pattern)}, message: '${item.message}', trigger: '${ruleTrigger[config.tag]}' }`
+          )
+        }
+      })
+    }
+    ruleList.push(`${scheme.__vModel__}: [${rules.join(',')}],`)
+  }
+}
+
+// 构建options
+function buildOptions(scheme, optionsList) {
+  if (scheme.__vModel__ === undefined) return
+  // el-cascader直接有options属性,其他组件都是定义在slot中,所以有两处判断
+  let { options } = scheme
+  if (!options) options = scheme.__slot__.options
+  if (scheme.__config__.dataType === 'dynamic') { options = [] }
+  const str = `${scheme.__vModel__}Options: ${JSON.stringify(options)},`
+  optionsList.push(str)
+}
+
+function buildProps(scheme, propsList) {
+  const str = `${scheme.__vModel__}Props: ${JSON.stringify(scheme.props.props)},`
+  propsList.push(str)
+}
+
+// el-upload的BeforeUpload
+function buildBeforeUpload(scheme) {
+  const config = scheme.__config__
+  const unitNum = units[config.sizeUnit]; let rightSizeCode = ''; let acceptCode = ''; const
+    returnList = []
+  if (config.fileSize) {
+    rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${config.fileSize}
+    if(!isRightSize){
+      this.$message.error('文件大小超过 ${config.fileSize}${config.sizeUnit}')
+    }`
+    returnList.push('isRightSize')
+  }
+  if (scheme.accept) {
+    acceptCode = `let isAccept = new RegExp('${scheme.accept}').test(file.type)
+    if(!isAccept){
+      this.$message.error('应该选择${scheme.accept}类型的文件')
+    }`
+    returnList.push('isAccept')
+  }
+  const str = `${scheme.__vModel__}BeforeUpload(file) {
+    ${rightSizeCode}
+    ${acceptCode}
+    return ${returnList.join('&&')}
+  },`
+  return returnList.length ? str : ''
+}
+
+// el-upload的submit
+function buildSubmitUpload(scheme) {
+  const str = `submitUpload() {
+    this.$refs['${scheme.__vModel__}'].submit()
+  },`
+  return str
+}
+
+function buildOptionMethod(methodName, model, methodList) {
+  const str = `${methodName}() {
+    // TODO 发起请求获取数据
+    this.${model}
+  },`
+  methodList.push(str)
+}
+
+// js整体拼接
+function buildexport(conf, type, data, rules, selectOptions, uploadVar, props, methods) {
+  const str = `${exportDefault}{
+  ${inheritAttrs[type]}
+  components: {},
+  props: [],
+  data () {
+    return {
+      ${conf.formModel}: {
+        ${data}
+      },
+      ${conf.formRules}: {
+        ${rules}
+      },
+      ${uploadVar}
+      ${selectOptions}
+      ${props}
+    }
+  },
+  computed: {},
+  watch: {},
+  created () {},
+  mounted () {},
+  methods: {
+    ${methods}
+  }
+}`
+  return str
+}

+ 17 - 0
src/components/generator/ruleTrigger.js

@@ -0,0 +1,17 @@
+
+/**
+ * 用于生成表单校验,指定正则规则的触发方式。
+ * 未在此处声明无触发方式的组件将不生成rule!!
+ */
+export default {
+  'el-input': 'blur',
+  'el-input-number': 'blur',
+  'el-select': 'change',
+  'el-radio-group': 'change',
+  'el-checkbox-group': 'change',
+  'el-cascader': 'change',
+  'el-time-picker': 'change',
+  'el-date-picker': 'change',
+  'el-rate': 'change',
+  tinymce: 'blur'
+}

+ 186 - 0
src/components/parser/Parser.vue

@@ -0,0 +1,186 @@
+<script>
+import { deepClone } from '@/utils/generator/index'
+import render from '@/components/render/render.js'
+
+const ruleTrigger = {
+  'el-input': 'blur',
+  'el-input-number': 'blur',
+  'el-select': 'change',
+  'el-radio-group': 'change',
+  'el-checkbox-group': 'change',
+  'el-cascader': 'change',
+  'el-time-picker': 'change',
+  'el-date-picker': 'change',
+  'el-rate': 'change'
+}
+
+const layouts = {
+  colFormItem(h, scheme) {
+    const config = scheme.__config__
+    const listeners = buildListeners.call(this, scheme)
+
+    let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
+    if (config.showLabel === false) labelWidth = '0'
+    return (
+      <el-col span={config.span}>
+        <el-form-item label-width={labelWidth} prop={scheme.__vModel__}
+          label={config.showLabel ? config.label : ''}>
+          <render conf={scheme} {...{ on: listeners }} />
+        </el-form-item>
+      </el-col>
+    )
+  },
+  rowFormItem(h, scheme) {
+    let child = renderChildren.apply(this, arguments)
+    if (scheme.type === 'flex') {
+      child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}>
+              {child}
+            </el-row>
+    }
+    return (
+      <el-col span={scheme.span}>
+        <el-row gutter={scheme.gutter}>
+          {child}
+        </el-row>
+      </el-col>
+    )
+  }
+}
+
+function renderFrom(h) {
+  const { formConfCopy } = this
+
+  return (
+    <el-row gutter={formConfCopy.gutter}>
+      <el-form
+        size={formConfCopy.size}
+        label-position={formConfCopy.labelPosition}
+        disabled={formConfCopy.disabled}
+        label-width={`${formConfCopy.labelWidth}px`}
+        ref={formConfCopy.formRef}
+        // model不能直接赋值 https://github.com/vuejs/jsx/issues/49#issuecomment-472013664
+        props={{ model: this[formConfCopy.formModel] }}
+        rules={this[formConfCopy.formRules]}
+      >
+        {renderFormItem.call(this, h, formConfCopy.fields)}
+        {formConfCopy.formBtns && formBtns.call(this, h)}
+      </el-form>
+    </el-row>
+  )
+}
+
+function formBtns(h) {
+  return <el-col>
+    <el-form-item size="large">
+      <el-button type="primary" onClick={this.submitForm}>提交</el-button>
+      <el-button onClick={this.resetForm}>重置</el-button>
+    </el-form-item>
+  </el-col>
+}
+
+function renderFormItem(h, elementList) {
+  return elementList.map(scheme => {
+    const config = scheme.__config__
+    const layout = layouts[config.layout]
+
+    if (layout) {
+      return layout.call(this, h, scheme)
+    }
+    throw new Error(`没有与${config.layout}匹配的layout`)
+  })
+}
+
+function renderChildren(h, scheme) {
+  const config = scheme.__config__
+  if (!Array.isArray(config.children)) return null
+  return renderFormItem.call(this, h, config.children)
+}
+
+function setValue(event, config, scheme) {
+  this.$set(config, 'defaultValue', event)
+  this.$set(this[this.formConf.formModel], scheme.__vModel__, event)
+}
+
+function buildListeners(scheme) {
+  const config = scheme.__config__
+  const methods = this.formConf.__methods__ || {}
+  const listeners = {}
+
+  // 给__methods__中的方法绑定this和event
+  Object.keys(methods).forEach(key => {
+    listeners[key] = event => methods[key].call(this, event)
+  })
+  // 响应 render.js 中的 vModel $emit('input', val)
+  listeners.input = event => setValue.call(this, event, config, scheme)
+
+  return listeners
+}
+
+export default {
+  components: {
+    render
+  },
+  props: {
+    formConf: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    const data = {
+      formConfCopy: deepClone(this.formConf),
+      [this.formConf.formModel]: {},
+      [this.formConf.formRules]: {}
+    }
+    this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel])
+    this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules])
+    return data
+  },
+  methods: {
+    initFormData(componentList, formData) {
+      componentList.forEach(cur => {
+        const config = cur.__config__
+        if (cur.__vModel__) formData[cur.__vModel__] = config.defaultValue
+        if (config.children) this.initFormData(config.children, formData)
+      })
+    },
+    buildRules(componentList, rules) {
+      componentList.forEach(cur => {
+        const config = cur.__config__
+        if (Array.isArray(config.regList)) {
+          if (config.required) {
+            const required = { required: config.required, message: cur.placeholder }
+            if (Array.isArray(config.defaultValue)) {
+              required.type = 'array'
+              required.message = `请至少选择一个${config.label}`
+            }
+            required.message === undefined && (required.message = `${config.label}不能为空`)
+            config.regList.push(required)
+          }
+          rules[cur.__vModel__] = config.regList.map(item => {
+            item.pattern && (item.pattern = eval(item.pattern))
+            item.trigger = ruleTrigger && ruleTrigger[config.tag]
+            return item
+          })
+        }
+        if (config.children) this.buildRules(config.children, rules)
+      })
+    },
+    resetForm() {
+      this.formConfCopy = deepClone(this.formConf)
+      this.$refs[this.formConf.formRef].resetFields()
+    },
+    submitForm() {
+      this.$refs[this.formConf.formRef].validate(valid => {
+        if (!valid) return false
+        // 触发sumit事件
+        this.$emit('submit', this[this.formConf.formModel])
+        return true
+      })
+    }
+  },
+  render(h) {
+    return renderFrom.call(this, h)
+  }
+}
+</script>

+ 17 - 0
src/components/parser/README.md

@@ -0,0 +1,17 @@
+## form-generator JSON 解析器
+>用于将form-generator导出的JSON解析成一个表单。
+
+### 安装组件
+```
+npm i form-gen-parser
+```
+或者
+```
+yarn add form-gen-parser
+```
+
+### 使用示例
+> [查看在线示例](https://mrhj.gitee.io/form-generator/#/parser)  
+
+示例代码:  
+> [src\components\parser\example\Index.vue](https://github.com/JakHuang/form-generator/blob/dev/src/components/parser/example/Index.vue)

+ 324 - 0
src/components/parser/example/Index.vue

@@ -0,0 +1,324 @@
+<template>
+  <div class="test-form">
+    <parser :form-conf="formConf" @submit="sumbitForm1" />
+    <parser :key="key2" :form-conf="formConf" @submit="sumbitForm2" />
+    <el-button @click="change">
+      change
+    </el-button>
+  </div>
+</template>
+
+<script>
+import Parser from '../Parser'
+
+// 若parser是通过安装npm方式集成到项目中的,使用此行引入
+// import Parser from 'form-gen-parser'
+
+export default {
+  components: {
+    Parser
+  },
+  props: {},
+  data() {
+    return {
+      key2: +new Date(),
+      formConf: {
+        fields: [
+          {
+            __config__: {
+              label: '单行文本',
+              labelWidth: null,
+              showLabel: true,
+              changeTag: true,
+              tag: 'el-input',
+              tagIcon: 'input',
+              required: true,
+              layout: 'colFormItem',
+              span: 24,
+              document: 'https://element.eleme.cn/#/zh-CN/component/input',
+              regList: [
+                {
+                  pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
+                  message: '手机号格式错误'
+                }
+              ]
+            },
+            __slot__: {
+              prepend: '',
+              append: ''
+            },
+            __vModel__: 'mobile',
+            placeholder: '请输入手机号',
+            style: {
+              width: '100%'
+            },
+            clearable: true,
+            'prefix-icon': 'el-icon-mobile',
+            'suffix-icon': '',
+            maxlength: 11,
+            'show-word-limit': true,
+            readonly: false,
+            disabled: false
+          },
+          {
+            __config__: {
+              label: '日期范围',
+              tag: 'el-date-picker',
+              tagIcon: 'date-range',
+              defaultValue: null,
+              span: 24,
+              showLabel: true,
+              labelWidth: null,
+              required: true,
+              layout: 'colFormItem',
+              regList: [],
+              changeTag: true,
+              document:
+                'https://element.eleme.cn/#/zh-CN/component/date-picker',
+              formId: 101,
+              renderKey: 1585980082729
+            },
+            style: {
+              width: '100%'
+            },
+            type: 'daterange',
+            'range-separator': '至',
+            'start-placeholder': '开始日期',
+            'end-placeholder': '结束日期',
+            disabled: false,
+            clearable: true,
+            format: 'yyyy-MM-dd',
+            'value-format': 'yyyy-MM-dd',
+            readonly: false,
+            __vModel__: 'field101'
+          },
+          {
+            __config__: {
+              layout: 'rowFormItem',
+              tagIcon: 'row',
+              label: '行容器',
+              layoutTree: true,
+              children: [
+                {
+                  __config__: {
+                    label: '评分',
+                    tag: 'el-rate',
+                    tagIcon: 'rate',
+                    defaultValue: 0,
+                    span: 24,
+                    showLabel: true,
+                    labelWidth: null,
+                    layout: 'colFormItem',
+                    required: true,
+                    regList: [],
+                    changeTag: true,
+                    document: 'https://element.eleme.cn/#/zh-CN/component/rate',
+                    formId: 102,
+                    renderKey: 1586839671259
+                  },
+                  style: {},
+                  max: 5,
+                  'allow-half': false,
+                  'show-text': false,
+                  'show-score': false,
+                  disabled: false,
+                  __vModel__: 'field102'
+                }
+              ],
+              document: 'https://element.eleme.cn/#/zh-CN/component/layout',
+              formId: 101,
+              span: 24,
+              renderKey: 1586839668999,
+              componentName: 'row101',
+              gutter: 15
+            },
+            type: 'default',
+            justify: 'start',
+            align: 'top'
+          },
+          {
+            __config__: {
+              label: '按钮',
+              showLabel: true,
+              changeTag: true,
+              labelWidth: null,
+              tag: 'el-button',
+              tagIcon: 'button',
+              span: 24,
+              layout: 'colFormItem',
+              document: 'https://element.eleme.cn/#/zh-CN/component/button',
+              renderKey: 1594288459289
+            },
+            __slot__: {
+              default: '测试按钮1'
+            },
+            type: 'primary',
+            icon: 'el-icon-search',
+            round: false,
+            size: 'medium',
+            plain: false,
+            circle: false,
+            disabled: false,
+            on: {
+              click: 'clickTestButton1'
+            }
+          }
+        ],
+        __methods__: {
+          clickTestButton1() {
+            console.log(
+              `%c【测试按钮1】点击事件里可以访问当前表单:
+                1) formModel='formData', 所以this.formData可以拿到当前表单的model
+                2) formRef='elForm', 所以this.$refs.elForm可以拿到当前表单的ref(vue组件)
+              `,
+              'color:#409EFF;font-size: 15px'
+            )
+            console.log('表单的Model:', this.formData)
+            console.log('表单的ref:', this.$refs.elForm)
+          }
+        },
+        formRef: 'elForm',
+        formModel: 'formData',
+        size: 'small',
+        labelPosition: 'right',
+        labelWidth: 100,
+        formRules: 'rules',
+        gutter: 15,
+        disabled: false,
+        span: 24,
+        formBtns: true,
+        unFocusedComponentBorder: false
+      },
+      formConf2: {
+        fields: [
+          {
+            __config__: {
+              label: '单行文本',
+              labelWidth: null,
+              showLabel: true,
+              changeTag: true,
+              tag: 'el-input',
+              tagIcon: 'input',
+              required: true,
+              layout: 'colFormItem',
+              span: 24,
+              document: 'https://element.eleme.cn/#/zh-CN/component/input',
+              regList: [
+                {
+                  pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
+                  message: '手机号格式错误'
+                }
+              ]
+            },
+            __slot__: {
+              prepend: '',
+              append: ''
+            },
+            __vModel__: 'mobile',
+            placeholder: '请输入手机号',
+            style: {
+              width: '100%'
+            },
+            clearable: true,
+            'prefix-icon': 'el-icon-mobile',
+            'suffix-icon': '',
+            maxlength: 11,
+            'show-word-limit': true,
+            readonly: false,
+            disabled: false
+          },
+          {
+            __config__: {
+              label: '日期范围',
+              tag: 'el-date-picker',
+              tagIcon: 'date-range',
+              defaultValue: null,
+              span: 24,
+              showLabel: true,
+              labelWidth: null,
+              required: true,
+              layout: 'colFormItem',
+              regList: [],
+              changeTag: true,
+              document:
+                'https://element.eleme.cn/#/zh-CN/component/date-picker',
+              formId: 101,
+              renderKey: 1585980082729
+            },
+            style: {
+              width: '100%'
+            },
+            type: 'daterange',
+            'range-separator': '至',
+            'start-placeholder': '开始日期',
+            'end-placeholder': '结束日期',
+            disabled: false,
+            clearable: true,
+            format: 'yyyy-MM-dd',
+            'value-format': 'yyyy-MM-dd',
+            readonly: false,
+            __vModel__: 'field101'
+          }
+        ],
+        formRef: 'elForm',
+        formModel: 'formData',
+        size: 'small',
+        labelPosition: 'right',
+        labelWidth: 100,
+        formRules: 'rules',
+        gutter: 15,
+        disabled: false,
+        span: 24,
+        formBtns: true,
+        unFocusedComponentBorder: false
+      }
+    }
+  },
+  computed: {},
+  watch: {},
+  created() {},
+  mounted() {
+    // 表单数据回填,模拟异步请求场景
+    setTimeout(() => {
+      // 请求回来的表单数据
+      const data = {
+        mobile: '18836662555'
+      }
+      // 回填数据
+      this.fillFormData(this.formConf, data)
+      // 更新表单
+      this.key2 = +new Date()
+    }, 2000)
+  },
+  methods: {
+    fillFormData(form, data) {
+      form.fields.forEach(item => {
+        const val = data[item.__vModel__]
+        if (val) {
+          item.__config__.defaultValue = val
+        }
+      })
+    },
+    change() {
+      this.key2 = +new Date()
+      const t = this.formConf
+      this.formConf = this.formConf2
+      this.formConf2 = t
+    },
+    sumbitForm1(data) {
+      console.log('sumbitForm1提交数据:', data)
+    },
+    sumbitForm2(data) {
+      console.log('sumbitForm2提交数据:', data)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.test-form {
+  margin: 15px auto;
+  width: 800px;
+  padding: 15px;
+}
+</style>

+ 3 - 0
src/components/parser/index.js

@@ -0,0 +1,3 @@
+import Parser from './Parser'
+
+export default Parser

+ 25 - 0
src/components/parser/package.json

@@ -0,0 +1,25 @@
+{
+  "name": "form-gen-parser",
+  "version": "1.0.3",
+  "description": "表单json解析器",
+  "main": "lib/form-gen-parser.umd.js",
+  "directories": {
+    "example": "example"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/JakHuang/form-generator.git"
+  },
+  "dependencies": {
+    "form-gen-render": "^1.0.0"
+  },
+  "author": "jakHuang",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/JakHuang/form-generator/issues"
+  },
+  "homepage": "https://github.com/JakHuang/form-generator/blob/dev/src/components/parser"
+}

+ 19 - 0
src/components/render/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "form-gen-render",
+  "version": "1.0.4",
+  "description": "表单核心render",
+  "main": "lib/form-gen-render.umd.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/JakHuang/form-generator.git"
+  },
+  "author": "jakhuang",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/JakHuang/form-generator/issues"
+  },
+  "homepage": "https://github.com/JakHuang/form-generator#readme"
+}

+ 104 - 0
src/components/render/render.js

@@ -0,0 +1,104 @@
+import { deepClone } from '@/utils/generator/index'
+
+const componentChild = {}
+/**
+ * 将./slots中的文件挂载到对象componentChild上
+ * 文件名为key,对应JSON配置中的__config__.tag
+ * 文件内容为value,解析JSON配置中的__slot__
+ */
+const slotsFiles = require.context('./slots', false, /\.js$/)
+const keys = slotsFiles.keys() || []
+keys.forEach(key => {
+  const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
+  const value = slotsFiles(key).default
+  componentChild[tag] = value
+})
+
+function vModel(dataObject, defaultValue) {
+  dataObject.props.value = defaultValue
+
+  dataObject.on.input = val => {
+    this.$emit('input', val)
+  }
+}
+
+function mountSlotFlies(h, confClone, children) {
+  const childObjs = componentChild[confClone.__config__.tag]
+  if (childObjs) {
+    Object.keys(childObjs).forEach(key => {
+      const childFunc = childObjs[key]
+      if (confClone.__slot__ && confClone.__slot__[key]) {
+        children.push(childFunc(h, confClone, key))
+      }
+    })
+  }
+}
+
+function emitEvents(confClone) {
+  ['on', 'nativeOn'].forEach(attr => {
+    const eventKeyList = Object.keys(confClone[attr] || {})
+    eventKeyList.forEach(key => {
+      const val = confClone[attr][key]
+      if (typeof val === 'string') {
+        confClone[attr][key] = event => this.$emit(val, event)
+      }
+    })
+  })
+}
+
+function buildDataObject(confClone, dataObject) {
+  Object.keys(confClone).forEach(key => {
+    const val = confClone[key]
+    if (key === '__vModel__') {
+      vModel.call(this, dataObject, confClone.__config__.defaultValue)
+    } else if (dataObject[key]) {
+      dataObject[key] = { ...dataObject[key], ...val }
+    } else {
+      dataObject.attrs[key] = val
+    }
+  })
+
+  // 清理属性
+  clearAttrs(dataObject)
+}
+
+function clearAttrs(dataObject) {
+  delete dataObject.attrs.__config__
+  delete dataObject.attrs.__slot__
+  delete dataObject.attrs.__methods__
+}
+
+function makeDataObject() {
+  return {
+    attrs: {},
+    props: {},
+    nativeOn: {},
+    on: {},
+    style: {}
+  }
+}
+
+export default {
+  props: {
+    conf: {
+      type: Object,
+      required: true
+    }
+  },
+  render(h) {
+    const dataObject = makeDataObject()
+    const confClone = deepClone(this.conf)
+    const children = []
+
+    // 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码
+    mountSlotFlies.call(this, h, confClone, children)
+
+    // 将字符串类型的事件,发送为消息
+    emitEvents.call(this, confClone)
+
+    // 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)”
+    buildDataObject.call(this, confClone, dataObject)
+
+    return h(this.conf.__config__.tag, dataObject, children)
+  }
+}

+ 5 - 0
src/components/render/slots/el-button.js

@@ -0,0 +1,5 @@
+export default {
+  default(h, conf, key) {
+    return conf.__slot__[key]
+  }
+}

+ 13 - 0
src/components/render/slots/el-checkbox-group.js

@@ -0,0 +1,13 @@
+export default {
+  options(h, conf, key) {
+    const list = []
+    conf.__slot__.options.forEach(item => {
+      if (conf.__config__.optionType === 'button') {
+        list.push(<el-checkbox-button label={item.value}>{item.label}</el-checkbox-button>)
+      } else {
+        list.push(<el-checkbox label={item.value} border={conf.border}>{item.label}</el-checkbox>)
+      }
+    })
+    return list
+  }
+}

+ 8 - 0
src/components/render/slots/el-input.js

@@ -0,0 +1,8 @@
+export default {
+  prepend(h, conf, key) {
+    return <template slot="prepend">{conf.__slot__[key]}</template>
+  },
+  append(h, conf, key) {
+    return <template slot="append">{conf.__slot__[key]}</template>
+  }
+}

+ 13 - 0
src/components/render/slots/el-radio-group.js

@@ -0,0 +1,13 @@
+export default {
+  options(h, conf, key) {
+    const list = []
+    conf.__slot__.options.forEach(item => {
+      if (conf.__config__.optionType === 'button') {
+        list.push(<el-radio-button label={item.value}>{item.label}</el-radio-button>)
+      } else {
+        list.push(<el-radio label={item.value} border={conf.border}>{item.label}</el-radio>)
+      }
+    })
+    return list
+  }
+}

+ 9 - 0
src/components/render/slots/el-select.js

@@ -0,0 +1,9 @@
+export default {
+  options(h, conf, key) {
+    const list = []
+    conf.__slot__.options.forEach(item => {
+      list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
+    })
+    return list
+  }
+}

+ 17 - 0
src/components/render/slots/el-upload.js

@@ -0,0 +1,17 @@
+export default {
+  'list-type': (h, conf, key) => {
+    const list = []
+    const config = conf.__config__
+    if (conf['list-type'] === 'picture-card') {
+      list.push(<i class="el-icon-plus"></i>)
+    } else {
+      list.push(<el-button size="small" type="primary" icon="el-icon-upload">{config.buttonText}</el-button>)
+    }
+    if (config.showTip) {
+      list.push(
+        <div slot="tip" class="el-upload__tip">只能上传不超过 {config.fileSize}{config.sizeUnit} 的{conf.accept}文件</div>
+      )
+    }
+    return list
+  }
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/icons/svg/button.svg


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/icons/svg/rich-text.svg


+ 12 - 0
src/main.js

@@ -23,6 +23,16 @@ import './icons' // icon
 import './permission' // permission control
 import './utils/error-log' // error log
 
+import '@/styles/index.scss'
+
+import '@/styles/generator/index.scss'
+
+import Tinymce from '@/components/tinymce/index.vue'
+
+
+import '@/icons'
+
+
 import * as filters from './filters' // global filters
 
 import Pagination from '@/components/Pagination'
@@ -39,6 +49,8 @@ Vue.prototype.download = download
 // 全局组件挂载
 Vue.component('Pagination', Pagination)
 
+Vue.component('tinymce', Tinymce)
+
 Vue.prototype.msgSuccess = function(msg) {
   this.$message({ showClose: true, message: msg, type: 'success' })
 }

+ 271 - 0
src/styles/generator.scss

@@ -0,0 +1,271 @@
+$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(.active-from-item) > 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;
+    }
+  }
+}

+ 271 - 0
src/styles/generator/home.scss

@@ -0,0 +1,271 @@
+$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(.active-from-item) > 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;
+    }
+  }
+}

+ 137 - 0
src/styles/generator/index.scss

@@ -0,0 +1,137 @@
+$editorTabsborderColor: #121315;
+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: $editorTabsborderColor;
+  .el-tabs__header{
+    margin: 0;
+    border-bottom-color: $editorTabsborderColor;
+    .el-tabs__nav{
+      border-color: $editorTabsborderColor;
+    }
+  }
+  .el-tabs__item{
+    height: 32px;
+    line-height: 32px;
+    color: #888a8e;
+    border-left: 1px solid $editorTabsborderColor!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;
+  }
+}
+.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;
+  }
+}
+
+.el-scrollbar__view{
+  overflow-x: hidden;
+}
+
+.el-rate{
+  display: inline-block;
+  vertical-align: text-top;
+}
+.el-upload__tip{
+  line-height: 1.2;
+}

+ 33 - 0
src/styles/generator/mixin.scss

@@ -0,0 +1,33 @@
+@mixin action-bar {
+  .action-bar {
+    height: 33px;
+    background: #f2fafb;
+    padding: 0 15px;
+    box-sizing: border-box;
+  
+    .bar-btn {
+      display: inline-block;
+      padding: 0 6px;
+      line-height: 32px;
+      color: #8285f5;
+      cursor: pointer;
+      font-size: 14px;
+      user-select: none;
+      & i {
+        font-size: 20px;
+      }
+      &:hover {
+        color: #4348d4;
+      }
+    }
+    .bar-btn + .bar-btn {
+      margin-left: 8px;
+    }
+    .delete-btn {
+      color: #f56c6c;
+      &:hover {
+        color: #ea0b30;
+      }
+    }
+  }
+}

+ 0 - 423
src/utils/generator/config.js

@@ -1,423 +0,0 @@
-export const formConf = {
-  formRef: 'elForm',
-  formModel: 'formData',
-  size: 'medium',
-  labelPosition: 'right',
-  labelWidth: 100,
-  formRules: 'rules',
-  gutter: 15,
-  disabled: false,
-  span: 24,
-  formBtns: true
-}
-
-export const inputComponents = [
-  {
-    label: '单行文本',
-    tag: 'el-input',
-    tagIcon: 'input',
-    placeholder: '请输入',
-    defaultValue: undefined,
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    clearable: true,
-    prepend: '',
-    append: '',
-    'prefix-icon': '',
-    'suffix-icon': '',
-    maxlength: null,
-    'show-word-limit': false,
-    readonly: false,
-    disabled: false,
-    required: true,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/input'
-  },
-  {
-    label: '多行文本',
-    tag: 'el-input',
-    tagIcon: 'textarea',
-    type: 'textarea',
-    placeholder: '请输入',
-    defaultValue: undefined,
-    span: 24,
-    labelWidth: null,
-    autosize: {
-      minRows: 4,
-      maxRows: 4
-    },
-    style: { width: '100%' },
-    maxlength: null,
-    'show-word-limit': false,
-    readonly: false,
-    disabled: false,
-    required: true,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/input'
-  },
-  {
-    label: '密码',
-    tag: 'el-input',
-    tagIcon: 'password',
-    placeholder: '请输入',
-    defaultValue: undefined,
-    span: 24,
-    'show-password': true,
-    labelWidth: null,
-    style: { width: '100%' },
-    clearable: true,
-    prepend: '',
-    append: '',
-    'prefix-icon': '',
-    'suffix-icon': '',
-    maxlength: null,
-    'show-word-limit': false,
-    readonly: false,
-    disabled: false,
-    required: true,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/input'
-  },
-  {
-    label: '计数器',
-    tag: 'el-input-number',
-    tagIcon: 'number',
-    placeholder: '',
-    defaultValue: undefined,
-    span: 24,
-    labelWidth: null,
-    min: undefined,
-    max: undefined,
-    step: undefined,
-    'step-strictly': false,
-    precision: undefined,
-    'controls-position': '',
-    disabled: false,
-    required: true,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/input-number'
-  }
-]
-
-export const selectComponents = [
-  {
-    label: '下拉选择',
-    tag: 'el-select',
-    tagIcon: 'select',
-    placeholder: '请选择',
-    defaultValue: undefined,
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    clearable: true,
-    disabled: false,
-    required: true,
-    filterable: false,
-    multiple: false,
-    options: [{
-      label: '选项一',
-      value: 1
-    }, {
-      label: '选项二',
-      value: 2
-    }],
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/select'
-  },
-  {
-    label: '级联选择',
-    tag: 'el-cascader',
-    tagIcon: 'cascader',
-    placeholder: '请选择',
-    defaultValue: [],
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    props: {
-      props: {
-        multiple: false
-      }
-    },
-    'show-all-levels': true,
-    disabled: false,
-    clearable: true,
-    filterable: false,
-    required: true,
-    options: [{
-      id: 1,
-      value: 1,
-      label: '选项1',
-      children: [{
-        id: 2,
-        value: 2,
-        label: '选项1-1'
-      }]
-    }],
-    dataType: 'dynamic',
-    labelKey: 'label',
-    valueKey: 'value',
-    childrenKey: 'children',
-    separator: '/',
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/cascader'
-  },
-  {
-    label: '单选框组',
-    tag: 'el-radio-group',
-    tagIcon: 'radio',
-    defaultValue: undefined,
-    span: 24,
-    labelWidth: null,
-    style: {},
-    optionType: 'default',
-    border: false,
-    size: 'medium',
-    disabled: false,
-    required: true,
-    options: [{
-      label: '选项一',
-      value: 1
-    }, {
-      label: '选项二',
-      value: 2
-    }],
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/radio'
-  },
-  {
-    label: '多选框组',
-    tag: 'el-checkbox-group',
-    tagIcon: 'checkbox',
-    defaultValue: [],
-    span: 24,
-    labelWidth: null,
-    style: {},
-    optionType: 'default',
-    border: false,
-    size: 'medium',
-    disabled: false,
-    required: true,
-    options: [{
-      label: '选项一',
-      value: 1
-    }, {
-      label: '选项二',
-      value: 2
-    }],
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/checkbox'
-  },
-  {
-    label: '开关',
-    tag: 'el-switch',
-    tagIcon: 'switch',
-    defaultValue: false,
-    span: 24,
-    labelWidth: null,
-    style: {},
-    disabled: false,
-    required: true,
-    'active-text': '',
-    'inactive-text': '',
-    'active-color': null,
-    'inactive-color': null,
-    'active-value': true,
-    'inactive-value': false,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/switch'
-  },
-  {
-    label: '滑块',
-    tag: 'el-slider',
-    tagIcon: 'slider',
-    defaultValue: null,
-    span: 24,
-    labelWidth: null,
-    disabled: false,
-    required: true,
-    min: 0,
-    max: 100,
-    step: 1,
-    'show-stops': false,
-    range: false,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/slider'
-  },
-  {
-    label: '时间选择',
-    tag: 'el-time-picker',
-    tagIcon: 'time',
-    placeholder: '请选择',
-    defaultValue: null,
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    disabled: false,
-    clearable: true,
-    required: true,
-    'picker-options': {
-      selectableRange: '00:00:00-23:59:59'
-    },
-    format: 'HH:mm:ss',
-    'value-format': 'HH:mm:ss',
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
-  },
-  {
-    label: '时间范围',
-    tag: 'el-time-picker',
-    tagIcon: 'time-range',
-    defaultValue: null,
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    disabled: false,
-    clearable: true,
-    required: true,
-    'is-range': true,
-    'range-separator': '至',
-    'start-placeholder': '开始时间',
-    'end-placeholder': '结束时间',
-    format: 'HH:mm:ss',
-    'value-format': 'HH:mm:ss',
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
-  },
-  {
-    label: '日期选择',
-    tag: 'el-date-picker',
-    tagIcon: 'date',
-    placeholder: '请选择',
-    defaultValue: null,
-    type: 'date',
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    disabled: false,
-    clearable: true,
-    required: true,
-    format: 'yyyy-MM-dd',
-    'value-format': 'yyyy-MM-dd',
-    readonly: false,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/date-picker'
-  },
-  {
-    label: '日期范围',
-    tag: 'el-date-picker',
-    tagIcon: 'date-range',
-    defaultValue: null,
-    span: 24,
-    labelWidth: null,
-    style: { width: '100%' },
-    type: 'daterange',
-    'range-separator': '至',
-    'start-placeholder': '开始日期',
-    'end-placeholder': '结束日期',
-    disabled: false,
-    clearable: true,
-    required: true,
-    format: 'yyyy-MM-dd',
-    'value-format': 'yyyy-MM-dd',
-    readonly: false,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/date-picker'
-  },
-  {
-    label: '评分',
-    tag: 'el-rate',
-    tagIcon: 'rate',
-    defaultValue: 0,
-    span: 24,
-    labelWidth: null,
-    style: {},
-    max: 5,
-    'allow-half': false,
-    'show-text': false,
-    'show-score': false,
-    disabled: false,
-    required: true,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/rate'
-  },
-  {
-    label: '颜色选择',
-    tag: 'el-color-picker',
-    tagIcon: 'color',
-    defaultValue: null,
-    labelWidth: null,
-    'show-alpha': false,
-    'color-format': '',
-    disabled: false,
-    required: true,
-    size: 'medium',
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/color-picker'
-  },
-  {
-    label: '上传',
-    tag: 'el-upload',
-    tagIcon: 'upload',
-    action: 'https://jsonplaceholder.typicode.com/posts/',
-    defaultValue: null,
-    labelWidth: null,
-    disabled: false,
-    required: true,
-    accept: '',
-    name: 'file',
-    'auto-upload': true,
-    showTip: false,
-    buttonText: '点击上传',
-    fileSize: 2,
-    sizeUnit: 'MB',
-    'list-type': 'text',
-    multiple: false,
-    regList: [],
-    changeTag: true,
-    document: 'https://element.eleme.cn/#/zh-CN/component/upload'
-  }
-]
-
-export const layoutComponents = [
-  {
-    layout: 'rowFormItem',
-    tagIcon: 'row',
-    type: 'default',
-    justify: 'start',
-    align: 'top',
-    label: '行容器',
-    layoutTree: true,
-    children: [],
-    document: 'https://element.eleme.cn/#/zh-CN/component/layout'
-  }
-]
-
-// 组件rule的触发方式,无触发方式的组件不生成rule
-export const trigger = {
-  'el-input': 'blur',
-  'el-input-number': 'blur',
-  'el-select': 'change',
-  'el-radio-group': 'change',
-  'el-checkbox-group': 'change',
-  'el-cascader': 'change',
-  'el-time-picker': 'change',
-  'el-date-picker': 'change',
-  'el-rate': 'change'
-}

+ 54 - 0
src/utils/generator/db.js

@@ -0,0 +1,54 @@
+const DRAWING_ITEMS = 'drawingItems'
+const DRAWING_ITEMS_VERSION = '1.1'
+const DRAWING_ITEMS_VERSION_KEY = 'DRAWING_ITEMS_VERSION'
+const DRAWING_ID = 'idGlobal'
+const TREE_NODE_ID = 'treeNodeId'
+const FORM_CONF = 'formConf'
+
+export function getDrawingList() {
+  // 加入缓存版本的概念,保证缓存数据与程序匹配
+  const version = localStorage.getItem(DRAWING_ITEMS_VERSION_KEY)
+  if (version !== DRAWING_ITEMS_VERSION) {
+    localStorage.setItem(DRAWING_ITEMS_VERSION_KEY, DRAWING_ITEMS_VERSION)
+    saveDrawingList([])
+    return null
+  }
+
+  const str = localStorage.getItem(DRAWING_ITEMS)
+  if (str) return JSON.parse(str)
+  return null
+}
+
+export function saveDrawingList(list) {
+  localStorage.setItem(DRAWING_ITEMS, JSON.stringify(list))
+}
+
+export function getIdGlobal() {
+  const str = localStorage.getItem(DRAWING_ID)
+  if (str) return parseInt(str, 10)
+  return 100
+}
+
+export function saveIdGlobal(id) {
+  localStorage.setItem(DRAWING_ID, `${id}`)
+}
+
+export function getTreeNodeId() {
+  const str = localStorage.getItem(TREE_NODE_ID)
+  if (str) return parseInt(str, 10)
+  return 100
+}
+
+export function saveTreeNodeId(id) {
+  localStorage.setItem(TREE_NODE_ID, `${id}`)
+}
+
+export function getFormConf() {
+  const str = localStorage.getItem(FORM_CONF)
+  if (str) return JSON.parse(str)
+  return null
+}
+
+export function saveFormConf(obj) {
+  localStorage.setItem(FORM_CONF, JSON.stringify(obj))
+}

+ 0 - 29
src/utils/generator/drawingDefalut.js

@@ -1,29 +0,0 @@
-export default [
-  {
-    layout: 'colFormItem',
-    tagIcon: 'input',
-    label: '手机号',
-    vModel: 'mobile',
-    formId: 6,
-    tag: 'el-input',
-    placeholder: '请输入手机号',
-    defaultValue: '',
-    span: 24,
-    style: { width: '100%' },
-    clearable: true,
-    prepend: '',
-    append: '',
-    'prefix-icon': 'el-icon-mobile',
-    'suffix-icon': '',
-    maxlength: 11,
-    'show-word-limit': true,
-    readonly: false,
-    disabled: false,
-    required: true,
-    changeTag: true,
-    regList: [{
-      pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
-      message: '手机号格式错误'
-    }]
-  }
-]

+ 0 - 339
src/utils/generator/html.js

@@ -1,339 +0,0 @@
-/* eslint-disable max-len */
-import { trigger } from './config'
-
-let confGlobal
-let someSpanIsNot24
-
-export function dialogWrapper(str) {
-  return `<el-dialog v-bind="$attrs" v-on="$listeners" @open="onOpen" @close="onClose" title="Dialog Titile">
-    ${str}
-    <div slot="footer">
-      <el-button @click="close">取消</el-button>
-      <el-button type="primary" @click="handelConfirm">确定</el-button>
-    </div>
-  </el-dialog>`
-}
-
-export function vueTemplate(str) {
-  return `<template>
-    <div>
-      ${str}
-    </div>
-  </template>`
-}
-
-export function vueScript(str) {
-  return `<script>
-    ${str}
-  </script>`
-}
-
-export function cssStyle(cssStr) {
-  return `<style>
-    ${cssStr}
-  </style>`
-}
-
-function buildFormTemplate(conf, child, type) {
-  let labelPosition = ''
-  if (conf.labelPosition !== 'right') {
-    labelPosition = `label-position="${conf.labelPosition}"`
-  }
-  const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : ''
-  let str = `<el-form ref="${conf.formRef}" :model="${conf.formModel}" :rules="${conf.formRules}" size="${conf.size}" ${disabled} label-width="${conf.labelWidth}px" ${labelPosition}>
-      ${child}
-      ${buildFromBtns(conf, type)}
-    </el-form>`
-  if (someSpanIsNot24) {
-    str = `<el-row :gutter="${conf.gutter}">
-        ${str}
-      </el-row>`
-  }
-  return str
-}
-
-function buildFromBtns(conf, type) {
-  let str = ''
-  if (conf.formBtns && type === 'file') {
-    str = `<el-form-item size="large">
-          <el-button type="primary" @click="submitForm">提交</el-button>
-          <el-button @click="resetForm">重置</el-button>
-        </el-form-item>`
-    if (someSpanIsNot24) {
-      str = `<el-col :span="24">
-          ${str}
-        </el-col>`
-    }
-  }
-  return str
-}
-
-// span不为24的用el-col包裹
-function colWrapper(element, str) {
-  if (someSpanIsNot24 || element.span !== 24) {
-    return `<el-col :span="${element.span}">
-      ${str}
-    </el-col>`
-  }
-  return str
-}
-
-const layouts = {
-  colFormItem(element) {
-    let labelWidth = ''
-    if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) {
-      labelWidth = `label-width="${element.labelWidth}px"`
-    }
-    const required = !trigger[element.tag] && element.required ? 'required' : ''
-    const tagDom = tags[element.tag] ? tags[element.tag](element) : null
-    let str = `<el-form-item ${labelWidth} label="${element.label}" prop="${element.vModel}" ${required}>
-        ${tagDom}
-      </el-form-item>`
-    str = colWrapper(element, str)
-    return str
-  },
-  rowFormItem(element) {
-    const type = element.type === 'default' ? '' : `type="${element.type}"`
-    const justify = element.type === 'default' ? '' : `justify="${element.justify}"`
-    const align = element.type === 'default' ? '' : `align="${element.align}"`
-    const gutter = element.gutter ? `gutter="${element.gutter}"` : ''
-    const children = element.children.map(el => layouts[el.layout](el))
-    let str = `<el-row ${type} ${justify} ${align} ${gutter}>
-      ${children.join('\n')}
-    </el-row>`
-    str = colWrapper(element, str)
-    return str
-  }
-}
-
-const tags = {
-  'el-input': el => {
-    const {
-      disabled, vModel, clearable, placeholder, width
-    } = attrBuilder(el)
-    const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
-    const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
-    const readonly = el.readonly ? 'readonly' : ''
-    const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
-    const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
-    const showPassword = el['show-password'] ? 'show-password' : ''
-    const type = el.type ? `type="${el.type}"` : ''
-    const autosize = el.autosize && el.autosize.minRows
-      ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
-      : ''
-    let child = buildElInputChild(el)
-
-    if (child) child = `\n${child}\n` // 换行
-    return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${el.tag}>`
-  },
-  'el-input-number': el => {
-    const { disabled, vModel, placeholder } = attrBuilder(el)
-    const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
-    const min = el.min ? `:min='${el.min}'` : ''
-    const max = el.max ? `:max='${el.max}'` : ''
-    const step = el.step ? `:step='${el.step}'` : ''
-    const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
-    const precision = el.precision ? `:precision='${el.precision}'` : ''
-
-    return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}></${el.tag}>`
-  },
-  'el-select': el => {
-    const {
-      disabled, vModel, clearable, placeholder, width
-    } = attrBuilder(el)
-    const filterable = el.filterable ? 'filterable' : ''
-    const multiple = el.multiple ? 'multiple' : ''
-    let child = buildElSelectChild(el)
-
-    if (child) child = `\n${child}\n` // 换行
-    return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}</${el.tag}>`
-  },
-  'el-radio-group': el => {
-    const { disabled, vModel } = attrBuilder(el)
-    const size = `size="${el.size}"`
-    let child = buildElRadioGroupChild(el)
-
-    if (child) child = `\n${child}\n` // 换行
-    return `<${el.tag} ${vModel} ${size} ${disabled}>${child}</${el.tag}>`
-  },
-  'el-checkbox-group': el => {
-    const { disabled, vModel } = attrBuilder(el)
-    const size = `size="${el.size}"`
-    const min = el.min ? `:min="${el.min}"` : ''
-    const max = el.max ? `:max="${el.max}"` : ''
-    let child = buildElCheckboxGroupChild(el)
-
-    if (child) child = `\n${child}\n` // 换行
-    return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}</${el.tag}>`
-  },
-  'el-switch': el => {
-    const { disabled, vModel } = attrBuilder(el)
-    const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
-    const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
-    const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
-    const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
-    const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
-    const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
-
-    return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}></${el.tag}>`
-  },
-  'el-cascader': el => {
-    const {
-      disabled, vModel, clearable, placeholder, width
-    } = attrBuilder(el)
-    const options = el.options ? `:options="${el.vModel}Options"` : ''
-    const props = el.props ? `:props="${el.vModel}Props"` : ''
-    const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
-    const filterable = el.filterable ? 'filterable' : ''
-    const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
-
-    return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}></${el.tag}>`
-  },
-  'el-slider': el => {
-    const { disabled, vModel } = attrBuilder(el)
-    const min = el.min ? `:min='${el.min}'` : ''
-    const max = el.max ? `:max='${el.max}'` : ''
-    const step = el.step ? `:step='${el.step}'` : ''
-    const range = el.range ? 'range' : ''
-    const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
-
-    return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}></${el.tag}>`
-  },
-  'el-time-picker': el => {
-    const {
-      disabled, vModel, clearable, placeholder, width
-    } = attrBuilder(el)
-    const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
-    const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
-    const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
-    const isRange = el['is-range'] ? 'is-range' : ''
-    const format = el.format ? `format="${el.format}"` : ''
-    const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
-    const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
-
-    return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}></${el.tag}>`
-  },
-  'el-date-picker': el => {
-    const {
-      disabled, vModel, clearable, placeholder, width
-    } = attrBuilder(el)
-    const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
-    const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
-    const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
-    const format = el.format ? `format="${el.format}"` : ''
-    const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
-    const type = el.type === 'date' ? '' : `type="${el.type}"`
-    const readonly = el.readonly ? 'readonly' : ''
-
-    return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}></${el.tag}>`
-  },
-  'el-rate': el => {
-    const { disabled, vModel } = attrBuilder(el)
-    // eslint-disable-next-line no-unused-vars
-    const max = el.max ? `:max='${el.max}'` : ''
-    const allowHalf = el['allow-half'] ? 'allow-half' : ''
-    const showText = el['show-text'] ? 'show-text' : ''
-    const showScore = el['show-score'] ? 'show-score' : ''
-
-    return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}></${el.tag}>`
-  },
-  'el-color-picker': el => {
-    const { disabled, vModel } = attrBuilder(el)
-    const size = `size="${el.size}"`
-    const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
-    const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
-
-    return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}></${el.tag}>`
-  },
-  'el-upload': el => {
-    const disabled = el.disabled ? ':disabled=\'true\'' : ''
-    const action = el.action ? `:action="${el.vModel}Action"` : ''
-    const multiple = el.multiple ? 'multiple' : ''
-    const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
-    const accept = el.accept ? `accept="${el.accept}"` : ''
-    const name = el.name !== 'file' ? `name="${el.name}"` : ''
-    const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
-    const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"`
-    const fileList = `:file-list="${el.vModel}fileList"`
-    const ref = `ref="${el.vModel}"`
-    let child = buildElUploadChild(el)
-
-    if (child) child = `\n${child}\n` // 换行
-    return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}</${el.tag}>`
-  }
-}
-
-function attrBuilder(el) {
-  return {
-    vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`,
-    clearable: el.clearable ? 'clearable' : '',
-    placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
-    width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
-    disabled: el.disabled ? ':disabled=\'true\'' : ''
-  }
-}
-
-// el-input innerHTML
-function buildElInputChild(conf) {
-  const children = []
-  if (conf.prepend) {
-    children.push(`<template slot="prepend">${conf.prepend}</template>`)
-  }
-  if (conf.append) {
-    children.push(`<template slot="append">${conf.append}</template>`)
-  }
-  return children.join('\n')
-}
-
-function buildElSelectChild(conf) {
-  const children = []
-  if (conf.options && conf.options.length) {
-    children.push(`<el-option v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>`)
-  }
-  return children.join('\n')
-}
-
-function buildElRadioGroupChild(conf) {
-  const children = []
-  if (conf.options && conf.options.length) {
-    const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
-    const border = conf.border ? 'border' : ''
-    children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
-  }
-  return children.join('\n')
-}
-
-function buildElCheckboxGroupChild(conf) {
-  const children = []
-  if (conf.options && conf.options.length) {
-    const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
-    const border = conf.border ? 'border' : ''
-    children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
-  }
-  return children.join('\n')
-}
-
-function buildElUploadChild(conf) {
-  const list = []
-  if (conf['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
-  else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${conf.buttonText}</el-button>`)
-  if (conf.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${conf.fileSize}${conf.sizeUnit} 的${conf.accept}文件</div>`)
-  return list.join('\n')
-}
-
-export function makeUpHtml(conf, type) {
-  const htmlList = []
-  confGlobal = conf
-  someSpanIsNot24 = conf.fields.some(item => item.span !== 24)
-  conf.fields.forEach(el => {
-    htmlList.push(layouts[el.layout](el))
-  })
-  const htmlStr = htmlList.join('\n')
-
-  let temp = buildFormTemplate(conf, htmlStr, type)
-  if (type === 'dialog') {
-    temp = dialogWrapper(temp)
-  }
-  confGlobal = null
-  return temp
-}

+ 41 - 12
src/utils/generator/index.js

@@ -1,14 +1,6 @@
-export function makeMap(str, expectsLowerCase) {
-  const map = Object.create(null)
-  const list = str.split(',')
-  for (let i = 0; i < list.length; i++) {
-    map[list[i]] = true
-  }
-  return expectsLowerCase
-    ? val => map[val.toLowerCase()]
-    : val => map[val]
-}
-
+/* eslint-disable no-nested-ternary */
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable guard-for-in */
 /**
  * num 小于0,左缩进num*2个空格; 大于0,右缩进num*2个空格。
  * @param {string} str 代码
@@ -102,7 +94,6 @@ function stringify(obj) {
 function parse(str) {
   JSON.parse(str, (k, v) => {
     if (v.indexOf && v.indexOf('function') > -1) {
-      // eslint-disable-next-line no-eval
       return eval(`(${v})`)
     }
     return v
@@ -112,3 +103,41 @@ function parse(str) {
 export function jsonClone(obj) {
   return parse(stringify(obj))
 }
+
+// 深拷贝对象
+export function deepClone(obj) {
+  const _toString = Object.prototype.toString
+
+  // null, undefined, non-object, function
+  if (!obj || typeof obj !== 'object') {
+    return obj
+  }
+
+  // DOM Node
+  if (obj.nodeType && 'cloneNode' in obj) {
+    return obj.cloneNode(true)
+  }
+
+  // Date
+  if (_toString.call(obj) === '[object Date]') {
+    return new Date(obj.getTime())
+  }
+
+  // RegExp
+  if (_toString.call(obj) === '[object RegExp]') {
+    const flags = []
+    if (obj.global) { flags.push('g') }
+    if (obj.multiline) { flags.push('m') }
+    if (obj.ignoreCase) { flags.push('i') }
+
+    return new RegExp(obj.source, flags.join(''))
+  }
+
+  const result = Array.isArray(obj) ? [] : obj.constructor ? new obj.constructor() : {}
+
+  for (const key in obj) {
+    result[key] = deepClone(obj[key])
+  }
+
+  return result
+}

+ 0 - 236
src/utils/generator/js.js

@@ -1,236 +0,0 @@
-import { isArray } from 'util'
-import { exportDefault, titleCase } from '@/utils/generator/index'
-import { trigger } from './config'
-
-const units = {
-  KB: '1024',
-  MB: '1024 / 1024',
-  GB: '1024 / 1024 / 1024'
-}
-let confGlobal
-const inheritAttrs = {
-  file: '',
-  dialog: 'inheritAttrs: false,'
-}
-
-export function makeUpJs(conf, type) {
-  confGlobal = conf = JSON.parse(JSON.stringify(conf))
-  const dataList = []
-  const ruleList = []
-  const optionsList = []
-  const propsList = []
-  const methodList = mixinMethod(type)
-  const uploadVarList = []
-
-  conf.fields.forEach(el => {
-    buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
-  })
-
-  const script = buildexport(
-    conf,
-    type,
-    dataList.join('\n'),
-    ruleList.join('\n'),
-    optionsList.join('\n'),
-    uploadVarList.join('\n'),
-    propsList.join('\n'),
-    methodList.join('\n')
-  )
-  confGlobal = null
-  return script
-}
-
-function buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) {
-  buildData(el, dataList)
-  buildRules(el, ruleList)
-
-  if (el.options && el.options.length) {
-    buildOptions(el, optionsList)
-    if (el.dataType === 'dynamic') {
-      const model = `${el.vModel}Options`
-      const options = titleCase(model)
-      buildOptionMethod(`get${options}`, model, methodList)
-    }
-  }
-
-  if (el.props && el.props.props) {
-    buildProps(el, propsList)
-  }
-
-  if (el.action && el.tag === 'el-upload') {
-    uploadVarList.push(
-      `${el.vModel}Action: '${el.action}',
-      ${el.vModel}fileList: [],`
-    )
-    methodList.push(buildBeforeUpload(el))
-    if (!el['auto-upload']) {
-      methodList.push(buildSubmitUpload(el))
-    }
-  }
-
-  if (el.children) {
-    el.children.forEach(el2 => {
-      buildAttributes(el2, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
-    })
-  }
-}
-
-function mixinMethod(type) {
-  const list = []; const
-    minxins = {
-      file: confGlobal.formBtns ? {
-        submitForm: `submitForm() {
-        this.$refs['${confGlobal.formRef}'].validate(valid => {
-          if(!valid) return
-          // TODO 提交表单
-        })
-      },`,
-        resetForm: `resetForm() {
-        this.$refs['${confGlobal.formRef}'].resetFields()
-      },`
-      } : null,
-      dialog: {
-        onOpen: 'onOpen() {},',
-        onClose: `onClose() {
-        this.$refs['${confGlobal.formRef}'].resetFields()
-      },`,
-        close: `close() {
-        this.$emit('update:visible', false)
-      },`,
-        handelConfirm: `handelConfirm() {
-        this.$refs['${confGlobal.formRef}'].validate(valid => {
-          if(!valid) return
-          this.close()
-        })
-      },`
-      }
-    }
-
-  const methods = minxins[type]
-  if (methods) {
-    Object.keys(methods).forEach(key => {
-      list.push(methods[key])
-    })
-  }
-
-  return list
-}
-
-function buildData(conf, dataList) {
-  if (conf.vModel === undefined) return
-  let defaultValue
-  if (typeof (conf.defaultValue) === 'string' && !conf.multiple) {
-    defaultValue = `'${conf.defaultValue}'`
-  } else {
-    defaultValue = `${JSON.stringify(conf.defaultValue)}`
-  }
-  dataList.push(`${conf.vModel}: ${defaultValue},`)
-}
-
-function buildRules(conf, ruleList) {
-  if (conf.vModel === undefined) return
-  const rules = []
-  if (trigger[conf.tag]) {
-    if (conf.required) {
-      const type = isArray(conf.defaultValue) ? 'type: \'array\',' : ''
-      let message = isArray(conf.defaultValue) ? `请至少选择一个${conf.vModel}` : conf.placeholder
-      if (message === undefined) message = `${conf.label}不能为空`
-      rules.push(`{ required: true, ${type} message: '${message}', trigger: '${trigger[conf.tag]}' }`)
-    }
-    if (conf.regList && isArray(conf.regList)) {
-      conf.regList.forEach(item => {
-        if (item.pattern) {
-          // eslint-disable-next-line no-eval
-          rules.push(`{ pattern: ${eval(item.pattern)}, message: '${item.message}', trigger: '${trigger[conf.tag]}' }`)
-        }
-      })
-    }
-    ruleList.push(`${conf.vModel}: [${rules.join(',')}],`)
-  }
-}
-
-function buildOptions(conf, optionsList) {
-  if (conf.vModel === undefined) return
-  if (conf.dataType === 'dynamic') { conf.options = [] }
-  const str = `${conf.vModel}Options: ${JSON.stringify(conf.options)},`
-  optionsList.push(str)
-}
-
-function buildProps(conf, propsList) {
-  if (conf.dataType === 'dynamic') {
-    conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey)
-    conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey)
-    conf.childrenKey !== 'children' && (conf.props.props.children = conf.childrenKey)
-  }
-  const str = `${conf.vModel}Props: ${JSON.stringify(conf.props.props)},`
-  propsList.push(str)
-}
-
-function buildBeforeUpload(conf) {
-  const unitNum = units[conf.sizeUnit]; let rightSizeCode = ''; let acceptCode = ''; const
-    returnList = []
-  if (conf.fileSize) {
-    rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize}
-    if(!isRightSize){
-      this.$message.error('文件大小超过 ${conf.fileSize}${conf.sizeUnit}')
-    }`
-    returnList.push('isRightSize')
-  }
-  if (conf.accept) {
-    acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type)
-    if(!isAccept){
-      this.$message.error('应该选择${conf.accept}类型的文件')
-    }`
-    returnList.push('isAccept')
-  }
-  const str = `${conf.vModel}BeforeUpload(file) {
-    ${rightSizeCode}
-    ${acceptCode}
-    return ${returnList.join('&&')}
-  },`
-  return returnList.length ? str : ''
-}
-
-function buildSubmitUpload(conf) {
-  const str = `submitUpload() {
-    this.$refs['${conf.vModel}'].submit()
-  },`
-  return str
-}
-
-function buildOptionMethod(methodName, model, methodList) {
-  const str = `${methodName}() {
-    // TODO 发起请求获取数据
-    this.${model}
-  },`
-  methodList.push(str)
-}
-
-function buildexport(conf, type, data, rules, selectOptions, uploadVar, props, methods) {
-  const str = `${exportDefault}{
-  ${inheritAttrs[type]}
-  components: {},
-  props: [],
-  data () {
-    return {
-      ${conf.formModel}: {
-        ${data}
-      },
-      ${conf.formRules}: {
-        ${rules}
-      },
-      ${uploadVar}
-      ${selectOptions}
-      ${props}
-    }
-  },
-  computed: {},
-  watch: {},
-  created () {},
-  mounted () {},
-  methods: {
-    ${methods}
-  }
-}`
-  return str
-}

+ 26 - 0
src/utils/generator/loadBeautifier.js

@@ -0,0 +1,26 @@
+import loadScript from './loadScript'
+import ELEMENT from 'element-ui'
+
+let beautifierObj
+
+export default function loadBeautifier(cb) {
+  if (beautifierObj) {
+    cb(beautifierObj)
+    return
+  }
+
+  const loading = ELEMENT.Loading.service({
+    fullscreen: true,
+    lock: true,
+    text: '格式化资源加载中...',
+    spinner: 'el-icon-loading',
+    background: 'rgba(255, 255, 255, 0.5)'
+  })
+
+  loadScript('https://lib.baomitu.com/js-beautify/1.10.2/beautifier.min.js', () => {
+    loading.close()
+    // eslint-disable-next-line no-undef
+    beautifierObj = beautifier
+    cb(beautifierObj)
+  })
+}

+ 42 - 0
src/utils/generator/loadMonaco.js

@@ -0,0 +1,42 @@
+import { loadScriptQueue } from './loadScript'
+import ELEMENT from 'element-ui'
+
+// monaco-editor单例
+let monacoEidtor
+
+/**
+ * 动态加载monaco-editor cdn资源
+ * @param {Function} cb 回调,必填
+ */
+export default function loadMonaco(cb) {
+  if (monacoEidtor) {
+    cb(monacoEidtor)
+    return
+  }
+
+  const vs = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.20.0/min/vs'
+
+  // 使用element ui实现加载提示
+  const loading = ELEMENT.Loading.service({
+    fullscreen: true,
+    lock: true,
+    text: '编辑器资源初始化中...',
+    spinner: 'el-icon-loading',
+    background: 'rgba(255, 255, 255, 0.5)'
+  })
+
+  !window.require && (window.require = {})
+  !window.require.paths && (window.require.paths = {})
+  window.require.paths.vs = vs
+
+  loadScriptQueue([
+    `${vs}/loader.js`,
+    `${vs}/editor/editor.main.nls.js`,
+    `${vs}/editor/editor.main.js`
+  ], () => {
+    loading.close()
+    // eslint-disable-next-line no-undef
+    monacoEidtor = monaco
+    cb(monacoEidtor)
+  })
+}

+ 60 - 0
src/utils/generator/loadScript.js

@@ -0,0 +1,60 @@
+const callbacks = {}
+
+/**
+ * 加载一个远程脚本
+ * @param {String} src 一个远程脚本
+ * @param {Function} callback 回调
+ */
+function loadScript(src, callback) {
+  const existingScript = document.getElementById(src)
+  const cb = callback || (() => {})
+  if (!existingScript) {
+    callbacks[src] = []
+    const $script = document.createElement('script')
+    $script.src = src
+    $script.id = src
+    $script.async = 1
+    document.body.appendChild($script)
+    const onEnd = 'onload' in $script ? stdOnEnd.bind($script) : ieOnEnd.bind($script)
+    onEnd($script)
+  }
+
+  callbacks[src].push(cb)
+
+  function stdOnEnd(script) {
+    script.onload = () => {
+      this.onerror = this.onload = null
+      callbacks[src].forEach(item => {
+        item(null, script)
+      })
+      delete callbacks[src]
+    }
+    script.onerror = () => {
+      this.onerror = this.onload = null
+      cb(new Error(`Failed to load ${src}`), script)
+    }
+  }
+
+  function ieOnEnd(script) {
+    script.onreadystatechange = () => {
+      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
+      this.onreadystatechange = null
+      callbacks[src].forEach(item => {
+        item(null, script)
+      })
+      delete callbacks[src]
+    }
+  }
+}
+
+/**
+ * 顺序加载一组远程脚本
+ * @param {Array} list 一组远程脚本
+ * @param {Function} cb 回调
+ */
+export function loadScriptQueue(list, cb) {
+  const first = list.shift()
+  list.length ? loadScript(first, () => loadScriptQueue(list, cb)) : loadScript(first, cb)
+}
+
+export default loadScript

+ 26 - 0
src/utils/generator/loadTinymce.js

@@ -0,0 +1,26 @@
+import loadScript from './loadScript'
+import ELEMENT from 'element-ui'
+
+let tinymceObj
+
+export default function loadTinymce(cb) {
+  if (tinymceObj) {
+    cb(tinymceObj)
+    return
+  }
+
+  const loading = ELEMENT.Loading.service({
+    fullscreen: true,
+    lock: true,
+    text: '富文本资源加载中...',
+    spinner: 'el-icon-loading',
+    background: 'rgba(255, 255, 255, 0.5)'
+  })
+
+  loadScript('https://cdn.bootcdn.net/ajax/libs/tinymce/5.3.2/tinymce.min.js', () => {
+    loading.close()
+    // eslint-disable-next-line no-undef
+    tinymceObj = tinymce
+    cb(tinymceObj)
+  })
+}

+ 0 - 121
src/utils/generator/render.js

@@ -1,121 +0,0 @@
-import { makeMap } from '@/utils/generator/index'
-
-// 参考https://github.com/vuejs/vue/blob/v2.6.10/src/platforms/web/server/util.js
-const isAttr = makeMap(
-  'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
-  'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
-  'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
-  'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
-  'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
-  'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' +
-  'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
-  'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
-  'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
-  'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
-  'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
-  'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
-  'target,title,type,usemap,value,width,wrap'
-)
-
-function vModel(self, dataObject, defaultValue) {
-  dataObject.props.value = defaultValue
-
-  dataObject.on.input = val => {
-    self.$emit('input', val)
-  }
-}
-
-const componentChild = {
-  'el-input': {
-    prepend(h, conf, key) {
-      return <template slot='prepend'>{conf[key]}</template>
-    },
-    append(h, conf, key) {
-      return <template slot='append'>{conf[key]}</template>
-    }
-  },
-  'el-select': {
-    options(h, conf, key) {
-      const list = []
-      conf.options.forEach(item => {
-        list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
-      })
-      return list
-    }
-  },
-  'el-radio-group': {
-    options(h, conf, key) {
-      const list = []
-      conf.options.forEach(item => {
-        if (conf.optionType === 'button') list.push(<el-radio-button label={item.value}>{item.label}</el-radio-button>)
-        else list.push(<el-radio label={item.value} border={conf.border}>{item.label}</el-radio>)
-      })
-      return list
-    }
-  },
-  'el-checkbox-group': {
-    options(h, conf, key) {
-      const list = []
-      conf.options.forEach(item => {
-        if (conf.optionType === 'button') {
-          list.push(<el-checkbox-button label={item.value}>{item.label}</el-checkbox-button>)
-        } else {
-          list.push(<el-checkbox label={item.value} border={conf.border}>{item.label}</el-checkbox>)
-        }
-      })
-      return list
-    }
-  },
-  'el-upload': {
-    'list-type': (h, conf, key) => {
-      const list = []
-      if (conf['list-type'] === 'picture-card') {
-        list.push(<i class='el-icon-plus'></i>)
-      } else {
-        list.push(<el-button size='small' type='primary' icon='el-icon-upload'>{conf.buttonText}</el-button>)
-      }
-      if (conf.showTip) {
-        list.push(<div slot='tip' class='el-upload__tip'>只能上传不超过 {conf.fileSize}{conf.sizeUnit} 的{conf.accept}文件</div>)
-      }
-      return list
-    }
-  }
-}
-
-export default {
-  render(h) {
-    const dataObject = {
-      attrs: {},
-      props: {},
-      on: {},
-      style: {}
-    }
-    const confClone = JSON.parse(JSON.stringify(this.conf))
-    const children = []
-
-    const childObjs = componentChild[confClone.tag]
-    if (childObjs) {
-      Object.keys(childObjs).forEach(key => {
-        const childFunc = childObjs[key]
-        if (confClone[key]) {
-          children.push(childFunc(h, confClone, key))
-        }
-      })
-    }
-
-    Object.keys(confClone).forEach(key => {
-      const val = confClone[key]
-      if (key === 'vModel') {
-        vModel(this, dataObject, confClone.defaultValue)
-      } else if (dataObject[key]) {
-        dataObject[key] = val
-      } else if (!isAttr(key)) {
-        dataObject.props[key] = val
-      } else {
-        dataObject.attrs[key] = val
-      }
-    })
-    return h(this.conf.tag, dataObject, children)
-  },
-  props: ['conf']
-}

+ 4 - 1
src/views/tools/build/CodeTypeDialog.vue

@@ -51,7 +51,6 @@
 <script>
 export default {
   inheritAttrs: false,
-  // eslint-disable-next-line
   props: ['showFileName'],
   data() {
     return {
@@ -105,3 +104,7 @@ export default {
   }
 }
 </script>
+
+<style lang="scss" scoped>
+
+</style>

+ 39 - 28
src/views/tools/build/DraggableItem.vue

@@ -1,20 +1,20 @@
 <script>
 import draggable from 'vuedraggable'
-import render from '@/utils/generator/render'
+import render from '@/components/render/render'
 
 const components = {
   itemBtns(h, element, index, parent) {
     const { copyItem, deleteItem } = this.$listeners
     return [
-      <span class='drawing-item-copy' title='复制' onClick={event => {
+      <span class="drawing-item-copy" title="复制" onClick={event => {
         copyItem(element, parent); event.stopPropagation()
       }}>
-        <i class='el-icon-copy-document' />
+        <i class="el-icon-copy-document" />
       </span>,
-      <span class='drawing-item-delete' title='删除' onClick={event => {
+      <span class="drawing-item-delete" title="删除" onClick={event => {
         deleteItem(index, parent); event.stopPropagation()
       }}>
-        <i class='el-icon-delete' />
+        <i class="el-icon-delete" />
       </span>
     ]
   }
@@ -22,15 +22,18 @@ const components = {
 const layouts = {
   colFormItem(h, element, index, parent) {
     const { activeItem } = this.$listeners
-    let className = this.activeId === element.formId ? 'drawing-item active-from-item' : 'drawing-item'
+    const config = element.__config__
+    let className = this.activeId === config.formId ? 'drawing-item active-from-item' : 'drawing-item'
     if (this.formConf.unFocusedComponentBorder) className += ' unfocus-bordered'
+    let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
+    if (config.showLabel === false) labelWidth = '0'
     return (
-      <el-col span={element.span} class={className}
+      <el-col span={config.span} class={className}
         nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
-        <el-form-item label-width={element.labelWidth ? `${element.labelWidth}px` : null}
-          label={element.label} required={element.required}>
-          <render key={element.renderKey} conf={element} onInput={ event => {
-            this.$set(element, 'defaultValue', event)
+        <el-form-item label-width={labelWidth}
+          label={config.showLabel ? config.label : ''} required={config.required}>
+          <render key={config.renderKey} conf={element} onInput={ event => {
+            this.$set(config, 'defaultValue', event)
           }} />
         </el-form-item>
         {components.itemBtns.apply(this, arguments)}
@@ -39,19 +42,21 @@ const layouts = {
   },
   rowFormItem(h, element, index, parent) {
     const { activeItem } = this.$listeners
-    const className = this.activeId === element.formId ? 'drawing-row-item active-from-item' : 'drawing-row-item'
+    const className = this.activeId === element.__config__.formId
+      ? 'drawing-row-item active-from-item'
+      : 'drawing-row-item'
     let child = renderChildren.apply(this, arguments)
     if (element.type === 'flex') {
       child = <el-row type={element.type} justify={element.justify} align={element.align}>
-        {child}
-      </el-row>
+              {child}
+            </el-row>
     }
     return (
-      <el-col span={element.span}>
-        <el-row gutter={element.gutter} class={className}
+      <el-col span={element.__config__.span}>
+        <el-row gutter={element.__config__.gutter} class={className}
           nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
-          <span class='component-name'>{element.componentName}</span>
-          <draggable list={element.children} animation={340} group='componentsGroup' class='drag-wrapper'>
+          <span class="component-name">{element.__config__.componentName}</span>
+          <draggable list={element.__config__.children} animation={340} group="componentsGroup" class="drag-wrapper">
             {child}
           </draggable>
           {components.itemBtns.apply(this, arguments)}
@@ -62,18 +67,19 @@ const layouts = {
 }
 
 function renderChildren(h, element, index, parent) {
-  if (!Array.isArray(element.children)) return null
-  return element.children.map((el, i) => {
-    const layout = layouts[el.layout]
+  const config = element.__config__
+  if (!Array.isArray(config.children)) return null
+  return config.children.map((el, i) => {
+    const layout = layouts[el.__config__.layout]
     if (layout) {
-      return layout.call(this, h, el, i, element.children)
+      return layout.call(this, h, el, i, config.children)
     }
-    return layoutIsNotFound()
+    return layoutIsNotFound.call(this)
   })
 }
 
 function layoutIsNotFound() {
-  throw new Error(`没有与${this.element.layout}匹配的layout`)
+  throw new Error(`没有与${this.element.__config__.layout}匹配的layout`)
 }
 
 export default {
@@ -81,15 +87,20 @@ export default {
     render,
     draggable
   },
-  // eslint-disable-next-line vue/require-prop-types
-  props: ['element', 'index', 'drawingList', 'activeId', 'formConf'],
+  props: [
+    'element',
+    'index',
+    'drawingList',
+    'activeId',
+    'formConf'
+  ],
   render(h) {
-    const layout = layouts[this.element.layout]
+    const layout = layouts[this.element.__config__.layout]
 
     if (layout) {
       return layout.call(this, h, this.element, this.index, this.drawingList)
     }
-    return layoutIsNotFound()
+    return layoutIsNotFound.call(this)
   }
 }
 </script>

+ 332 - 0
src/views/tools/build/FormDrawer.vue

@@ -0,0 +1,332 @@
+<template>
+  <div>
+    <el-drawer v-bind="$attrs" v-on="$listeners" @opened="onOpen" @close="onClose">
+      <div style="height:100%">
+        <el-row style="height:100%;overflow:auto">
+          <el-col :md="24" :lg="12" class="left-editor">
+            <div class="setting" title="资源引用" @click="showResource">
+              <el-badge :is-dot="!!resources.length" class="item">
+                <i class="el-icon-setting" />
+              </el-badge>
+            </div>
+            <el-tabs v-model="activeTab" type="card" class="editor-tabs">
+              <el-tab-pane name="html">
+                <span slot="label">
+                  <i v-if="activeTab==='html'" class="el-icon-edit" />
+                  <i v-else class="el-icon-document" />
+                  template
+                </span>
+              </el-tab-pane>
+              <el-tab-pane name="js">
+                <span slot="label">
+                  <i v-if="activeTab==='js'" class="el-icon-edit" />
+                  <i v-else class="el-icon-document" />
+                  script
+                </span>
+              </el-tab-pane>
+              <el-tab-pane name="css">
+                <span slot="label">
+                  <i v-if="activeTab==='css'" class="el-icon-edit" />
+                  <i v-else class="el-icon-document" />
+                  css
+                </span>
+              </el-tab-pane>
+            </el-tabs>
+            <div v-show="activeTab==='html'" id="editorHtml" class="tab-editor" />
+            <div v-show="activeTab==='js'" id="editorJs" class="tab-editor" />
+            <div v-show="activeTab==='css'" id="editorCss" class="tab-editor" />
+          </el-col>
+          <el-col :md="24" :lg="12" class="right-preview">
+            <div class="action-bar" :style="{'text-align': 'left'}">
+              <span class="bar-btn" @click="runCode">
+                <i class="el-icon-refresh" />
+                刷新
+              </span>
+              <span class="bar-btn" @click="exportFile">
+                <i class="el-icon-download" />
+                导出vue文件
+              </span>
+              <span ref="copyBtn" class="bar-btn copy-btn">
+                <i class="el-icon-document-copy" />
+                复制代码
+              </span>
+              <span class="bar-btn delete-btn" @click="$emit('update:visible', false)">
+                <i class="el-icon-circle-close" />
+                关闭
+              </span>
+            </div>
+            <iframe
+              v-show="isIframeLoaded"
+              ref="previewPage"
+              class="result-wrapper"
+              frameborder="0"
+              src="preview.html"
+              @load="iframeLoad"
+            />
+            <div v-show="!isIframeLoaded" v-loading="true" class="result-wrapper" />
+          </el-col>
+        </el-row>
+      </div>
+    </el-drawer>
+    <resource-dialog
+      :visible.sync="resourceVisible"
+      :origin-resource="resources"
+      @save="setResource"
+    />
+  </div>
+</template>
+<script>
+import { parse } from '@babel/parser'
+import ClipboardJS from 'clipboard'
+import { saveAs } from 'file-saver'
+import {
+  makeUpHtml, vueTemplate, vueScript, cssStyle
+} from '@/components/generator/html'
+import { makeUpJs } from '@/components/generator/js'
+import { makeUpCss } from '@/components/generator/css'
+import { exportDefault, beautifierConf, titleCase } from '@/utils/generator/index'
+import ResourceDialog from './ResourceDialog'
+import loadMonaco from '@/utils/generator/loadMonaco'
+import loadBeautifier from '@/utils/generator/loadBeautifier'
+
+const editorObj = {
+  html: null,
+  js: null,
+  css: null
+}
+const mode = {
+  html: 'html',
+  js: 'javascript',
+  css: 'css'
+}
+let beautifier
+let monaco
+
+export default {
+  components: { ResourceDialog },
+  props: ['formData', 'generateConf'],
+  data() {
+    return {
+      activeTab: 'html',
+      htmlCode: '',
+      jsCode: '',
+      cssCode: '',
+      codeFrame: '',
+      isIframeLoaded: false,
+      isInitcode: false, // 保证open后两个异步只执行一次runcode
+      isRefreshCode: false, // 每次打开都需要重新刷新代码
+      resourceVisible: false,
+      scripts: [],
+      links: [],
+      monaco: null
+    }
+  },
+  computed: {
+    resources() {
+      return this.scripts.concat(this.links)
+    }
+  },
+  watch: {},
+  created() {
+  },
+  mounted() {
+    window.addEventListener('keydown', this.preventDefaultSave)
+    const clipboard = new ClipboardJS('.copy-btn', {
+      text: trigger => {
+        const codeStr = this.generateCode()
+        this.$notify({
+          title: '成功',
+          message: '代码已复制到剪切板,可粘贴。',
+          type: 'success'
+        })
+        return codeStr
+      }
+    })
+    clipboard.on('error', e => {
+      this.$message.error('代码复制失败')
+    })
+  },
+  beforeDestroy() {
+    window.removeEventListener('keydown', this.preventDefaultSave)
+  },
+  methods: {
+    preventDefaultSave(e) {
+      if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
+        e.preventDefault()
+      }
+    },
+    onOpen() {
+      const { type } = this.generateConf
+      this.htmlCode = makeUpHtml(this.formData, type)
+      this.jsCode = makeUpJs(this.formData, type)
+      this.cssCode = makeUpCss(this.formData)
+
+      loadBeautifier(btf => {
+        beautifier = btf
+        this.htmlCode = beautifier.html(this.htmlCode, beautifierConf.html)
+        this.jsCode = beautifier.js(this.jsCode, beautifierConf.js)
+        this.cssCode = beautifier.css(this.cssCode, beautifierConf.html)
+
+        loadMonaco(val => {
+          monaco = val
+          this.setEditorValue('editorHtml', 'html', this.htmlCode)
+          this.setEditorValue('editorJs', 'js', this.jsCode)
+          this.setEditorValue('editorCss', 'css', this.cssCode)
+          if (!this.isInitcode) {
+            this.isRefreshCode = true
+            this.isIframeLoaded && (this.isInitcode = true) && this.runCode()
+          }
+        })
+      })
+    },
+    onClose() {
+      this.isInitcode = false
+      this.isRefreshCode = false
+    },
+    iframeLoad() {
+      if (!this.isInitcode) {
+        this.isIframeLoaded = true
+        this.isRefreshCode && (this.isInitcode = true) && this.runCode()
+      }
+    },
+    setEditorValue(id, type, codeStr) {
+      if (editorObj[type]) {
+        editorObj[type].setValue(codeStr)
+      } else {
+        editorObj[type] = monaco.editor.create(document.getElementById(id), {
+          value: codeStr,
+          theme: 'vs-dark',
+          language: mode[type],
+          automaticLayout: true
+        })
+      }
+      // ctrl + s 刷新
+      editorObj[type].onKeyDown(e => {
+        if (e.keyCode === 49 && (e.metaKey || e.ctrlKey)) {
+          this.runCode()
+        }
+      })
+    },
+    runCode() {
+      const jsCodeStr = editorObj.js.getValue()
+      try {
+        const ast = parse(jsCodeStr, { sourceType: 'module' })
+        const astBody = ast.program.body
+        if (astBody.length > 1) {
+          this.$confirm(
+            'js格式不能识别,仅支持修改export default的对象内容',
+            '提示',
+            {
+              type: 'warning'
+            }
+          )
+          return
+        }
+        if (astBody[0].type === 'ExportDefaultDeclaration') {
+          const postData = {
+            type: 'refreshFrame',
+            data: {
+              generateConf: this.generateConf,
+              html: editorObj.html.getValue(),
+              js: jsCodeStr.replace(exportDefault, ''),
+              css: editorObj.css.getValue(),
+              scripts: this.scripts,
+              links: this.links
+            }
+          }
+
+          this.$refs.previewPage.contentWindow.postMessage(
+            postData,
+            location.origin
+          )
+        } else {
+          this.$message.error('请使用export default')
+        }
+      } catch (err) {
+        this.$message.error(`js错误:${err}`)
+        console.error(err)
+      }
+    },
+    generateCode() {
+      const html = vueTemplate(editorObj.html.getValue())
+      const script = vueScript(editorObj.js.getValue())
+      const css = cssStyle(editorObj.css.getValue())
+      return beautifier.html(html + script + css, beautifierConf.html)
+    },
+    exportFile() {
+      this.$prompt('文件名:', '导出文件', {
+        inputValue: `${+new Date()}.vue`,
+        closeOnClickModal: false,
+        inputPlaceholder: '请输入文件名'
+      }).then(({ value }) => {
+        if (!value) value = `${+new Date()}.vue`
+        const codeStr = this.generateCode()
+        const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
+        saveAs(blob, value)
+      })
+    },
+    showResource() {
+      this.resourceVisible = true
+    },
+    setResource(arr) {
+      const scripts = []; const
+        links = []
+      if (Array.isArray(arr)) {
+        arr.forEach(item => {
+          if (item.endsWith('.css')) {
+            links.push(item)
+          } else {
+            scripts.push(item)
+          }
+        })
+        this.scripts = scripts
+        this.links = links
+      } else {
+        this.scripts = []
+        this.links = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/styles/generator/mixin.scss';
+.tab-editor {
+  position: absolute;
+  top: 33px;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  font-size: 14px;
+}
+.left-editor {
+  position: relative;
+  height: 100%;
+  background: #1e1e1e;
+  overflow: hidden;
+}
+.setting{
+  position: absolute;
+  right: 15px;
+  top: 3px;
+  color: #a9f122;
+  font-size: 18px;
+  cursor: pointer;
+  z-index: 1;
+}
+.right-preview {
+  height: 100%;
+  .result-wrapper {
+    height: calc(100vh - 33px);
+    width: 100%;
+    overflow: auto;
+    padding: 12px;
+    box-sizing: border-box;
+  }
+}
+@include action-bar;
+::v-deep .el-drawer__header {
+  display: none;
+}
+</style>

+ 0 - 1
src/views/tools/build/IconsDialog.vue

@@ -40,7 +40,6 @@ const originList = iconList.map(name => `el-icon-${name}`)
 
 export default {
   inheritAttrs: false,
-  // eslint-disable-next-line vue/require-prop-types
   props: ['current'],
   data() {
     return {

+ 144 - 0
src/views/tools/build/JsonDrawer.vue

@@ -0,0 +1,144 @@
+<template>
+  <div>
+    <el-drawer v-bind="$attrs" v-on="$listeners" @opened="onOpen" @close="onClose">
+      <div class="action-bar" :style="{'text-align': 'left'}">
+        <span class="bar-btn" @click="refresh">
+          <i class="el-icon-refresh" />
+          刷新
+        </span>
+        <span ref="copyBtn" class="bar-btn copy-json-btn">
+          <i class="el-icon-document-copy" />
+          复制JSON
+        </span>
+        <span class="bar-btn" @click="exportJsonFile">
+          <i class="el-icon-download" />
+          导出JSON文件
+        </span>
+        <span class="bar-btn delete-btn" @click="$emit('update:visible', false)">
+          <i class="el-icon-circle-close" />
+          关闭
+        </span>
+      </div>
+      <div id="editorJson" class="json-editor" />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { beautifierConf } from '@/utils/generator/index'
+import ClipboardJS from 'clipboard'
+import { saveAs } from 'file-saver'
+import loadMonaco from '@/utils/generator/loadMonaco'
+import loadBeautifier from '@/utils/generator/loadBeautifier'
+
+let beautifier
+let monaco
+
+export default {
+  components: {},
+  props: {
+    jsonStr: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {}
+  },
+  computed: {},
+  watch: {},
+  created() {},
+  mounted() {
+    window.addEventListener('keydown', this.preventDefaultSave)
+    const clipboard = new ClipboardJS('.copy-json-btn', {
+      text: trigger => {
+        this.$notify({
+          title: '成功',
+          message: '代码已复制到剪切板,可粘贴。',
+          type: 'success'
+        })
+        return this.beautifierJson
+      }
+    })
+    clipboard.on('error', e => {
+      this.$message.error('代码复制失败')
+    })
+  },
+  beforeDestroy() {
+    window.removeEventListener('keydown', this.preventDefaultSave)
+  },
+  methods: {
+    preventDefaultSave(e) {
+      if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
+        e.preventDefault()
+      }
+    },
+    onOpen() {
+      loadBeautifier(btf => {
+        beautifier = btf
+        this.beautifierJson = beautifier.js(this.jsonStr, beautifierConf.js)
+
+        loadMonaco(val => {
+          monaco = val
+          this.setEditorValue('editorJson', this.beautifierJson)
+        })
+      })
+    },
+    onClose() {},
+    setEditorValue(id, codeStr) {
+      if (this.jsonEditor) {
+        this.jsonEditor.setValue(codeStr)
+      } else {
+        this.jsonEditor = monaco.editor.create(document.getElementById(id), {
+          value: codeStr,
+          theme: 'vs-dark',
+          language: 'json',
+          automaticLayout: true
+        })
+        // ctrl + s 刷新
+        this.jsonEditor.onKeyDown(e => {
+          if (e.keyCode === 49 && (e.metaKey || e.ctrlKey)) {
+            this.refresh()
+          }
+        })
+      }
+    },
+    exportJsonFile() {
+      this.$prompt('文件名:', '导出文件', {
+        inputValue: `${+new Date()}.json`,
+        closeOnClickModal: false,
+        inputPlaceholder: '请输入文件名'
+      }).then(({ value }) => {
+        if (!value) value = `${+new Date()}.json`
+        const codeStr = this.jsonEditor.getValue()
+        const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
+        saveAs(blob, value)
+      })
+    },
+    refresh() {
+      try {
+        this.$emit('refresh', JSON.parse(this.jsonEditor.getValue()))
+      } catch (error) {
+        this.$notify({
+          title: '错误',
+          message: 'JSON格式错误,请检查',
+          type: 'error'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/styles/generator/mixin.scss';
+
+::v-deep .el-drawer__header {
+  display: none;
+}
+@include action-bar;
+
+.json-editor{
+  height: calc(100vh - 33px);
+}
+</style>

+ 116 - 0
src/views/tools/build/ResourceDialog.vue

@@ -0,0 +1,116 @@
+<template>
+  <div>
+    <el-dialog
+      v-bind="$attrs"
+      title="外部资源引用"
+      width="600px"
+      :close-on-click-modal="false"
+      v-on="$listeners"
+      @open="onOpen"
+      @close="onClose"
+    >
+      <el-input
+        v-for="(item, index) in resources"
+        :key="index"
+        v-model="resources[index]"
+        class="url-item"
+        placeholder="请输入 css 或 js 资源路径"
+        prefix-icon="el-icon-link"
+        clearable
+      >
+        <el-button
+          slot="append"
+          icon="el-icon-delete"
+          @click="deleteOne(index)"
+        />
+      </el-input>
+      <el-button-group class="add-item">
+        <el-button
+          plain
+          @click="addOne('https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js')"
+        >
+          jQuery1.8.3
+        </el-button>
+        <el-button
+          plain
+          @click="addOne('https://unpkg.com/http-vue-loader')"
+        >
+          http-vue-loader
+        </el-button>
+        <el-button
+          icon="el-icon-circle-plus-outline"
+          plain
+          @click="addOne('')"
+        >
+          添加其他
+        </el-button>
+      </el-button-group>
+      <div slot="footer">
+        <el-button @click="close">
+          取消
+        </el-button>
+        <el-button
+          type="primary"
+          @click="handelConfirm"
+        >
+          确定
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { deepClone } from '@/utils/generator/index'
+
+export default {
+  components: {},
+  inheritAttrs: false,
+  props: ['originResource'],
+  data() {
+    return {
+      resources: null
+    }
+  },
+  computed: {},
+  watch: {},
+  created() {},
+  mounted() {},
+  methods: {
+    onOpen() {
+      this.resources = this.originResource.length ? deepClone(this.originResource) : ['']
+    },
+    onClose() {
+    },
+    close() {
+      this.$emit('update:visible', false)
+    },
+    handelConfirm() {
+      const results = this.resources.filter(item => !!item) || []
+      this.$emit('save', results)
+      this.close()
+      if (results.length) {
+        this.resources = results
+      }
+    },
+    deleteOne(index) {
+      this.resources.splice(index, 1)
+    },
+    addOne(url) {
+      if (this.resources.indexOf(url) > -1) {
+        this.$message('资源已存在')
+      } else {
+        this.resources.push(url)
+      }
+    }
+  }
+}
+
+</script>
+<style lang="scss" scoped>
+.add-item{
+  margin-top: 8px;
+}
+.url-item{
+  margin-bottom: 12px;
+}
+</style>

+ 178 - 114
src/views/tools/build/RightPanel.vue

@@ -11,9 +11,9 @@
       <el-scrollbar class="right-scrollbar">
         <!-- 组件属性 -->
         <el-form v-show="currentTab==='field' && showField" size="small" label-width="90px">
-          <el-form-item v-if="activeData.changeTag" label="组件类型">
+          <el-form-item v-if="activeData.__config__.changeTag" label="组件类型">
             <el-select
-              v-model="activeData.tagIcon"
+              v-model="activeData.__config__.tagIcon"
               placeholder="请选择组件类型"
               :style="{width: '100%'}"
               @change="tagChange"
@@ -21,27 +21,27 @@
               <el-option-group v-for="group in tagList" :key="group.label" :label="group.label">
                 <el-option
                   v-for="item in group.options"
-                  :key="item.label"
-                  :label="item.label"
-                  :value="item.tagIcon"
+                  :key="item.__config__.label"
+                  :label="item.__config__.label"
+                  :value="item.__config__.tagIcon"
                 >
-                  <svg-icon class="node-icon" :icon-class="item.tagIcon" />
-                  <span> {{ item.label }}</span>
+                  <svg-icon class="node-icon" :icon-class="item.__config__.tagIcon" />
+                  <span> {{ item.__config__.label }}</span>
                 </el-option>
               </el-option-group>
             </el-select>
           </el-form-item>
-          <el-form-item v-if="activeData.vModel!==undefined" label="字段名">
-            <el-input v-model="activeData.vModel" placeholder="请输入字段名(v-model)" />
+          <el-form-item v-if="activeData.__vModel__!==undefined" label="字段名">
+            <el-input v-model="activeData.__vModel__" placeholder="请输入字段名(v-model)" />
           </el-form-item>
-          <el-form-item v-if="activeData.componentName!==undefined" label="组件名">
-            {{ activeData.componentName }}
+          <el-form-item v-if="activeData.__config__.componentName!==undefined" label="组件名">
+            {{ activeData.__config__.componentName }}
           </el-form-item>
-          <el-form-item v-if="activeData.label!==undefined" label="标题">
-            <el-input v-model="activeData.label" placeholder="请输入标题" />
+          <el-form-item v-if="activeData.__config__.label!==undefined" label="标题">
+            <el-input v-model="activeData.__config__.label" placeholder="请输入标题" @input="changeRenderKey" />
           </el-form-item>
           <el-form-item v-if="activeData.placeholder!==undefined" label="占位提示">
-            <el-input v-model="activeData.placeholder" placeholder="请输入占位提示" />
+            <el-input v-model="activeData.placeholder" placeholder="请输入占位提示" @input="changeRenderKey" />
           </el-form-item>
           <el-form-item v-if="activeData['start-placeholder']!==undefined" label="开始占位">
             <el-input v-model="activeData['start-placeholder']" placeholder="请输入占位提示" />
@@ -49,13 +49,13 @@
           <el-form-item v-if="activeData['end-placeholder']!==undefined" label="结束占位">
             <el-input v-model="activeData['end-placeholder']" placeholder="请输入占位提示" />
           </el-form-item>
-          <el-form-item v-if="activeData.span!==undefined" label="表单栅格">
-            <el-slider v-model="activeData.span" :max="24" :min="1" :marks="{12:''}" @change="spanChange" />
+          <el-form-item v-if="activeData.__config__.span!==undefined" label="表单栅格">
+            <el-slider v-model="activeData.__config__.span" :max="24" :min="1" :marks="{12:''}" @change="spanChange" />
           </el-form-item>
-          <el-form-item v-if="activeData.layout==='rowFormItem'" label="栅格间隔">
+          <el-form-item v-if="activeData.__config__.layout==='rowFormItem'&&activeData.gutter!==undefined" label="栅格间隔">
             <el-input-number v-model="activeData.gutter" :min="0" placeholder="栅格间隔" />
           </el-form-item>
-          <el-form-item v-if="activeData.layout==='rowFormItem'" label="布局模式">
+          <el-form-item v-if="activeData.__config__.layout==='rowFormItem'&&activeData.type!==undefined" label="布局模式">
             <el-radio-group v-model="activeData.type">
               <el-radio-button label="default" />
               <el-radio-button label="flex" />
@@ -78,20 +78,20 @@
               <el-radio-button label="bottom" />
             </el-radio-group>
           </el-form-item>
-          <el-form-item v-if="activeData.labelWidth!==undefined" label="标签宽度">
-            <el-input v-model.number="activeData.labelWidth" type="number" placeholder="请输入标签宽度" />
+          <el-form-item v-if="activeData.__config__.labelWidth!==undefined" label="标签宽度">
+            <el-input v-model.number="activeData.__config__.labelWidth" type="number" placeholder="请输入标签宽度" />
           </el-form-item>
           <el-form-item v-if="activeData.style&&activeData.style.width!==undefined" label="组件宽度">
             <el-input v-model="activeData.style.width" placeholder="请输入组件宽度" clearable />
           </el-form-item>
-          <el-form-item v-if="activeData.vModel!==undefined" label="默认值">
+          <el-form-item v-if="activeData.__vModel__!==undefined" label="默认值">
             <el-input
-              :value="setDefaultValue(activeData.defaultValue)"
+              :value="setDefaultValue(activeData.__config__.defaultValue)"
               placeholder="请输入默认值"
               @input="onDefaultValueInput"
             />
           </el-form-item>
-          <el-form-item v-if="activeData.tag==='el-checkbox-group'" label="至少应选">
+          <el-form-item v-if="activeData.__config__.tag==='el-checkbox-group'" label="至少应选">
             <el-input-number
               :value="activeData.min"
               :min="0"
@@ -99,7 +99,7 @@
               @input="$set(activeData, 'min', $event?$event:undefined)"
             />
           </el-form-item>
-          <el-form-item v-if="activeData.tag==='el-checkbox-group'" label="最多可选">
+          <el-form-item v-if="activeData.__config__.tag==='el-checkbox-group'" label="最多可选">
             <el-input-number
               :value="activeData.max"
               :min="0"
@@ -107,11 +107,11 @@
               @input="$set(activeData, 'max', $event?$event:undefined)"
             />
           </el-form-item>
-          <el-form-item v-if="activeData.prepend!==undefined" label="前缀">
-            <el-input v-model="activeData.prepend" placeholder="请输入前缀" />
+          <el-form-item v-if="activeData.__slot__&&activeData.__slot__.prepend!==undefined" label="前缀">
+            <el-input v-model="activeData.__slot__.prepend" placeholder="请输入前缀" />
           </el-form-item>
-          <el-form-item v-if="activeData.append!==undefined" label="后缀">
-            <el-input v-model="activeData.append" placeholder="请输入后缀" />
+          <el-form-item v-if="activeData.__slot__&&activeData.__slot__.append!==undefined" label="后缀">
+            <el-input v-model="activeData.__slot__.append" placeholder="请输入后缀" />
           </el-form-item>
           <el-form-item v-if="activeData['prefix-icon']!==undefined" label="前图标">
             <el-input v-model="activeData['prefix-icon']" placeholder="请输入前图标名称">
@@ -127,7 +127,17 @@
               </el-button>
             </el-input>
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-cascader'" label="选项分隔符">
+          <el-form-item
+            v-if="activeData['icon']!==undefined && activeData.__config__.tag === 'el-button'"
+            label="按钮图标"
+          >
+            <el-input v-model="activeData['icon']" placeholder="请输入按钮图标名称">
+              <el-button slot="append" icon="el-icon-thumb" @click="openIconsDialog('icon')">
+                选择
+              </el-button>
+            </el-input>
+          </el-form-item>
+          <el-form-item v-if="activeData.__config__.tag === 'el-cascader'" label="选项分隔符">
             <el-input v-model="activeData.separator" placeholder="请输入选项分隔符" />
           </el-form-item>
           <el-form-item v-if="activeData.autosize !== undefined" label="最小行数">
@@ -136,19 +146,22 @@
           <el-form-item v-if="activeData.autosize !== undefined" label="最大行数">
             <el-input-number v-model="activeData.autosize.maxRows" :min="1" placeholder="最大行数" />
           </el-form-item>
-          <el-form-item v-if="activeData.min !== undefined" label="最小值">
+          <el-form-item v-if="isShowMin" label="最小值">
             <el-input-number v-model="activeData.min" placeholder="最小值" />
           </el-form-item>
-          <el-form-item v-if="activeData.max !== undefined" label="最大值">
+          <el-form-item v-if="isShowMax" label="最大值">
             <el-input-number v-model="activeData.max" placeholder="最大值" />
           </el-form-item>
-          <el-form-item v-if="activeData.step !== undefined" label="步长">
+          <el-form-item v-if="activeData.height!==undefined" label="组件高度">
+            <el-input-number v-model="activeData.height" placeholder="高度" @input="changeRenderKey" />
+          </el-form-item>
+          <el-form-item v-if="isShowStep" label="步长">
             <el-input-number v-model="activeData.step" placeholder="步数" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-input-number'" label="精度">
+          <el-form-item v-if="activeData.__config__.tag === 'el-input-number'" label="精度">
             <el-input-number v-model="activeData.precision" :min="0" placeholder="精度" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-input-number'" label="按钮位置">
+          <el-form-item v-if="activeData.__config__.tag === 'el-input-number'" label="按钮位置">
             <el-radio-group v-model="activeData['controls-position']">
               <el-radio-button label="">
                 默认
@@ -186,7 +199,7 @@
             />
           </el-form-item>
           <el-form-item
-            v-if="activeData.type !== undefined && 'el-date-picker' === activeData.tag"
+            v-if="activeData.type !== undefined && 'el-date-picker' === activeData.__config__.tag"
             label="时间类型"
           >
             <el-select
@@ -222,9 +235,9 @@
               <el-option label="txt" value=".txt" />
             </el-select>
           </el-form-item>
-          <el-form-item v-if="activeData.fileSize !== undefined" label="文件大小">
-            <el-input v-model.number="activeData.fileSize" placeholder="请输入文件大小">
-              <el-select slot="append" v-model="activeData.sizeUnit" :style="{ width: '66px' }">
+          <el-form-item v-if="activeData.__config__.fileSize !== undefined" label="文件大小">
+            <el-input v-model.number="activeData.__config__.fileSize" placeholder="请输入文件大小">
+              <el-select slot="append" v-model="activeData.__config__.sizeUnit" :style="{ width: '66px' }">
                 <el-option label="KB" value="KB" />
                 <el-option label="MB" value="MB" />
                 <el-option label="GB" value="GB" />
@@ -248,11 +261,24 @@
             </el-radio-group>
           </el-form-item>
           <el-form-item
-            v-if="activeData.buttonText !== undefined"
+            v-if="activeData.type !== undefined && activeData.__config__.tag === 'el-button'"
+            label="按钮类型"
+          >
+            <el-select v-model="activeData.type" :style="{ width: '100%' }">
+              <el-option label="primary" value="primary" />
+              <el-option label="success" value="success" />
+              <el-option label="warning" value="warning" />
+              <el-option label="danger" value="danger" />
+              <el-option label="info" value="info" />
+              <el-option label="text" value="text" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            v-if="activeData.__config__.buttonText !== undefined"
             v-show="'picture-card' !== activeData['list-type']"
             label="按钮文字"
           >
-            <el-input v-model="activeData.buttonText" placeholder="请输入按钮文字" />
+            <el-input v-model="activeData.__config__.buttonText" placeholder="请输入按钮文字" />
           </el-form-item>
           <el-form-item v-if="activeData['range-separator'] !== undefined" label="分隔符">
             <el-input v-model="activeData['range-separator']" placeholder="请输入分隔符" />
@@ -270,15 +296,15 @@
               @input="setTimeValue($event)"
             />
           </el-form-item>
-          <template v-if="['el-checkbox-group', 'el-radio-group', 'el-select'].indexOf(activeData.tag) > -1">
+          <template v-if="['el-checkbox-group', 'el-radio-group', 'el-select'].indexOf(activeData.__config__.tag) > -1">
             <el-divider>选项</el-divider>
             <draggable
-              :list="activeData.options"
+              :list="activeData.__slot__.options"
               :animation="340"
               group="selectItem"
               handle=".option-drag"
             >
-              <div v-for="(item, index) in activeData.options" :key="index" class="select-item">
+              <div v-for="(item, index) in activeData.__slot__.options" :key="index" class="select-item">
                 <div class="select-line-icon option-drag">
                   <i class="el-icon-s-operation" />
                 </div>
@@ -289,7 +315,7 @@
                   :value="item.value"
                   @input="setOptionValue(item, $event)"
                 />
-                <div class="close-btn select-line-icon" @click="activeData.options.splice(index, 1)">
+                <div class="close-btn select-line-icon" @click="activeData.__slot__.options.splice(index, 1)">
                   <i class="el-icon-remove-outline" />
                 </div>
               </div>
@@ -307,10 +333,10 @@
             <el-divider />
           </template>
 
-          <template v-if="['el-cascader'].indexOf(activeData.tag) > -1">
+          <template v-if="['el-cascader'].indexOf(activeData.__config__.tag) > -1">
             <el-divider>选项</el-divider>
             <el-form-item label="数据类型">
-              <el-radio-group v-model="activeData.dataType" size="small">
+              <el-radio-group v-model="activeData.__config__.dataType" size="small">
                 <el-radio-button label="dynamic">
                   动态数据
                 </el-radio-button>
@@ -320,27 +346,28 @@
               </el-radio-group>
             </el-form-item>
 
-            <template v-if="activeData.dataType === 'dynamic'">
+            <template v-if="activeData.__config__.dataType === 'dynamic'">
               <el-form-item label="标签键名">
-                <el-input v-model="activeData.labelKey" placeholder="请输入标签键名" />
+                <el-input v-model="activeData.props.props.label" placeholder="请输入标签键名" />
               </el-form-item>
               <el-form-item label="值键名">
-                <el-input v-model="activeData.valueKey" placeholder="请输入值键名" />
+                <el-input v-model="activeData.props.props.value" placeholder="请输入值键名" />
               </el-form-item>
               <el-form-item label="子级键名">
-                <el-input v-model="activeData.childrenKey" placeholder="请输入子级键名" />
+                <el-input v-model="activeData.props.props.children" placeholder="请输入子级键名" />
               </el-form-item>
             </template>
 
+            <!-- 级联选择静态树 -->
             <el-tree
-              v-if="activeData.dataType === 'static'"
+              v-if="activeData.__config__.dataType === 'static'"
               draggable
               :data="activeData.options"
               node-key="id"
               :expand-on-click-node="false"
               :render-content="renderContent"
             />
-            <div v-if="activeData.dataType === 'static'" style="margin-left: 20px">
+            <div v-if="activeData.__config__.dataType === 'static'" style="margin-left: 20px">
               <el-button
                 style="padding-bottom: 0"
                 icon="el-icon-circle-plus-outline"
@@ -353,8 +380,8 @@
             <el-divider />
           </template>
 
-          <el-form-item v-if="activeData.optionType !== undefined" label="选项样式">
-            <el-radio-group v-model="activeData.optionType">
+          <el-form-item v-if="activeData.__config__.optionType !== undefined" label="选项样式">
+            <el-radio-group v-model="activeData.__config__.optionType">
               <el-radio-button label="default">
                 默认
               </el-radio-button>
@@ -370,6 +397,14 @@
             <el-color-picker v-model="activeData['inactive-color']" />
           </el-form-item>
 
+          <el-form-item v-if="activeData.__config__.showLabel !== undefined
+            && activeData.__config__.labelWidth !== undefined" label="显示标签"
+          >
+            <el-switch v-model="activeData.__config__.showLabel" />
+          </el-form-item>
+          <el-form-item v-if="activeData.branding !== undefined" label="品牌烙印">
+            <el-switch v-model="activeData.branding" @input="changeRenderKey" />
+          </el-form-item>
           <el-form-item v-if="activeData['allow-half'] !== undefined" label="允许半选">
             <el-switch v-model="activeData['allow-half']" />
           </el-form-item>
@@ -386,16 +421,17 @@
             <el-switch v-model="activeData.range" @change="rangeChange" />
           </el-form-item>
           <el-form-item
-            v-if="activeData.border !== undefined && activeData.optionType === 'default'"
+            v-if="activeData.__config__.border !== undefined && activeData.__config__.optionType === 'default'"
             label="是否带边框"
           >
-            <el-switch v-model="activeData.border" />
+            <el-switch v-model="activeData.__config__.border" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-color-picker'" label="颜色格式">
+          <el-form-item v-if="activeData.__config__.tag === 'el-color-picker'" label="颜色格式">
             <el-select
               v-model="activeData['color-format']"
               placeholder="请选择颜色格式"
               :style="{ width: '100%' }"
+              clearable
               @change="colorFormatChange"
             >
               <el-option
@@ -408,10 +444,11 @@
           </el-form-item>
           <el-form-item
             v-if="activeData.size !== undefined &&
-              (activeData.optionType === 'button' ||
-              activeData.border ||
-              activeData.tag === 'el-color-picker')"
-            label="选项尺寸"
+              (activeData.__config__.optionType === 'button' ||
+                activeData.__config__.border ||
+                activeData.__config__.tag === 'el-color-picker' ||
+                activeData.__config__.tag === 'el-button')"
+            label="组件尺寸"
           >
             <el-radio-group v-model="activeData.size">
               <el-radio-button label="medium">
@@ -428,25 +465,25 @@
           <el-form-item v-if="activeData['show-word-limit'] !== undefined" label="输入统计">
             <el-switch v-model="activeData['show-word-limit']" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-input-number'" label="严格步数">
+          <el-form-item v-if="activeData.__config__.tag === 'el-input-number'" label="严格步数">
             <el-switch v-model="activeData['step-strictly']" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-cascader'" label="是否多选">
+          <el-form-item v-if="activeData.__config__.tag === 'el-cascader'" label="是否多选">
             <el-switch v-model="activeData.props.props.multiple" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-cascader'" label="展示全路径">
+          <el-form-item v-if="activeData.__config__.tag === 'el-cascader'" label="展示全路径">
             <el-switch v-model="activeData['show-all-levels']" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-cascader'" label="可否筛选">
+          <el-form-item v-if="activeData.__config__.tag === 'el-cascader'" label="可否筛选">
             <el-switch v-model="activeData.filterable" />
           </el-form-item>
           <el-form-item v-if="activeData.clearable !== undefined" label="能否清空">
             <el-switch v-model="activeData.clearable" />
           </el-form-item>
-          <el-form-item v-if="activeData.showTip !== undefined" label="显示提示">
-            <el-switch v-model="activeData.showTip" />
+          <el-form-item v-if="activeData.__config__.showTip !== undefined" label="显示提示">
+            <el-switch v-model="activeData.__config__.showTip" />
           </el-form-item>
-          <el-form-item v-if="activeData.multiple !== undefined" label="多选文件">
+          <el-form-item v-if="activeData.__config__.tag === 'el-upload'" label="多选文件">
             <el-switch v-model="activeData.multiple" />
           </el-form-item>
           <el-form-item v-if="activeData['auto-upload'] !== undefined" label="自动上传">
@@ -458,20 +495,20 @@
           <el-form-item v-if="activeData.disabled !== undefined" label="是否禁用">
             <el-switch v-model="activeData.disabled" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-select'" label="是否可搜索">
+          <el-form-item v-if="activeData.__config__.tag === 'el-select'" label="能否搜索">
             <el-switch v-model="activeData.filterable" />
           </el-form-item>
-          <el-form-item v-if="activeData.tag === 'el-select'" label="是否多选">
+          <el-form-item v-if="activeData.__config__.tag === 'el-select'" label="是否多选">
             <el-switch v-model="activeData.multiple" @change="multipleChange" />
           </el-form-item>
-          <el-form-item v-if="activeData.required !== undefined" label="是否必填">
-            <el-switch v-model="activeData.required" />
+          <el-form-item v-if="activeData.__config__.required !== undefined" label="是否必填">
+            <el-switch v-model="activeData.__config__.required" />
           </el-form-item>
 
-          <template v-if="activeData.layoutTree">
+          <template v-if="activeData.__config__.layoutTree">
             <el-divider>布局结构树</el-divider>
             <el-tree
-              :data="[activeData]"
+              :data="[activeData.__config__]"
               :props="layoutTreeProps"
               node-key="renderKey"
               default-expand-all
@@ -479,21 +516,21 @@
             >
               <span slot-scope="{ node, data }">
                 <span class="node-label">
-                  <svg-icon class="node-icon" :icon-class="data.tagIcon" />
+                  <svg-icon class="node-icon" :icon-class="data.__config__?data.__config__.tagIcon:data.tagIcon" />
                   {{ node.label }}
                 </span>
               </span>
             </el-tree>
           </template>
 
-          <template v-if="activeData.layout === 'colFormItem'">
+          <template v-if="Array.isArray(activeData.__config__.regList)">
             <el-divider>正则校验</el-divider>
             <div
-              v-for="(item, index) in activeData.regList"
+              v-for="(item, index) in activeData.__config__.regList"
               :key="index"
               class="reg-item"
             >
-              <span class="close-btn" @click="activeData.regList.splice(index, 1)">
+              <span class="close-btn" @click="activeData.__config__.regList.splice(index, 1)">
                 <i class="el-icon-close" />
               </span>
               <el-form-item label="表达式">
@@ -548,7 +585,7 @@
             </el-radio-group>
           </el-form-item>
           <el-form-item label="标签宽度">
-            <el-input-number v-model="formConf.labelWidth" placeholder="标签宽度" />
+            <el-input v-model.number="formConf.labelWidth" type="number" placeholder="请输入标签宽度" />
           </el-form-item>
           <el-form-item label="栅格间隔">
             <el-input-number v-model="formConf.gutter" :min="0" placeholder="栅格间隔" />
@@ -573,15 +610,13 @@
 
 <script>
 import { isArray } from 'util'
-import TreeNodeDialog from './TreeNodeDialog'
-import { isNumberStr } from '@/utils/index'
+import TreeNodeDialog from '@/views/tools/build/TreeNodeDialog'
+import { isNumberStr } from '@/utils/generator/index'
 import IconsDialog from './IconsDialog'
 import {
-  inputComponents,
-  selectComponents,
-  // eslint-disable-next-line no-unused-vars
-  layoutComponents
-} from '@/utils/generator/config'
+  inputComponents, selectComponents, layoutComponents
+} from '@/components/generator/config'
+import { saveFormConf } from '@/utils/generator/db'
 
 const dateTimeFormat = {
   date: 'yyyy-MM-dd',
@@ -594,12 +629,14 @@ const dateTimeFormat = {
   datetimerange: 'yyyy-MM-dd HH:mm:ss'
 }
 
+// 使changeRenderKey在目标组件改变时可用
+const needRerenderList = ['tinymce']
+
 export default {
   components: {
     TreeNodeDialog,
     IconsDialog
   },
-  // eslint-disable-next-line vue/require-prop-types
   props: ['showField', 'activeData', 'formConf'],
   data() {
     return {
@@ -690,7 +727,8 @@ export default {
       ],
       layoutTreeProps: {
         label(data, node) {
-          return data.componentName || `${data.label}: ${data.vModel}`
+          const config = data.__config__
+          return data.componentName || `${config.label}: ${data.__vModel__}`
         }
       }
     }
@@ -698,14 +736,14 @@ export default {
   computed: {
     documentLink() {
       return (
-        this.activeData.document ||
-        'https://element.eleme.cn/#/zh-CN/component/installation'
+        this.activeData.__config__.document
+        || 'https://element.eleme.cn/#/zh-CN/component/installation'
       )
     },
     dateOptions() {
       if (
-        this.activeData.type !== undefined &&
-        this.activeData.tag === 'el-date-picker'
+        this.activeData.type !== undefined
+        && this.activeData.__config__.tag === 'el-date-picker'
       ) {
         if (this.activeData['start-placeholder'] === undefined) {
           return this.dateTypeOptions
@@ -725,17 +763,37 @@ export default {
           options: selectComponents
         }
       ]
+    },
+    activeTag() {
+      return this.activeData.__config__.tag
+    },
+    isShowMin() {
+      return ['el-input-number', 'el-slider'].indexOf(this.activeTag) > -1
+    },
+    isShowMax() {
+      return ['el-input-number', 'el-slider', 'el-rate'].indexOf(this.activeTag) > -1
+    },
+    isShowStep() {
+      return ['el-input-number', 'el-slider'].indexOf(this.activeTag) > -1
+    }
+  },
+  watch: {
+    formConf: {
+      handler(val) {
+        saveFormConf(val)
+      },
+      deep: true
     }
   },
   methods: {
     addReg() {
-      this.activeData.regList.push({
+      this.activeData.__config__.regList.push({
         pattern: '',
         message: ''
       })
     },
     addSelectItem() {
-      this.activeData.options.push({
+      this.activeData.__slot__.options.push({
         label: '',
         value: ''
       })
@@ -747,16 +805,16 @@ export default {
     },
     renderContent(h, { node, data, store }) {
       return (
-        <div class='custom-tree-node'>
+        <div class="custom-tree-node">
           <span>{node.label}</span>
-          <span class='node-operation'>
+          <span class="node-operation">
             <i on-click={() => this.append(data)}
-              class='el-icon-plus'
-              title='添加'
+              class="el-icon-plus"
+              title="添加"
             ></i>
             <i on-click={() => this.remove(node, data)}
-              class='el-icon-delete'
-              title='删除'
+              class="el-icon-delete"
+              title="删除"
             ></i>
           </span>
         </div>
@@ -770,6 +828,7 @@ export default {
       this.currentNode = data.children
     },
     remove(node, data) {
+      this.activeData.__config__.defaultValue = [] // 避免删除时报错
       const { parent } = node
       const children = parent.data.children || parent.data
       const index = children.findIndex(d => d.id === data.id)
@@ -785,29 +844,29 @@ export default {
       if (Array.isArray(val)) {
         return val.join(',')
       }
-      if (['string', 'number'].indexOf(val) > -1) {
-        return val
-      }
+      // if (['string', 'number'].indexOf(typeof val) > -1) {
+      //   return val
+      // }
       if (typeof val === 'boolean') {
         return `${val}`
       }
       return val
     },
     onDefaultValueInput(str) {
-      if (isArray(this.activeData.defaultValue)) {
+      if (isArray(this.activeData.__config__.defaultValue)) {
         // 数组
         this.$set(
-          this.activeData,
+          this.activeData.__config__,
           'defaultValue',
           str.split(',').map(val => (isNumberStr(val) ? +val : val))
         )
       } else if (['true', 'false'].indexOf(str) > -1) {
         // 布尔
-        this.$set(this.activeData, 'defaultValue', JSON.parse(str))
+        this.$set(this.activeData.__config__, 'defaultValue', JSON.parse(str))
       } else {
         // 字符串和数字
         this.$set(
-          this.activeData,
+          this.activeData.__config__,
           'defaultValue',
           isNumberStr(str) ? +str : str
         )
@@ -822,7 +881,7 @@ export default {
     },
     setTimeValue(val, type) {
       const valueFormat = type === 'week' ? dateTimeFormat.date : val
-      this.$set(this.activeData, 'defaultValue', null)
+      this.$set(this.activeData.__config__, 'defaultValue', null)
       this.$set(this.activeData, 'value-format', valueFormat)
       this.$set(this.activeData, 'format', val)
     },
@@ -830,14 +889,14 @@ export default {
       this.formConf.span = val
     },
     multipleChange(val) {
-      this.$set(this.activeData, 'defaultValue', val ? [] : '')
+      this.$set(this.activeData.__config__, 'defaultValue', val ? [] : '')
     },
     dateTypeChange(val) {
       this.setTimeValue(dateTimeFormat[val], val)
     },
     rangeChange(val) {
       this.$set(
-        this.activeData,
+        this.activeData.__config__,
         'defaultValue',
         val ? [this.activeData.min, this.activeData.max] : this.activeData.min
       )
@@ -849,9 +908,9 @@ export default {
       if (val) this.activeData['show-text'] = false
     },
     colorFormatChange(val) {
-      this.activeData.defaultValue = null
+      this.activeData.__config__.defaultValue = null
       this.activeData['show-alpha'] = val.indexOf('a') > -1
-      this.activeData.renderKey = +new Date() // 更新renderKey,重新渲染该组件
+      this.activeData.__config__.renderKey = +new Date() // 更新renderKey,重新渲染该组件
     },
     openIconsDialog(model) {
       this.iconsVisible = true
@@ -861,9 +920,14 @@ export default {
       this.activeData[this.currentIconModel] = val
     },
     tagChange(tagIcon) {
-      let target = inputComponents.find(item => item.tagIcon === tagIcon)
-      if (!target) target = selectComponents.find(item => item.tagIcon === tagIcon)
+      let target = inputComponents.find(item => item.__config__.tagIcon === tagIcon)
+      if (!target) target = selectComponents.find(item => item.__config__.tagIcon === tagIcon)
       this.$emit('tag-change', target)
+    },
+    changeRenderKey() {
+      if (needRerenderList.includes(this.activeData.__config__.tag)) {
+        this.activeData.__config__.renderKey = +new Date()
+      }
     }
   }
 }

+ 13 - 3
src/views/tools/build/TreeNodeDialog.vue

@@ -71,7 +71,10 @@
   </div>
 </template>
 <script>
-import { isNumberStr } from '@/utils/index'
+import { isNumberStr } from '@/utils/generator/index'
+import { getTreeNodeId, saveTreeNodeId } from '@/utils/generator/db'
+
+const id = getTreeNodeId()
 
 export default {
   components: {},
@@ -79,7 +82,7 @@ export default {
   props: [],
   data() {
     return {
-      id: 100,
+      id,
       formData: {
         label: undefined,
         value: undefined
@@ -115,8 +118,12 @@ export default {
   },
   computed: {},
   watch: {
-    'formData.value': function(val) {
+    // eslint-disable-next-line func-names
+    'formData.value': function (val) {
       this.dataType = isNumberStr(val) ? 'number' : 'string'
+    },
+    id(val) {
+      saveTreeNodeId(val)
     }
   },
   created() {},
@@ -146,3 +153,6 @@ export default {
   }
 }
 </script>
+
+<style lang="scss" scoped>
+</style>

+ 170 - 542
src/views/tools/build/index.vue

@@ -1,91 +1,54 @@
+
 <template>
-  <div class="container">
+  <div class="container" id="previewApp">
     <div class="left-board">
       <div class="logo-wrapper">
         <div class="logo">
-          <img :src="logo" alt="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 v-for="(item, listIndex) in leftComponents" :key="listIndex">
+            <div class="components-title">
+              <svg-icon icon-class="component" />
+              {{ item.title }}
             </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)"
+            <draggable
+              class="components-draggable"
+              :list="item.list"
+              :group="{ name: 'componentsGroup', pull: 'clone', put: false }"
+              :clone="cloneComponent"
+              draggable=".components-item"
+              :sort="false"
+              @end="onEnd"
             >
-              <div class="components-body">
-                <svg-icon :icon-class="element.tagIcon" />
-                {{ element.label }}
+              <div
+                v-for="(element, index) in item.list"
+                :key="index"
+                class="components-item"
+                @click="addComponent(element)"
+              >
+                <div class="components-body">
+                  <svg-icon :icon-class="element.__config__.tagIcon" />
+                  {{ element.__config__.label }}
+                </div>
               </div>
-            </div>
-          </draggable>
-          <div class="components-title">
-            <svg-icon icon-class="component" /> 布局型组件
+            </draggable>
           </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-video-play" type="text" @click="run">
+          运行
+        </el-button>
+        <el-button icon="el-icon-view" type="text" @click="showJson">
+          查看json
+        </el-button>
         <el-button icon="el-icon-download" type="text" @click="download">
           导出vue文件
         </el-button>
@@ -133,6 +96,18 @@
       @tag-change="tagChange"
     />
 
+    <form-drawer
+      :visible.sync="drawerVisible"
+      :form-data="formData"
+      size="100%"
+      :generate-conf="generateConf"
+    />
+    <json-drawer
+      size="60%"
+      :visible.sync="jsonDrawerVisible"
+      :json-str="JSON.stringify(formData)"
+      @refresh="refreshJson"
+    />
     <code-type-dialog
       :visible.sync="dialogVisible"
       title="选择生成类型"
@@ -145,40 +120,48 @@
 
 <script>
 import draggable from 'vuedraggable'
+import { debounce } from 'throttle-debounce'
 import { saveAs } from 'file-saver'
-import beautifier from 'js-beautify'
 import ClipboardJS from 'clipboard'
-import render from '@/utils/generator/render'
+import render from '@/components/render/render'
+import FormDrawer from './FormDrawer'
+import JsonDrawer from './JsonDrawer'
 import RightPanel from './RightPanel'
 import {
-  inputComponents,
-  selectComponents,
-  layoutComponents,
-  formConf
-} from '@/utils/generator/config'
-// eslint-disable-next-line
-import {exportDefault, beautifierConf, isNumberStr, titleCase} from '@/utils/generator/index'
+  inputComponents, selectComponents, layoutComponents, formConf
+} from '@/components/generator/config'
+import {
+  exportDefault, beautifierConf, isNumberStr, titleCase, deepClone
+} from '@/utils/generator/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'
+} from '@/components/generator/html'
+import { makeUpJs } from '@/components/generator/js'
+import { makeUpCss } from '@/components/generator/css'
+import drawingDefalut from '@/components/generator/drawingDefalut'
 import logo from '@/assets/logo/logo.png'
 import CodeTypeDialog from './CodeTypeDialog'
 import DraggableItem from './DraggableItem'
+import {
+  getDrawingList, saveDrawingList, getIdGlobal, saveIdGlobal, getFormConf
+} from '@/utils/generator/db'
+import loadBeautifier from '@/utils/generator/loadBeautifier'
 
-// eslint-disable-next-line
-const emptyActiveData = { style: {}, autosize: {}}
 
+let beautifier
+const emptyActiveData = { style: {}, autosize: {} }
 let oldActiveId
 let tempActiveData
+const drawingListInDB = getDrawingList()
+const formConfInDB = getFormConf()
+const idGlobal = getIdGlobal()
 
 export default {
   components: {
     draggable,
-    // eslint-disable-next-line
     render,
+    FormDrawer,
+    JsonDrawer,
     RightPanel,
     CodeTypeDialog,
     DraggableItem
@@ -186,7 +169,7 @@ export default {
   data() {
     return {
       logo,
-      idGlobal: 100,
+      idGlobal,
       formConf,
       inputComponents,
       selectComponents,
@@ -198,19 +181,37 @@ export default {
       drawerVisible: false,
       formData: {},
       dialogVisible: false,
+      jsonDrawerVisible: false,
       generateConf: null,
       showFileName: false,
-      activeData: drawingDefalut[0]
+      activeData: drawingDefalut[0],
+      saveDrawingListDebounce: debounce(340, saveDrawingList),
+      saveIdGlobalDebounce: debounce(340, saveIdGlobal),
+      leftComponents: [
+        {
+          title: '输入型组件',
+          list: inputComponents
+        },
+        {
+          title: '选择型组件',
+          list: selectComponents
+        },
+        {
+          title: '布局型组件',
+          list: layoutComponents
+        }
+      ]
     }
   },
   computed: {
   },
   watch: {
-    'activeData.label': function(val, oldVal) {
+    // eslint-disable-next-line func-names
+    'activeData.__config__.label': function (val, oldVal) {
       if (
-        this.activeData.placeholder === undefined ||
-        !this.activeData.tag ||
-        oldActiveId !== this.activeId
+        this.activeData.placeholder === undefined
+        || !this.activeData.__config__.tag
+        || oldActiveId !== this.activeId
       ) {
         return
       }
@@ -221,9 +222,34 @@ export default {
         oldActiveId = val
       },
       immediate: true
+    },
+    drawingList: {
+      handler(val) {
+        this.saveDrawingListDebounce(val)
+        if (val.length === 0) this.idGlobal = 100
+      },
+      deep: true
+    },
+    idGlobal: {
+      handler(val) {
+        this.saveIdGlobalDebounce(val)
+      },
+      immediate: true
     }
   },
   mounted() {
+    if (Array.isArray(drawingListInDB) && drawingListInDB.length > 0) {
+      this.drawingList = drawingListInDB
+    } else {
+      this.drawingList = drawingDefalut
+    }
+    this.activeFormItem(this.drawingList[0])
+    if (formConfInDB) {
+      this.formConf = formConfInDB
+    }
+    loadBeautifier(btf => {
+      beautifier = btf
+    })
     const clipboard = new ClipboardJS('#copyNode', {
       text: trigger => {
         const codeStr = this.generateCode()
@@ -242,9 +268,9 @@ export default {
   methods: {
     activeFormItem(element) {
       this.activeData = element
-      this.activeId = element.formId
+      this.activeId = element.__config__.formId
     },
-    onEnd(obj, a) {
+    onEnd(obj) {
       if (obj.from !== obj.to) {
         this.activeData = tempActiveData
         this.activeId = this.idGlobal
@@ -256,26 +282,33 @@ export default {
       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
-      }
+      const clone = deepClone(origin)
+      const config = clone.__config__
+      config.span = this.formConf.span // 生成代码时,会根据span做精简判断
+      this.createIdAndKey(clone)
+      clone.placeholder !== undefined && (clone.placeholder += config.label)
+      tempActiveData = clone
       return tempActiveData
     },
+    createIdAndKey(item) {
+      const config = item.__config__
+      config.formId = ++this.idGlobal
+      config.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
+      if (config.layout === 'colFormItem') {
+        item.__vModel__ = `field${this.idGlobal}`
+      } else if (config.layout === 'rowFormItem') {
+        config.componentName = `row${this.idGlobal}`
+        !Array.isArray(config.children) && (config.children = [])
+        delete config.label // rowFormItem无需配置label属性
+      }
+      if (Array.isArray(config.children)) {
+        config.children = config.children.map(childItem => this.createIdAndKey(childItem))
+      }
+      return item
+    },
     AssembleFormData() {
       this.formData = {
-        fields: JSON.parse(JSON.stringify(this.drawingList)),
+        fields: deepClone(this.drawingList),
         ...this.formConf
       }
     },
@@ -300,28 +333,16 @@ export default {
       this.$confirm('确定要清空所有组件吗?', '提示', { type: 'warning' }).then(
         () => {
           this.drawingList = []
+          this.idGlobal = 100
         }
       )
     },
     drawingItemCopy(item, parent) {
-      let clone = JSON.parse(JSON.stringify(item))
+      let clone = deepClone(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(() => {
@@ -339,6 +360,10 @@ export default {
       const css = cssStyle(makeUpCss(this.formData))
       return beautifier.html(html + script + css, beautifierConf.html)
     },
+    showJson() {
+      this.AssembleFormData()
+      this.jsonDrawerVisible = true
+    },
     download() {
       this.dialogVisible = true
       this.showFileName = true
@@ -356,15 +381,18 @@ export default {
     },
     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
+      const config = newTag.__config__
+      newTag.__vModel__ = this.activeData.__vModel__
+      config.formId = this.activeId
+      config.span = this.activeData.__config__.span
+      this.activeData.__config__.tag = config.tag
+      this.activeData.__config__.tagIcon = config.tagIcon
+      this.activeData.__config__.document = config.document
+      if (typeof this.activeData.__config__.defaultValue === typeof config.defaultValue) {
+        config.defaultValue = this.activeData.__config__.defaultValue
+      }
       Object.keys(newTag).forEach(key => {
-        if (this.activeData[key] !== undefined &&
-          typeof this.activeData[key] === typeof newTag[key]) {
+        if (this.activeData[key] !== undefined) {
           newTag[key] = this.activeData[key]
         }
       })
@@ -372,427 +400,27 @@ export default {
       this.updateDrawingList(newTag, this.drawingList)
     },
     updateDrawingList(newTag, list) {
-      const index = list.findIndex(item => item.formId === this.activeId)
+      const index = list.findIndex(item => item.__config__.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)
+          if (Array.isArray(item.__config__.children)) this.updateDrawingList(newTag, item.__config__.children)
         })
       }
+    },
+    refreshJson(data) {
+      this.drawingList = deepClone(data.fields)
+      delete data.fields
+      this.formConf = data
     }
   }
 }
 </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;
-  }
-}
-.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;
-  }
-}
-
-.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;
-    }
-  }
-}
+<style lang="scss" scoped>
+@import '@/styles/generator/home.scss';
+</style>
 
-.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>

+ 0 - 0
src/views/tools/preview/index.vue


+ 60 - 0
src/views/tools/preview/main.js

@@ -0,0 +1,60 @@
+import Vue from 'vue'
+import { loadScriptQueue } from '@/utils/generator/loadScript'
+import Tinymce from '@/components/tinymce/index.vue'
+
+Vue.component('tinymce', Tinymce)
+
+const $previewApp = document.getElementById('previewApp')
+console.log($previewApp)
+const childAttrs = {
+  file: '',
+  dialog: ' width="600px" class="dialog-width" v-if="visible" :visible.sync="visible" :modal-append-to-body="false" '
+}
+
+window.addEventListener('message', init, false)
+
+function buildLinks(links) {
+  let strs = ''
+  links.forEach(url => {
+    strs += `<link href="${url}" rel="stylesheet">`
+  })
+  return strs
+}
+
+function init(event) {
+  if (event.data.type === 'refreshFrame') {
+    const code = event.data.data
+    const attrs = childAttrs[code.generateConf.type]
+    let links = ''
+
+    if (Array.isArray(code.links) && code.links.length > 0) {
+      links = buildLinks(code.links)
+    }
+
+    $previewApp.innerHTML = `${links}<style>${code.css}</style><div id="app"></div>`
+
+    if (Array.isArray(code.scripts) && code.scripts.length > 0) {
+      loadScriptQueue(code.scripts, () => {
+        newVue(attrs, code.js, code.html)
+      })
+    } else {
+      newVue(attrs, code.js, code.html)
+    }
+  }
+}
+
+function newVue(attrs, main, html) {
+  main = eval(`(${main})`)
+  main.template = `<div>${html}</div>`
+  new Vue({
+    components: {
+      child: main
+    },
+    data() {
+      return {
+        visible: true
+      }
+    },
+    template: `<div><child ${attrs}/></div>`
+  }).$mount('#app')
+}

+ 11 - 0
vue.config.js

@@ -2,6 +2,17 @@
 const path = require('path')
 const defaultSettings = require('./src/settings.js')
 
+const minify = process.env.NODE_ENV === 'development' ? false : {
+  collapseWhitespace: true,
+  removeComments: true,
+  removeRedundantAttributes: true,
+  removeScriptTypeAttributes: true,
+  removeStyleLinkTypeAttributes: true,
+  useShortDoctype: true,
+  minifyCSS: true,
+  minifyJS: true
+}
+
 function resolve(dir) {
   return path.join(__dirname, dir)
 }

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно