lex-xin 3 years ago
parent
commit
b19e36af18
100 changed files with 11585 additions and 0 deletions
  1. 163 0
      README.md
  2. 26 0
      plop-templates/component/index.hbs
  3. 55 0
      plop-templates/component/prompt.js
  4. 16 0
      plop-templates/store/index.hbs
  5. 62 0
      plop-templates/store/prompt.js
  6. 9 0
      plop-templates/utils.js
  7. 26 0
      plop-templates/view/index.hbs
  8. 55 0
      plop-templates/view/prompt.js
  9. 9 0
      plopfile.js
  10. 5 0
      postcss.config.js
  11. BIN
      public/favicon.ico
  12. 215 0
      public/index.html
  13. 22 0
      src/App.vue
  14. 9 0
      src/api/dashboard.js
  15. 9 0
      src/api/login.js
  16. 9 0
      src/api/monitor/server.js
  17. 37 0
      src/api/process/admin/classify.js
  18. 63 0
      src/api/process/admin/process.js
  19. 46 0
      src/api/process/admin/task.js
  20. 54 0
      src/api/process/admin/template.js
  21. 89 0
      src/api/process/work-order.js
  22. 17 0
      src/api/remote-search.js
  23. 67 0
      src/api/system/dept.js
  24. 35 0
      src/api/system/loginlog.js
  25. 60 0
      src/api/system/menu.js
  26. 45 0
      src/api/system/post.js
  27. 87 0
      src/api/system/role.js
  28. 19 0
      src/api/system/settings.js
  29. 134 0
      src/api/system/sysuser.js
  30. 32 0
      src/api/user.js
  31. BIN
      src/assets/401_images/401.gif
  32. BIN
      src/assets/404_images/404.png
  33. BIN
      src/assets/404_images/404_cloud.png
  34. BIN
      src/assets/custom-theme/fonts/element-icons.ttf
  35. BIN
      src/assets/custom-theme/fonts/element-icons.woff
  36. 0 0
      src/assets/custom-theme/index.css
  37. 39 0
      src/assets/dark.svg
  38. 39 0
      src/assets/light.svg
  39. BIN
      src/assets/login.png
  40. BIN
      src/assets/logo/ferry_logo.png
  41. BIN
      src/assets/logo/ferry_logo_meitu_1.png
  42. BIN
      src/assets/logo/ferry_logo_white.png
  43. BIN
      src/assets/logo/logo.png
  44. 110 0
      src/assets/particles.json
  45. 111 0
      src/components/BackToTop/index.vue
  46. 82 0
      src/components/Breadcrumb/index.vue
  47. 155 0
      src/components/Charts/Keyboard.vue
  48. 227 0
      src/components/Charts/LineMarker.vue
  49. 271 0
      src/components/Charts/MixChart.vue
  50. 34 0
      src/components/Charts/mixins/resize.js
  51. 166 0
      src/components/DndList/index.vue
  52. 61 0
      src/components/DragSelect/index.vue
  53. 297 0
      src/components/Dropzone/index.vue
  54. 78 0
      src/components/ErrorLog/index.vue
  55. 54 0
      src/components/GithubCorner/index.vue
  56. 44 0
      src/components/Hamburger/index.vue
  57. 180 0
      src/components/HeaderSearch/index.vue
  58. 68 0
      src/components/IconSelect/index.vue
  59. 10 0
      src/components/IconSelect/requireIcons.js
  60. 1778 0
      src/components/ImageCropper/index.vue
  61. 19 0
      src/components/ImageCropper/utils/data2blob.js
  62. 39 0
      src/components/ImageCropper/utils/effectRipple.js
  63. 232 0
      src/components/ImageCropper/utils/language.js
  64. 7 0
      src/components/ImageCropper/utils/mimes.js
  65. 72 0
      src/components/JsonEditor/index.vue
  66. 99 0
      src/components/Kanban/index.vue
  67. 360 0
      src/components/MDinput/index.vue
  68. 31 0
      src/components/MarkdownEditor/default-options.js
  69. 118 0
      src/components/MarkdownEditor/index.vue
  70. 101 0
      src/components/Pagination/index.vue
  71. 142 0
      src/components/PanThumb/index.vue
  72. 145 0
      src/components/RightPanel/index.vue
  73. 60 0
      src/components/Screenfull/index.vue
  74. 100 0
      src/components/Share/DropdownMenu.vue
  75. 57 0
      src/components/SizeSelect/index.vue
  76. 91 0
      src/components/Sticky/index.vue
  77. 62 0
      src/components/SvgIcon/index.vue
  78. 113 0
      src/components/TextHoverEffect/Mallki.vue
  79. 174 0
      src/components/ThemePicker/index.vue
  80. 111 0
      src/components/Tinymce/components/EditorImage.vue
  81. 59 0
      src/components/Tinymce/dynamicLoadScript.js
  82. 237 0
      src/components/Tinymce/index.vue
  83. 7 0
      src/components/Tinymce/plugins.js
  84. 6 0
      src/components/Tinymce/toolbar.js
  85. 134 0
      src/components/Upload/SingleImage.vue
  86. 130 0
      src/components/Upload/SingleImage2.vue
  87. 157 0
      src/components/Upload/SingleImage3.vue
  88. 138 0
      src/components/UploadExcel/index.vue
  89. 134 0
      src/components/VueFormMaking/App.vue
  90. BIN
      src/components/VueFormMaking/assets/logo.png
  91. 445 0
      src/components/VueFormMaking/components/Container.vue
  92. 139 0
      src/components/VueFormMaking/components/CusDialog.vue
  93. 32 0
      src/components/VueFormMaking/components/FormConfig.vue
  94. 248 0
      src/components/VueFormMaking/components/GenerateForm.vue
  95. 468 0
      src/components/VueFormMaking/components/GenerateFormItem.vue
  96. 63 0
      src/components/VueFormMaking/components/Upload/file.vue
  97. 474 0
      src/components/VueFormMaking/components/Upload/index.vue
  98. 780 0
      src/components/VueFormMaking/components/WidgetConfig.vue
  99. 290 0
      src/components/VueFormMaking/components/WidgetForm.vue
  100. 232 0
      src/components/VueFormMaking/components/WidgetFormFields.vue

+ 163 - 0
README.md

@@ -0,0 +1,163 @@
+<p align="center">
+  <img src="https://www.fdevops.com/wp-content/uploads/2020/09/1599039924-ferry_log.png">
+</p>
+
+<p align="center">
+  <a href="https://github.com/lanyulei/ferry">
+    <img src="https://www.fdevops.com/wp-content/uploads/2020/07/1595067271-badge.png">
+  </a>
+  <a href="https://github.com/lanyulei/ferry">
+    <img src="https://www.fdevops.com/wp-content/uploads/2020/07/1595067272-apistatus.png" alt="license">
+  </a>
+    <a href="https://github.com/lanyulei/ferry">
+    <img src="https://www.fdevops.com/wp-content/uploads/2020/07/1595067269-donate.png" alt="donate">
+  </a>
+</p>
+
+## 基于Gin + Vue + Element UI前后端分离的工单系统
+
+**流程中心**
+
+通过灵活的配置流程、模版等数据,非常快速方便的生成工单流程,通过对流程进行任务绑定,实现流程中的钩子操作,目前支持绑定邮件来通知处理,当然为兼容更多的通知方式,也可以自己写任务脚本来进行任务通知,可根据自己的需求定制。
+
+兼容了多种处理情况,包括串行处理、并行处理以及根据条件判断进行节点跳转。
+
+可通过变量设置处理人,例如:直接负责人、部门负责人、HRBP等变量数据。
+
+**系统管理**
+
+基于casbin的RBAC权限控制,借鉴了go-admin项目的前端权限管理,可以在页面对API、菜单、页面按钮等操作,进行灵活且简单的配置。
+
+演示demo: [http://fdevops.com:8001/#/dashboard](http://fdevops.com:8001/#/dashboard)
+
+```
+账号:admin
+密码:123456
+
+演示demo登陆需要取消ldap验证,就是登陆页面取消ldap的打勾。
+```
+
+文档: [https://www.fdevops.com/docs/ferry](https://www.fdevops.com/docs/ferry-tutorial-document/introduction)
+
+视频教程(由群内好友<稳定>提供,非常感谢。):
+
+* ferry工单系统需要的软件准备 <https://www.bilibili.com/video/BV1sA411s7jE>
+* ferry源代码下载后第一次运行 <https://www.bilibili.com/video/BV1oy4y1v7LR>
+
+官网:[http://ferry.fdevops.com](http://ferry.fdevops.com)
+
+```
+需注意,因有人恶意删除演示数据,将可删除的数据全都删除了,因此演示的Demo上已经将删除操作的隐藏了。
+
+但是直接在Github或者Gitee下载下来的代码是完整的,请放心。
+
+如果总是出现此类删除数据,关闭演示用户的情况的话,可能考虑不在维护demo,仅放置一些项目截图。
+
+请大家一起监督。
+```
+
+## 功能介绍
+
+<!-- wp:paragraph -->
+<p>下面对本系统的功能做一个简单介绍。</p>
+<!-- /wp:paragraph -->
+
+<!-- wp:paragraph -->
+<p>工单系统相关功能:</p>
+<!-- /wp:paragraph -->
+
+<!-- wp:list -->
+<ul><li>工单提交申请</li><li>工单统计</li><li>多维度工单列表,包括(我创建的、我相关的、我待办的、所有工单)</li><li>自定义流程</li><li>自定义模版</li><li>任务钩子</li><li>任务管理</li><li>催办</li><li>转交</li><li>手动结单</li><li>加签</li><li>多维度处理人,包括(个人,变量(创建者、创建者负责人))</li><li>排他网关,即根据条件判断进行工单跳转</li><li>并行网关,即多个节点同时进行审批处理</li><li>通知提醒(目前仅支持邮件)</li><li>流程分类管理</li></ul>
+<!-- /wp:list -->
+
+<!-- wp:paragraph -->
+<p>权限管理相关功能,使用casbin实现接口权限控制:</p>
+<!-- /wp:paragraph -->
+
+<!-- wp:list -->
+<ul><li>用户、角色、岗位的增删查改,批量删除,多条件搜索</li><li>角色、岗位数据导出Excel</li><li>重置用户密码</li><li>维护个人信息,上传管理头像,修改当前账户密码</li><li>部门的增删查改</li><li>菜单目录、跳转、按钮及API接口的增删查改</li><li>登陆日志管理</li><li>左菜单权限控制</li><li>页面按钮权限控制</li><li>API接口权限控制</li></ul>
+<!-- /wp:list -->
+
+快速安装部署:  
+
+```
+bash build.sh install
+```
+
+启动服务:
+
+```
+bash build.sh start
+```
+
+## 交流
+
+加群条件是需给项目一个star,不需要您费多大的功夫与力气,一个小小的star是作者能维护下去的动力。
+
+如果您只是使用本项目的话,您可以在群内提出您使用中需要改进的地方,我会尽快修改。
+
+如果您是想基于此项目二次开发的话,您可以在群里提出您在开发过程中的任何疑问,我会尽快答复并讲解。
+
+群里只要不说骂人、侮辱人之类人身攻击的话,您就可以畅所欲言,有bug我及时修改,使用中有不懂的,我会及时回复,感谢。
+
+<p>
+  <img width="300" src="https://www.fdevops.com/wp-content/uploads/2021/05/1620470212-WechatIMG391.jpeg">
+</p>
+
+QQ群 2:1043807251
+
+[兰玉磊的技术博客](https://www.fdevops.com/)
+
+个人微信,添加好友请描述地区、公司及名字,例如:北京-阿里巴巴-xxx。
+
+微信号:fdevops
+
+<p>
+  <img width="300" src="https://www.fdevops.com/wp-content/uploads/2021/03/1616727212-WechatIMG3.jpeg">
+</p>
+
+## 特别感谢
+
+[go-amdin # 不错的后台开发框架](https://github.com/go-admin-team/go-admin)
+
+[vue-element-admin # 不错的前端模版框架](https://github.com/PanJiaChen/vue-element-admin)
+
+[vue-form-making # 表单设计器,开源版本比较简单,如果有能力的话可以自己进行二次开发](https://github.com/GavinZhuLei/vue-form-making.git)
+
+[wfd-vue # 流程设计器](https://github.com/guozhaolong/wfd-vue)
+
+[machinery # 任务队列](https://github.com/RichardKnop/machinery.git)
+
+等等...
+
+## 打赏
+
+> 如果您觉得这个项目帮助到了您,您可以请作者喝一杯咖啡表示鼓励:
+
+[打赏名人榜](https://www.fdevops.com/docs/ferry-tutorial-document/reward-celebrity-list)
+
+<img class="no-margin" src="https://www.fdevops.com/wp-content/uploads/2020/07/1595075890-81595075871_.pic_hd.png"  height="200px" >
+
+## 鸣谢
+
+特别感谢 [JetBrains](https://www.jetbrains.com/?from=ferry) 为本开源项目提供免费的 [IntelliJ GoLand](https://www.jetbrains.com/go/?from=ferry) 授权
+
+<p>
+ <a href="https://www.jetbrains.com/?from=ferry">
+   <img height="200" src="https://www.fdevops.com/wp-content/uploads/2020/09/1599213857-jetbrains-variant-4.png">
+ </a>
+</p>
+
+## License
+
+开源不易,请尊重作者的付出,感谢。
+
+在此处声明,本系统目前不建议商业产品使用,因本系统使用的`流程设计器`未设置开源协议,`表单设计器`是LGPL v3的协议。
+
+因此避免纠纷,不建议商业产品使用,若执意使用,请联系原作者获得授权。
+
+再次声明,若是未联系作者直接将本系统使用于商业产品,出现的商业纠纷,本系统概不承担,感谢。
+
+[LGPL-3.0](https://github.com/lanyulei/ferry/blob/master/LICENSE)
+
+Copyright (c) 2021 lanyulei

+ 26 - 0
plop-templates/component/index.hbs

@@ -0,0 +1,26 @@
+{{#if template}}
+<template>
+  <div />
+</template>
+{{/if}}
+
+{{#if script}}
+<script>
+export default {
+  name: '{{ properCase name }}',
+  props: {},
+  data() {
+    return {}
+  },
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+{{/if}}
+
+{{#if style}}
+<style lang="scss" scoped>
+
+</style>
+{{/if}}

+ 55 - 0
plop-templates/component/prompt.js

@@ -0,0 +1,55 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate vue component',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'component name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: '<template>',
+      value: 'template',
+      checked: true
+    },
+    {
+      name: '<script>',
+      value: 'script',
+      checked: true
+    },
+    {
+      name: 'style',
+      value: 'style',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
+        return 'Components require at least a <script> or <template> tag.'
+      }
+      return true
+    }
+  }
+  ],
+  actions: data => {
+    const name = '{{properCase name}}'
+    const actions = [{
+      type: 'add',
+      path: `src/components/${name}/index.vue`,
+      templateFile: 'plop-templates/component/index.hbs',
+      data: {
+        name: name,
+        template: data.blocks.includes('template'),
+        script: data.blocks.includes('script'),
+        style: data.blocks.includes('style')
+      }
+    }]
+
+    return actions
+  }
+}

+ 16 - 0
plop-templates/store/index.hbs

@@ -0,0 +1,16 @@
+{{#if state}}
+const state = {}
+{{/if}}
+
+{{#if mutations}}
+const mutations = {}
+{{/if}}
+
+{{#if actions}}
+const actions = {}
+{{/if}}
+
+export default {
+  namespaced: true,
+  {{options}}
+}

+ 62 - 0
plop-templates/store/prompt.js

@@ -0,0 +1,62 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate store',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'store name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: 'state',
+      value: 'state',
+      checked: true
+    },
+    {
+      name: 'mutations',
+      value: 'mutations',
+      checked: true
+    },
+    {
+      name: 'actions',
+      value: 'actions',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (!value.includes('state') || !value.includes('mutations')) {
+        return 'store require at least state and mutations'
+      }
+      return true
+    }
+  }
+  ],
+  actions(data) {
+    const name = '{{name}}'
+    const { blocks } = data
+    const options = ['state', 'mutations']
+    const joinFlag = `,
+  `
+    if (blocks.length === 3) {
+      options.push('actions')
+    }
+
+    const actions = [{
+      type: 'add',
+      path: `src/store/modules/${name}.js`,
+      templateFile: 'plop-templates/store/index.hbs',
+      data: {
+        options: options.join(joinFlag),
+        state: blocks.includes('state'),
+        mutations: blocks.includes('mutations'),
+        actions: blocks.includes('actions')
+      }
+    }]
+    return actions
+  }
+}

+ 9 - 0
plop-templates/utils.js

@@ -0,0 +1,9 @@
+exports.notEmpty = name => {
+  return v => {
+    if (!v || v.trim === '') {
+      return `${name} is required`
+    } else {
+      return true
+    }
+  }
+}

+ 26 - 0
plop-templates/view/index.hbs

@@ -0,0 +1,26 @@
+{{#if template}}
+<template>
+  <div />
+</template>
+{{/if}}
+
+{{#if script}}
+<script>
+export default {
+  name: '{{ properCase name }}',
+  props: {},
+  data() {
+    return {}
+  },
+  created() {},
+  mounted() {},
+  methods: {}
+}
+</script>
+{{/if}}
+
+{{#if style}}
+<style lang="scss" scoped>
+
+</style>
+{{/if}}

+ 55 - 0
plop-templates/view/prompt.js

@@ -0,0 +1,55 @@
+const { notEmpty } = require('../utils.js')
+
+module.exports = {
+  description: 'generate a view',
+  prompts: [{
+    type: 'input',
+    name: 'name',
+    message: 'view name please',
+    validate: notEmpty('name')
+  },
+  {
+    type: 'checkbox',
+    name: 'blocks',
+    message: 'Blocks:',
+    choices: [{
+      name: '<template>',
+      value: 'template',
+      checked: true
+    },
+    {
+      name: '<script>',
+      value: 'script',
+      checked: true
+    },
+    {
+      name: 'style',
+      value: 'style',
+      checked: true
+    }
+    ],
+    validate(value) {
+      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
+        return 'View require at least a <script> or <template> tag.'
+      }
+      return true
+    }
+  }
+  ],
+  actions: data => {
+    const name = '{{name}}'
+    const actions = [{
+      type: 'add',
+      path: `src/views/${name}/index.vue`,
+      templateFile: 'plop-templates/view/index.hbs',
+      data: {
+        name: name,
+        template: data.blocks.includes('template'),
+        script: data.blocks.includes('script'),
+        style: data.blocks.includes('style')
+      }
+    }]
+
+    return actions
+  }
+}

+ 9 - 0
plopfile.js

@@ -0,0 +1,9 @@
+const viewGenerator = require('./plop-templates/view/prompt')
+const componentGenerator = require('./plop-templates/component/prompt')
+const storeGenerator = require('./plop-templates/store/prompt.js')
+
+module.exports = function(plop) {
+  plop.setGenerator('view', viewGenerator)
+  plop.setGenerator('component', componentGenerator)
+  plop.setGenerator('store', storeGenerator)
+}

+ 5 - 0
postcss.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  plugins: {
+    autoprefixer: {}
+  }
+}

BIN
public/favicon.ico


+ 215 - 0
public/index.html

@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="renderer" content="webkit">
+  <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 %> - ferry</title>
+  <meta name="keywords" content="ferry,vue,gin,go">
+  <meta name="description" content="FERRY 管理平台">
+  <style>
+    html,
+    body,
+    #app {
+      height: 100%;
+      margin: 0px;
+      padding: 0px;
+    }
+
+    .chromeframe {
+      margin: 0.2em 0;
+      background: #ccc;
+      color: #000;
+      padding: 0.2em 0;
+    }
+
+    #loader-wrapper {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      z-index: 999999;
+    }
+
+    #loader {
+      display: block;
+      position: relative;
+      left: 50%;
+      top: 50%;
+      width: 150px;
+      height: 150px;
+      margin: -75px 0 0 -75px;
+      border-radius: 50%;
+      border: 3px solid transparent;
+      border-top-color: #FFF;
+      -webkit-animation: spin 2s linear infinite;
+      -ms-animation: spin 2s linear infinite;
+      -moz-animation: spin 2s linear infinite;
+      -o-animation: spin 2s linear infinite;
+      animation: spin 2s linear infinite;
+      z-index: 1001;
+    }
+
+    #loader:before {
+      content: "";
+      position: absolute;
+      top: 5px;
+      left: 5px;
+      right: 5px;
+      bottom: 5px;
+      border-radius: 50%;
+      border: 3px solid transparent;
+      border-top-color: #FFF;
+      -webkit-animation: spin 3s linear infinite;
+      -moz-animation: spin 3s linear infinite;
+      -o-animation: spin 3s linear infinite;
+      -ms-animation: spin 3s linear infinite;
+      animation: spin 3s linear infinite;
+    }
+
+    #loader:after {
+      content: "";
+      position: absolute;
+      top: 15px;
+      left: 15px;
+      right: 15px;
+      bottom: 15px;
+      border-radius: 50%;
+      border: 3px solid transparent;
+      border-top-color: #FFF;
+      -moz-animation: spin 1.5s linear infinite;
+      -o-animation: spin 1.5s linear infinite;
+      -ms-animation: spin 1.5s linear infinite;
+      -webkit-animation: spin 1.5s linear infinite;
+      animation: spin 1.5s linear infinite;
+    }
+
+
+    @-webkit-keyframes spin {
+      0% {
+        -webkit-transform: rotate(0deg);
+        -ms-transform: rotate(0deg);
+        transform: rotate(0deg);
+      }
+
+      100% {
+        -webkit-transform: rotate(360deg);
+        -ms-transform: rotate(360deg);
+        transform: rotate(360deg);
+      }
+    }
+
+    @keyframes spin {
+      0% {
+        -webkit-transform: rotate(0deg);
+        -ms-transform: rotate(0deg);
+        transform: rotate(0deg);
+      }
+
+      100% {
+        -webkit-transform: rotate(360deg);
+        -ms-transform: rotate(360deg);
+        transform: rotate(360deg);
+      }
+    }
+
+
+    #loader-wrapper .loader-section {
+      position: fixed;
+      top: 0;
+      width: 51%;
+      height: 100%;
+      background: #7171C6;
+      z-index: 1000;
+      -webkit-transform: translateX(0);
+      -ms-transform: translateX(0);
+      transform: translateX(0);
+    }
+
+    #loader-wrapper .loader-section.section-left {
+      left: 0;
+    }
+
+    #loader-wrapper .loader-section.section-right {
+      right: 0;
+    }
+
+
+    .loaded #loader-wrapper .loader-section.section-left {
+      -webkit-transform: translateX(-100%);
+      -ms-transform: translateX(-100%);
+      transform: translateX(-100%);
+      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+    }
+
+    .loaded #loader-wrapper .loader-section.section-right {
+      -webkit-transform: translateX(100%);
+      -ms-transform: translateX(100%);
+      transform: translateX(100%);
+      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+    }
+
+    .loaded #loader {
+      opacity: 0;
+      -webkit-transition: all 0.3s ease-out;
+      transition: all 0.3s ease-out;
+    }
+
+    .loaded #loader-wrapper {
+      visibility: hidden;
+      -webkit-transform: translateY(-100%);
+      -ms-transform: translateY(-100%);
+      transform: translateY(-100%);
+      -webkit-transition: all 0.3s 1s ease-out;
+      transition: all 0.3s 1s ease-out;
+    }
+
+    .no-js #loader-wrapper {
+      display: none;
+    }
+
+    .no-js h1 {
+      color: #222222;
+    }
+
+    #loader-wrapper .load_title {
+      font-family: 'Open Sans';
+      color: #FFF;
+      font-size: 19px;
+      width: 100%;
+      text-align: center;
+      z-index: 9999999999999;
+      position: absolute;
+      top: 60%;
+      opacity: 1;
+      line-height: 30px;
+    }
+
+    #loader-wrapper .load_title span {
+      font-weight: normal;
+      font-style: italic;
+      font-size: 13px;
+      color: #FFF;
+      opacity: 0.5;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="app">
+    <div id="loader-wrapper">
+      <div id="loader"></div>
+      <div class="loader-section section-left"></div>
+      <div class="loader-section section-right"></div>
+      <div class="load_title">正在加载系统资源,请耐心等待</div>
+    </div>
+  </div>
+</body>
+
+</html>

+ 22 - 0
src/App.vue

@@ -0,0 +1,22 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+
+</script>
+<script>
+var _hmt = _hmt || [];
+(function() {
+  var hm = document.createElement("script");
+  hm.src = "https://hm.baidu.com/hm.js?1d2d61263f13e4b288c8da19ad3ff56d";
+  var s = document.getElementsByTagName("script")[0];
+  s.parentNode.insertBefore(hm, s);
+})();
+</script>
+

+ 9 - 0
src/api/dashboard.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+export function initData(params) {
+  return request({
+    url: '/api/v1/dashboard',
+    method: 'get',
+    params
+  })
+}

+ 9 - 0
src/api/login.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 获取验证码
+export function getCodeImg() {
+  return request({
+    url: '/api/v1/getCaptcha',
+    method: 'get'
+  })
+}

+ 9 - 0
src/api/monitor/server.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 查询服务器详细
+export function getServer() {
+  return request({
+    url: '/api/v1/monitor/server',
+    method: 'get'
+  })
+}

+ 37 - 0
src/api/process/admin/classify.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// 创建流程分类
+export function createClassify(data) {
+  return request({
+    url: '/api/v1/classify',
+    method: 'post',
+    data
+  })
+}
+
+// 流程分类列表
+export function classifyList(params) {
+  return request({
+    url: '/api/v1/classify',
+    method: 'get',
+    params
+  })
+}
+
+// 更新流程分类
+export function updateClassify(data) {
+  return request({
+    url: '/api/v1/classify',
+    method: 'put',
+    data
+  })
+}
+
+// 删除流程分类
+export function deleteClassify(params) {
+  return request({
+    url: '/api/v1/classify',
+    method: 'delete',
+    params
+  })
+}

+ 63 - 0
src/api/process/admin/process.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 创建流程
+export function createProcess(data) {
+  return request({
+    url: '/api/v1/process',
+    method: 'post',
+    data
+  })
+}
+
+// 流程列表
+export function processList(params) {
+  return request({
+    url: '/api/v1/process',
+    method: 'get',
+    params
+  })
+}
+
+// 更新流程
+export function updateProcess(data) {
+  return request({
+    url: '/api/v1/process',
+    method: 'put',
+    data
+  })
+}
+
+// 删除流程
+export function deleteProcess(params) {
+  return request({
+    url: '/api/v1/process',
+    method: 'delete',
+    params
+  })
+}
+
+// 流程详情
+export function processDetails(params) {
+  return request({
+    url: '/api/v1/process/details',
+    method: 'get',
+    params
+  })
+}
+
+// 分类流程列表
+export function classifyProcessList(params) {
+  return request({
+    url: '/api/v1/process/classify',
+    method: 'get',
+    params
+  })
+}
+
+// 克隆流程
+export function cloneProcess(id) {
+  return request({
+    url: `/api/v1/process/clone/${id}`,
+    method: 'post'
+  })
+}

+ 46 - 0
src/api/process/admin/task.js

@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+
+// 新建任务
+export function createTask(data) {
+  return request({
+    url: '/api/v1/task',
+    method: 'post',
+    data
+  })
+}
+
+// 任务列表
+export function taskList(params) {
+  return request({
+    url: '/api/v1/task',
+    method: 'get',
+    params
+  })
+}
+
+// 更新任务
+export function updateTask(data) {
+  return request({
+    url: '/api/v1/task',
+    method: 'put',
+    data
+  })
+}
+
+// 删除任务
+export function deleteTask(params) {
+  return request({
+    url: '/api/v1/task',
+    method: 'delete',
+    params
+  })
+}
+
+// 任务详情
+export function taskDetails(params) {
+  return request({
+    url: '/api/v1/task/details',
+    method: 'get',
+    params
+  })
+}

+ 54 - 0
src/api/process/admin/template.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 创建模版
+export function createTemplate(data) {
+  return request({
+    url: '/api/v1/tpl',
+    method: 'post',
+    data
+  })
+}
+
+// 模版列表
+export function templateList(params) {
+  return request({
+    url: '/api/v1/tpl',
+    method: 'get',
+    params
+  })
+}
+
+// 模版详情
+export function templateDetails(params) {
+  return request({
+    url: '/api/v1/tpl/details',
+    method: 'get',
+    params
+  })
+}
+
+// 更新模版
+export function editTemplate(data) {
+  return request({
+    url: '/api/v1/tpl',
+    method: 'put',
+    data
+  })
+}
+
+// 删除模版
+export function deleteTemplate(params) {
+  return request({
+    url: '/api/v1/tpl',
+    method: 'delete',
+    params
+  })
+}
+
+// 克隆模版
+export function cloneTemplate(id) {
+  return request({
+    url: `/api/v1/tpl/clone/${id}`,
+    method: 'post'
+  })
+}

+ 89 - 0
src/api/process/work-order.js

@@ -0,0 +1,89 @@
+import request from '@/utils/request'
+
+// 流程结构
+export function processStructure(params) {
+  return request({
+    url: '/api/v1/work-order/process-structure',
+    method: 'get',
+    params
+  })
+}
+
+// 新建工单
+export function createWorkOrder(data) {
+  return request({
+    url: '/api/v1/work-order/create',
+    method: 'post',
+    data
+  })
+}
+
+// 工单列表
+export function workOrderList(params) {
+  return request({
+    url: '/api/v1/work-order/list',
+    method: 'get',
+    params
+  })
+}
+
+// 处理工单
+export function handleWorkOrder(data) {
+  return request({
+    url: '/api/v1/work-order/handle',
+    method: 'post',
+    data
+  })
+}
+
+// 结束工单
+export function unityWorkOrder(params) {
+  return request({
+    url: '/api/v1/work-order/unity',
+    method: 'get',
+    params
+  })
+}
+
+// 转交工单
+export function inversionWorkOrder(data) {
+  return request({
+    url: '/api/v1/work-order/inversion',
+    method: 'post',
+    data
+  })
+}
+
+// 催办工单
+export function urgeWorkOrder(params) {
+  return request({
+    url: '/api/v1/work-order/urge',
+    method: 'get',
+    params
+  })
+}
+
+// 主动接单
+export function activeOrder(data, workOrderId) {
+  return request({
+    url: `/api/v1/work-order/active-order/${workOrderId}`,
+    method: 'put',
+    data
+  })
+}
+
+// 删除工单
+export function deleteWorkOrder(workOrderId) {
+  return request({
+    url: `/api/v1/work-order/delete/${workOrderId}`,
+    method: 'delete'
+  })
+}
+
+// 删除工单
+export function reopenWorkOrder(id) {
+  return request({
+    url: `/api/v1/work-order/reopen/${id}`,
+    method: 'post'
+  })
+}

+ 17 - 0
src/api/remote-search.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+export function searchUser(name) {
+  return request({
+    url: '/search/user',
+    method: 'get',
+    params: { name }
+  })
+}
+
+export function transactionList(query) {
+  return request({
+    url: '/transaction/list',
+    method: 'get',
+    params: query
+  })
+}

+ 67 - 0
src/api/system/dept.js

@@ -0,0 +1,67 @@
+import request from '@/utils/request'
+
+export function getDeptList(query) {
+  return request({
+    url: '/api/v1/deptList',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getOrdinaryDeptList(params) {
+  return request({
+    url: '/api/v1/ordinaryDeptList',
+    method: 'get',
+    params
+  })
+}
+
+// 查询部门详细
+export function getDept(deptId) {
+  return request({
+    url: '/api/v1/dept/' + deptId,
+    method: 'get'
+  })
+}
+
+// 查询部门下拉树结构
+export function treeselect() {
+  return request({
+    url: '/api/v1/deptTree',
+    method: 'get'
+  })
+}
+
+// 根据角色ID查询部门树结构
+export function roleDeptTreeselect(roleId) {
+  return request({
+    url: '/api/v1/roleDeptTreeselect/' + roleId,
+    method: 'get'
+  })
+}
+
+// 新增部门
+export function addDept(data) {
+  return request({
+    url: '/api/v1/dept',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改部门
+export function updateDept(data) {
+  return request({
+    url: '/api/v1/dept',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除部门
+export function delDept(deptId) {
+  return request({
+    url: '/api/v1/dept/' + deptId,
+    method: 'delete'
+  })
+}

+ 35 - 0
src/api/system/loginlog.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// 查询登录日志列表
+export function list(query) {
+  return request({
+    url: '/api/v1/loginloglist',
+    method: 'get',
+    params: query
+  })
+}
+
+// 删除登录日志
+export function delLogininfor(infoId) {
+  return request({
+    url: '/api/v1/loginlog/' + infoId,
+    method: 'delete'
+  })
+}
+
+// 清空登录日志
+export function cleanLogininfor() {
+  return request({
+    url: '/api/v1/loginlog',
+    method: 'delete'
+  })
+}
+
+// 导出登录日志
+export function exportLogininfor(query) {
+  return request({
+    url: '/api/v1/loginlog/export',
+    method: 'get',
+    params: query
+  })
+}

+ 60 - 0
src/api/system/menu.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询菜单列表
+export function listMenu(query) {
+  return request({
+    url: '/api/v1/menulist',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询菜单详细
+export function getMenu(menuId) {
+  return request({
+    url: '/api/v1/menu/' + menuId,
+    method: 'get'
+  })
+}
+
+// 查询菜单下拉树结构
+export function treeselect() {
+  return request({
+    url: '/api/v1/menuTreeselect',
+    method: 'get'
+  })
+}
+
+// 根据角色ID查询菜单下拉树结构
+export function roleMenuTreeselect(roleId) {
+  return request({
+    url: '/api/v1/roleMenuTreeselect/' + roleId,
+    method: 'get'
+  })
+}
+
+// 新增菜单
+export function addMenu(data) {
+  return request({
+    url: '/api/v1/menu',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改菜单
+export function updateMenu(data) {
+  return request({
+    url: '/api/v1/menu',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除菜单
+export function delMenu(menuId) {
+  return request({
+    url: '/api/v1/menu/' + menuId,
+    method: 'delete'
+  })
+}

+ 45 - 0
src/api/system/post.js

@@ -0,0 +1,45 @@
+import request from '@/utils/request'
+
+// 查询岗位列表
+export function listPost(query) {
+  return request({
+    url: '/api/v1/postlist',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询岗位详细
+export function getPost(postId) {
+  return request({
+    url: '/api/v1/post/' + postId,
+    method: 'get'
+  })
+}
+
+// 新增岗位
+export function addPost(data) {
+  return request({
+    url: '/api/v1/post',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改岗位
+export function updatePost(data) {
+  return request({
+    url: '/api/v1/post',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除岗位
+export function delPost(postId) {
+  return request({
+    url: '/api/v1/post/' + postId,
+    method: 'delete'
+  })
+}
+

+ 87 - 0
src/api/system/role.js

@@ -0,0 +1,87 @@
+import request from '@/utils/request'
+
+// 查询角色列表
+export function listRole(query) {
+  return request({
+    url: '/api/v1/rolelist',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询角色详细
+export function getRole(roleId) {
+  return request({
+    url: '/api/v1/role/' + roleId,
+    method: 'get'
+  })
+}
+
+// 新增角色
+export function addRole(data) {
+  return request({
+    url: '/api/v1/role',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改角色
+export function updateRole(data) {
+  return request({
+    url: '/api/v1/role',
+    method: 'put',
+    data: data
+  })
+}
+
+// 角色数据权限
+export function dataScope(data) {
+  return request({
+    url: '/api/v1/roledatascope',
+    method: 'put',
+    data: data
+  })
+}
+
+// 角色状态修改
+export function changeRoleStatus(roleId, status) {
+  const data = {
+    roleId,
+    status
+  }
+  return request({
+    url: '/api/v1/role',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除角色
+export function delRole(roleId) {
+  return request({
+    url: '/api/v1/role/' + roleId,
+    method: 'delete'
+  })
+}
+
+export function getListrole(id) {
+  return request({
+    url: '/api/v1/menu/role/' + id,
+    method: 'get'
+  })
+}
+
+export function getRoutes() {
+  return request({
+    url: '/api/v1/menurole',
+    method: 'get'
+  })
+}
+
+export function getMenuNames() {
+  return request({
+    url: '/api/v1/menuids',
+    method: 'get'
+  })
+}

+ 19 - 0
src/api/system/settings.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// 设置系统配置信息
+export function setSettings(data) {
+  return request({
+    url: '/api/v1/settings',
+    method: 'post',
+    data
+  })
+}
+
+// 获取系统配置信息
+export function getSettings(params) {
+  return request({
+    url: '/api/v1/settings',
+    method: 'get',
+    params
+  })
+}

+ 134 - 0
src/api/system/sysuser.js

@@ -0,0 +1,134 @@
+import request from '@/utils/request'
+
+// 查询用户列表
+export function listUser(query) {
+  return request({
+    url: '/api/v1/sysUserList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询用户详细
+export function getUser(userId) {
+  return request({
+    url: '/api/v1/sysUser/' + userId,
+    method: 'get'
+  })
+}
+
+export function getUserInit() {
+  return request({
+    url: '/api/v1/sysUser/',
+    method: 'get'
+  })
+}
+
+// 新增用户
+export function addUser(data) {
+  return request({
+    url: '/api/v1/sysUser',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改用户
+export function updateUser(data) {
+  return request({
+    url: '/api/v1/sysUser',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除用户
+export function delUser(userId) {
+  return request({
+    url: '/api/v1/sysUser/' + userId,
+    method: 'delete'
+  })
+}
+
+// 导出用户
+export function exportUser(query) {
+  return request({
+    url: '/api/v1/sysUser/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 用户密码重置
+export function resetUserPwd(userId, password) {
+  const data = {
+    userId,
+    password
+  }
+  return request({
+    url: '/api/v1/sysUser',
+    method: 'put',
+    data: data
+  })
+}
+
+// 用户状态修改
+export function changeUserStatus(userId, status) {
+  const data = {
+    userId,
+    status
+  }
+  return request({
+    url: '/api/v1/sysUser',
+    method: 'put',
+    data: data
+  })
+}
+
+// 查询用户个人信息
+export function getUserProfile() {
+  return request({
+    url: '/api/v1/user/profile',
+    method: 'get'
+  })
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+  return request({
+    url: '/api/v1/sysUser/profile',
+    method: 'put',
+    data: data
+  })
+}
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword, passwordType) {
+  const data = {
+    oldPassword,
+    newPassword,
+    passwordType
+  }
+  return request({
+    url: '/api/v1/user/pwd',
+    method: 'put',
+    data: data
+  })
+}
+
+// 用户头像上传
+export function uploadAvatar(data) {
+  return request({
+    url: '/api/v1/user/avatar',
+    method: 'post',
+    data: data
+  })
+}
+
+// 下载用户导入模板
+export function importTemplate() {
+  return request({
+    url: '/api/v1/sysUser/importTemplate',
+    method: 'get'
+  })
+}

+ 32 - 0
src/api/user.js

@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function login(data) {
+  return request({
+    url: `/login`,
+    method: 'post',
+    data
+  })
+}
+
+export function refreshtoken(data) {
+  return request({
+    url: '/refreshtoken',
+    method: 'post',
+    data
+  })
+}
+
+export function getInfo() {
+  return request({
+    url: '/api/v1/getinfo',
+    method: 'get'
+  })
+}
+
+export function logout() {
+  return request({
+    url: '/api/v1/logout',
+    method: 'post'
+  })
+}
+

BIN
src/assets/401_images/401.gif


BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


BIN
src/assets/custom-theme/fonts/element-icons.ttf


BIN
src/assets/custom-theme/fonts/element-icons.woff


File diff suppressed because it is too large
+ 0 - 0
src/assets/custom-theme/index.css


+ 39 - 0
src/assets/dark.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+            <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                    <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 39 - 0
src/assets/light.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
+            <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                    <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/assets/login.png


BIN
src/assets/logo/ferry_logo.png


BIN
src/assets/logo/ferry_logo_meitu_1.png


BIN
src/assets/logo/ferry_logo_white.png


BIN
src/assets/logo/logo.png


+ 110 - 0
src/assets/particles.json

@@ -0,0 +1,110 @@
+{
+    "particles": {
+      "number": {
+        "value": 60,
+        "density": {
+          "enable": true,
+          "value_area": 800
+        }
+      },
+      "color": {
+        "value": "#ffffff"
+      },
+      "shape": {
+        "type": "circle",
+        "stroke": {
+          "width": 0,
+          "color": "#000000"
+        },
+        "polygon": {
+          "nb_sides": 5
+        },
+        "image": {
+          "src": "img/github.svg",
+          "width": 100,
+          "height": 100
+        }
+      },
+      "opacity": {
+        "value": 0.5,
+        "random": false,
+        "anim": {
+          "enable": false,
+          "speed": 1,
+          "opacity_min": 0.1,
+          "sync": false
+        }
+      },
+      "size": {
+        "value": 3,
+        "random": true,
+        "anim": {
+          "enable": false,
+          "speed": 40,
+          "size_min": 0.1,
+          "sync": false
+        }
+      },
+      "line_linked": {
+        "enable": true,
+        "distance": 150,
+        "color": "#ffffff",
+        "opacity": 0.4,
+        "width": 1
+      },
+      "move": {
+        "enable": true,
+        "speed": 4,
+        "direction": "none",
+        "random": false,
+        "straight": false,
+        "out_mode": "out",
+        "bounce": false,
+        "attract": {
+          "enable": false,
+          "rotateX": 100,
+          "rotateY": 1200
+        }
+      }
+    },
+    "interactivity": {
+      "detect_on": "Window",
+      "events": {
+        "onhover": {
+          "enable": true,
+          "mode": "grab"
+        },
+        "onclick": {
+          "enable": true,
+          "mode": "push"
+        },
+        "resize": true
+      },
+      "modes": {
+        "grab": {
+          "distance": 140,
+          "line_linked": {
+            "opacity": 1
+          }
+        },
+        "bubble": {
+          "distance": 400,
+          "size": 40,
+          "duration": 2,
+          "opacity": 8,
+          "speed": 3
+        },
+        "repulse": {
+          "distance": 200,
+          "duration": 0.4
+        },
+        "push": {
+          "particles_nb": 4
+        },
+        "remove": {
+          "particles_nb": 2
+        }
+      }
+    },
+    "retina_detect": true
+  }

+ 111 - 0
src/components/BackToTop/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <transition :name="transitionName">
+    <div v-show="visible" :style="customStyle" class="back-to-ceiling" @click="backToTop">
+      <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height:16px;width:16px"><path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'BackToTop',
+  props: {
+    visibilityHeight: {
+      type: Number,
+      default: 400
+    },
+    backPosition: {
+      type: Number,
+      default: 0
+    },
+    customStyle: {
+      type: Object,
+      default: function() {
+        return {
+          right: '50px',
+          bottom: '50px',
+          width: '40px',
+          height: '40px',
+          'border-radius': '4px',
+          'line-height': '45px',
+          background: '#e7eaf1'
+        }
+      }
+    },
+    transitionName: {
+      type: String,
+      default: 'fade'
+    }
+  },
+  data() {
+    return {
+      visible: false,
+      interval: null,
+      isMoving: false
+    }
+  },
+  mounted() {
+    window.addEventListener('scroll', this.handleScroll)
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', this.handleScroll)
+    if (this.interval) {
+      clearInterval(this.interval)
+    }
+  },
+  methods: {
+    handleScroll() {
+      this.visible = window.pageYOffset > this.visibilityHeight
+    },
+    backToTop() {
+      if (this.isMoving) return
+      const start = window.pageYOffset
+      let i = 0
+      this.isMoving = true
+      this.interval = setInterval(() => {
+        const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
+        if (next <= this.backPosition) {
+          window.scrollTo(0, this.backPosition)
+          clearInterval(this.interval)
+          this.isMoving = false
+        } else {
+          window.scrollTo(0, next)
+        }
+        i++
+      }, 16.7)
+    },
+    easeInOutQuad(t, b, c, d) {
+      if ((t /= d / 2) < 1) return c / 2 * t * t + b
+      return -c / 2 * (--t * (t - 2) - 1) + b
+    }
+  }
+}
+</script>
+
+<style scoped>
+.back-to-ceiling {
+  position: fixed;
+  display: inline-block;
+  text-align: center;
+  cursor: pointer;
+}
+
+.back-to-ceiling:hover {
+  background: #d5dbe7;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity .5s;
+}
+
+.fade-enter,
+.fade-leave-to {
+  opacity: 0
+}
+
+.back-to-ceiling .Icon {
+  fill: #9aaabf;
+  background: none;
+}
+</style>

+ 82 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim() === '首页'
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 155 - 0
src/components/Charts/Keyboard.vue

@@ -0,0 +1,155 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+
+      const xAxisData = []
+      const data = []
+      const data2 = []
+      for (let i = 0; i < 50; i++) {
+        xAxisData.push(i)
+        data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
+        data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
+      }
+      this.chart.setOption({
+        backgroundColor: '#08263a',
+        grid: {
+          left: '5%',
+          right: '5%'
+        },
+        xAxis: [{
+          show: false,
+          data: xAxisData
+        }, {
+          show: false,
+          data: xAxisData
+        }],
+        visualMap: {
+          show: false,
+          min: 0,
+          max: 50,
+          dimension: 0,
+          inRange: {
+            color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
+          }
+        },
+        yAxis: {
+          axisLine: {
+            show: false
+          },
+          axisLabel: {
+            textStyle: {
+              color: '#4a657a'
+            }
+          },
+          splitLine: {
+            show: true,
+            lineStyle: {
+              color: '#08263f'
+            }
+          },
+          axisTick: {
+            show: false
+          }
+        },
+        series: [{
+          name: 'back',
+          type: 'bar',
+          data: data2,
+          z: 1,
+          itemStyle: {
+            normal: {
+              opacity: 0.4,
+              barBorderRadius: 5,
+              shadowBlur: 3,
+              shadowColor: '#111'
+            }
+          }
+        }, {
+          name: 'Simulate Shadow',
+          type: 'line',
+          data,
+          z: 2,
+          showSymbol: false,
+          animationDelay: 0,
+          animationEasing: 'linear',
+          animationDuration: 1200,
+          lineStyle: {
+            normal: {
+              color: 'transparent'
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: '#08263a',
+              shadowBlur: 50,
+              shadowColor: '#000'
+            }
+          }
+        }, {
+          name: 'front',
+          type: 'bar',
+          data,
+          xAxisIndex: 1,
+          z: 3,
+          itemStyle: {
+            normal: {
+              barBorderRadius: 5
+            }
+          }
+        }],
+        animationEasing: 'elasticOut',
+        animationEasingUpdate: 'elasticOut',
+        animationDelay(idx) {
+          return idx * 20
+        },
+        animationDelayUpdate(idx) {
+          return idx * 20
+        }
+      })
+    }
+  }
+}
+</script>

+ 227 - 0
src/components/Charts/LineMarker.vue

@@ -0,0 +1,227 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+
+      this.chart.setOption({
+        backgroundColor: '#394056',
+        title: {
+          top: 20,
+          text: 'Requests',
+          textStyle: {
+            fontWeight: 'normal',
+            fontSize: 16,
+            color: '#F1F1F3'
+          },
+          left: '1%'
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          }
+        },
+        legend: {
+          top: 20,
+          icon: 'rect',
+          itemWidth: 14,
+          itemHeight: 5,
+          itemGap: 13,
+          data: ['CMCC', 'CTCC', 'CUCC'],
+          right: '4%',
+          textStyle: {
+            fontSize: 12,
+            color: '#F1F1F3'
+          }
+        },
+        grid: {
+          top: 100,
+          left: '2%',
+          right: '2%',
+          bottom: '2%',
+          containLabel: true
+        },
+        xAxis: [{
+          type: 'category',
+          boundaryGap: false,
+          axisLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          },
+          data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
+        }],
+        yAxis: [{
+          type: 'value',
+          name: '(%)',
+          axisTick: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          },
+          axisLabel: {
+            margin: 10,
+            textStyle: {
+              fontSize: 14
+            }
+          },
+          splitLine: {
+            lineStyle: {
+              color: '#57617B'
+            }
+          }
+        }],
+        series: [{
+          name: 'CMCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(137, 189, 27, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(137, 189, 27, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(137,189,27)',
+              borderColor: 'rgba(137,189,2,0.27)',
+              borderWidth: 12
+
+            }
+          },
+          data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
+        }, {
+          name: 'CTCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(0, 136, 212, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(0, 136, 212, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(0,136,212)',
+              borderColor: 'rgba(0,136,212,0.2)',
+              borderWidth: 12
+
+            }
+          },
+          data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
+        }, {
+          name: 'CUCC',
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 5,
+          showSymbol: false,
+          lineStyle: {
+            normal: {
+              width: 1
+            }
+          },
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: 'rgba(219, 50, 51, 0.3)'
+              }, {
+                offset: 0.8,
+                color: 'rgba(219, 50, 51, 0)'
+              }], false),
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 10
+            }
+          },
+          itemStyle: {
+            normal: {
+              color: 'rgb(219,50,51)',
+              borderColor: 'rgba(219,50,51,0.2)',
+              borderWidth: 12
+            }
+          },
+          data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
+        }]
+      })
+    }
+  }
+}
+</script>

+ 271 - 0
src/components/Charts/MixChart.vue

@@ -0,0 +1,271 @@
+<template>
+  <div :id="id" :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    id: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '200px'
+    },
+    height: {
+      type: String,
+      default: '200px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.initChart()
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(document.getElementById(this.id))
+      const xData = (function() {
+        const data = []
+        for (let i = 1; i < 13; i++) {
+          data.push(i + 'month')
+        }
+        return data
+      }())
+      this.chart.setOption({
+        backgroundColor: '#344b58',
+        title: {
+          text: 'statistics',
+          x: '20',
+          top: '20',
+          textStyle: {
+            color: '#fff',
+            fontSize: '22'
+          },
+          subtextStyle: {
+            color: '#90979c',
+            fontSize: '16'
+          }
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            textStyle: {
+              color: '#fff'
+            }
+          }
+        },
+        grid: {
+          left: '5%',
+          right: '5%',
+          borderWidth: 0,
+          top: 150,
+          bottom: 95,
+          textStyle: {
+            color: '#fff'
+          }
+        },
+        legend: {
+          x: '5%',
+          top: '10%',
+          textStyle: {
+            color: '#90979c'
+          },
+          data: ['female', 'male', 'average']
+        },
+        calculable: true,
+        xAxis: [{
+          type: 'category',
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          splitLine: {
+            show: false
+          },
+          axisTick: {
+            show: false
+          },
+          splitArea: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+
+          },
+          data: xData
+        }],
+        yAxis: [{
+          type: 'value',
+          splitLine: {
+            show: false
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#90979c'
+            }
+          },
+          axisTick: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0
+          },
+          splitArea: {
+            show: false
+          }
+        }],
+        dataZoom: [{
+          show: true,
+          height: 30,
+          xAxisIndex: [
+            0
+          ],
+          bottom: 30,
+          start: 10,
+          end: 80,
+          handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
+          handleSize: '110%',
+          handleStyle: {
+            color: '#d3dee5'
+
+          },
+          textStyle: {
+            color: '#fff' },
+          borderColor: '#90979c'
+
+        }, {
+          type: 'inside',
+          show: true,
+          height: 15,
+          start: 1,
+          end: 35
+        }],
+        series: [{
+          name: 'female',
+          type: 'bar',
+          stack: 'total',
+          barMaxWidth: 35,
+          barGap: '10%',
+          itemStyle: {
+            normal: {
+              color: 'rgba(255,144,128,1)',
+              label: {
+                show: true,
+                textStyle: {
+                  color: '#fff'
+                },
+                position: 'insideTop',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            709,
+            1917,
+            2455,
+            2610,
+            1719,
+            1433,
+            1544,
+            3285,
+            5208,
+            3372,
+            2484,
+            4078
+          ]
+        },
+
+        {
+          name: 'male',
+          type: 'bar',
+          stack: 'total',
+          itemStyle: {
+            normal: {
+              color: 'rgba(0,191,183,1)',
+              barBorderRadius: 0,
+              label: {
+                show: true,
+                position: 'top',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            327,
+            1776,
+            507,
+            1200,
+            800,
+            482,
+            204,
+            1390,
+            1001,
+            951,
+            381,
+            220
+          ]
+        }, {
+          name: 'average',
+          type: 'line',
+          stack: 'total',
+          symbolSize: 10,
+          symbol: 'circle',
+          itemStyle: {
+            normal: {
+              color: 'rgba(252,230,48,1)',
+              barBorderRadius: 0,
+              label: {
+                show: true,
+                position: 'top',
+                formatter(p) {
+                  return p.value > 0 ? p.value : ''
+                }
+              }
+            }
+          },
+          data: [
+            1036,
+            3693,
+            2962,
+            3810,
+            2519,
+            1915,
+            1748,
+            4675,
+            6209,
+            4323,
+            2865,
+            4298
+          ]
+        }
+        ]
+      })
+    }
+  }
+}
+</script>

+ 34 - 0
src/components/Charts/mixins/resize.js

@@ -0,0 +1,34 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      $_sidebarElm: null
+    }
+  },
+  mounted() {
+    this.__resizeHandler = debounce(() => {
+      if (this.chart) {
+        this.chart.resize()
+      }
+    }, 100)
+    window.addEventListener('resize', this.__resizeHandler)
+
+    this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+    this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.__resizeHandler)
+
+    this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.__resizeHandler()
+      }
+    }
+  }
+}

+ 166 - 0
src/components/DndList/index.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="dndList">
+    <div :style="{width:width1}" class="dndList-list">
+      <h3>{{ list1Title }}</h3>
+      <draggable :set-data="setData" :list="list1" group="article" class="dragArea">
+        <div v-for="element in list1" :key="element.id" class="list-complete-item">
+          <div class="list-complete-item-handle">
+            {{ element.id }}[{{ element.author }}] {{ element.title }}
+          </div>
+          <div style="position:absolute;right:0px;">
+            <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
+              <i style="color:#ff4949" class="el-icon-delete" />
+            </span>
+          </div>
+        </div>
+      </draggable>
+    </div>
+    <div :style="{width:width2}" class="dndList-list">
+      <h3>{{ list2Title }}</h3>
+      <draggable :list="list2" group="article" class="dragArea">
+        <div v-for="element in list2" :key="element.id" class="list-complete-item">
+          <div class="list-complete-item-handle2" @click="pushEle(element)">
+            {{ element.id }} [{{ element.author }}] {{ element.title }}
+          </div>
+        </div>
+      </draggable>
+    </div>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DndList',
+  components: { draggable },
+  props: {
+    list1: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    list2: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    list1Title: {
+      type: String,
+      default: 'list1'
+    },
+    list2Title: {
+      type: String,
+      default: 'list2'
+    },
+    width1: {
+      type: String,
+      default: '48%'
+    },
+    width2: {
+      type: String,
+      default: '48%'
+    }
+  },
+  methods: {
+    isNotInList1(v) {
+      return this.list1.every(k => v.id !== k.id)
+    },
+    isNotInList2(v) {
+      return this.list2.every(k => v.id !== k.id)
+    },
+    deleteEle(ele) {
+      for (const item of this.list1) {
+        if (item.id === ele.id) {
+          const index = this.list1.indexOf(item)
+          this.list1.splice(index, 1)
+          break
+        }
+      }
+      if (this.isNotInList2(ele)) {
+        this.list2.unshift(ele)
+      }
+    },
+    pushEle(ele) {
+      for (const item of this.list2) {
+        if (item.id === ele.id) {
+          const index = this.list2.indexOf(item)
+          this.list2.splice(index, 1)
+          break
+        }
+      }
+      if (this.isNotInList1(ele)) {
+        this.list1.push(ele)
+      }
+    },
+    setData(dataTransfer) {
+      // to avoid Firefox bug
+      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+      dataTransfer.setData('Text', '')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.dndList {
+  background: #fff;
+  padding-bottom: 40px;
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+  .dndList-list {
+    float: left;
+    padding-bottom: 30px;
+    &:first-of-type {
+      margin-right: 2%;
+    }
+    .dragArea {
+      margin-top: 15px;
+      min-height: 50px;
+      padding-bottom: 30px;
+    }
+  }
+}
+
+.list-complete-item {
+  cursor: pointer;
+  position: relative;
+  font-size: 14px;
+  padding: 5px 12px;
+  margin-top: 4px;
+  border: 1px solid #bfcbd9;
+  transition: all 1s;
+}
+
+.list-complete-item-handle {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 50px;
+}
+
+.list-complete-item-handle2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 20px;
+}
+
+.list-complete-item.sortable-chosen {
+  background: #4AB7BD;
+}
+
+.list-complete-item.sortable-ghost {
+  background: #30B08F;
+}
+
+.list-complete-enter,
+.list-complete-leave-active {
+  opacity: 0;
+}
+</style>

+ 61 - 0
src/components/DragSelect/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
+    <slot />
+  </el-select>
+</template>
+
+<script>
+import Sortable from 'sortablejs'
+
+export default {
+  name: 'DragSelect',
+  props: {
+    value: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    selectVal: {
+      get() {
+        return [...this.value]
+      },
+      set(val) {
+        this.$emit('input', [...val])
+      }
+    }
+  },
+  mounted() {
+    this.setSort()
+  },
+  methods: {
+    setSort() {
+      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
+      this.sortable = Sortable.create(el, {
+        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
+        setData: function(dataTransfer) {
+          dataTransfer.setData('Text', '')
+          // to avoid Firefox bug
+          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+        },
+        onEnd: evt => {
+          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
+          this.value.splice(evt.newIndex, 0, targetRow)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.drag-select >>> .sortable-ghost {
+  opacity: .8;
+  color: #fff!important;
+  background: #42b983!important;
+}
+
+.drag-select >>> .el-tag {
+  cursor: pointer;
+}
+</style>

+ 297 - 0
src/components/Dropzone/index.vue

@@ -0,0 +1,297 @@
+<template>
+  <div :id="id" :ref="id" :action="url" class="dropzone">
+    <input type="file" name="file">
+  </div>
+</template>
+
+<script>
+import Dropzone from 'dropzone'
+import 'dropzone/dist/dropzone.css'
+// import { getToken } from 'api/qiniu';
+
+Dropzone.autoDiscover = false
+
+export default {
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+    url: {
+      type: String,
+      required: true
+    },
+    clickable: {
+      type: Boolean,
+      default: true
+    },
+    defaultMsg: {
+      type: String,
+      default: '上传图片'
+    },
+    acceptedFiles: {
+      type: String,
+      default: ''
+    },
+    thumbnailHeight: {
+      type: Number,
+      default: 200
+    },
+    thumbnailWidth: {
+      type: Number,
+      default: 200
+    },
+    showRemoveLink: {
+      type: Boolean,
+      default: true
+    },
+    maxFilesize: {
+      type: Number,
+      default: 2
+    },
+    maxFiles: {
+      type: Number,
+      default: 3
+    },
+    autoProcessQueue: {
+      type: Boolean,
+      default: true
+    },
+    useCustomDropzoneOptions: {
+      type: Boolean,
+      default: false
+    },
+    defaultImg: {
+      default: '',
+      type: [String, Array]
+    },
+    couldPaste: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      dropzone: '',
+      initOnce: true
+    }
+  },
+  watch: {
+    defaultImg(val) {
+      if (val.length === 0) {
+        this.initOnce = false
+        return
+      }
+      if (!this.initOnce) return
+      this.initImages(val)
+      this.initOnce = false
+    }
+  },
+  mounted() {
+    const element = document.getElementById(this.id)
+    const vm = this
+    this.dropzone = new Dropzone(element, {
+      clickable: this.clickable,
+      thumbnailWidth: this.thumbnailWidth,
+      thumbnailHeight: this.thumbnailHeight,
+      maxFiles: this.maxFiles,
+      maxFilesize: this.maxFilesize,
+      dictRemoveFile: 'Remove',
+      addRemoveLinks: this.showRemoveLink,
+      acceptedFiles: this.acceptedFiles,
+      autoProcessQueue: this.autoProcessQueue,
+      dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
+      dictMaxFilesExceeded: '只能一个图',
+      previewTemplate: '<div class="dz-preview dz-file-preview">  <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div>  <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>  <div class="dz-error-message"><span data-dz-errormessage></span></div>  <div class="dz-success-mark"> <i class="material-icons">done</i> </div>  <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
+      init() {
+        const val = vm.defaultImg
+        if (!val) return
+        if (Array.isArray(val)) {
+          if (val.length === 0) return
+          val.map((v, i) => {
+            const mockFile = { name: 'name' + i, size: 12345, url: v }
+            this.options.addedfile.call(this, mockFile)
+            this.options.thumbnail.call(this, mockFile, v)
+            mockFile.previewElement.classList.add('dz-success')
+            mockFile.previewElement.classList.add('dz-complete')
+            vm.initOnce = false
+            return true
+          })
+        } else {
+          const mockFile = { name: 'name', size: 12345, url: val }
+          this.options.addedfile.call(this, mockFile)
+          this.options.thumbnail.call(this, mockFile, val)
+          mockFile.previewElement.classList.add('dz-success')
+          mockFile.previewElement.classList.add('dz-complete')
+          vm.initOnce = false
+        }
+      },
+      accept: (file, done) => {
+        /* 七牛*/
+        // const token = this.$store.getters.token;
+        // getToken(token).then(response => {
+        //   file.token = response.data.qiniu_token;
+        //   file.key = response.data.qiniu_key;
+        //   file.url = response.data.qiniu_url;
+        //   done();
+        // })
+        done()
+      },
+      sending: (file, xhr, formData) => {
+        // formData.append('token', file.token);
+        // formData.append('key', file.key);
+        vm.initOnce = false
+      }
+    })
+
+    if (this.couldPaste) {
+      document.addEventListener('paste', this.pasteImg)
+    }
+
+    this.dropzone.on('success', file => {
+      vm.$emit('dropzone-success', file, vm.dropzone.element)
+    })
+    this.dropzone.on('addedfile', file => {
+      vm.$emit('dropzone-fileAdded', file)
+    })
+    this.dropzone.on('removedfile', file => {
+      vm.$emit('dropzone-removedFile', file)
+    })
+    this.dropzone.on('error', (file, error, xhr) => {
+      vm.$emit('dropzone-error', file, error, xhr)
+    })
+    this.dropzone.on('successmultiple', (file, error, xhr) => {
+      vm.$emit('dropzone-successmultiple', file, error, xhr)
+    })
+  },
+  destroyed() {
+    document.removeEventListener('paste', this.pasteImg)
+    this.dropzone.destroy()
+  },
+  methods: {
+    removeAllFiles() {
+      this.dropzone.removeAllFiles(true)
+    },
+    processQueue() {
+      this.dropzone.processQueue()
+    },
+    pasteImg(event) {
+      const items = (event.clipboardData || event.originalEvent.clipboardData).items
+      if (items[0].kind === 'file') {
+        this.dropzone.addFile(items[0].getAsFile())
+      }
+    },
+    initImages(val) {
+      if (!val) return
+      if (Array.isArray(val)) {
+        val.map((v, i) => {
+          const mockFile = { name: 'name' + i, size: 12345, url: v }
+          this.dropzone.options.addedfile.call(this.dropzone, mockFile)
+          this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
+          mockFile.previewElement.classList.add('dz-success')
+          mockFile.previewElement.classList.add('dz-complete')
+          return true
+        })
+      } else {
+        const mockFile = { name: 'name', size: 12345, url: val }
+        this.dropzone.options.addedfile.call(this.dropzone, mockFile)
+        this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
+        mockFile.previewElement.classList.add('dz-success')
+        mockFile.previewElement.classList.add('dz-complete')
+      }
+    }
+
+  }
+}
+</script>
+
+<style scoped>
+    .dropzone {
+        border: 2px solid #E5E5E5;
+        font-family: 'Roboto', sans-serif;
+        color: #777;
+        transition: background-color .2s linear;
+        padding: 5px;
+    }
+
+    .dropzone:hover {
+        background-color: #F6F6F6;
+    }
+
+    i {
+        color: #CCC;
+    }
+
+    .dropzone .dz-image img {
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone input[name='file'] {
+        display: none;
+    }
+
+    .dropzone .dz-preview .dz-image {
+        border-radius: 0px;
+    }
+
+    .dropzone .dz-preview:hover .dz-image img {
+        transform: none;
+        filter: none;
+        width: 100%;
+        height: 100%;
+    }
+
+    .dropzone .dz-preview .dz-details {
+        bottom: 0px;
+        top: 0px;
+        color: white;
+        background-color: rgba(33, 150, 243, 0.8);
+        transition: opacity .2s linear;
+        text-align: left;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
+        background-color: transparent;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-details .dz-filename:hover span {
+        background-color: transparent;
+        border: none;
+    }
+
+    .dropzone .dz-preview .dz-remove {
+        position: absolute;
+        z-index: 30;
+        color: white;
+        margin-left: 15px;
+        padding: 10px;
+        top: inherit;
+        bottom: 15px;
+        border: 2px white solid;
+        text-decoration: none;
+        text-transform: uppercase;
+        font-size: 0.8rem;
+        font-weight: 800;
+        letter-spacing: 1.1px;
+        opacity: 0;
+    }
+
+    .dropzone .dz-preview:hover .dz-remove {
+        opacity: 1;
+    }
+
+    .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
+        margin-left: -40px;
+        margin-top: -50px;
+    }
+
+    .dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
+        color: white;
+        font-size: 5rem;
+    }
+</style>

+ 78 - 0
src/components/ErrorLog/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div v-if="errorLogs.length>0">
+    <el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
+      <el-button style="padding: 8px 10px;" size="small" type="danger">
+        <svg-icon icon-class="bug" />
+      </el-button>
+    </el-badge>
+
+    <el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
+      <div slot="title">
+        <span style="padding-right: 10px;">Error Log</span>
+        <el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
+      </div>
+      <el-table :data="errorLogs" border>
+        <el-table-column label="Message">
+          <template slot-scope="{row}">
+            <div>
+              <span class="message-title">Msg:</span>
+              <el-tag type="danger">
+                {{ row.err.message }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 10px;">Info: </span>
+              <el-tag type="warning">
+                {{ row.vm.$vnode.tag }} error in {{ row.info }}
+              </el-tag>
+            </div>
+            <br>
+            <div>
+              <span class="message-title" style="padding-right: 16px;">Url: </span>
+              <el-tag type="success">
+                {{ row.url }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="Stack">
+          <template slot-scope="scope">
+            {{ scope.row.err.stack }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorLog',
+  data() {
+    return {
+      dialogTableVisible: false
+    }
+  },
+  computed: {
+    errorLogs() {
+      return this.$store.getters.errorLogs
+    }
+  },
+  methods: {
+    clearAll() {
+      this.dialogTableVisible = false
+      this.$store.dispatch('errorLog/clearErrorLog')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.message-title {
+  font-size: 16px;
+  color: #333;
+  font-weight: bold;
+  padding-right: 8px;
+}
+</style>

+ 54 - 0
src/components/GithubCorner/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
+    <svg
+      width="80"
+      height="80"
+      viewBox="0 0 250 250"
+      style="fill:#40c9c6; color:#fff;"
+      aria-hidden="true"
+    >
+      <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
+      <path
+        d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
+        fill="currentColor"
+        style="transform-origin: 130px 106px;"
+        class="octo-arm"
+      />
+      <path
+        d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
+        fill="currentColor"
+        class="octo-body"
+      />
+    </svg>
+  </a>
+</template>
+
+<style scoped>
+.github-corner:hover .octo-arm {
+  animation: octocat-wave 560ms ease-in-out
+}
+
+@keyframes octocat-wave {
+  0%,
+  100% {
+    transform: rotate(0)
+  }
+  20%,
+  60% {
+    transform: rotate(-25deg)
+  }
+  40%,
+  80% {
+    transform: rotate(10deg)
+  }
+}
+
+@media (max-width:500px) {
+  .github-corner:hover .octo-arm {
+    animation: none
+  }
+  .github-corner .octo-arm {
+    animation: octocat-wave 560ms ease-in-out
+  }
+}
+</style>

+ 44 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div style="padding: 0 15px;" @click="toggleClick">
+    <svg
+      :class="{'is-active':isActive}"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+    </svg>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Hamburger',
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    toggleClick() {
+      this.$emit('toggleClick')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 180 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,180 @@
+<template>
+  <div :class="{'show':show}" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select
+      ref="headerSearchSelect"
+      v-model="search"
+      :remote-method="querySearch"
+      filterable
+      default-first-option
+      remote
+      placeholder="Search"
+      class="header-search-select"
+      @change="change"
+    >
+      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script>
+// fuse is a lightweight fuzzy-search module
+// make search results more in line with expectations
+import Fuse from 'fuse.js'
+import path from 'path'
+
+export default {
+  name: 'HeaderSearch',
+  data() {
+    return {
+      search: '',
+      options: [],
+      searchPool: [],
+      show: false,
+      fuse: undefined
+    }
+  },
+  computed: {
+    routes() {
+      return this.$store.getters.permission_routes
+    }
+  },
+  watch: {
+    routes() {
+      this.searchPool = this.generateRoutes(this.routes)
+    },
+    searchPool(list) {
+      this.initFuse(list)
+    },
+    show(value) {
+      if (value) {
+        document.body.addEventListener('click', this.close)
+      } else {
+        document.body.removeEventListener('click', this.close)
+      }
+    }
+  },
+  mounted() {
+    this.searchPool = this.generateRoutes(this.routes)
+  },
+  methods: {
+    click() {
+      this.show = !this.show
+      if (this.show) {
+        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
+      }
+    },
+    close() {
+      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
+      this.options = []
+      this.show = false
+    },
+    change(val) {
+      this.$router.push(val.path)
+      this.search = ''
+      this.options = []
+      this.$nextTick(() => {
+        this.show = false
+      })
+    },
+    initFuse(list) {
+      this.fuse = new Fuse(list, {
+        shouldSort: true,
+        threshold: 0.4,
+        location: 0,
+        distance: 100,
+        maxPatternLength: 32,
+        minMatchCharLength: 1,
+        keys: [{
+          name: 'title',
+          weight: 0.7
+        }, {
+          name: 'path',
+          weight: 0.3
+        }]
+      })
+    },
+    // Filter out the routes that can be displayed in the sidebar
+    // And generate the internationalized title
+    generateRoutes(routes, basePath = '/', prefixTitle = []) {
+      let res = []
+
+      for (const router of routes) {
+        // skip hidden router
+        if (router.hidden) { continue }
+
+        const data = {
+          path: path.resolve(basePath, router.path),
+          title: [...prefixTitle]
+        }
+
+        if (router.meta && router.meta.title) {
+          data.title = [...data.title, router.meta.title]
+
+          if (router.redirect !== 'noRedirect') {
+            // only push the routes with title
+            // special case: need to exclude parent router without redirect
+            res.push(data)
+          }
+        }
+
+        // recursive child routes
+        if (router.children) {
+          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
+          if (tempRoutes.length >= 1) {
+            res = [...res, ...tempRoutes]
+          }
+        }
+      }
+      return res
+    },
+    querySearch(query) {
+      if (query !== '') {
+        this.options = this.fuse.search(query)
+      } else {
+        this.options = []
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 18px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+
+    /deep/ .el-input__inner {
+      border-radius: 0;
+      border: 0;
+      padding-left: 0;
+      padding-right: 0;
+      box-shadow: none !important;
+      border-bottom: 1px solid #d9d9d9;
+      vertical-align: middle;
+    }
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>

+ 68 - 0
src/components/IconSelect/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="icon-body">
+    <el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input.native="filterIcons">
+      <i slot="suffix" class="el-icon-search el-input__icon" />
+    </el-input>
+    <div class="icon-list">
+      <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
+        <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
+        <span>{{ item }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import icons from './requireIcons'
+export default {
+  name: 'IconSelect',
+  data() {
+    return {
+      name: '',
+      iconList: icons
+    }
+  },
+  methods: {
+    filterIcons() {
+      if (this.name) {
+        this.iconList = this.iconList.filter(item => item.includes(this.name))
+      } else {
+        this.iconList = icons
+      }
+    },
+    selectedIcon(name) {
+      this.$emit('selected', name)
+      document.body.click()
+    },
+    reset() {
+      this.name = ''
+      this.iconList = icons
+    }
+  }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+  .icon-body {
+    width: 100%;
+    padding: 10px;
+    .icon-list {
+      height: 200px;
+      overflow-y: scroll;
+      div {
+        height: 30px;
+        line-height: 30px;
+        margin-bottom: -5px;
+        cursor: pointer;
+        width: 33%;
+        float: left;
+      }
+      span {
+        display: inline-block;
+        vertical-align: -0.15em;
+        fill: currentColor;
+        overflow: hidden;
+      }
+    }
+  }
+</style>

+ 10 - 0
src/components/IconSelect/requireIcons.js

@@ -0,0 +1,10 @@
+const req = require.context('../../icons/svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys()
+
+const re = /\.\/(.*)\.svg/
+
+const icons = requireAll(req).map(i => {
+  return i.match(re)[1]
+})
+
+export default icons

+ 1778 - 0
src/components/ImageCropper/index.vue

@@ -0,0 +1,1778 @@
+<template>
+  <div v-show="value" class="vue-image-crop-upload">
+    <div class="vicp-wrap">
+      <div class="vicp-close" @click="off">
+        <i class="vicp-icon4" />
+      </div>
+
+      <div v-show="step == 1" class="vicp-step1">
+        <div
+          class="vicp-drop-area"
+          @dragleave="preventDefault"
+          @dragover="preventDefault"
+          @dragenter="preventDefault"
+          @click="handleClick"
+          @drop="handleChange"
+        >
+          <i v-show="loading != 1" class="vicp-icon1">
+            <i class="vicp-icon1-arrow" />
+            <i class="vicp-icon1-body" />
+            <i class="vicp-icon1-bottom" />
+          </i>
+          <span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span>
+          <span v-show="!isSupported" class="vicp-no-supported-hint">{{ lang.noSupported }}</span>
+          <input v-show="false" v-if="step == 1" ref="fileinput" type="file" @change="handleChange">
+        </div>
+        <div v-show="hasError" class="vicp-error">
+          <i class="vicp-icon2" />
+          {{ errorMsg }}
+        </div>
+        <div class="vicp-operate">
+          <a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
+        </div>
+      </div>
+
+      <div v-if="step == 2" class="vicp-step2">
+        <div class="vicp-crop">
+          <div v-show="true" class="vicp-crop-left">
+            <div class="vicp-img-container">
+              <img
+                ref="img"
+                :src="sourceImgUrl"
+                :style="sourceImgStyle"
+                class="vicp-img"
+                draggable="false"
+                @drag="preventDefault"
+                @dragstart="preventDefault"
+                @dragend="preventDefault"
+                @dragleave="preventDefault"
+                @dragover="preventDefault"
+                @dragenter="preventDefault"
+                @drop="preventDefault"
+                @touchstart="imgStartMove"
+                @touchmove="imgMove"
+                @touchend="createImg"
+                @touchcancel="createImg"
+                @mousedown="imgStartMove"
+                @mousemove="imgMove"
+                @mouseup="createImg"
+                @mouseout="createImg"
+              >
+              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1" />
+              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2" />
+            </div>
+
+            <div class="vicp-range">
+              <input
+                :value="scale.range"
+                type="range"
+                step="1"
+                min="0"
+                max="100"
+                @input="zoomChange"
+              >
+              <i
+                class="vicp-icon5"
+                @mousedown="startZoomSub"
+                @mouseout="endZoomSub"
+                @mouseup="endZoomSub"
+              />
+              <i
+                class="vicp-icon6"
+                @mousedown="startZoomAdd"
+                @mouseout="endZoomAdd"
+                @mouseup="endZoomAdd"
+              />
+            </div>
+
+            <div v-if="!noRotate" class="vicp-rotate">
+              <i @mousedown="startRotateLeft" @mouseout="endRotate" @mouseup="endRotate">↺</i>
+              <i @mousedown="startRotateRight" @mouseout="endRotate" @mouseup="endRotate">↻</i>
+            </div>
+          </div>
+          <div v-show="true" class="vicp-crop-right">
+            <div class="vicp-preview">
+              <div v-if="!noSquare" class="vicp-preview-item">
+                <img :src="createImgUrl" :style="previewStyle">
+                <span>{{ lang.preview }}</span>
+              </div>
+              <div v-if="!noCircle" class="vicp-preview-item vicp-preview-item-circle">
+                <img :src="createImgUrl" :style="previewStyle">
+                <span>{{ lang.preview }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="vicp-operate">
+          <a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
+          <a class="vicp-operate-btn" @click="prepareUpload" @mousedown="ripple">{{ lang.btn.save }}</a>
+        </div>
+      </div>
+
+      <div v-if="step == 3" class="vicp-step3">
+        <div class="vicp-upload">
+          <span v-show="loading === 1" class="vicp-loading">{{ lang.loading }}</span>
+          <div class="vicp-progress-wrap">
+            <span v-show="loading === 1" :style="progressStyle" class="vicp-progress" />
+          </div>
+          <div v-show="hasError" class="vicp-error">
+            <i class="vicp-icon2" />
+            {{ errorMsg }}
+          </div>
+          <div v-show="loading === 2" class="vicp-success">
+            <i class="vicp-icon3" />
+            {{ lang.success }}
+          </div>
+        </div>
+        <div class="vicp-operate">
+          <a @click="setStep(2)" @mousedown="ripple">{{ lang.btn.back }}</a>
+          <a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
+        </div>
+      </div>
+      <canvas v-show="false" ref="canvas" :width="width" :height="height" />
+    </div>
+  </div>
+</template>
+
+<script>
+'use strict'
+import request from '@/utils/request'
+import language from './utils/language.js'
+import mimes from './utils/mimes.js'
+import data2blob from './utils/data2blob.js'
+import effectRipple from './utils/effectRipple.js'
+export default {
+  props: {
+    // 域,上传文件name,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+    field: {
+      type: String,
+      default: 'avatar'
+    },
+    // 原名key,类似于id,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
+    ki: {
+      type: Number,
+      default: 0
+    },
+    // 显示该控件与否
+    value: {
+      type: Boolean,
+      default: true
+    },
+    // 上传地址
+    url: {
+      type: String,
+      default: ''
+    },
+    // 其他要上传文件附带的数据,对象格式
+    params: {
+      type: Object,
+      default: null
+    },
+    // Add custom headers
+    headers: {
+      type: Object,
+      default: null
+    },
+    // 剪裁图片的宽
+    width: {
+      type: Number,
+      default: 200
+    },
+    // 剪裁图片的高
+    height: {
+      type: Number,
+      default: 200
+    },
+    // 不显示旋转功能
+    noRotate: {
+      type: Boolean,
+      default: true
+    },
+    // 不预览圆形图片
+    noCircle: {
+      type: Boolean,
+      default: false
+    },
+    // 不预览方形图片
+    noSquare: {
+      type: Boolean,
+      default: false
+    },
+    // 单文件大小限制
+    maxSize: {
+      type: Number,
+      default: 10240
+    },
+    // 语言类型
+    langType: {
+      type: String,
+      default: 'zh'
+    },
+    // 语言包
+    langExt: {
+      type: Object,
+      default: null
+    },
+    // 图片上传格式
+    imgFormat: {
+      type: String,
+      default: 'png'
+    },
+    // 是否支持跨域
+    withCredentials: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    const { imgFormat, langType, langExt, width, height } = this
+    let isSupported = true
+    const allowImgFormat = ['jpg', 'png']
+    const tempImgFormat =
+      allowImgFormat.indexOf(imgFormat) === -1 ? 'jpg' : imgFormat
+    const lang = language[langType] ? language[langType] : language['en']
+    const mime = mimes[tempImgFormat]
+    // 规范图片格式
+    this.imgFormat = tempImgFormat
+    if (langExt) {
+      Object.assign(lang, langExt)
+    }
+    if (typeof FormData !== 'function') {
+      isSupported = false
+    }
+    return {
+      // 图片的mime
+      mime,
+      // 语言包
+      lang,
+      // 浏览器是否支持该控件
+      isSupported,
+      // 浏览器是否支持触屏事件
+      isSupportTouch: document.hasOwnProperty('ontouchstart'),
+      // 步骤
+      step: 1, // 1选择文件 2剪裁 3上传
+      // 上传状态及进度
+      loading: 0, // 0未开始 1正在 2成功 3错误
+      progress: 0,
+      // 是否有错误及错误信息
+      hasError: false,
+      errorMsg: '',
+      // 需求图宽高比
+      ratio: width / height,
+      // 原图地址、生成图片地址
+      sourceImg: null,
+      sourceImgUrl: '',
+      createImgUrl: '',
+      // 原图片拖动事件初始值
+      sourceImgMouseDown: {
+        on: false,
+        mX: 0, // 鼠标按下的坐标
+        mY: 0,
+        x: 0, // scale原图坐标
+        y: 0
+      },
+      // 生成图片预览的容器大小
+      previewContainer: {
+        width: 100,
+        height: 100
+      },
+      // 原图容器宽高
+      sourceImgContainer: {
+        // sic
+        width: 240,
+        height: 184 // 如果生成图比例与此一致会出现bug,先改成特殊的格式吧,哈哈哈
+      },
+      // 原图展示属性
+      scale: {
+        zoomAddOn: false, // 按钮缩放事件开启
+        zoomSubOn: false, // 按钮缩放事件开启
+        range: 1, // 最大100
+        rotateLeft: false, // 按钮向左旋转事件开启
+        rotateRight: false, // 按钮向右旋转事件开启
+        degree: 0, // 旋转度数
+        x: 0,
+        y: 0,
+        width: 0,
+        height: 0,
+        maxWidth: 0,
+        maxHeight: 0,
+        minWidth: 0, // 最宽
+        minHeight: 0,
+        naturalWidth: 0, // 原宽
+        naturalHeight: 0
+      }
+    }
+  },
+  computed: {
+    // 进度条样式
+    progressStyle() {
+      const { progress } = this
+      return {
+        width: progress + '%'
+      }
+    },
+    // 原图样式
+    sourceImgStyle() {
+      const { scale, sourceImgMasking } = this
+      const top = scale.y + sourceImgMasking.y + 'px'
+      const left = scale.x + sourceImgMasking.x + 'px'
+      return {
+        top,
+        left,
+        width: scale.width + 'px',
+        height: scale.height + 'px',
+        transform: 'rotate(' + scale.degree + 'deg)', // 旋转时 左侧原始图旋转样式
+        '-ms-transform': 'rotate(' + scale.degree + 'deg)', // 兼容IE9
+        '-moz-transform': 'rotate(' + scale.degree + 'deg)', // 兼容FireFox
+        '-webkit-transform': 'rotate(' + scale.degree + 'deg)', // 兼容Safari 和 chrome
+        '-o-transform': 'rotate(' + scale.degree + 'deg)' // 兼容 Opera
+      }
+    },
+    // 原图蒙版属性
+    sourceImgMasking() {
+      const { width, height, ratio, sourceImgContainer } = this
+      const sic = sourceImgContainer
+      const sicRatio = sic.width / sic.height // 原图容器宽高比
+      let x = 0
+      let y = 0
+      let w = sic.width
+      let h = sic.height
+      let scale = 1
+      if (ratio < sicRatio) {
+        scale = sic.height / height
+        w = sic.height * ratio
+        x = (sic.width - w) / 2
+      }
+      if (ratio > sicRatio) {
+        scale = sic.width / width
+        h = sic.width / ratio
+        y = (sic.height - h) / 2
+      }
+      return {
+        scale, // 蒙版相对需求宽高的缩放
+        x,
+        y,
+        width: w,
+        height: h
+      }
+    },
+    // 原图遮罩样式
+    sourceImgShadeStyle() {
+      const { sourceImgMasking, sourceImgContainer } = this
+      const sic = sourceImgContainer
+      const sim = sourceImgMasking
+      const w =
+        sim.width === sic.width ? sim.width : (sic.width - sim.width) / 2
+      const h =
+        sim.height === sic.height ? sim.height : (sic.height - sim.height) / 2
+      return {
+        width: w + 'px',
+        height: h + 'px'
+      }
+    },
+    previewStyle() {
+      const { ratio, previewContainer } = this
+      const pc = previewContainer
+      let w = pc.width
+      let h = pc.height
+      const pcRatio = w / h
+      if (ratio < pcRatio) {
+        w = pc.height * ratio
+      }
+      if (ratio > pcRatio) {
+        h = pc.width / ratio
+      }
+      return {
+        width: w + 'px',
+        height: h + 'px'
+      }
+    }
+  },
+  watch: {
+    value(newValue) {
+      if (newValue && this.loading !== 1) {
+        this.reset()
+      }
+    }
+  },
+  created() {
+    // 绑定按键esc隐藏此插件事件
+    document.addEventListener('keyup', this.closeHandler)
+  },
+  destroyed() {
+    document.removeEventListener('keyup', this.closeHandler)
+  },
+  methods: {
+    // 点击波纹效果
+    ripple(e) {
+      effectRipple(e)
+    },
+    // 关闭控件
+    off() {
+      setTimeout(() => {
+        this.$emit('input', false)
+        this.$emit('close')
+        if (this.step === 3 && this.loading === 2) {
+          this.setStep(1)
+        }
+      }, 200)
+    },
+    // 设置步骤
+    setStep(no) {
+      // 延时是为了显示动画效果呢,哈哈哈
+      setTimeout(() => {
+        this.step = no
+      }, 200)
+    },
+    /* 图片选择区域函数绑定
+     ---------------------------------------------------------------*/
+    preventDefault(e) {
+      e.preventDefault()
+      return false
+    },
+    handleClick(e) {
+      if (this.loading !== 1) {
+        if (e.target !== this.$refs.fileinput) {
+          e.preventDefault()
+          if (document.activeElement !== this.$refs) {
+            this.$refs.fileinput.click()
+          }
+        }
+      }
+    },
+    handleChange(e) {
+      e.preventDefault()
+      if (this.loading !== 1) {
+        const files = e.target.files || e.dataTransfer.files
+        this.reset()
+        if (this.checkFile(files[0])) {
+          this.setSourceImg(files[0])
+        }
+      }
+    },
+    /* ---------------------------------------------------------------*/
+    // 检测选择的文件是否合适
+    checkFile(file) {
+      const { lang, maxSize } = this
+      // 仅限图片
+      if (file.type.indexOf('image') === -1) {
+        this.hasError = true
+        this.errorMsg = lang.error.onlyImg
+        return false
+      }
+      // 超出大小
+      if (file.size / 1024 > maxSize) {
+        this.hasError = true
+        this.errorMsg = lang.error.outOfSize + maxSize + 'kb'
+        return false
+      }
+      return true
+    },
+    // 重置控件
+    reset() {
+      this.loading = 0
+      this.hasError = false
+      this.errorMsg = ''
+      this.progress = 0
+    },
+    // 设置图片源
+    setSourceImg(file) {
+      const fr = new FileReader()
+      fr.onload = e => {
+        this.sourceImgUrl = fr.result
+        this.startCrop()
+      }
+      fr.readAsDataURL(file)
+    },
+    // 剪裁前准备工作
+    startCrop() {
+      const {
+        width,
+        height,
+        ratio,
+        scale,
+        sourceImgUrl,
+        sourceImgMasking,
+        lang
+      } = this
+      const sim = sourceImgMasking
+      const img = new Image()
+      img.src = sourceImgUrl
+      img.onload = () => {
+        const nWidth = img.naturalWidth
+        const nHeight = img.naturalHeight
+        const nRatio = nWidth / nHeight
+        let w = sim.width
+        let h = sim.height
+        let x = 0
+        let y = 0
+        // 图片像素不达标
+        if (nWidth < width || nHeight < height) {
+          this.hasError = true
+          this.errorMsg = lang.error.lowestPx + width + '*' + height
+          return false
+        }
+        if (ratio > nRatio) {
+          h = w / nRatio
+          y = (sim.height - h) / 2
+        }
+        if (ratio < nRatio) {
+          w = h * nRatio
+          x = (sim.width - w) / 2
+        }
+        scale.range = 0
+        scale.x = x
+        scale.y = y
+        scale.width = w
+        scale.height = h
+        scale.degree = 0
+        scale.minWidth = w
+        scale.minHeight = h
+        scale.maxWidth = nWidth * sim.scale
+        scale.maxHeight = nHeight * sim.scale
+        scale.naturalWidth = nWidth
+        scale.naturalHeight = nHeight
+        this.sourceImg = img
+        this.createImg()
+        this.setStep(2)
+      }
+    },
+    // 鼠标按下图片准备移动
+    imgStartMove(e) {
+      e.preventDefault()
+      // 支持触摸事件,则鼠标事件无效
+      if (this.isSupportTouch && !e.targetTouches) {
+        return false
+      }
+      const et = e.targetTouches ? e.targetTouches[0] : e
+      const { sourceImgMouseDown, scale } = this
+      const simd = sourceImgMouseDown
+      simd.mX = et.screenX
+      simd.mY = et.screenY
+      simd.x = scale.x
+      simd.y = scale.y
+      simd.on = true
+    },
+    // 鼠标按下状态下移动,图片移动
+    imgMove(e) {
+      e.preventDefault()
+      // 支持触摸事件,则鼠标事件无效
+      if (this.isSupportTouch && !e.targetTouches) {
+        return false
+      }
+      const et = e.targetTouches ? e.targetTouches[0] : e
+      const {
+        sourceImgMouseDown: { on, mX, mY, x, y },
+        scale,
+        sourceImgMasking
+      } = this
+      const sim = sourceImgMasking
+      const nX = et.screenX
+      const nY = et.screenY
+      const dX = nX - mX
+      const dY = nY - mY
+      let rX = x + dX
+      let rY = y + dY
+      if (!on) return
+      if (rX > 0) {
+        rX = 0
+      }
+      if (rY > 0) {
+        rY = 0
+      }
+      if (rX < sim.width - scale.width) {
+        rX = sim.width - scale.width
+      }
+      if (rY < sim.height - scale.height) {
+        rY = sim.height - scale.height
+      }
+      scale.x = rX
+      scale.y = rY
+    },
+    // 按钮按下开始向右旋转
+    startRotateRight(e) {
+      const { scale } = this
+      scale.rotateRight = true
+      const rotate = () => {
+        if (scale.rotateRight) {
+          const degree = ++scale.degree
+          this.createImg(degree)
+          setTimeout(function() {
+            rotate()
+          }, 60)
+        }
+      }
+      rotate()
+    },
+    // 按钮按下开始向左旋转
+    startRotateLeft(e) {
+      const { scale } = this
+      scale.rotateLeft = true
+      const rotate = () => {
+        if (scale.rotateLeft) {
+          const degree = --scale.degree
+          this.createImg(degree)
+          setTimeout(function() {
+            rotate()
+          }, 60)
+        }
+      }
+      rotate()
+    },
+    // 停止旋转
+    endRotate() {
+      const { scale } = this
+      scale.rotateLeft = false
+      scale.rotateRight = false
+    },
+    // 按钮按下开始放大
+    startZoomAdd(e) {
+      const { scale } = this
+      scale.zoomAddOn = true
+      const zoom = () => {
+        if (scale.zoomAddOn) {
+          const range = scale.range >= 100 ? 100 : ++scale.range
+          this.zoomImg(range)
+          setTimeout(function() {
+            zoom()
+          }, 60)
+        }
+      }
+      zoom()
+    },
+    // 按钮松开或移开取消放大
+    endZoomAdd(e) {
+      this.scale.zoomAddOn = false
+    },
+    // 按钮按下开始缩小
+    startZoomSub(e) {
+      const { scale } = this
+      scale.zoomSubOn = true
+      const zoom = () => {
+        if (scale.zoomSubOn) {
+          const range = scale.range <= 0 ? 0 : --scale.range
+          this.zoomImg(range)
+          setTimeout(function() {
+            zoom()
+          }, 60)
+        }
+      }
+      zoom()
+    },
+    // 按钮松开或移开取消缩小
+    endZoomSub(e) {
+      const { scale } = this
+      scale.zoomSubOn = false
+    },
+    zoomChange(e) {
+      this.zoomImg(e.target.value)
+    },
+    // 缩放原图
+    zoomImg(newRange) {
+      const { sourceImgMasking, scale } = this
+      const {
+        maxWidth,
+        maxHeight,
+        minWidth,
+        minHeight,
+        width,
+        height,
+        x,
+        y
+      } = scale
+      const sim = sourceImgMasking
+      // 蒙版宽高
+      const sWidth = sim.width
+      const sHeight = sim.height
+      // 新宽高
+      const nWidth = minWidth + ((maxWidth - minWidth) * newRange) / 100
+      const nHeight = minHeight + ((maxHeight - minHeight) * newRange) / 100
+      // 新坐标(根据蒙版中心点缩放)
+      let nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x)
+      let nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y)
+      // 判断新坐标是否超过蒙版限制
+      if (nX > 0) {
+        nX = 0
+      }
+      if (nY > 0) {
+        nY = 0
+      }
+      if (nX < sWidth - nWidth) {
+        nX = sWidth - nWidth
+      }
+      if (nY < sHeight - nHeight) {
+        nY = sHeight - nHeight
+      }
+      // 赋值处理
+      scale.x = nX
+      scale.y = nY
+      scale.width = nWidth
+      scale.height = nHeight
+      scale.range = newRange
+      setTimeout(() => {
+        if (scale.range === newRange) {
+          this.createImg()
+        }
+      }, 300)
+    },
+    // 生成需求图片
+    createImg(e) {
+      const {
+        mime,
+        sourceImg,
+        scale: { x, y, width, height, degree },
+        sourceImgMasking: { scale }
+      } = this
+      const canvas = this.$refs.canvas
+      const ctx = canvas.getContext('2d')
+      if (e) {
+        // 取消鼠标按下移动状态
+        this.sourceImgMouseDown.on = false
+      }
+      canvas.width = this.width
+      canvas.height = this.height
+      ctx.clearRect(0, 0, this.width, this.height)
+      // 将透明区域设置为白色底边
+      ctx.fillStyle = '#fff'
+      ctx.fillRect(0, 0, this.width, this.height)
+      ctx.translate(this.width * 0.5, this.height * 0.5)
+      ctx.rotate((Math.PI * degree) / 180)
+      ctx.translate(-this.width * 0.5, -this.height * 0.5)
+      ctx.drawImage(
+        sourceImg,
+        x / scale,
+        y / scale,
+        width / scale,
+        height / scale
+      )
+      this.createImgUrl = canvas.toDataURL(mime)
+    },
+    prepareUpload() {
+      const { url, createImgUrl, field, ki } = this
+      this.$emit('crop-success', createImgUrl, field, ki)
+      if (typeof url === 'string' && url) {
+        this.upload()
+      } else {
+        this.off()
+      }
+    },
+    // 上传图片
+    upload() {
+      const {
+        lang,
+        imgFormat,
+        mime,
+        url,
+        params,
+        field,
+        ki,
+        createImgUrl
+      } = this
+      const fmData = new FormData()
+      fmData.append(
+        field,
+        data2blob(createImgUrl, mime),
+        field + '.' + imgFormat
+      )
+      // 添加其他参数
+      if (typeof params === 'object' && params) {
+        Object.keys(params).forEach(k => {
+          fmData.append(k, params[k])
+        })
+      }
+      // 监听进度回调
+      // const uploadProgress = (event) => {
+      //   if (event.lengthComputable) {
+      //     this.progress = 100 * Math.round(event.loaded) / event.total
+      //   }
+      // }
+      // 上传文件
+      this.reset()
+      this.loading = 1
+      this.setStep(3)
+      request({
+        url,
+        method: 'post',
+        data: fmData
+      })
+        .then(resData => {
+          this.loading = 2
+          this.$emit('crop-upload-success', resData.data)
+        })
+        .catch(err => {
+          if (this.value) {
+            this.loading = 3
+            this.hasError = true
+            this.errorMsg = lang.fail
+            this.$emit('crop-upload-fail', err, field, ki)
+          }
+        })
+    },
+    closeHandler(e) {
+      if (this.value && (e.key === 'Escape' || e.keyCode === 27)) {
+        this.off()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@charset "UTF-8";
+@-webkit-keyframes vicp_progress {
+  0% {
+    background-position-y: 0;
+  }
+  100% {
+    background-position-y: 40px;
+  }
+}
+@keyframes vicp_progress {
+  0% {
+    background-position-y: 0;
+  }
+  100% {
+    background-position-y: 40px;
+  }
+}
+@-webkit-keyframes vicp {
+  0% {
+    opacity: 0;
+    -webkit-transform: scale(0) translatey(-60px);
+    transform: scale(0) translatey(-60px);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: scale(1) translatey(0);
+    transform: scale(1) translatey(0);
+  }
+}
+@keyframes vicp {
+  0% {
+    opacity: 0;
+    -webkit-transform: scale(0) translatey(-60px);
+    transform: scale(0) translatey(-60px);
+  }
+  100% {
+    opacity: 1;
+    -webkit-transform: scale(1) translatey(0);
+    transform: scale(1) translatey(0);
+  }
+}
+.vue-image-crop-upload {
+  position: fixed;
+  display: block;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  z-index: 10000;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.65);
+  -webkit-tap-highlight-color: transparent;
+  -moz-tap-highlight-color: transparent;
+}
+.vue-image-crop-upload .vicp-wrap {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  position: fixed;
+  display: block;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  z-index: 10000;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 600px;
+  height: 330px;
+  padding: 25px;
+  background-color: #fff;
+  border-radius: 2px;
+  -webkit-animation: vicp 0.12s ease-in;
+  animation: vicp 0.12s ease-in;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close {
+  position: absolute;
+  right: -30px;
+  top: -30px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4 {
+  position: relative;
+  display: block;
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+  -webkit-transition: -webkit-transform 0.18s;
+  transition: -webkit-transform 0.18s;
+  transition: transform 0.18s;
+  transition: transform 0.18s, -webkit-transform 0.18s;
+  -webkit-transform: rotate(0);
+  -ms-transform: rotate(0);
+  transform: rotate(0);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after,
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::before {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  content: "";
+  position: absolute;
+  top: 12px;
+  left: 4px;
+  width: 20px;
+  height: 3px;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  background-color: #fff;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after {
+  -webkit-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4:hover {
+  -webkit-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  transform: rotate(90deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area {
+  position: relative;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 35px;
+  height: 170px;
+  background-color: rgba(0, 0, 0, 0.03);
+  text-align: center;
+  border: 1px dashed rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 {
+  display: block;
+  margin: 0 auto 6px;
+  width: 42px;
+  height: 42px;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-arrow {
+  display: block;
+  margin: 0 auto;
+  width: 0;
+  height: 0;
+  border-bottom: 14.7px solid rgba(0, 0, 0, 0.3);
+  border-left: 14.7px solid transparent;
+  border-right: 14.7px solid transparent;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-body {
+  display: block;
+  width: 12.6px;
+  height: 14.7px;
+  margin: 0 auto;
+  background-color: rgba(0, 0, 0, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-icon1
+  .vicp-icon1-bottom {
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  display: block;
+  height: 12.6px;
+  border: 6px solid rgba(0, 0, 0, 0.3);
+  border-top: none;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-hint {
+  display: block;
+  padding: 15px;
+  font-size: 14px;
+  color: #666;
+  line-height: 30px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step1
+  .vicp-drop-area
+  .vicp-no-supported-hint {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding: 30px;
+  width: 100%;
+  height: 60px;
+  line-height: 30px;
+  background-color: #eee;
+  text-align: center;
+  color: #666;
+  font-size: 14px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area:hover {
+  cursor: pointer;
+  border-color: rgba(0, 0, 0, 0.1);
+  background-color: rgba(0, 0, 0, 0.05);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop {
+  overflow: hidden;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left {
+  float: left;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container {
+  position: relative;
+  display: block;
+  width: 240px;
+  height: 180px;
+  background-color: #e5e5e0;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img {
+  position: absolute;
+  display: block;
+  cursor: move;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade {
+  -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  position: absolute;
+  background-color: rgba(241, 242, 243, 0.8);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade.vicp-img-shade-1 {
+  top: 0;
+  left: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-img-container
+  .vicp-img-shade.vicp-img-shade-2 {
+  bottom: 0;
+  right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate {
+  position: relative;
+  width: 240px;
+  height: 18px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i {
+  display: block;
+  width: 18px;
+  height: 18px;
+  border-radius: 100%;
+  line-height: 18px;
+  text-align: center;
+  font-size: 12px;
+  font-weight: bold;
+  background-color: rgba(0, 0, 0, 0.08);
+  color: #fff;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:hover {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+  background-color: rgba(0, 0, 0, 0.14);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:first-child {
+  float: left;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-rotate
+  i:last-child {
+  float: right;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range {
+  position: relative;
+  margin: 30px 0 10px 0;
+  width: 240px;
+  height: 18px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5,
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6 {
+  position: absolute;
+  top: 0;
+  width: 18px;
+  height: 18px;
+  border-radius: 100%;
+  background-color: rgba(0, 0, 0, 0.08);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5:hover,
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6:hover {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  cursor: pointer;
+  background-color: rgba(0, 0, 0, 0.14);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5 {
+  left: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon5::before {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 3px;
+  top: 8px;
+  width: 12px;
+  height: 2px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6 {
+  right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6::before {
+  position: absolute;
+  content: "";
+  display: block;
+  left: 3px;
+  top: 8px;
+  width: 12px;
+  height: 2px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  .vicp-icon6::after {
+  position: absolute;
+  content: "";
+  display: block;
+  top: 3px;
+  left: 8px;
+  width: 2px;
+  height: 12px;
+  background-color: #fff;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"] {
+  display: block;
+  padding-top: 5px;
+  margin: 0 auto;
+  width: 180px;
+  height: 8px;
+  vertical-align: top;
+  background: transparent;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  cursor: pointer;
+  /* 滑块
+               ---------------------------------------------------------------*/
+  /* 轨道
+               ---------------------------------------------------------------*/
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus {
+  outline: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  -webkit-appearance: none;
+  appearance: none;
+  margin-top: -3px;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border-radius: 100%;
+  border: none;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-moz-range-thumb {
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  -moz-appearance: none;
+  appearance: none;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border-radius: 100%;
+  border: none;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-thumb {
+  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
+  appearance: none;
+  width: 12px;
+  height: 12px;
+  background-color: #61c091;
+  border: none;
+  border-radius: 100%;
+  -webkit-transition: 0.2s;
+  transition: 0.2s;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-moz-range-thumb {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-ms-thumb {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:active::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
+  margin-top: -4px;
+  width: 14px;
+  height: 14px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-webkit-slider-runnable-track {
+  -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  height: 6px;
+  cursor: pointer;
+  border-radius: 2px;
+  border: none;
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-moz-range-track {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  height: 6px;
+  cursor: pointer;
+  border-radius: 2px;
+  border: none;
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-track {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
+  width: 100%;
+  cursor: pointer;
+  background: transparent;
+  border-color: transparent;
+  color: transparent;
+  height: 6px;
+  border-radius: 2px;
+  border: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-fill-lower {
+  background-color: rgba(68, 170, 119, 0.3);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]::-ms-fill-upper {
+  background-color: rgba(68, 170, 119, 0.15);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-webkit-slider-runnable-track {
+  background-color: rgba(68, 170, 119, 0.5);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-moz-range-track {
+  background-color: rgba(68, 170, 119, 0.5);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-ms-fill-lower {
+  background-color: rgba(68, 170, 119, 0.45);
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-left
+  .vicp-range
+  input[type="range"]:focus::-ms-fill-upper {
+  background-color: rgba(68, 170, 119, 0.25);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right {
+  float: right;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview {
+  height: 150px;
+  overflow: hidden;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item {
+  position: relative;
+  padding: 5px;
+  width: 100px;
+  height: 100px;
+  float: left;
+  margin-right: 16px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item
+  span {
+  position: absolute;
+  bottom: -30px;
+  width: 100%;
+  font-size: 14px;
+  color: #bbb;
+  display: block;
+  text-align: center;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item
+  img {
+  position: absolute;
+  display: block;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  padding: 3px;
+  background-color: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item.vicp-preview-item-circle {
+  margin-right: 0;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step2
+  .vicp-crop
+  .vicp-crop-right
+  .vicp-preview
+  .vicp-preview-item.vicp-preview-item-circle
+  img {
+  border-radius: 100%;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload {
+  position: relative;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 35px;
+  height: 170px;
+  background-color: rgba(0, 0, 0, 0.03);
+  text-align: center;
+  border: 1px dashed #ddd;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-loading {
+  display: block;
+  padding: 15px;
+  font-size: 16px;
+  color: #999;
+  line-height: 30px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap {
+  margin-top: 12px;
+  background-color: rgba(0, 0, 0, 0.08);
+  border-radius: 3px;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step3
+  .vicp-upload
+  .vicp-progress-wrap
+  .vicp-progress {
+  position: relative;
+  display: block;
+  height: 5px;
+  border-radius: 3px;
+  background-color: #4a7;
+  -webkit-box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+  box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
+  -webkit-transition: width 0.15s linear;
+  transition: width 0.15s linear;
+  background-image: -webkit-linear-gradient(
+    135deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-image: linear-gradient(
+    -45deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  background-size: 40px 40px;
+  -webkit-animation: vicp_progress 0.5s linear infinite;
+  animation: vicp_progress 0.5s linear infinite;
+}
+.vue-image-crop-upload
+  .vicp-wrap
+  .vicp-step3
+  .vicp-upload
+  .vicp-progress-wrap
+  .vicp-progress::after {
+  content: "";
+  position: absolute;
+  display: block;
+  top: -3px;
+  right: -3px;
+  width: 9px;
+  height: 9px;
+  border: 1px solid rgba(245, 246, 247, 0.7);
+  -webkit-box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+  box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
+  border-radius: 100%;
+  background-color: #4a7;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-success {
+  height: 100px;
+  line-height: 100px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate {
+  position: absolute;
+  right: 20px;
+  bottom: 20px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate a {
+  position: relative;
+  float: left;
+  display: block;
+  margin-left: 10px;
+  width: 100px;
+  height: 36px;
+  line-height: 36px;
+  text-align: center;
+  cursor: pointer;
+  font-size: 14px;
+  color: #4a7;
+  border-radius: 2px;
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-operate a:hover {
+  background-color: rgba(0, 0, 0, 0.03);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-error,
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+  display: block;
+  font-size: 14px;
+  line-height: 24px;
+  height: 24px;
+  color: #d10;
+  text-align: center;
+  vertical-align: top;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-success {
+  color: #4a7;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon3 {
+  position: relative;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  top: 4px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon3::after {
+  position: absolute;
+  top: 3px;
+  left: 6px;
+  width: 6px;
+  height: 10px;
+  border-width: 0 2px 2px 0;
+  border-color: #4a7;
+  border-style: solid;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  content: "";
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2 {
+  position: relative;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  top: 4px;
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after,
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::before {
+  content: "";
+  position: absolute;
+  top: 9px;
+  left: 4px;
+  width: 13px;
+  height: 2px;
+  background-color: #d10;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+}
+.vue-image-crop-upload .vicp-wrap .vicp-icon2::after {
+  -webkit-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+}
+.e-ripple {
+  position: absolute;
+  border-radius: 100%;
+  background-color: rgba(0, 0, 0, 0.15);
+  background-clip: padding-box;
+  pointer-events: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  -webkit-transform: scale(0);
+  -ms-transform: scale(0);
+  transform: scale(0);
+  opacity: 1;
+}
+.e-ripple.z-active {
+  opacity: 0;
+  -webkit-transform: scale(2);
+  -ms-transform: scale(2);
+  transform: scale(2);
+  -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+  transition: opacity 1.2s ease-out, transform 0.6s ease-out,
+    -webkit-transform 0.6s ease-out;
+}
+</style>

+ 19 - 0
src/components/ImageCropper/utils/data2blob.js

@@ -0,0 +1,19 @@
+/**
+ * database64文件格式转换为2进制
+ *
+ * @param  {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
+ * @param  {[String]} mime [description]
+ * @return {[blob]}      [description]
+ */
+export default function(data, mime) {
+  data = data.split(',')[1]
+  data = window.atob(data)
+  var ia = new Uint8Array(data.length)
+  for (var i = 0; i < data.length; i++) {
+    ia[i] = data.charCodeAt(i)
+  }
+  // canvas.toDataURL 返回的默认格式就是 image/png
+  return new Blob([ia], {
+    type: mime
+  })
+}

+ 39 - 0
src/components/ImageCropper/utils/effectRipple.js

@@ -0,0 +1,39 @@
+/**
+ * 点击波纹效果
+ *
+ * @param  {[event]} e        [description]
+ * @param  {[Object]} arg_opts [description]
+ * @return {[bollean]}          [description]
+ */
+export default function(e, arg_opts) {
+  var opts = Object.assign({
+    ele: e.target, // 波纹作用元素
+    type: 'hit', // hit点击位置扩散center中心点扩展
+    bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+  }, arg_opts)
+  var target = opts.ele
+  if (target) {
+    var rect = target.getBoundingClientRect()
+    var ripple = target.querySelector('.e-ripple')
+    if (!ripple) {
+      ripple = document.createElement('span')
+      ripple.className = 'e-ripple'
+      ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+      target.appendChild(ripple)
+    } else {
+      ripple.className = 'e-ripple'
+    }
+    switch (opts.type) {
+      case 'center':
+        ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
+        ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
+        break
+      default:
+        ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
+        ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
+    }
+    ripple.style.backgroundColor = opts.bgc
+    ripple.className = 'e-ripple z-active'
+    return false
+  }
+}

+ 232 - 0
src/components/ImageCropper/utils/language.js

@@ -0,0 +1,232 @@
+export default {
+  zh: {
+    hint: '点击,或拖动图片至此处',
+    loading: '正在上传……',
+    noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
+    success: '上传成功',
+    fail: '图片上传失败',
+    preview: '头像预览',
+    btn: {
+      off: '取消',
+      close: '关闭',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '仅限图片格式',
+      outOfSize: '单文件大小不能超过 ',
+      lowestPx: '图片最低像素为(宽*高):'
+    }
+  },
+  'zh-tw': {
+    hint: '點擊,或拖動圖片至此處',
+    loading: '正在上傳……',
+    noSupported: '瀏覽器不支持該功能,請使用IE10以上或其他現代瀏覽器!',
+    success: '上傳成功',
+    fail: '圖片上傳失敗',
+    preview: '頭像預覽',
+    btn: {
+      off: '取消',
+      close: '關閉',
+      back: '上一步',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '僅限圖片格式',
+      outOfSize: '單文件大小不能超過 ',
+      lowestPx: '圖片最低像素為(寬*高):'
+    }
+  },
+  en: {
+    hint: 'Click or drag the file here to upload',
+    loading: 'Uploading…',
+    noSupported: 'Browser is not supported, please use IE10+ or other browsers',
+    success: 'Upload success',
+    fail: 'Upload failed',
+    preview: 'Preview',
+    btn: {
+      off: 'Cancel',
+      close: 'Close',
+      back: 'Back',
+      save: 'Save'
+    },
+    error: {
+      onlyImg: 'Image only',
+      outOfSize: 'Image exceeds size limit: ',
+      lowestPx: 'Image\'s size is too low. Expected at least: '
+    }
+  },
+  ro: {
+    hint: 'Atinge sau trage fișierul aici',
+    loading: 'Se încarcă',
+    noSupported: 'Browser-ul tău nu suportă acest feature. Te rugăm încearcă cu alt browser.',
+    success: 'S-a încărcat cu succes',
+    fail: 'A apărut o problemă la încărcare',
+    preview: 'Previzualizează',
+
+    btn: {
+      off: 'Anulează',
+      close: 'Închide',
+      back: 'Înapoi',
+      save: 'Salvează'
+    },
+
+    error: {
+      onlyImg: 'Doar imagini',
+      outOfSize: 'Imaginea depășește limita de: ',
+      loewstPx: 'Imaginea este prea mică; Minim: '
+    }
+  },
+  ru: {
+    hint: 'Нажмите, или перетащите файл в это окно',
+    loading: 'Загружаю……',
+    noSupported: 'Ваш браузер не поддерживается, пожалуйста, используйте IE10 + или другие браузеры',
+    success: 'Загрузка выполнена успешно',
+    fail: 'Ошибка загрузки',
+    preview: 'Предпросмотр',
+    btn: {
+      off: 'Отменить',
+      close: 'Закрыть',
+      back: 'Назад',
+      save: 'Сохранить'
+    },
+    error: {
+      onlyImg: 'Только изображения',
+      outOfSize: 'Изображение превышает предельный размер: ',
+      lowestPx: 'Минимальный размер изображения: '
+    }
+  },
+  'pt-br': {
+    hint: 'Clique ou arraste o arquivo aqui para carregar',
+    loading: 'Carregando…',
+    noSupported: 'Browser não suportado, use o IE10+ ou outro browser',
+    success: 'Sucesso ao carregar imagem',
+    fail: 'Falha ao carregar imagem',
+    preview: 'Pré-visualizar',
+    btn: {
+      off: 'Cancelar',
+      close: 'Fechar',
+      back: 'Voltar',
+      save: 'Salvar'
+    },
+    error: {
+      onlyImg: 'Apenas imagens',
+      outOfSize: 'A imagem excede o limite de tamanho: ',
+      lowestPx: 'O tamanho da imagem é muito pequeno. Tamanho mínimo: '
+    }
+  },
+  fr: {
+    hint: 'Cliquez ou glissez le fichier ici.',
+    loading: 'Téléchargement…',
+    noSupported: 'Votre navigateur n\'est pas supporté. Utilisez IE10 + ou un autre navigateur s\'il vous plaît.',
+    success: 'Téléchargement réussit',
+    fail: 'Téléchargement echoué',
+    preview: 'Aperçu',
+    btn: {
+      off: 'Annuler',
+      close: 'Fermer',
+      back: 'Retour',
+      save: 'Enregistrer'
+    },
+    error: {
+      onlyImg: 'Image uniquement',
+      outOfSize: 'L\'image sélectionnée dépasse la taille maximum: ',
+      lowestPx: 'L\'image sélectionnée est trop petite. Dimensions attendues: '
+    }
+  },
+  nl: {
+    hint: 'Klik hier of sleep een afbeelding in dit vlak',
+    loading: 'Uploaden…',
+    noSupported: 'Je browser wordt helaas niet ondersteund. Gebruik IE10+ of een andere browser.',
+    success: 'Upload succesvol',
+    fail: 'Upload mislukt',
+    preview: 'Voorbeeld',
+    btn: {
+      off: 'Annuleren',
+      close: 'Sluiten',
+      back: 'Terug',
+      save: 'Opslaan'
+    },
+    error: {
+      onlyImg: 'Alleen afbeeldingen',
+      outOfSize: 'De afbeelding is groter dan: ',
+      lowestPx: 'De afbeelding is te klein! Minimale afmetingen: '
+    }
+  },
+  tr: {
+    hint: 'Tıkla veya yüklemek istediğini buraya sürükle',
+    loading: 'Yükleniyor…',
+    noSupported: 'Tarayıcı desteklenmiyor, lütfen IE10+ veya farklı tarayıcı kullanın',
+    success: 'Yükleme başarılı',
+    fail: 'Yüklemede hata oluştu',
+    preview: 'Önizle',
+    btn: {
+      off: 'İptal',
+      close: 'Kapat',
+      back: 'Geri',
+      save: 'Kaydet'
+    },
+    error: {
+      onlyImg: 'Sadece resim',
+      outOfSize: 'Resim yükleme limitini aşıyor: ',
+      lowestPx: 'Resmin boyutu çok küçük. En az olması gereken: '
+    }
+  },
+  'es-MX': {
+    hint: 'Selecciona o arrastra una imagen',
+    loading: 'Subiendo...',
+    noSupported: 'Tu navegador no es soportado, porfavor usa IE10+ u otros navegadores mas recientes',
+    success: 'Subido exitosamente',
+    fail: 'Sucedió un error',
+    preview: 'Vista previa',
+    btn: {
+      off: 'Cancelar',
+      close: 'Cerrar',
+      back: 'Atras',
+      save: 'Guardar'
+    },
+    error: {
+      onlyImg: 'Unicamente imagenes',
+      outOfSize: 'La imagen excede el tamaño maximo:',
+      lowestPx: 'La imagen es demasiado pequeño. Se espera por lo menos:'
+    }
+  },
+  de: {
+    hint: 'Klick hier oder zieh eine Datei hier rein zum Hochladen',
+    loading: 'Hochladen…',
+    noSupported: 'Browser wird nicht unterstützt, bitte verwende IE10+ oder andere Browser',
+    success: 'Upload erfolgreich',
+    fail: 'Upload fehlgeschlagen',
+    preview: 'Vorschau',
+    btn: {
+      off: 'Abbrechen',
+      close: 'Schließen',
+      back: 'Zurück',
+      save: 'Speichern'
+    },
+    error: {
+      onlyImg: 'Nur Bilder',
+      outOfSize: 'Das Bild ist zu groß: ',
+      lowestPx: 'Das Bild ist zu klein. Mindestens: '
+    }
+  },
+  ja: {
+    hint: 'クリック・ドラッグしてファイルをアップロード',
+    loading: 'アップロード中...',
+    noSupported: 'このブラウザは対応されていません。IE10+かその他の主要ブラウザをお使いください。',
+    success: 'アップロード成功',
+    fail: 'アップロード失敗',
+    preview: 'プレビュー',
+    btn: {
+      off: 'キャンセル',
+      close: '閉じる',
+      back: '戻る',
+      save: '保存'
+    },
+    error: {
+      onlyImg: '画像のみ',
+      outOfSize: '画像サイズが上限を超えています。上限: ',
+      lowestPx: '画像が小さすぎます。最小サイズ: '
+    }
+  }
+}

+ 7 - 0
src/components/ImageCropper/utils/mimes.js

@@ -0,0 +1,7 @@
+export default {
+  'jpg': 'image/jpeg',
+  'png': 'image/png',
+  'gif': 'image/gif',
+  'svg': 'image/svg+xml',
+  'psd': 'image/photoshop'
+}

+ 72 - 0
src/components/JsonEditor/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="json-editor">
+    <textarea ref="textarea" />
+  </div>
+</template>
+
+<script>
+import CodeMirror from 'codemirror'
+import 'codemirror/addon/lint/lint.css'
+import 'codemirror/lib/codemirror.css'
+import 'codemirror/theme/rubyblue.css'
+require('script-loader!jsonlint')
+import 'codemirror/mode/javascript/javascript'
+import 'codemirror/addon/lint/lint'
+import 'codemirror/addon/lint/json-lint'
+
+export default {
+  name: 'JsonEditor',
+  /* eslint-disable vue/require-prop-types */
+  props: ['value'],
+  data() {
+    return {
+      jsonEditor: false
+    }
+  },
+  watch: {
+    value(value) {
+      const editorValue = this.jsonEditor.getValue()
+      if (value !== editorValue) {
+        this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
+      }
+    }
+  },
+  mounted() {
+    this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
+      lineNumbers: true,
+      mode: 'application/json',
+      gutters: ['CodeMirror-lint-markers'],
+      theme: 'rubyblue',
+      lint: true
+    })
+
+    this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
+    this.jsonEditor.on('change', cm => {
+      this.$emit('changed', cm.getValue())
+      this.$emit('input', cm.getValue())
+    })
+  },
+  methods: {
+    getValue() {
+      return this.jsonEditor.getValue()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.json-editor{
+  height: 100%;
+  position: relative;
+}
+.json-editor >>> .CodeMirror {
+  height: auto;
+  min-height: 300px;
+}
+.json-editor >>> .CodeMirror-scroll{
+  min-height: 300px;
+}
+.json-editor >>> .cm-s-rubyblue span.cm-string {
+  color: #F08047;
+}
+</style>

+ 99 - 0
src/components/Kanban/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="board-column">
+    <div class="board-column-header">
+      {{ headerText }}
+    </div>
+    <draggable
+      :list="list"
+      v-bind="$attrs"
+      class="board-column-content"
+      :set-data="setData"
+    >
+      <div v-for="element in list" :key="element.id" class="board-item">
+        {{ element.name }} {{ element.id }}
+      </div>
+    </draggable>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DragKanbanDemo',
+  components: {
+    draggable
+  },
+  props: {
+    headerText: {
+      type: String,
+      default: 'Header'
+    },
+    options: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    list: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  methods: {
+    setData(dataTransfer) {
+      // to avoid Firefox bug
+      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+      dataTransfer.setData('Text', '')
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.board-column {
+  min-width: 300px;
+  min-height: 100px;
+  height: auto;
+  overflow: hidden;
+  background: #f0f0f0;
+  border-radius: 3px;
+
+  .board-column-header {
+    height: 50px;
+    line-height: 50px;
+    overflow: hidden;
+    padding: 0 20px;
+    text-align: center;
+    background: #333;
+    color: #fff;
+    border-radius: 3px 3px 0 0;
+  }
+
+  .board-column-content {
+    height: auto;
+    overflow: hidden;
+    border: 10px solid transparent;
+    min-height: 60px;
+    display: flex;
+    justify-content: flex-start;
+    flex-direction: column;
+    align-items: center;
+
+    .board-item {
+      cursor: pointer;
+      width: 100%;
+      height: 64px;
+      margin: 5px 0;
+      background-color: #fff;
+      text-align: left;
+      line-height: 54px;
+      padding: 5px 10px;
+      box-sizing: border-box;
+      box-shadow: 0px 1px 3px 0 rgba(0, 0, 0, 0.2);
+    }
+  }
+}
+</style>
+

+ 360 - 0
src/components/MDinput/index.vue

@@ -0,0 +1,360 @@
+<template>
+  <div :class="computedClasses" class="material-input__component">
+    <div :class="{iconClass:icon}">
+      <i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon" />
+      <input
+        v-if="type === 'email'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="email"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'url'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="url"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'number'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :step="step"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :max="max"
+        :min="min"
+        :minlength="minlength"
+        :maxlength="maxlength"
+        :required="required"
+        type="number"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'password'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :max="max"
+        :min="min"
+        :required="required"
+        type="password"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'tel'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :required="required"
+        type="tel"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <input
+        v-if="type === 'text'"
+        v-model="currentValue"
+        :name="name"
+        :placeholder="fillPlaceHolder"
+        :readonly="readonly"
+        :disabled="disabled"
+        :autocomplete="autoComplete"
+        :minlength="minlength"
+        :maxlength="maxlength"
+        :required="required"
+        type="text"
+        class="material-input"
+        @focus="handleMdFocus"
+        @blur="handleMdBlur"
+        @input="handleModelInput"
+      >
+      <span class="material-input-bar" />
+      <label class="material-label">
+        <slot />
+      </label>
+    </div>
+  </div>
+</template>
+
+<script>
+// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
+
+export default {
+  name: 'MdInput',
+  props: {
+    /* eslint-disable */
+    icon: String,
+    name: String,
+    type: {
+      type: String,
+      default: 'text'
+    },
+    value: [String, Number],
+    placeholder: String,
+    readonly: Boolean,
+    disabled: Boolean,
+    min: String,
+    max: String,
+    step: String,
+    minlength: Number,
+    maxlength: Number,
+    required: {
+      type: Boolean,
+      default: true
+    },
+    autoComplete: {
+      type: String,
+      default: 'off'
+    },
+    validateEvent: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      currentValue: this.value,
+      focus: false,
+      fillPlaceHolder: null
+    }
+  },
+  computed: {
+    computedClasses() {
+      return {
+        'material--active': this.focus,
+        'material--disabled': this.disabled,
+        'material--raised': Boolean(this.focus || this.currentValue) // has value
+      }
+    }
+  },
+  watch: {
+    value(newValue) {
+      this.currentValue = newValue
+    }
+  },
+  methods: {
+    handleModelInput(event) {
+      const value = event.target.value
+      this.$emit('input', value)
+      if (this.$parent.$options.componentName === 'ElFormItem') {
+        if (this.validateEvent) {
+          this.$parent.$emit('el.form.change', [value])
+        }
+      }
+      this.$emit('change', value)
+    },
+    handleMdFocus(event) {
+      this.focus = true
+      this.$emit('focus', event)
+      if (this.placeholder && this.placeholder !== '') {
+        this.fillPlaceHolder = this.placeholder
+      }
+    },
+    handleMdBlur(event) {
+      this.focus = false
+      this.$emit('blur', event)
+      this.fillPlaceHolder = null
+      if (this.$parent.$options.componentName === 'ElFormItem') {
+        if (this.validateEvent) {
+          this.$parent.$emit('el.form.blur', [this.currentValue])
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  // Fonts:
+  $font-size-base: 16px;
+  $font-size-small: 18px;
+  $font-size-smallest: 12px;
+  $font-weight-normal: normal;
+  $font-weight-bold: bold;
+  $apixel: 1px;
+  // Utils
+  $spacer: 12px;
+  $transition: 0.2s ease all;
+  $index: 0px;
+  $index-has-icon: 30px;
+  // Theme:
+  $color-white: white;
+  $color-grey: #9E9E9E;
+  $color-grey-light: #E0E0E0;
+  $color-blue: #2196F3;
+  $color-red: #F44336;
+  $color-black: black;
+  // Base clases:
+  %base-bar-pseudo {
+    content: '';
+    height: 1px;
+    width: 0;
+    bottom: 0;
+    position: absolute;
+    transition: $transition;
+  }
+
+  // Mixins:
+  @mixin slided-top() {
+    top: - ($font-size-base + $spacer);
+    left: 0;
+    font-size: $font-size-base;
+    font-weight: $font-weight-bold;
+  }
+
+  // Component:
+  .material-input__component {
+    margin-top: 36px;
+    position: relative;
+    * {
+      box-sizing: border-box;
+    }
+    .iconClass {
+      .material-input__icon {
+        position: absolute;
+        left: 0;
+        line-height: $font-size-base;
+        color: $color-blue;
+        top: $spacer;
+        width: $index-has-icon;
+        height: $font-size-base;
+        font-size: $font-size-base;
+        font-weight: $font-weight-normal;
+        pointer-events: none;
+      }
+      .material-label {
+        left: $index-has-icon;
+      }
+      .material-input {
+        text-indent: $index-has-icon;
+      }
+    }
+    .material-input {
+      font-size: $font-size-base;
+      padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
+      display: block;
+      width: 100%;
+      border: none;
+      line-height: 1;
+      border-radius: 0;
+      &:focus {
+        outline: none;
+        border: none;
+        border-bottom: 1px solid transparent; // fixes the height issue
+      }
+    }
+    .material-label {
+      font-weight: $font-weight-normal;
+      position: absolute;
+      pointer-events: none;
+      left: $index;
+      top: 0;
+      transition: $transition;
+      font-size: $font-size-small;
+    }
+    .material-input-bar {
+      position: relative;
+      display: block;
+      width: 100%;
+      &:before {
+        @extend %base-bar-pseudo;
+        left: 50%;
+      }
+      &:after {
+        @extend %base-bar-pseudo;
+        right: 50%;
+      }
+    }
+    // Disabled state:
+    &.material--disabled {
+      .material-input {
+        border-bottom-style: dashed;
+      }
+    }
+    // Raised state:
+    &.material--raised {
+      .material-label {
+        @include slided-top();
+      }
+    }
+    // Active state:
+    &.material--active {
+      .material-input-bar {
+        &:before,
+        &:after {
+          width: 50%;
+        }
+      }
+    }
+  }
+
+  .material-input__component {
+    background: $color-white;
+    .material-input {
+      background: none;
+      color: $color-black;
+      text-indent: $index;
+      border-bottom: 1px solid $color-grey-light;
+    }
+    .material-label {
+      color: $color-grey;
+    }
+    .material-input-bar {
+      &:before,
+      &:after {
+        background: $color-blue;
+      }
+    }
+    // Active state:
+    &.material--active {
+      .material-label {
+        color: $color-blue;
+      }
+    }
+    // Errors:
+    &.material--has-errors {
+      &.material--active .material-label {
+        color: $color-red;
+      }
+      .material-input-bar {
+        &:before,
+        &:after {
+          background: transparent;
+        }
+      }
+    }
+  }
+</style>

+ 31 - 0
src/components/MarkdownEditor/default-options.js

@@ -0,0 +1,31 @@
+// doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
+export default {
+  minHeight: '200px',
+  previewStyle: 'vertical',
+  useCommandShortcut: true,
+  useDefaultHTMLSanitizer: true,
+  usageStatistics: false,
+  hideModeSwitch: false,
+  toolbarItems: [
+    'heading',
+    'bold',
+    'italic',
+    'strike',
+    'divider',
+    'hr',
+    'quote',
+    'divider',
+    'ul',
+    'ol',
+    'task',
+    'indent',
+    'outdent',
+    'divider',
+    'table',
+    'image',
+    'link',
+    'divider',
+    'code',
+    'codeblock'
+  ]
+}

+ 118 - 0
src/components/MarkdownEditor/index.vue

@@ -0,0 +1,118 @@
+<template>
+  <div :id="id" />
+</template>
+
+<script>
+// deps for editor
+import 'codemirror/lib/codemirror.css' // codemirror
+import 'tui-editor/dist/tui-editor.css' // editor ui
+import 'tui-editor/dist/tui-editor-contents.css' // editor content
+
+import Editor from 'tui-editor'
+import defaultOptions from './default-options'
+
+export default {
+  name: 'MarkdownEditor',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    },
+    id: {
+      type: String,
+      required: false,
+      default() {
+        return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    options: {
+      type: Object,
+      default() {
+        return defaultOptions
+      }
+    },
+    mode: {
+      type: String,
+      default: 'markdown'
+    },
+    height: {
+      type: String,
+      required: false,
+      default: '300px'
+    },
+    language: {
+      type: String,
+      required: false,
+      default: 'en_US' // https://github.com/nhnent/tui.editor/tree/master/src/js/langs
+    }
+  },
+  data() {
+    return {
+      editor: null
+    }
+  },
+  computed: {
+    editorOptions() {
+      const options = Object.assign({}, defaultOptions, this.options)
+      options.initialEditType = this.mode
+      options.height = this.height
+      options.language = this.language
+      return options
+    }
+  },
+  watch: {
+    value(newValue, preValue) {
+      if (newValue !== preValue && newValue !== this.editor.getValue()) {
+        this.editor.setValue(newValue)
+      }
+    },
+    language(val) {
+      this.destroyEditor()
+      this.initEditor()
+    },
+    height(newValue) {
+      this.editor.height(newValue)
+    },
+    mode(newValue) {
+      this.editor.changeMode(newValue)
+    }
+  },
+  mounted() {
+    this.initEditor()
+  },
+  destroyed() {
+    this.destroyEditor()
+  },
+  methods: {
+    initEditor() {
+      this.editor = new Editor({
+        el: document.getElementById(this.id),
+        ...this.editorOptions
+      })
+      if (this.value) {
+        this.editor.setValue(this.value)
+      }
+      this.editor.on('change', () => {
+        this.$emit('input', this.editor.getValue())
+      })
+    },
+    destroyEditor() {
+      if (!this.editor) return
+      this.editor.off('change')
+      this.editor.remove()
+    },
+    setValue(value) {
+      this.editor.setValue(value)
+    },
+    getValue() {
+      return this.editor.getValue()
+    },
+    setHtml(value) {
+      this.editor.setHtml(value)
+    },
+    getHtml() {
+      return this.editor.getHtml()
+    }
+  }
+}
+</script>

+ 101 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scroll-to'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 142 - 0
src/components/PanThumb/index.vue

@@ -0,0 +1,142 @@
+<template>
+  <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
+    <div class="pan-info">
+      <div class="pan-info-roles-container">
+        <slot />
+      </div>
+    </div>
+    <!-- eslint-disable-next-line -->
+    <div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PanThumb',
+  props: {
+    image: {
+      type: String,
+      required: true
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    width: {
+      type: String,
+      default: '150px'
+    },
+    height: {
+      type: String,
+      default: '150px'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pan-item {
+  width: 200px;
+  height: 200px;
+  border-radius: 50%;
+  display: inline-block;
+  position: relative;
+  cursor: default;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.pan-info-roles-container {
+  padding: 20px;
+  text-align: center;
+}
+
+.pan-thumb {
+  width: 100%;
+  height: 100%;
+  background-position: center center;
+  background-size: cover;
+  border-radius: 50%;
+  overflow: hidden;
+  position: absolute;
+  transform-origin: 95% 40%;
+  transition: all 0.3s ease-in-out;
+}
+
+/* .pan-thumb:after {
+  content: '';
+  width: 8px;
+  height: 8px;
+  position: absolute;
+  border-radius: 50%;
+  top: 40%;
+  left: 95%;
+  margin: -4px 0 0 -4px;
+  background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
+  box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
+} */
+
+.pan-info {
+  position: absolute;
+  width: inherit;
+  height: inherit;
+  border-radius: 50%;
+  overflow: hidden;
+  box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
+}
+
+.pan-info h3 {
+  color: #fff;
+  text-transform: uppercase;
+  position: relative;
+  letter-spacing: 2px;
+  font-size: 18px;
+  margin: 0 60px;
+  padding: 22px 0 0 0;
+  height: 85px;
+  font-family: 'Open Sans', Arial, sans-serif;
+  text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.pan-info p {
+  color: #fff;
+  padding: 10px 5px;
+  font-style: italic;
+  margin: 0 30px;
+  font-size: 12px;
+  border-top: 1px solid rgba(255, 255, 255, 0.5);
+}
+
+.pan-info p a {
+  display: block;
+  color: #333;
+  width: 80px;
+  height: 80px;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 50%;
+  color: #fff;
+  font-style: normal;
+  font-weight: 700;
+  text-transform: uppercase;
+  font-size: 9px;
+  letter-spacing: 1px;
+  padding-top: 24px;
+  margin: 7px auto 0;
+  font-family: 'Open Sans', Arial, sans-serif;
+  opacity: 0;
+  transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
+  transform: translateX(60px) rotate(90deg);
+}
+
+.pan-info p a:hover {
+  background: rgba(255, 255, 255, 0.5);
+}
+
+.pan-item:hover .pan-thumb {
+  transform: rotate(-110deg);
+}
+
+.pan-item:hover .pan-info p a {
+  opacity: 1;
+  transform: translateX(0px) rotate(0deg);
+}
+</style>

+ 145 - 0
src/components/RightPanel/index.vue

@@ -0,0 +1,145 @@
+<template>
+  <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
+    <div class="rightPanel-background" />
+    <div class="rightPanel">
+      <div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
+        <i :class="show?'el-icon-close':'el-icon-setting'" />
+      </div>
+      <div class="rightPanel-items">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { addClass, removeClass } from '@/utils'
+
+export default {
+  name: 'RightPanel',
+  props: {
+    clickNotClose: {
+      default: false,
+      type: Boolean
+    },
+    buttonTop: {
+      default: 250,
+      type: Number
+    }
+  },
+  data() {
+    return {
+      show: false
+    }
+  },
+  computed: {
+    theme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    show(value) {
+      if (value && !this.clickNotClose) {
+        this.addEventClick()
+      }
+      if (value) {
+        addClass(document.body, 'showRightPanel')
+      } else {
+        removeClass(document.body, 'showRightPanel')
+      }
+    }
+  },
+  mounted() {
+    this.insertToBody()
+  },
+  beforeDestroy() {
+    const elx = this.$refs.rightPanel
+    elx.remove()
+  },
+  methods: {
+    addEventClick() {
+      window.addEventListener('click', this.closeSidebar)
+    },
+    closeSidebar(evt) {
+      const parent = evt.target.closest('.rightPanel')
+      if (!parent) {
+        this.show = false
+        window.removeEventListener('click', this.closeSidebar)
+      }
+    },
+    insertToBody() {
+      const elx = this.$refs.rightPanel
+      const body = document.querySelector('body')
+      body.insertBefore(elx, body.firstChild)
+    }
+  }
+}
+</script>
+
+<style>
+.showRightPanel {
+  overflow: hidden;
+  position: relative;
+  width: calc(100% - 15px);
+}
+</style>
+
+<style lang="scss" scoped>
+.rightPanel-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  opacity: 0;
+  transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
+  background: rgba(0, 0, 0, .2);
+  z-index: -1;
+}
+
+.rightPanel {
+  width: 100%;
+  max-width: 260px;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  right: 0;
+  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
+  transition: all .25s cubic-bezier(.7, .3, .1, 1);
+  transform: translate(100%);
+  background: #fff;
+  z-index: 40000;
+}
+
+.show {
+  transition: all .3s cubic-bezier(.7, .3, .1, 1);
+
+  .rightPanel-background {
+    z-index: 20000;
+    opacity: 1;
+    width: 100%;
+    height: 100%;
+  }
+
+  .rightPanel {
+    transform: translate(0);
+  }
+}
+
+.handle-button {
+  width: 48px;
+  height: 48px;
+  position: absolute;
+  left: -48px;
+  text-align: center;
+  font-size: 24px;
+  border-radius: 6px 0 0 6px !important;
+  z-index: 0;
+  pointer-events: auto;
+  cursor: pointer;
+  color: #fff;
+  line-height: 48px;
+  i {
+    font-size: 24px;
+    line-height: 48px;
+  }
+}
+</style>

+ 60 - 0
src/components/Screenfull/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+  </div>
+</template>
+
+<script>
+import screenfull from 'screenfull'
+
+export default {
+  name: 'Screenfull',
+  data() {
+    return {
+      isFullscreen: false
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  beforeDestroy() {
+    this.destroy()
+  },
+  methods: {
+    click() {
+      if (!screenfull.enabled) {
+        this.$message({
+          message: 'you browser can not work',
+          type: 'warning'
+        })
+        return false
+      }
+      screenfull.toggle()
+    },
+    change() {
+      this.isFullscreen = screenfull.isFullscreen
+    },
+    init() {
+      if (screenfull.enabled) {
+        screenfull.on('change', this.change)
+      }
+    },
+    destroy() {
+      if (screenfull.enabled) {
+        screenfull.off('change', this.change)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.screenfull-svg {
+  display: inline-block;
+  cursor: pointer;
+  fill: #5a5e66;;
+  width: 20px;
+  height: 20px;
+  vertical-align: 10px;
+}
+</style>

+ 100 - 0
src/components/Share/DropdownMenu.vue

@@ -0,0 +1,100 @@
+<template>
+  <div :class="{active:isActive}" class="share-dropdown-menu">
+    <div class="share-dropdown-menu-wrapper">
+      <span class="share-dropdown-menu-title" @click.self="clickTitle">{{ title }}</span>
+      <div v-for="(item,index) of items" :key="index" class="share-dropdown-menu-item">
+        <a v-if="item.href" :href="item.href" target="_blank">{{ item.title }}</a>
+        <span v-else>{{ item.title }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    items: {
+      type: Array,
+      default: function() {
+        return []
+      }
+    },
+    title: {
+      type: String,
+      default: 'vue'
+    }
+  },
+  data() {
+    return {
+      isActive: false
+    }
+  },
+  methods: {
+    clickTitle() {
+      this.isActive = !this.isActive
+    }
+  }
+}
+</script>
+
+<style lang="scss" >
+$n: 9; //和items.length 相同
+$t: .1s;
+.share-dropdown-menu {
+  width: 250px;
+  position: relative;
+  z-index: 1;
+  &-title {
+    width: 100%;
+    display: block;
+    cursor: pointer;
+    background: black;
+    color: white;
+    height: 60px;
+    line-height: 60px;
+    font-size: 20px;
+    text-align: center;
+    z-index: 2;
+    transform: translate3d(0,0,0);
+  }
+  &-wrapper {
+    position: relative;
+  }
+  &-item {
+    text-align: center;
+    position: absolute;
+    width: 100%;
+    background: #e0e0e0;
+    line-height: 60px;
+    height: 60px;
+    cursor: pointer;
+    font-size: 20px;
+    opacity: 1;
+    transition: transform 0.28s ease;
+    &:hover {
+      background: black;
+      color: white;
+    }
+    @for $i from 1 through $n {
+      &:nth-of-type(#{$i}) {
+        z-index: -1;
+        transition-delay: $i*$t;
+        transform: translate3d(0, -60px, 0);
+      }
+    }
+  }
+  &.active {
+    .share-dropdown-menu-wrapper {
+      z-index: 1;
+    }
+    .share-dropdown-menu-item {
+      @for $i from 1 through $n {
+        &:nth-of-type(#{$i}) {
+          transition-delay: ($n - $i)*$t;
+          transform: translate3d(0, ($i - 1)*60px, 0);
+        }
+      }
+    }
+  }
+}
+</style>

+ 57 - 0
src/components/SizeSelect/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-dropdown trigger="click" @command="handleSetSize">
+    <div>
+      <svg-icon class-name="size-icon" icon-class="size" />
+    </div>
+    <el-dropdown-menu slot="dropdown">
+      <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
+        {{
+          item.label }}
+      </el-dropdown-item>
+    </el-dropdown-menu>
+  </el-dropdown>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      sizeOptions: [
+        { label: 'Default', value: 'default' },
+        { label: 'Medium', value: 'medium' },
+        { label: 'Small', value: 'small' },
+        { label: 'Mini', value: 'mini' }
+      ]
+    }
+  },
+  computed: {
+    size() {
+      return this.$store.getters.size
+    }
+  },
+  methods: {
+    handleSetSize(size) {
+      this.$ELEMENT.size = size
+      this.$store.dispatch('app/setSize', size)
+      this.refreshView()
+      this.$message({
+        message: 'Switch Size Success',
+        type: 'success'
+      })
+    },
+    refreshView() {
+      // In order to make the cached page re-rendered
+      this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
+
+      const { fullPath } = this.$route
+
+      this.$nextTick(() => {
+        this.$router.replace({
+          path: '/redirect' + fullPath
+        })
+      })
+    }
+  }
+
+}
+</script>

+ 91 - 0
src/components/Sticky/index.vue

@@ -0,0 +1,91 @@
+<template>
+  <div :style="{height:height+'px',zIndex:zIndex}">
+    <div
+      :class="className"
+      :style="{top:(isSticky ? stickyTop +'px' : ''),zIndex:zIndex,position:position,width:width,height:height+'px'}"
+    >
+      <slot>
+        <div>sticky</div>
+      </slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Sticky',
+  props: {
+    stickyTop: {
+      type: Number,
+      default: 0
+    },
+    zIndex: {
+      type: Number,
+      default: 1
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      active: false,
+      position: '',
+      width: undefined,
+      height: undefined,
+      isSticky: false
+    }
+  },
+  mounted() {
+    this.height = this.$el.getBoundingClientRect().height
+    window.addEventListener('scroll', this.handleScroll)
+    window.addEventListener('resize', this.handleResize)
+  },
+  activated() {
+    this.handleScroll()
+  },
+  destroyed() {
+    window.removeEventListener('scroll', this.handleScroll)
+    window.removeEventListener('resize', this.handleResize)
+  },
+  methods: {
+    sticky() {
+      if (this.active) {
+        return
+      }
+      this.position = 'fixed'
+      this.active = true
+      this.width = this.width + 'px'
+      this.isSticky = true
+    },
+    handleReset() {
+      if (!this.active) {
+        return
+      }
+      this.reset()
+    },
+    reset() {
+      this.position = ''
+      this.width = 'auto'
+      this.active = false
+      this.isSticky = false
+    },
+    handleScroll() {
+      const width = this.$el.getBoundingClientRect().width
+      this.width = width || 'auto'
+      const offsetTop = this.$el.getBoundingClientRect().top
+      if (offsetTop < this.stickyTop) {
+        this.sticky()
+        return
+      }
+      this.handleReset()
+    },
+    handleResize() {
+      if (this.isSticky) {
+        this.width = this.$el.getBoundingClientRect().width + 'px'
+      }
+    }
+  }
+}
+</script>

+ 62 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,62 @@
+<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" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 113 - 0
src/components/TextHoverEffect/Mallki.vue

@@ -0,0 +1,113 @@
+<template>
+  <a :class="className" class="link--mallki" href="#">
+    {{ text }}
+    <span :data-letters="text" />
+    <span :data-letters="text" />
+  </a>
+</template>
+
+<script>
+export default {
+  props: {
+    className: {
+      type: String,
+      default: ''
+    },
+    text: {
+      type: String,
+      default: 'vue-element-admin'
+    }
+  }
+}
+</script>
+
+<style>
+/* Mallki */
+
+.link--mallki {
+  font-weight: 800;
+  color: #4dd9d5;
+  font-family: 'Dosis', sans-serif;
+  -webkit-transition: color 0.5s 0.25s;
+  transition: color 0.5s 0.25s;
+  overflow: hidden;
+  position: relative;
+  display: inline-block;
+  line-height: 1;
+  outline: none;
+  text-decoration: none;
+}
+
+.link--mallki:hover {
+  -webkit-transition: none;
+  transition: none;
+  color: transparent;
+}
+
+.link--mallki::before {
+  content: '';
+  width: 100%;
+  height: 6px;
+  margin: -3px 0 0 0;
+  background: #3888fa;
+  position: absolute;
+  left: 0;
+  top: 50%;
+  -webkit-transform: translate3d(-100%, 0, 0);
+  transform: translate3d(-100%, 0, 0);
+  -webkit-transition: -webkit-transform 0.4s;
+  transition: transform 0.4s;
+  -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
+}
+
+.link--mallki:hover::before {
+  -webkit-transform: translate3d(100%, 0, 0);
+  transform: translate3d(100%, 0, 0);
+}
+
+.link--mallki span {
+  position: absolute;
+  height: 50%;
+  width: 100%;
+  left: 0;
+  top: 0;
+  overflow: hidden;
+}
+
+.link--mallki span::before {
+  content: attr(data-letters);
+  color: red;
+  position: absolute;
+  left: 0;
+  width: 100%;
+  color: #3888fa;
+  -webkit-transition: -webkit-transform 0.5s;
+  transition: transform 0.5s;
+}
+
+.link--mallki span:nth-child(2) {
+  top: 50%;
+}
+
+.link--mallki span:first-child::before {
+  top: 0;
+  -webkit-transform: translate3d(0, 100%, 0);
+  transform: translate3d(0, 100%, 0);
+}
+
+.link--mallki span:nth-child(2)::before {
+  bottom: 0;
+  -webkit-transform: translate3d(0, -100%, 0);
+  transform: translate3d(0, -100%, 0);
+}
+
+.link--mallki:hover span::before {
+  -webkit-transition-delay: 0.3s;
+  transition-delay: 0.3s;
+  -webkit-transform: translate3d(0, 0, 0);
+  transform: translate3d(0, 0, 0);
+  -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+  transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
+}
+</style>

+ 174 - 0
src/components/ThemePicker/index.vue

@@ -0,0 +1,174 @@
+<template>
+  <el-color-picker
+    v-model="theme"
+    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
+    class="theme-picker"
+    popper-class="theme-picker-dropdown"
+  />
+</template>
+
+<script>
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // default color
+
+export default {
+  data() {
+    return {
+      chalk: '', // content of theme-chalk css
+      theme: ''
+    }
+  },
+  computed: {
+    defaultTheme() {
+      return this.$store.state.settings.theme
+    }
+  },
+  watch: {
+    defaultTheme: {
+      handler: function(val, oldVal) {
+        this.theme = val
+      },
+      immediate: true
+    },
+    async theme(val) {
+      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
+      if (typeof val !== 'string') return
+      const themeCluster = this.getThemeCluster(val.replace('#', ''))
+      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
+
+      const $message = this.$message({
+        message: '  Compiling the theme',
+        customClass: 'theme-message',
+        type: 'success',
+        duration: 0,
+        iconClass: 'el-icon-loading'
+      })
+
+      const getHandler = (variable, id) => {
+        return () => {
+          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
+
+          let styleTag = document.getElementById(id)
+          if (!styleTag) {
+            styleTag = document.createElement('style')
+            styleTag.setAttribute('id', id)
+            document.head.appendChild(styleTag)
+          }
+          styleTag.innerText = newStyle
+        }
+      }
+
+      if (!this.chalk) {
+        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
+        await this.getCSSString(url, 'chalk')
+      }
+
+      const chalkHandler = getHandler('chalk', 'chalk-style')
+
+      chalkHandler()
+
+      const styles = [].slice.call(document.querySelectorAll('style'))
+        .filter(style => {
+          const text = style.innerText
+          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
+        })
+      styles.forEach(style => {
+        const { innerText } = style
+        if (typeof innerText !== 'string') return
+        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
+      })
+
+      this.$emit('change', val)
+
+      $message.close()
+    }
+  },
+
+  methods: {
+    updateStyle(style, oldCluster, newCluster) {
+      let newStyle = style
+      oldCluster.forEach((color, index) => {
+        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+      })
+      return newStyle
+    },
+
+    getCSSString(url, variable) {
+      return new Promise(resolve => {
+        const xhr = new XMLHttpRequest()
+        xhr.onreadystatechange = () => {
+          if (xhr.readyState === 4 && xhr.status === 200) {
+            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+            resolve()
+          }
+        }
+        xhr.open('GET', url)
+        xhr.send()
+      })
+    },
+
+    getThemeCluster(theme) {
+      const tintColor = (color, tint) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        if (tint === 0) { // when primary color is in its rgb space
+          return [red, green, blue].join(',')
+        } else {
+          red += Math.round(tint * (255 - red))
+          green += Math.round(tint * (255 - green))
+          blue += Math.round(tint * (255 - blue))
+
+          red = red.toString(16)
+          green = green.toString(16)
+          blue = blue.toString(16)
+
+          return `#${red}${green}${blue}`
+        }
+      }
+
+      const shadeColor = (color, shade) => {
+        let red = parseInt(color.slice(0, 2), 16)
+        let green = parseInt(color.slice(2, 4), 16)
+        let blue = parseInt(color.slice(4, 6), 16)
+
+        red = Math.round((1 - shade) * red)
+        green = Math.round((1 - shade) * green)
+        blue = Math.round((1 - shade) * blue)
+
+        red = red.toString(16)
+        green = green.toString(16)
+        blue = blue.toString(16)
+
+        return `#${red}${green}${blue}`
+      }
+
+      const clusters = [theme]
+      for (let i = 0; i <= 9; i++) {
+        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
+      }
+      clusters.push(shadeColor(theme, 0.1))
+      return clusters
+    }
+  }
+}
+</script>
+
+<style>
+.theme-message,
+.theme-picker-dropdown {
+  z-index: 99999 !important;
+}
+
+.theme-picker .el-color-picker__trigger {
+  height: 26px !important;
+  width: 26px !important;
+  padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+  display: none;
+}
+</style>

+ 111 - 0
src/components/Tinymce/components/EditorImage.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="upload-container">
+    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
+      upload
+    </el-button>
+    <el-dialog :visible.sync="dialogVisible">
+      <el-upload
+        :multiple="true"
+        :file-list="fileList"
+        :show-file-list="true"
+        :on-remove="handleRemove"
+        :on-success="handleSuccess"
+        :before-upload="beforeUpload"
+        class="editor-slide-upload"
+        action="https://httpbin.org/post"
+        list-type="picture-card"
+      >
+        <el-button size="small" type="primary">
+          Click upload
+        </el-button>
+      </el-upload>
+      <el-button @click="dialogVisible = false">
+        Cancel
+      </el-button>
+      <el-button type="primary" @click="handleSubmit">
+        Confirm
+      </el-button>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+// import { getToken } from 'api/qiniu'
+
+export default {
+  name: 'EditorSlideUpload',
+  props: {
+    color: {
+      type: String,
+      default: '#1890ff'
+    }
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      listObj: {},
+      fileList: []
+    }
+  },
+  methods: {
+    checkAllSuccess() {
+      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
+    },
+    handleSubmit() {
+      const arr = Object.keys(this.listObj).map(v => this.listObj[v])
+      if (!this.checkAllSuccess()) {
+        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
+        return
+      }
+      this.$emit('successCBK', arr)
+      this.listObj = {}
+      this.fileList = []
+      this.dialogVisible = false
+    },
+    handleSuccess(response, file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          this.listObj[objKeyArr[i]].url = response.files.file
+          this.listObj[objKeyArr[i]].hasSuccess = true
+          return
+        }
+      }
+    },
+    handleRemove(file) {
+      const uid = file.uid
+      const objKeyArr = Object.keys(this.listObj)
+      for (let i = 0, len = objKeyArr.length; i < len; i++) {
+        if (this.listObj[objKeyArr[i]].uid === uid) {
+          delete this.listObj[objKeyArr[i]]
+          return
+        }
+      }
+    },
+    beforeUpload(file) {
+      const _self = this
+      const _URL = window.URL || window.webkitURL
+      const fileName = file.uid
+      this.listObj[fileName] = {}
+      return new Promise((resolve, reject) => {
+        const img = new Image()
+        img.src = _URL.createObjectURL(file)
+        img.onload = function() {
+          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
+        }
+        resolve(true)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.editor-slide-upload {
+  margin-bottom: 20px;
+  /deep/ .el-upload--picture-card {
+    width: 100%;
+  }
+}
+</style>

+ 59 - 0
src/components/Tinymce/dynamicLoadScript.js

@@ -0,0 +1,59 @@
+let callbacks = []
+
+function loadedTinymce() {
+  // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
+  // check is successfully downloaded script
+  return window.tinymce
+}
+
+const dynamicLoadScript = (src, callback) => {
+  const existingScript = document.getElementById(src)
+  const cb = callback || function() {}
+
+  if (!existingScript) {
+    const script = document.createElement('script')
+    script.src = src // src url for the third-party library being loaded.
+    script.id = src
+    document.body.appendChild(script)
+    callbacks.push(cb)
+    const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
+    onEnd(script)
+  }
+
+  if (existingScript && cb) {
+    if (loadedTinymce()) {
+      cb(null, existingScript)
+    } else {
+      callbacks.push(cb)
+    }
+  }
+
+  function stdOnEnd(script) {
+    script.onload = function() {
+      // this.onload = null here is necessary
+      // because even IE9 works not like others
+      this.onerror = this.onload = null
+      for (const cb of callbacks) {
+        cb(null, script)
+      }
+      callbacks = null
+    }
+    script.onerror = function() {
+      this.onerror = this.onload = null
+      cb(new Error('Failed to load ' + src), script)
+    }
+  }
+
+  function ieOnEnd(script) {
+    script.onreadystatechange = function() {
+      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
+      this.onreadystatechange = null
+      for (const cb of callbacks) {
+        cb(null, script) // there is no way to catch loading errors in IE8
+      }
+      callbacks = null
+    }
+  }
+}
+
+export default dynamicLoadScript

+ 237 - 0
src/components/Tinymce/index.vue

@@ -0,0 +1,237 @@
+<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>
+</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'
+
+// 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'
+
+export default {
+  name: 'Tinymce',
+  components: { editorImage },
+  props: {
+    id: {
+      type: String,
+      default: function() {
+        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
+      }
+    },
+    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 || ''))
+      }
+    }
+  },
+  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({
+        selector: `#${this.tinymceId}`,
+        language: this.languageTypeList['en'],
+        height: this.height,
+        body_class: 'panel-body ',
+        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
+          })
+        }
+        // 整合七牛上传
+        // 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);
+        //   });
+        // },
+      })
+    },
+    destroyTinymce() {
+      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>

+ 7 - 0
src/components/Tinymce/plugins.js

@@ -0,0 +1,7 @@
+// Any plugins you want to use has to be imported
+// Detail plugins list see https://www.tinymce.com/docs/plugins/
+// Custom builds see https://www.tinymce.com/download/custom-builds/
+
+const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
+
+export default plugins

+ 6 - 0
src/components/Tinymce/toolbar.js

@@ -0,0 +1,6 @@
+// Here is a list of the toolbar
+// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
+
+const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
+
+export default toolbar

+ 134 - 0
src/components/Upload/SingleImage.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        将文件拖到此处,或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl+'?imageView2/1/w/200/h/200'">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess() {
+      this.emitInput(this.tempUrl)
+    },
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          this.tempUrl = response.data.qiniu_url
+          resolve(true)
+        }).catch(err => {
+          console.log(err)
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+    @import "~@/styles/mixin.scss";
+    .upload-container {
+        width: 100%;
+        position: relative;
+        @include clearfix;
+        .image-uploader {
+            width: 60%;
+            float: left;
+        }
+        .image-preview {
+            width: 200px;
+            height: 200px;
+            position: relative;
+            border: 1px dashed #d9d9d9;
+            float: left;
+            margin-left: 50px;
+            .image-preview-wrapper {
+                position: relative;
+                width: 100%;
+                height: 100%;
+                img {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+            .image-preview-action {
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                left: 0;
+                top: 0;
+                cursor: default;
+                text-align: center;
+                color: #fff;
+                opacity: 0;
+                font-size: 20px;
+                background-color: rgba(0, 0, 0, .5);
+                transition: opacity .3s;
+                cursor: pointer;
+                text-align: center;
+                line-height: 200px;
+                .el-icon-delete {
+                    font-size: 36px;
+                }
+            }
+            &:hover {
+                .image-preview-action {
+                    opacity: 1;
+                }
+            }
+        }
+    }
+
+</style>

+ 130 - 0
src/components/Upload/SingleImage2.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="singleImageUpload2 upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        Drag或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div v-show="imageUrl.length>0" class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload2',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess() {
+      this.emitInput(this.tempUrl)
+    },
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          this.tempUrl = response.data.qiniu_url
+          resolve(true)
+        }).catch(() => {
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.upload-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .image-uploader {
+    height: 100%;
+  }
+  .image-preview {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    border: 1px dashed #d9d9d9;
+    .image-preview-wrapper {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .image-preview-action {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      cursor: default;
+      text-align: center;
+      color: #fff;
+      opacity: 0;
+      font-size: 20px;
+      background-color: rgba(0, 0, 0, .5);
+      transition: opacity .3s;
+      cursor: pointer;
+      text-align: center;
+      line-height: 200px;
+      .el-icon-delete {
+        font-size: 36px;
+      }
+    }
+    &:hover {
+      .image-preview-action {
+        opacity: 1;
+      }
+    }
+  }
+}
+</style>

+ 157 - 0
src/components/Upload/SingleImage3.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="upload-container">
+    <el-upload
+      :data="dataObj"
+      :multiple="false"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      class="image-uploader"
+      drag
+      action="https://httpbin.org/post"
+    >
+      <i class="el-icon-upload" />
+      <div class="el-upload__text">
+        将文件拖到此处,或<em>点击上传</em>
+      </div>
+    </el-upload>
+    <div class="image-preview image-app-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+    <div class="image-preview">
+      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
+        <img :src="imageUrl">
+        <div class="image-preview-action">
+          <i class="el-icon-delete" @click="rmImage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getToken } from '@/api/qiniu'
+
+export default {
+  name: 'SingleImageUpload3',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      tempUrl: '',
+      dataObj: { token: '', key: '' }
+    }
+  },
+  computed: {
+    imageUrl() {
+      return this.value
+    }
+  },
+  methods: {
+    rmImage() {
+      this.emitInput('')
+    },
+    emitInput(val) {
+      this.$emit('input', val)
+    },
+    handleImageSuccess(file) {
+      this.emitInput(file.files.file)
+    },
+    beforeUpload() {
+      const _self = this
+      return new Promise((resolve, reject) => {
+        getToken().then(response => {
+          const key = response.data.qiniu_key
+          const token = response.data.qiniu_token
+          _self._data.dataObj.token = token
+          _self._data.dataObj.key = key
+          this.tempUrl = response.data.qiniu_url
+          resolve(true)
+        }).catch(err => {
+          console.log(err)
+          reject(false)
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/styles/mixin.scss";
+.upload-container {
+  width: 100%;
+  position: relative;
+  @include clearfix;
+  .image-uploader {
+    width: 35%;
+    float: left;
+  }
+  .image-preview {
+    width: 200px;
+    height: 200px;
+    position: relative;
+    border: 1px dashed #d9d9d9;
+    float: left;
+    margin-left: 50px;
+    .image-preview-wrapper {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .image-preview-action {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      cursor: default;
+      text-align: center;
+      color: #fff;
+      opacity: 0;
+      font-size: 20px;
+      background-color: rgba(0, 0, 0, .5);
+      transition: opacity .3s;
+      cursor: pointer;
+      text-align: center;
+      line-height: 200px;
+      .el-icon-delete {
+        font-size: 36px;
+      }
+    }
+    &:hover {
+      .image-preview-action {
+        opacity: 1;
+      }
+    }
+  }
+  .image-app-preview {
+    width: 320px;
+    height: 180px;
+    position: relative;
+    border: 1px dashed #d9d9d9;
+    float: left;
+    margin-left: 50px;
+    .app-fake-conver {
+      height: 44px;
+      position: absolute;
+      width: 100%; // background: rgba(0, 0, 0, .1);
+      text-align: center;
+      line-height: 64px;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 138 - 0
src/components/UploadExcel/index.vue

@@ -0,0 +1,138 @@
+<template>
+  <div>
+    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
+    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
+      Drop excel file here or
+      <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
+        Browse
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import XLSX from 'xlsx'
+
+export default {
+  props: {
+    beforeUpload: Function, // eslint-disable-line
+    onSuccess: Function// eslint-disable-line
+  },
+  data() {
+    return {
+      loading: false,
+      excelData: {
+        header: null,
+        results: null
+      }
+    }
+  },
+  methods: {
+    generateData({ header, results }) {
+      this.excelData.header = header
+      this.excelData.results = results
+      this.onSuccess && this.onSuccess(this.excelData)
+    },
+    handleDrop(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      if (this.loading) return
+      const files = e.dataTransfer.files
+      if (files.length !== 1) {
+        this.$message.error('Only support uploading one file!')
+        return
+      }
+      const rawFile = files[0] // only use files[0]
+
+      if (!this.isExcel(rawFile)) {
+        this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
+        return false
+      }
+      this.upload(rawFile)
+      e.stopPropagation()
+      e.preventDefault()
+    },
+    handleDragover(e) {
+      e.stopPropagation()
+      e.preventDefault()
+      e.dataTransfer.dropEffect = 'copy'
+    },
+    handleUpload() {
+      this.$refs['excel-upload-input'].click()
+    },
+    handleClick(e) {
+      const files = e.target.files
+      const rawFile = files[0] // only use files[0]
+      if (!rawFile) return
+      this.upload(rawFile)
+    },
+    upload(rawFile) {
+      this.$refs['excel-upload-input'].value = null // fix can't select the same excel
+
+      if (!this.beforeUpload) {
+        this.readerData(rawFile)
+        return
+      }
+      const before = this.beforeUpload(rawFile)
+      if (before) {
+        this.readerData(rawFile)
+      }
+    },
+    readerData(rawFile) {
+      this.loading = true
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = e => {
+          const data = e.target.result
+          const workbook = XLSX.read(data, { type: 'array' })
+          const firstSheetName = workbook.SheetNames[0]
+          const worksheet = workbook.Sheets[firstSheetName]
+          const header = this.getHeaderRow(worksheet)
+          const results = XLSX.utils.sheet_to_json(worksheet)
+          this.generateData({ header, results })
+          this.loading = false
+          resolve()
+        }
+        reader.readAsArrayBuffer(rawFile)
+      })
+    },
+    getHeaderRow(sheet) {
+      const headers = []
+      const range = XLSX.utils.decode_range(sheet['!ref'])
+      let C
+      const R = range.s.r
+      /* start in the first row */
+      for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
+        const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
+        /* find the cell in the first row */
+        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
+        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
+        headers.push(hdr)
+      }
+      return headers
+    },
+    isExcel(file) {
+      return /\.(xlsx|xls|csv)$/.test(file.name)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.excel-upload-input{
+  display: none;
+  z-index: -9999;
+}
+.drop{
+  border: 2px dashed #bbb;
+  width: 600px;
+  height: 160px;
+  line-height: 160px;
+  margin: 0 auto;
+  font-size: 24px;
+  border-radius: 5px;
+  text-align: center;
+  color: #bbb;
+  position: relative;
+}
+</style>

+ 134 - 0
src/components/VueFormMaking/App.vue

@@ -0,0 +1,134 @@
+<template>
+  <div id="app">
+    <div class="fm-header">
+      <img class="fm-logo" src="./assets/logo.png">
+      <div class="fm-title" @click="handleHome">{{ $t('header.title') }}</div>
+
+      <iframe style="vertical-align: middle;margin-top:10px;margin-left: 10px;" src="https://ghbtns.com/github-btn.html?user=GavinZhulei&repo=vue-form-making&type=star&count=true" frameborder="0" scrolling="0" width="160px" height="30px" />
+
+      <div class="fm-link">
+        <a target="_blank" href="http://form.xiaoyaoji.cn/pricing">{{ $t('header.pricing') }}</a>
+        <a target="_blank" href="http://docs.form.xiaoyaoji.cn">{{ $t('header.document') }}</a>
+        <a v-if="$lang == 'zh-CN'" target="_blank" href="http://docs.form.xiaoyaoji.cn/zh/other/course.html">学习课程</a>
+        <a target="_blank" href="https://github.com/GavinZhuLei/vue-form-making">GitHub</a>
+
+        <div class="action-item">
+          <el-dropdown trigger="click" @command="handleLangCommand">
+            <span class="el-dropdown-link">
+              {{ $route.params.lang == 'zh-CN' ? '简体中文' : 'English' }}<i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item command="zh-CN">简体中文</el-dropdown-item>
+              <el-dropdown-item command="en-US">English</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </div>
+
+        <a class="ad" href="http://form.xiaoyaoji.cn" target="_blank">{{ $t('header.advanced') }}</a>
+        <a v-if="$lang == 'zh-CN'" class="ad" href="http://www.xiaoyaoji.cn" target="_blank">小幺鸡接口工具</a>
+      </div>
+    </div>
+    <div class="fm-container"><router-view /></div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'App',
+  methods: {
+    handleHome() {
+      this.$router.push({ path: '/' })
+    },
+
+    handleLangCommand(command) {
+      this.$router.replace({ name: this.$route.name, params: { lang: command }})
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.fm-header{
+  height: 50px;
+  box-shadow: 0 2px 10px rgba(70,160,252, 0.6);
+  padding: 0 10px;
+  background-image: linear-gradient(to right,#1278f6,#00b4aa);
+  position: relative;
+
+  .fm-logo{
+    height: 26px;
+    vertical-align: middle;
+  }
+  .fm-title{
+    display: inline-block;
+    line-height: 50px;
+    vertical-align: middle;
+    color: #fff;
+    font-size: 20px;
+    font-weight: 600;
+    opacity: 0.8;
+    margin-left: 6px;
+    cursor: pointer;
+  }
+  .fm-link{
+    height: 50px;
+    float: right;
+
+    a{
+      color: #fff;
+      text-decoration: none;
+      font-size: 14px;
+      line-height: 50px;
+      font-weight: 500;
+      margin-left: 15px;
+
+      &:hover{
+        opacity: 0.8;
+      }
+
+      &.ad{
+        color: #f5dab1;
+      }
+    }
+
+    .action-item{
+      display: inline-block;
+      margin-left: 15px;
+      .el-dropdown-link{
+        cursor: pointer;
+        color: #fff;
+
+        &:hover{
+          opacity: 0.8;
+        }
+      }
+
+      &.action-item-user{
+        .el-dropdown-link{
+          color: #f5dab1;
+        }
+      }
+    }
+  }
+}
+.fm-container{
+  height: calc(100% - 50px);
+}
+*, :after, :before {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+html,body{
+  height: 100%;
+}
+#app {
+  font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  color: #2c3e50;
+  min-height: 100%;
+  height: 100%;
+}
+</style>

BIN
src/components/VueFormMaking/assets/logo.png


+ 445 - 0
src/components/VueFormMaking/components/Container.vue

@@ -0,0 +1,445 @@
+<template>
+  <el-container class="fm2-container">
+    <el-main class="fm2-main">
+      <el-container>
+        <el-aside width="250px">
+          <div class="components-list">
+            <template v-if="basicFields.length">
+              <div class="widget-cate">{{ $t('fm.components.basic.title') }}</div>
+              <draggable
+                tag="ul"
+                :list="basicComponents"
+                v-bind="{group:{ name:'people', pull:'clone',put:false},sort:false, ghostClass: 'ghost'}"
+                :move="handleMove"
+                @end="handleMoveEnd"
+                @start="handleMoveStart"
+              >
+                <template v-for="(item, index) in basicComponents">
+                  <li v-if="basicFields.indexOf(item.type)>=0" :key="index" class="form-edit-widget-label" :class="{'no-put': item.type == 'divider'}">
+                    <a>
+                      <i class="icon iconfont" :class="item.icon" />
+                      <span>{{ item.name }}</span>
+                    </a>
+                  </li>
+                </template>
+
+              </draggable>
+            </template>
+
+            <template v-if="advanceFields.length">
+              <div class="widget-cate">{{ $t('fm.components.advance.title') }}</div>
+              <draggable
+                tag="ul"
+                :list="advanceComponents"
+                v-bind="{group:{ name:'people', pull:'clone',put:false},sort:false, ghostClass: 'ghost'}"
+                :move="handleMove"
+                @end="handleMoveEnd"
+                @start="handleMoveStart"
+              >
+                <template v-for="(item, index) in advanceComponents">
+                  <li v-if="advanceFields.indexOf(item.type) >= 0" :key="index" class="form-edit-widget-label" :class="{'no-put': item.type == 'table'}">
+                    <a>
+                      <i class="icon iconfont" :class="item.icon" />
+                      <span>{{ item.name }}</span>
+                    </a>
+                  </li>
+                </template>
+
+              </draggable>
+            </template>
+
+            <template v-if="layoutFields.length">
+              <div class="widget-cate">{{ $t('fm.components.layout.title') }}</div>
+              <draggable
+                tag="ul"
+                :list="layoutComponents"
+                v-bind="{group:{ name:'people', pull:'clone',put:false},sort:false, ghostClass: 'ghost'}"
+                :move="handleMove"
+                @end="handleMoveEnd"
+                @start="handleMoveStart"
+              >
+                <template v-for="(item, index) in layoutComponents">
+                  <li v-if="layoutFields.indexOf(item.type) >=0" :key="index" class="form-edit-widget-label no-put">
+                    <a>
+                      <i class="icon iconfont" :class="item.icon" />
+                      <span>{{ item.name }}</span>
+                    </a>
+                  </li>
+                </template>
+
+              </draggable>
+            </template>
+
+          </div>
+
+        </el-aside>
+        <el-container class="center-container" direction="vertical">
+          <el-header class="btn-bar" style="height: 45px;">
+            <slot name="action" />
+            <el-button v-if="upload" type="text" size="medium" icon="el-icon-upload2" @click="handleUpload">{{ $t('fm.actions.import') }}</el-button>
+            <el-button v-if="clearable" type="text" size="medium" icon="el-icon-delete" @click="handleClear">{{ $t('fm.actions.clear') }}</el-button>
+            <el-button v-if="preview" type="text" size="medium" icon="el-icon-view" @click="handlePreview">{{ $t('fm.actions.preview') }}</el-button>
+            <el-button v-if="generateJson" type="text" size="medium" icon="el-icon-tickets" @click="handleGenerateJson">{{ $t('fm.actions.json') }}</el-button>
+            <el-button v-if="generateCode" type="text" size="medium" icon="el-icon-document" @click="handleGenerateCode">{{ $t('fm.actions.code') }}</el-button>
+          </el-header>
+          <el-main :class="{'widget-empty': widgetForm.list.length == 0}">
+
+            <widget-form v-if="!resetJson" ref="widgetForm" :data="widgetForm" :select.sync="widgetFormSelect" />
+          </el-main>
+        </el-container>
+
+        <el-aside class="widget-config-container" style="width: 305px;">
+          <el-container>
+            <el-header height="45px">
+              <div class="config-tab" :class="{active: configTab=='widget'}" @click="handleConfigSelect('widget')">{{ $t('fm.config.widget.title') }}</div>
+              <div class="config-tab" :class="{active: configTab=='form'}" @click="handleConfigSelect('form')">{{ $t('fm.config.form.title') }}</div>
+            </el-header>
+            <el-main class="config-content">
+              <widget-config v-if="widgetFormSelect!==null" v-show="configTab=='widget'" :data="widgetFormSelect" />
+              <form-config v-show="configTab=='form'" :data="widgetForm.config" />
+            </el-main>
+          </el-container>
+
+        </el-aside>
+
+        <cus-dialog
+          ref="widgetPreview"
+          :visible="previewVisible"
+          width="1000px"
+          form
+          @on-close="previewVisible = false"
+        >
+          <generate-form
+            v-if="previewVisible"
+            ref="generateForm"
+            insite="true"
+            :data="widgetForm"
+            :value="widgetModels"
+            :remote="remoteFuncs"
+            @on-change="handleDataChange"
+          >
+
+            <template slot-scope="scope">
+              Width <el-input v-model="scope.model.blank.width" style="width: 100px" />
+              Height <el-input v-model="scope.model.blank.height" style="width: 100px" />
+            </template>
+          </generate-form>
+
+          <template slot="action">
+            <el-button type="primary" @click="handleTest">{{ $t('fm.actions.getData') }}</el-button>
+            <el-button @click="handleReset">{{ $t('fm.actions.reset') }}</el-button>
+          </template>
+        </cus-dialog>
+
+        <cus-dialog
+          ref="uploadJson"
+          :visible="uploadVisible"
+          width="800px"
+          form
+          @on-close="uploadVisible = false"
+          @on-submit="handleUploadJson"
+        >
+          <el-alert type="info" :title="$t('fm.description.uploadJsonInfo')" />
+          <div id="uploadeditor" style="height: 400px;width: 100%;">{{ jsonEg }}</div>
+        </cus-dialog>
+
+        <cus-dialog
+          ref="jsonPreview"
+          :visible="jsonVisible"
+          width="800px"
+          form
+          @on-close="jsonVisible = false"
+        >
+
+          <div id="jsoneditor" style="height: 400px;width: 100%;">{{ jsonTemplate }}</div>
+
+          <template slot="action">
+            <el-button type="primary" class="json-btn" :data-clipboard-text="jsonCopyValue">{{ $t('fm.actions.copyData') }}</el-button>
+          </template>
+        </cus-dialog>
+
+        <cus-dialog
+          ref="codePreview"
+          :visible="codeVisible"
+          width="800px"
+          form
+          :action="false"
+          @on-close="codeVisible = false"
+        >
+          <div id="codeeditor" style="height: 500px; width: 100%;">{{ htmlTemplate }}</div>
+        </cus-dialog>
+      </el-container>
+    </el-main>
+    <el-footer height="30px" style="font-weight: 600;">Powered by <a target="_blank" href="http://www.fdevops.com">fdevops</a></el-footer>
+  </el-container>
+
+</template>
+
+<script>
+import Draggable from 'vuedraggable'
+import WidgetConfig from './WidgetConfig'
+import FormConfig from './FormConfig'
+import WidgetForm from './WidgetForm'
+import CusDialog from './CusDialog'
+import GenerateForm from './GenerateForm'
+import Clipboard from 'clipboard'
+import { basicComponents, layoutComponents, advanceComponents } from './componentsConfig.js'
+import request from '../util/request.js'
+import generateCode from './generateCode.js'
+
+var ace = require('ace-builds/src-noconflict/ace')
+ace.config.set('basePath', '/lib/ace')
+ace.config.set('modePath', '/lib/ace')
+ace.config.set('themePath', '/lib/ace')
+window.define = window.define || ace.define
+window.require = window.require || ace.require
+
+export default {
+  name: 'FmMakingForm',
+  components: {
+    Draggable,
+    WidgetConfig,
+    FormConfig,
+    WidgetForm,
+    CusDialog,
+    GenerateForm
+  },
+  props: {
+    preview: {
+      type: Boolean,
+      default: false
+    },
+    generateCode: {
+      type: Boolean,
+      default: false
+    },
+    generateJson: {
+      type: Boolean,
+      default: false
+    },
+    upload: {
+      type: Boolean,
+      default: false
+    },
+    clearable: {
+      type: Boolean,
+      default: false
+    },
+    basicFields: {
+      type: Array,
+      default: () => ['input', 'textarea', 'number', 'radio', 'checkbox', 'time', 'date', 'rate', 'color', 'select', 'switch', 'slider', 'text']
+    },
+    advanceFields: {
+      type: Array,
+      default: () => ['blank', 'imgupload', 'editor', 'cascader']
+    },
+    layoutFields: {
+      type: Array,
+      default: () => ['grid', 'divider']
+    }
+  },
+  data() {
+    return {
+      basicComponents,
+      layoutComponents,
+      advanceComponents,
+      resetJson: false,
+      widgetForm: {
+        list: [],
+        config: {
+          labelWidth: 100,
+          labelPosition: 'right',
+          size: 'small'
+        }
+      },
+      configTab: 'widget',
+      widgetFormSelect: null,
+      previewVisible: false,
+      jsonVisible: false,
+      codeVisible: false,
+      uploadVisible: false,
+      remoteFuncs: {
+        func_test(resolve) {
+          setTimeout(() => {
+            const options = [
+              { id: '1', name: '1111' },
+              { id: '2', name: '2222' },
+              { id: '3', name: '3333' }
+            ]
+
+            resolve(options)
+          }, 2000)
+        },
+        funcGetToken(resolve) {
+          request.get('http://tools-server.xiaoyaoji.cn/api/uptoken').then(res => {
+            resolve(res.uptoken)
+          })
+        },
+        upload_callback(response, file, fileList) {
+        }
+      },
+      widgetModels: {},
+      blank: '',
+      htmlTemplate: '',
+      jsonTemplate: '',
+      uploadEditor: null,
+      jsonCopyValue: '',
+      jsonClipboard: null,
+      jsonEg: `{
+  "list": [],
+  "config": {
+    "labelWidth": 100,
+    "labelPosition": "top",
+    "size": "small"
+  }
+}`
+    }
+  },
+  watch: {
+    widgetForm: {
+      deep: true,
+      handler: function(val) {
+      }
+    },
+    '$lang': function(val) {
+      this._loadComponents()
+    }
+  },
+  mounted() {
+    this._loadComponents()
+  },
+  methods: {
+    _loadComponents() {
+      this.basicComponents = this.basicComponents.map(item => {
+        return {
+          ...item,
+          name: this.$t(`fm.components.fields.${item.type}`)
+        }
+      })
+      this.advanceComponents = this.advanceComponents.map(item => {
+        return {
+          ...item,
+          name: this.$t(`fm.components.fields.${item.type}`)
+        }
+      })
+      this.layoutComponents = this.layoutComponents.map(item => {
+        return {
+          ...item,
+          name: this.$t(`fm.components.fields.${item.type}`)
+        }
+      })
+    },
+    handleConfigSelect(value) {
+      this.configTab = value
+    },
+    handleMoveEnd(evt) {
+    },
+    handleMoveStart({ oldIndex }) {
+    },
+    handleMove() {
+      return true
+    },
+    handlePreview() {
+      this.previewVisible = true
+    },
+    handleTest() {
+      this.$refs.generateForm.getData().then(data => {
+        this.$alert(data, '').catch(e => {})
+        this.$refs.widgetPreview.end()
+      }).catch(e => {
+        this.$refs.widgetPreview.end()
+      })
+    },
+    handleReset() {
+      this.$refs.generateForm.reset()
+    },
+    handleGenerateJson() {
+      this.jsonVisible = true
+      this.jsonTemplate = this.widgetForm
+      this.$nextTick(() => {
+        const editor = ace.edit('jsoneditor')
+        editor.session.setMode('ace/mode/json')
+
+        if (!this.jsonClipboard) {
+          this.jsonClipboard = new Clipboard('.json-btn')
+          this.jsonClipboard.on('success', (e) => {
+            this.$message.success(this.$t('fm.message.copySuccess'))
+          })
+        }
+        this.jsonCopyValue = JSON.stringify(this.widgetForm)
+      })
+    },
+    handleGenerateCode() {
+      this.codeVisible = true
+      this.htmlTemplate = generateCode(JSON.stringify(this.widgetForm))
+      this.$nextTick(() => {
+        const editor = ace.edit('codeeditor')
+        editor.session.setMode('ace/mode/html')
+      })
+    },
+    handleUpload() {
+      this.uploadVisible = true
+      this.$nextTick(() => {
+        this.uploadEditor = ace.edit('uploadeditor')
+        this.uploadEditor.session.setMode('ace/mode/json')
+      })
+    },
+    handleUploadJson() {
+      try {
+        this.setJSON(JSON.parse(this.uploadEditor.getValue()))
+        this.uploadVisible = false
+      } catch (e) {
+        this.$message.error(e.message)
+        this.$refs.uploadJson.end()
+      }
+    },
+    handleClear() {
+      this.widgetForm = {
+        list: [],
+        config: {
+          labelWidth: 100,
+          labelPosition: 'right',
+          size: 'small',
+          customClass: ''
+        }
+      }
+
+      this.widgetFormSelect = {}
+    },
+    getJSON() {
+      return this.widgetForm
+    },
+    getHtml() {
+      return generateCode(JSON.stringify(this.widgetForm))
+    },
+    setJSON(json) {
+      this.widgetForm = json
+
+      if (json.list.length > 0) {
+        this.widgetFormSelect = json.list[0]
+      }
+    },
+    handleInput(val) {
+      this.blank = val
+    },
+    handleDataChange(field, value, data) {
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.widget-empty{
+  background-position: 50%;
+}
+
+aside {
+  background: #ffffff;
+  padding: 0;
+  margin-bottom: 0;
+
+  a {
+    color: #333333;
+  }
+}
+</style>

+ 139 - 0
src/components/VueFormMaking/components/CusDialog.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-dialog
+    :id="id"
+    ref="elDialog"
+    class="cus-dialog-container"
+    :title="title"
+    :visible.sync="dialogVisible"
+    :close-on-click-modal="false"
+    append-to-body
+    center
+    :width="width"
+  >
+    <span v-if="show">
+      <slot />
+    </span>
+
+    <span
+      v-if="action"
+      slot="footer"
+      v-loading="loading"
+      class="dialog-footer"
+      :element-loading-text="loadingText"
+    >
+      <slot name="action">
+        <el-button @click="close">{{ $t('fm.actions.cancel') }}</el-button>
+        <el-button type="primary" @click="submit">{{ $t('fm.actions.confirm') }}</el-button>
+      </slot>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  props: {
+    visible: Boolean,
+    loadingText: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    width: {
+      type: String,
+      default: '600px'
+    },
+    form: {
+      type: Boolean,
+      default: true
+    },
+    action: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      dialogVisible: this.visible,
+      id: 'dialog_' + new Date().getTime(),
+      showForm: false
+    }
+  },
+  computed: {
+    show() {
+      if (this.form) {
+        return this.showForm
+      } else {
+        return true
+      }
+    }
+  },
+  watch: {
+    dialogVisible(val) {
+      if (!val) {
+        this.loading = false
+        this.$emit('on-close')
+        setTimeout(() => {
+          this.showForm = false
+        }, 300)
+      } else {
+        this.showForm = true
+      }
+    },
+    visible(val) {
+      this.dialogVisible = val
+    }
+  },
+  mounted() {
+  },
+  methods: {
+    close() {
+      this.dialogVisible = false
+    },
+    submit() {
+      this.loading = true
+
+      this.$emit('on-submit')
+    },
+    end() {
+      this.loading = false
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.cus-dialog-container{
+  .el-dialog__footer{
+    margin: 0 20px;
+    // border-top: 1px dashed #ccc;
+    padding: 15px 0 16px;
+    text-align: center;
+    position: relative;
+
+    .dialog-footer{
+      display: block;
+
+      .circular{
+        display: inline-block;
+        vertical-align: middle;
+        margin-right: 5px;
+        width: 24px;
+        height: 24px;
+      }
+
+      .el-loading-text{
+        display: inline-block;
+        vertical-align: middle;
+      }
+
+      .el-loading-spinner{
+        margin-top: -12px;
+      }
+    }
+  }
+}
+</style>

+ 32 - 0
src/components/VueFormMaking/components/FormConfig.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="form-config-container">
+    <el-form label-position="top">
+      <el-form-item :label="$t('fm.config.form.labelPosition.title')">
+        <el-radio-group v-model="data.labelPosition">
+          <el-radio-button label="left">{{ $t('fm.config.form.labelPosition.left') }}</el-radio-button>
+          <el-radio-button label="right">{{ $t('fm.config.form.labelPosition.right') }}</el-radio-button>
+          <el-radio-button label="top">{{ $t('fm.config.form.labelPosition.top') }}</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item :label="$t('fm.config.form.labelWidth')">
+        <el-input-number v-model="data.labelWidth" :min="0" :max="200" :step="10" />
+      </el-form-item>
+
+      <el-form-item :label="$t('fm.config.form.size')">
+        <el-radio-group v-model="data.size">
+          <el-radio-button label="medium">medium</el-radio-button>
+          <el-radio-button label="small">small</el-radio-button>
+          <el-radio-button label="mini">mini</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+export default {
+  /* eslint-disable */
+    props: ['data']
+  }
+</script>

+ 248 - 0
src/components/VueFormMaking/components/GenerateForm.vue

@@ -0,0 +1,248 @@
+<template>
+  <div>
+    <el-form
+      ref="generateForm"
+      label-suffix=":"
+      :size="data.config.size"
+      :model="models"
+      :rules="rules"
+      :label-position="data.config.labelPosition"
+      :label-width="data.config.labelWidth + 'px'"
+    >
+      <template v-for="item in data.list">
+
+        <template v-if="item.type == 'grid'">
+          <el-row
+            :key="item.key"
+            type="flex"
+            :gutter="item.options.gutter ? item.options.gutter : 0"
+            :justify="item.options.justify"
+            :align="item.options.align"
+          >
+            <el-col v-for="(col, colIndex) in item.columns" :key="colIndex" :span="col.span">
+
+              <template v-for="citem in col.list">
+                <el-form-item v-if="citem.type=='blank'" :key="citem.key" :label="citem.name" :prop="citem.model">
+                  <slot :name="citem.model" :model="models" />
+                </el-form-item>
+                <genetate-form-item
+                  v-else
+                  :key="citem.key"
+                  :preview="preview"
+                  :models.sync="models"
+                  :remote="remote"
+                  :rules="rules"
+                  :widget="citem"
+                  :data="data"
+                  @input-change="onInputChange"
+                />
+              </template>
+            </el-col>
+          </el-row>
+        </template>
+
+        <template v-else-if="item.type == 'blank'">
+          <el-form-item :key="item.key" :label="item.name" :prop="item.model">
+            <slot :name="item.model" :model="models" />
+          </el-form-item>
+        </template>
+        <!-- 子表单 -->
+        <template v-if="item.type === 'subform'">
+          <el-form-item
+            :key="item.key"
+            :label-width="!item.options.labelWidthStatus?'0px': item.options.labelWidth + 'px'"
+            :label="!item.options.labelWidthStatus?'':item.name"
+            :prop="item.model"
+          >
+            <el-table
+              :data="models[item.model]"
+              border
+              style="width: 100%"
+              :header-cell-style="{padding: '5px 0'}"
+            >
+              <el-table-column
+                v-if="!preview"
+                fixed
+                width="50"
+              >
+                <template slot="header">
+                  <i style="font-size: 25px; color: #409EFF;cursor:pointer;" class="el-icon-circle-plus" @click="addSubformCol(item)" />
+                </template>
+                <template slot-scope="scope">
+                  <i style="font-size: 25px; color: red" class="el-icon-remove" @click="delSubformCol(item, scope.$index)" />
+                </template>
+
+              </el-table-column>
+              <template v-for="(c, i) in item.columns">
+                <div :key="i">
+                  <el-table-column
+                    v-for="v in c.list"
+                    :key="v.key"
+                    :prop="v.modal"
+                    :label="v.name"
+                    min-width="250"
+                  >
+                    <template slot-scope="scope">
+                      <genetate-form-item
+                        :preview="preview"
+                        :models.sync="models"
+                        :rules="rules"
+                        :widget="v"
+                        :remote="remote"
+                        :data="data"
+                        :disabled="disabled"
+                        :is-label="false"
+                        :subform-index="scope.$index"
+                        :subform-model="item.model"
+                      />
+                    </template>
+                  </el-table-column>
+                </div>
+              </template>
+            </el-table>
+          </el-form-item>
+        </template>
+
+        <template v-else>
+          <genetate-form-item
+            :key="item.key"
+            :preview="preview"
+            :models.sync="models"
+            :rules="rules"
+            :widget="item"
+            :remote="remote"
+            :data="data"
+            :disabled="disabled"
+            @input-change="onInputChange"
+          />
+        </template>
+
+      </template>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import GenetateFormItem from './GenerateFormItem'
+
+export default {
+  name: 'FmGenerateForm',
+  components: {
+    GenetateFormItem
+  },
+  /* eslint-disable */
+  props: ['data', 'remote', 'value', 'insite', 'disabled', 'preview'],
+  data() {
+    return {
+      tableData: [],
+      models: {},
+      rules: {},
+      subformFields: {}
+    }
+  },
+  watch: {
+    data: {
+      deep: true,
+      handler(val) {
+        this.generateModle(val.list)
+      }
+    },
+    value: {
+      deep: true,
+      handler(val) {
+        this.models = { ...this.models, ...val }
+      }
+    }
+  },
+  created() {
+    this.generateModle(this.data.list)
+  },
+  mounted() {
+  },
+  methods: {
+    addSubformCol(item) {
+      var subformFields = {}
+      for (var c of item.columns) {
+        for (var l of c.list) {
+          if (l.options !== null && l.options !== undefined) {
+            subformFields[l.model] = l.options.defaultValue !== undefined && l.options.defaultValue !== null ? l.options.defaultValue: ""
+          } else {
+            subformFields[l.model] = ""
+          }
+        }
+      }
+      this.models[item.model].push(subformFields)
+      this.models.status = 1
+    },
+    delSubformCol(item, index) {
+      this.models[item.model].splice(index, 1)
+      this.models.status = -1
+    },
+    generateModle(genList) {
+      for (let i = 0; i < genList.length; i++) {
+        if (genList[i].type === 'grid') {
+          genList[i].columns.forEach(item => {
+            this.generateModle(item.list)
+          })
+        } else {
+          if (this.value && Object.keys(this.value).indexOf(genList[i].model) >= 0) {
+            this.models[genList[i].model] = this.value[genList[i].model]
+          } else {
+            if (genList[i].type === 'blank') {
+              this.$set(this.models, genList[i].model, genList[i].options.defaultType === 'String' ? '' : (genList[i].options.defaultType === 'Object' ? {} : []))
+            } if (genList[i].type === 'subform') { 
+              this.$set(this.models, genList[i].model, [])
+            } else {
+              this.models[genList[i].model] = genList[i].options.defaultValue
+            }
+          }
+
+          if (!this.preview) {
+            if (this.rules[genList[i].model]) {
+              this.rules[genList[i].model] = [...this.rules[genList[i].model], ...genList[i].rules.map(item => {
+                if (item.pattern) {
+                  return { ...item, pattern: eval(item.pattern) }
+                } else {
+                  return { ...item }
+                }
+              })]
+            } else {
+              this.rules[genList[i].model] = [...genList[i].rules.map(item => {
+                if (item.pattern) {
+                  return { ...item, pattern: eval(item.pattern) }
+                } else {
+                  return { ...item }
+                }
+              })]
+            }
+          }
+        }
+      }
+    },
+    getData() {
+      return new Promise((resolve, reject) => {
+        this.$refs.generateForm.validate(valid => {
+          if (valid) {
+            resolve(this.models)
+          } else {
+            reject(new Error(this.$t('fm.message.validError')).message)
+          }
+        })
+      })
+    },
+    reset() {
+      this.$refs.generateForm.resetFields()
+    },
+    onInputChange(value, field) {
+      // this.$emit('on-change', field, value, this.models)
+    },
+    refresh() {
+
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+// @import '../styles/cover.scss';
+</style>

+ 468 - 0
src/components/VueFormMaking/components/GenerateFormItem.vue

@@ -0,0 +1,468 @@
+<template>
+  <el-form-item
+    v-if="showStatus"
+    :label-width="isLabel===false||!widget.options.labelWidthStatus?'0px': widgetLabelWidth + 'px'"
+    :label="isLabel===false||widget.type==='divider' || !widget.options.labelWidthStatus?'':widget.name"
+    :prop="widget.model"
+    :style="subformIndex !== undefined?{'margin-bottom': '0'}: {}"
+  >
+    <template v-if="preview">
+      <template v-if="widget.type === 'color'">
+        <div style="width: 32px; height: 20px; margin-top: 6px; border-radius: 3px" :style="{'background-color': dataModel}" />
+      </template>
+      <template v-else-if="widget.type=='switch'">
+        <el-switch
+          v-model="dataModel"
+          :disabled="true"
+        />
+      </template>
+      <template v-else-if="widget.type === 'editor'">
+        <div class="previewEditorDiv" v-html="dataModel" />
+      </template>
+
+      <template v-else-if="widget.type=='file'">
+        <div v-for="(uploadUrlItem, uploadUrlIndex) of dataModel" :key="uploadUrlIndex">
+          <i style="color: #909399;" class="el-icon-document" />
+          <a :href="uploadUrlItem.url" target="_blank">{{ uploadUrlItem.name }}</a>
+        </div>
+      </template>
+
+      <template v-else-if="widget.type=='imgupload'">
+        <fm-upload
+          v-model="dataModel"
+          :style="{'width': widget.options.width}"
+          :width="widget.options.size.width"
+          :height="widget.options.size.height"
+          :preview="preview"
+        />
+      </template>
+      <template v-else-if="widget.type =='rate'">
+        <el-rate
+          v-model="dataModel"
+          :max="widget.options.max"
+          :disabled="true"
+          :allow-half="widget.options.allowHalf"
+        />
+      </template>
+      <template v-else-if="widget.type === 'divider'">
+        <el-divider
+          :direction="widget.options.direction"
+          :content-position="widget.options.content_position"
+        >
+          <span
+            :style="{
+              'font-size': widget.options.font_size,
+              'font-family': widget.options.font_family,
+              'font-weight': widget.options.font_weight,
+              'color': widget.options.font_color
+            }"
+          >
+            {{ widget.options.defaultValue }}
+          </span>
+        </el-divider>
+      </template>
+      <template v-else-if="widget.type === 'input' && widget.options.showPassword">
+        <input :value="dataModel" type="password" style="border: none; background-color: #ffffff; color: #303133" disabled="disabled">
+      </template>
+      <template v-else-if="widget.type === 'cascader'">
+        <el-cascader
+          v-model="dataModel"
+          class="preview-cascader-class"
+          :disabled="true"
+          :show-all-levels="widget.options.showAllLevels"
+          :options="widget.options.remote?widget.options.remoteOptions:widget.options.options"
+        />
+      </template>
+      <template v-else>
+        <div>
+          {{ dataModel }}
+        </div>
+      </template>
+    </template>
+    <template v-else>
+      <template v-if="widget.type === 'input'">
+        <el-input
+          v-if="widget.options.dataType === 'number'
+            || widget.options.dataType === 'integer'
+            || widget.options.dataType === 'float'"
+          v-model.number="dataModel"
+          :type="widget.options.dataType"
+          :placeholder="widget.options.placeholder"
+          :style="{width: widget.options.width}"
+          :disabled="widget.options.disabled"
+          :show-password="widget.options.showPassword"
+        />
+        <el-input
+          v-else
+          v-model="dataModel"
+          :type="widget.options.dataType"
+          :disabled="widget.options.disabled"
+          :placeholder="widget.options.placeholder"
+          :style="{width: widget.options.width}"
+          :show-password="widget.options.showPassword"
+        />
+      </template>
+
+      <template v-if="widget.type === 'textarea'">
+        <el-input
+          v-model="dataModel"
+          type="textarea"
+          :rows="5"
+          :disabled="widget.options.disabled"
+          :placeholder="widget.options.placeholder"
+          :style="{width: widget.options.width}"
+        />
+      </template>
+
+      <template v-if="widget.type === 'number'">
+        <el-input-number
+          v-model="dataModel"
+          :style="{width: widget.options.width}"
+          :step="widget.options.step"
+          controls-position="right"
+          :disabled="widget.options.disabled"
+        />
+      </template>
+
+      <template v-if="widget.type === 'radio'">
+        <el-radio-group
+          v-model="dataModel"
+          :style="{width: widget.options.width}"
+          :disabled="widget.options.disabled"
+        >
+          <el-radio
+            v-for="(item, index) in (widget.options.remote ? widget.options.remoteOptions : widget.options.options)"
+            :key="index"
+            :style="{display: widget.options.inline ? 'inline-block' : 'block'}"
+            :label="item.value"
+          >
+            <template v-if="widget.options.remote">{{ item.label }}</template>
+            <template v-else>{{ widget.options.showLabel ? item.label : item.value }}</template>
+          </el-radio>
+        </el-radio-group>
+      </template>
+
+      <template v-if="widget.type === 'checkbox'">
+        <el-checkbox-group
+          v-model="dataModel"
+          :style="{width: widget.options.width}"
+          :disabled="widget.options.disabled"
+        >
+          <el-checkbox
+
+            v-for="(item, index) in (widget.options.remote ? widget.options.remoteOptions : widget.options.options)"
+            :key="index"
+            :style="{display: widget.options.inline ? 'inline-block' : 'block'}"
+            :label="item.value"
+          >
+            <template v-if="widget.options.remote">{{ item.label }}</template>
+            <template v-else>{{ widget.options.showLabel ? item.label : item.value }}</template>
+          </el-checkbox>
+        </el-checkbox-group>
+      </template>
+
+      <template v-if="widget.type === 'time'">
+        <el-time-picker
+          v-model="dataModel"
+          :is-range="widget.options.isRange"
+          :placeholder="widget.options.placeholder"
+          :start-placeholder="widget.options.startPlaceholder"
+          :end-placeholder="widget.options.endPlaceholder"
+          :readonly="widget.options.readonly"
+          :disabled="widget.options.disabled"
+          :editable="widget.options.editable"
+          :clearable="widget.options.clearable"
+          :arrow-control="widget.options.arrowControl"
+          :value-format="widget.options.format"
+          :style="{width: widget.options.width}"
+        />
+      </template>
+
+      <template v-if="widget.type=='date'">
+        <el-date-picker
+          v-model="dataModel"
+          :type="widget.options.type"
+          :placeholder="widget.options.placeholder"
+          :start-placeholder="widget.options.startPlaceholder"
+          :end-placeholder="widget.options.endPlaceholder"
+          :readonly="widget.options.readonly"
+          :disabled="widget.options.disabled"
+          :editable="widget.options.editable"
+          :clearable="widget.options.clearable"
+          :value-format="widget.options.timestamp ? 'timestamp' : widget.options.format"
+          :format="widget.options.format"
+          :style="{width: widget.options.width}"
+        />
+      </template>
+
+      <template v-if="widget.type =='rate'">
+        <el-rate
+          v-model="dataModel"
+          :max="widget.options.max"
+          :disabled="widget.options.disabled"
+          :allow-half="widget.options.allowHalf"
+        />
+      </template>
+
+      <template v-if="widget.type === 'color'">
+        <el-color-picker
+          v-model="dataModel"
+          :disabled="widget.options.disabled"
+          :show-alpha="widget.options.showAlpha"
+        />
+      </template>
+
+      <template v-if="widget.type === 'select'">
+        <el-select
+          v-model="dataModel"
+          :disabled="widget.options.disabled"
+          :multiple="widget.options.multiple"
+          :clearable="widget.options.clearable"
+          :placeholder="widget.options.placeholder"
+          :style="{width: widget.options.width}"
+          :filterable="widget.options.filterable"
+        >
+          <el-option v-for="item in (widget.options.remote ? widget.options.remoteOptions : widget.options.options)" :key="item.value" :value="item.value" :label="widget.options.showLabel || widget.options.remote?item.label:item.value" />
+        </el-select>
+      </template>
+
+      <template v-if="widget.type=='switch'">
+        <el-switch
+          v-model="dataModel"
+          :disabled="widget.options.disabled"
+        />
+      </template>
+
+      <template v-if="widget.type=='slider'">
+        <el-slider
+          v-model="dataModel"
+          :min="widget.options.min"
+          :max="widget.options.max"
+          :disabled="widget.options.disabled"
+          :step="widget.options.step"
+          :show-input="widget.options.showInput"
+          :range="widget.options.range"
+          :style="{width: widget.options.width}"
+        />
+      </template>
+
+      <template v-if="widget.type=='imgupload'">
+        <fm-upload
+          v-model="dataModel"
+          :disabled="widget.options.disabled"
+          :style="{'width': widget.options.width}"
+          :width="widget.options.size.width"
+          :height="widget.options.size.height"
+          :token="widget.options.token"
+          :domain="widget.options.domain"
+          :multiple="widget.options.multiple"
+          :length="widget.options.length"
+          :is-qiniu="widget.options.isQiniu"
+          :is-delete="widget.options.isDelete"
+          :min="widget.options.min"
+          :is-edit="widget.options.isEdit"
+          :action="widget.options.action"
+        />
+      </template>
+
+      <template v-if="widget.type=='file'">
+        <FileUpload :element="widget" :data-model="dataModel" @fileList="fileList" />
+      </template>
+
+      <template v-if="widget.type === 'editor'">
+        <vue-editor
+          v-model="dataModel"
+          :disabled="widget.options.disabled"
+          :style="{width: widget.options.width}"
+        />
+      </template>
+
+      <template v-if="widget.type === 'cascader'">
+        <el-cascader
+          v-model="dataModel"
+          :disabled="widget.options.disabled"
+          :show-all-levels="widget.options.showAllLevels"
+          :clearable="widget.options.clearable"
+          :placeholder="widget.options.placeholder"
+          :style="{width: widget.options.width}"
+          :options="widget.options.remote?widget.options.remoteOptions:widget.options.options"
+        />
+      </template>
+
+      <template v-if="widget.type === 'text'">
+        <span
+          :style="{
+            'font-size': widget.options.font_size,
+            'font-family': widget.options.font_family,
+            'font-weight': widget.options.font_weight,
+            'color': widget.options.font_color
+          }"
+        >
+          {{ widget.options.defaultValue }}
+        </span>
+      </template>
+
+      <template v-if="widget.type === 'divider'">
+        <el-divider
+          :direction="widget.options.direction"
+          :content-position="widget.options.content_position"
+        >
+          <span
+            :style="{
+              'font-size': widget.options.font_size,
+              'font-family': widget.options.font_family,
+              'font-weight': widget.options.font_weight,
+              'color': widget.options.font_color
+            }"
+          >
+            {{ widget.options.defaultValue }}
+          </span>
+        </el-divider>
+      </template>
+    </template>
+
+  </el-form-item>
+</template>
+
+<script>
+import FmUpload from './Upload'
+import FileUpload from './Upload/file'
+
+export default {
+  name: 'GenetateFormItem',
+  components: {
+    FmUpload,
+    FileUpload
+  },
+  /* eslint-disable */
+  props: ['widget', 'models', 'rules', 'remote', 'data', 'disabled', 'preview', 'isLabel', 'subformIndex', 'subformModel'],
+  data() {
+    return {
+      showStatus: true,
+      widgetLabelWidth: '',
+      dataModel: this.subformIndex===undefined?
+        this.models[this.widget.model]:
+        this.models[this.subformModel][this.subformIndex][this.widget.model],
+      tableData: []
+    }
+  },
+  watch: {
+    dataModel: {
+      deep: true,
+      handler(newValue) {
+        if (newValue !== undefined && newValue !== null) {
+          if (this.subformIndex !== undefined) {
+            this.models[this.subformModel][this.subformIndex][this.widget.model] = newValue
+            this.$emit('update:models', {
+              ...this.models,
+              [this.subformModel]: this.models[this.subformModel]
+            })
+            // this.$emit('input-change', val, this.widget.model, this.subformIndex)
+          } else {
+            this.models[this.widget.model] = newValue
+            this.$emit('update:models', {
+              ...this.models,
+              [this.widget.model]: newValue
+            })
+            // this.$emit('input-change', val, this.widget.model)
+          }
+        }
+      }
+    },
+    models: {
+      deep: true,
+      handler(val) {
+        if (val.status === undefined && val.status === null) {
+          if (this.subformIndex === undefined) {
+            this.dataModel = val[this.widget.model]
+          } else {
+            this.dataModel = val[this.subformModel][this.subformIndex][this.widget.model]
+          }
+        }
+        delete this.models.status
+        this.handleDisplayVerifiy()
+      }
+    }
+  },
+  created() {
+    if (this.widget.options.remote && this.remote[this.widget.options.remoteFunc]) {
+      this.remote[this.widget.options.remoteFunc]((data) => {
+        this.widget.options.remoteOptions = data.map(item => {
+          return {
+            value: item[this.widget.options.props.value],
+            label: item[this.widget.options.props.label],
+            children: item[this.widget.options.props.children]
+          }
+        })
+      })
+    }
+    
+    if (this.widget.type === 'imgupload' && this.widget.options.isQiniu) {
+      this.remote[this.widget.options.tokenFunc]((data) => {
+        this.widget.options.token = data
+      })
+    }
+
+    if (this.disabled !== undefined && this.disabled !== null) {
+      this.widget.options.disabled = this.disabled
+    }
+
+    // label width
+    if (this.widget.options.labelWidthDisabled) {
+      this.widgetLabelWidth = this.widget.options.labelWidth
+    } else if (this.widget.type==='divider') {
+      this.widgetLabelWidth = 0
+    } else {
+      this.widgetLabelWidth = this.data.config.labelWidth
+    }
+
+    this.handleDisplayVerifiy()
+  },
+  methods: {
+    fileList(files) {
+      this.dataModel = files
+    },
+    handleDisplayVerifiy() {
+      if (Object.keys(this.widget.options).indexOf('displayVerifiy')>=0) {
+        if (this.widget.options.displayVerifiy.type !== 'hide') {
+          var c = 0
+          for (var v of this.widget.options.displayVerifiy.list) {
+            if (this.models[v.model].toString() === v.value) {
+              c++
+            }
+          }
+          if (this.widget.options.displayVerifiy.type === 'and') {
+            if (c !== this.widget.options.displayVerifiy.list.length) {
+              this.showStatus = false
+            } else {
+              this.showStatus = true
+            }
+          } else if (this.widget.options.displayVerifiy.type === 'or')  {
+            if (c === 0) {
+              this.showStatus = false
+            } else {
+              this.showStatus = true
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style>
+  .previewEditorDiv > p {
+    margin: 0;
+  }
+
+  .preview-cascader-class .el-input.is-disabled .el-input__inner {
+    background-color: #fff;
+    border: none;
+    color: #303133;
+  }
+
+  .preview-cascader-class .el-input.is-disabled .el-input__suffix .el-input__suffix-inner .el-input__icon.el-icon-arrow-down:before {
+    content: ''
+  }
+</style>

+ 63 - 0
src/components/VueFormMaking/components/Upload/file.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <el-upload
+      :action="element.options.action"
+      :on-success="handleSuccess"
+      :on-preview="handlePreview"
+      :on-remove="handleRemove"
+      :before-remove="beforeRemove"
+      multiple
+      :limit="element.options.length"
+      :headers="element.options.headers"
+      :on-exceed="handleExceed"
+      :file-list="dataModel"
+      :disabled="element.options.disabled"
+      :style="{'width': element.options.width}"
+    >
+      <div v-if="!preview">
+        <el-button size="small" type="primary">点击上传</el-button>
+        <div slot="tip" class="el-upload__tip">{{ element.options.tip }}</div>
+      </div>
+    </el-upload>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FileUpload',
+  // eslint-disable-next-line vue/require-prop-types
+  props: ['element', 'preview', 'dataModel'],
+  data() {
+    return {
+      fileListTmp: []
+    }
+  },
+  methods: {
+    handleRemove(file, fileList) {
+      this.fileListTmp = fileList
+      this.$emit('fileList', fileList)
+    },
+    handlePreview(file) {
+      window.open(file.url, '_blank')
+    },
+    handleExceed(files, fileList) {
+      this.$message.warning(`最多允许上传 ${this.element.options.length} 个文件。`)
+    },
+    beforeRemove(file, fileList) {
+      return this.$confirm(`确定要移除 ${file.name}?`)
+    },
+    handleSuccess(response, file, fileList) {
+      this.fileListTmp.push({
+        uid: file.uid,
+        name: file.name,
+        url: response.data
+      })
+      this.$emit('fileList', this.fileListTmp)
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 474 - 0
src/components/VueFormMaking/components/Upload/index.vue

@@ -0,0 +1,474 @@
+<template>
+  <div
+    :id="uploadId"
+    class="fm-uplaod-container"
+  >
+    <draggable
+      v-model="fileList"
+      class="drag-img-list"
+      v-bind="{group: uploadId, ghostClass: 'ghost', animation: 200}"
+      :no-transition-on-drag="true"
+    >
+      <div
+        v-for="(item) in fileList"
+        :id="item.key"
+        :key="item.key"
+        :style="{width: width+'px', height: height+'px'}"
+        :class="{uploading: item.status=='uploading', 'is-success': item.status=='success', 'is-diabled': disabled}"
+        class="upload-file"
+      >
+        <img :src="item.url">
+
+        <el-progress v-if="item.status=='uploading'" :width="miniWidth*0.9" class="upload-progress" type="circle" :percentage="item.percent" />
+
+        <label v-if="item.status=='success'" class="item-status">
+          <i class="el-icon-upload-success el-icon-check" />
+        </label>
+
+        <div v-if="!disabled" class="uplaod-action" :style="{height: miniWidth / 4 + 'px'}">
+          <i class="iconfont icon-tupianyulan" :title="$t('fm.upload.preview')" :style="{'font-size': miniWidth/8+'px'}" @click="handlePreviewFile(item.key)" />
+          <i v-if="isEdit" class="iconfont icon-sync1" :title="$t('fm.upload.edit')" :style="{'font-size': miniWidth/8+'px'}" @click="handleEdit(item.key)" />
+          <i v-if="isDelete && fileList.length > min" class="iconfont icon-delete" :title="$t('fm.upload.delete')" :style="{'font-size': miniWidth/8+'px'}" @click="handleRemove(item.key)" />
+        </div>
+      </div>
+    </draggable>
+
+    <div
+      v-if="!preview"
+      v-show="(!isQiniu || (isQiniu && token)) && fileList.length < length"
+      class="el-upload el-upload--picture-card"
+      :class="{'is-disabled': disabled}"
+      :style="{width: width+'px', height: height+'px'}"
+      @click.self="handleAdd"
+    >
+      <i class="el-icon-plus" :style="{fontSize:miniWidth/4+'px',marginTop: (-miniWidth/8)+'px', marginLeft: (-miniWidth/8)+'px'}" @click.self="handleAdd" />
+      <input
+        v-if="multiple"
+        ref="uploadInput"
+        accept="image/*"
+        multiple
+        type="file"
+        :style="{width: 0, height: 0}"
+        name="file"
+        class="el-upload__input upload-input"
+        @change="handleChange"
+      >
+      <input v-else ref="uploadInput" accept="image/*" type="file" :style="{width:0, height: 0}" name="file" class="el-upload__input upload-input" @change="handleChange">
+    </div>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+import Viewer from 'viewerjs'
+import Draggable from 'vuedraggable'
+import * as qiniu from 'qiniu-js'
+require('viewerjs/dist/viewer.css')
+
+import VueI18n from 'vue-i18n'
+Vue.use(VueI18n)
+export default {
+  components: {
+    Draggable
+  },
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    },
+    width: {
+      type: Number,
+      default: 100
+    },
+    height: {
+      type: Number,
+      default: 100
+    },
+    token: {
+      type: String,
+      default: ''
+    },
+    domain: {
+      type: String,
+      default: ''
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    length: {
+      type: Number,
+      default: 9
+    },
+    isQiniu: {
+      type: Boolean,
+      default: false
+    },
+    isDelete: {
+      type: Boolean,
+      default: false
+    },
+    min: {
+      type: Number,
+      default: 0
+    },
+    meitu: {
+      type: Boolean,
+      default: false
+    },
+    isEdit: {
+      type: Boolean,
+      default: false
+    },
+    action: {
+      type: String,
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    preview: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      fileList: this.value.map(item => {
+        return {
+          key: item.key ? item.key : (new Date().getTime()) + '_' + Math.ceil(Math.random() * 99999),
+          url: item.url,
+          percent: item.percent ? item.percent : 100,
+          status: item.status ? item.status : 'success'
+        }
+      }),
+      viewer: null,
+      uploadId: 'upload_' + new Date().getTime(),
+      editIndex: -1,
+      meituIndex: -1
+    }
+  },
+  computed: {
+    miniWidth() {
+      if (this.width > this.height) {
+        return this.height
+      } else {
+        return this.width
+      }
+    }
+  },
+  watch: {
+    'fileList': {
+      deep: true,
+      handler(val) {
+        // this.$emit('input', this.fileList)
+      }
+    }
+  },
+  mounted() {
+    this.$emit('input', this.fileList)
+  },
+  methods: {
+    handleChange() {
+      const files = this.$refs.uploadInput.files
+
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i]
+        const reader = new FileReader()
+        const key = (new Date().getTime()) + '_' + Math.ceil(Math.random() * 99999)
+        reader.readAsDataURL(file)
+        reader.onload = () => {
+          if (this.editIndex >= 0) {
+            this.$set(this.fileList, this.editIndex, {
+              key,
+              url: reader.result,
+              percent: 0,
+              status: 'uploading'
+            })
+
+            this.editIndex = -1
+          } else {
+            this.fileList.push({
+              key,
+              url: reader.result,
+              percent: 0,
+              status: 'uploading'
+            })
+          }
+
+          this.$nextTick(() => {
+            if (this.isQiniu) {
+              this.uplaodAction2(reader.result, file, key)
+            } else {
+              this.uplaodAction(reader.result, file, key)
+            }
+          })
+        }
+      }
+      this.$refs.uploadInput.value = []
+    },
+    uplaodAction(res, file, key) {
+      // eslint-disable-next-line no-unused-vars
+      const changeIndex = this.fileList.findIndex(item => item.key === key)
+      const xhr = new XMLHttpRequest()
+
+      const url = this.action
+      xhr.open('POST', url, true)
+      // xhr.setRequestHeader('Content-Type', 'multipart/form-data')
+
+      const formData = new FormData()
+      formData.append('file', file)
+
+      xhr.send(formData)
+      xhr.onreadystatechange = () => {
+        if (xhr.readyState === 4) {
+          const resData = JSON.parse(xhr.response)
+          var uploadUrl = ''
+          if (resData.url !== undefined && resData.url !== null && resData.url !== '') {
+            uploadUrl = resData.url
+          } else if (resData.data !== undefined && resData.data !== null && resData.data !== '') {
+            uploadUrl = resData.data
+          }
+          if (resData && uploadUrl) {
+            this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
+              ...this.fileList[this.fileList.findIndex(item => item.key === key)],
+              url: uploadUrl,
+              percent: 100
+            })
+            setTimeout(() => {
+              this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
+                ...this.fileList[this.fileList.findIndex(item => item.key === key)],
+                status: 'success'
+              })
+              this.$emit('input', this.fileList)
+            }, 200)
+          } else {
+            this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
+              ...this.fileList[this.fileList.findIndex(item => item.key === key)],
+              status: 'error'
+            })
+            this.fileList.splice(this.fileList.findIndex(item => item.key === key), 1)
+          }
+        }
+      }
+      xhr.onprogress = (res) => {
+        if (res.total && res.loaded) {
+          this.$set(this.fileList[this.fileList.findIndex(item => item.key === key)], 'percent', res.loaded / res.total * 100)
+        }
+      }
+    },
+    uplaodAction2(res, file, key) {
+      const _this = this
+      const observable = qiniu.upload(file, key, this.token, {
+        fname: key,
+        mimeType: []
+      }, {
+        useCdnDomain: true,
+        region: qiniu.region.z2
+      })
+      observable.subscribe({
+        next(res) {
+          _this.$set(_this.fileList[_this.fileList.findIndex(item => item.key === key)], 'percent', parseInt(res.total.percent))
+        },
+        // eslint-disable-next-line handle-callback-err
+        error(err) {
+          _this.$set(_this.fileList, _this.fileList.findIndex(item => item.key === key), {
+            ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
+            status: 'error'
+          })
+          _this.fileList.splice(_this.fileList.findIndex(item => item.key === key), 1)
+        },
+        complete(res) {
+          _this.$set(_this.fileList, _this.fileList.findIndex(item => item.key === key), {
+            ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
+            url: _this.domain + res.key,
+            percent: 100
+          })
+          setTimeout(() => {
+            _this.$set(_this.fileList, _this.fileList.findIndex(item => item.key === key), {
+              ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
+              status: 'success'
+            })
+            _this.$emit('input', _this.fileList)
+          }, 200)
+        }
+      })
+    },
+    handleRemove(key) {
+      this.fileList.splice(this.fileList.findIndex(item => item.key === key), 1)
+    },
+    handleEdit(key) {
+      this.editIndex = this.fileList.findIndex(item => item.key === key)
+
+      this.$refs.uploadInput.click()
+    },
+    handleMeitu(key) {
+      this.$emit('on-meitu', this.fileList.findIndex(item => item.key === key))
+    },
+    handleAdd() {
+      if (!this.disabled) {
+        this.editIndex = -1
+        this.$refs.uploadInput.click()
+      }
+    },
+    handlePreviewFile(key) {
+      this.viewer && this.viewer.destroy()
+      this.uploadId = 'upload_' + new Date().getTime()
+
+      this.$nextTick(() => {
+        this.viewer = new Viewer(document.getElementById(this.uploadId))
+        this.viewer.view(this.fileList.findIndex(item => item.key === key))
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.fm-uplaod-container{
+  .is-disabled{
+    position: relative;
+
+    &::after{
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      // background: rgba(0,0,0,.1);
+      content: '';
+      display: block;
+      cursor:not-allowed;
+    }
+  }
+
+  .upload-file{
+    margin: 0 10px 10px 0;
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    // background: #fff;
+    overflow: hidden;
+    background-color: #fff;
+    border: 1px solid #c0ccda;
+    border-radius: 6px;
+    box-sizing: border-box;
+    position: relative;
+    vertical-align: top;
+    &:hover{
+      .uplaod-action{
+        display: flex;
+      }
+    }
+    .uplaod-action{
+      position: absolute;
+      // top: 0;
+      // height: 30px;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      background: rgba(0,0,0,0.6);
+      display: none;
+      justify-content: center;
+      align-items: center;
+      i{
+        color: #fff;
+        cursor: pointer;
+        margin: 0 5px;
+      }
+    }
+    &.is-success{
+      .item-status{
+        position: absolute;
+        right: -15px;
+        top: -6px;
+        width: 40px;
+        height: 24px;
+        background: #13ce66;
+        text-align: center;
+        transform: rotate(45deg);
+        box-shadow: 0 0 1pc 1px rgba(0,0,0,.2);
+        &>i{
+          font-size: 12px;
+          margin-top: 11px;
+          color: #fff;
+          transform: rotate(-45deg);
+        }
+      }
+    }
+    &.uploading{
+      &:before{
+        display: block;
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: rgba(0,0,0,0.3);
+      }
+    }
+    .upload-progress{
+      position: absolute;
+      .el-progress__text{
+        color: #fff;
+        font-size: 16px !important;
+      }
+    }
+    img{
+      max-width: 100%;
+      max-height: 100%;
+      vertical-align: middle;
+    }
+  }
+  .el-upload--picture-card{
+    position: relative;
+    overflow: hidden;
+    .el-icon-plus{
+      position: absolute;
+      top: 50%;
+      left: 50%;
+    }
+  }
+  .upload-input{
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: block;
+    opacity: 0;
+    cursor: pointer;
+  }
+
+  .drag-img-list{
+    display: inline;
+
+    .ghost{
+      position: relative;
+      &::after {
+        width: 100%;
+        height: 100%;
+        display: block;
+        content: '';
+        background: #fbfdff;
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        border: 1px dashed #3bb3c2;
+      }
+    }
+
+    &>div{
+      cursor: move;
+    }
+  }
+}
+
+.viewer-container{
+  z-index: 9999 !important;
+}
+</style>

+ 780 - 0
src/components/VueFormMaking/components/WidgetConfig.vue

@@ -0,0 +1,780 @@
+<template>
+  <div v-if="show">
+    <el-form label-position="top">
+      <!-- 字段标识 -->
+      <el-form-item v-if="data.type!='grid'" :label="$t('fm.config.widget.model')">
+        <el-input v-model="data.model" />
+      </el-form-item>
+      <!-- 标题 -->
+      <el-form-item v-if="data.type!=='grid' && data.type!=='divider'" :label="$t('fm.config.widget.name')">
+        <el-input v-model="data.name" />
+      </el-form-item>
+      <!-- 宽度 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('width')>=0" :label="$t('fm.config.widget.width')">
+        <el-input v-model="data.options.width" />
+      </el-form-item>
+
+      <!-- 兰玉磊开始添加 -->
+      <!-- 标签宽度 -->
+      <el-form-item
+        v-if="Object.keys(data.options).indexOf('labelWidth')>=0 &&
+          data.type!=='grid' &&
+          data.type!=='subform' &&
+          data.type !== 'divider'"
+        :label="$t('fm.config.widget.labelWidth')"
+      >
+        <el-checkbox v-model="data.options.labelWidthDisabled">自定义</el-checkbox>
+        <el-input-number v-model="data.options.labelWidth" :min="0" :step="10" :disabled="!data.options.labelWidthDisabled" />
+      </el-form-item>
+      <el-form-item
+        v-if="Object.keys(data.options).indexOf('labelWidthStatus')>=0 &&
+          data.type!=='grid' &&
+          data.type !== 'divider'"
+        label="显示标签"
+      >
+        <el-switch
+          v-model="data.options.labelWidthStatus"
+        />
+      </el-form-item>
+      <el-form-item v-if="Object.keys(data.options).indexOf('tip')>=0" :label="$t('fm.config.widget.tip')">
+        <el-input v-model="data.options.tip" />
+      </el-form-item>
+      <!-- 兰玉磊结束添加 -->
+
+      <!-- 高度 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('height')>=0" :label="$t('fm.config.widget.height')">
+        <el-input v-model="data.options.height" />
+      </el-form-item>
+      <!-- 大小 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('size')>=0" :label="$t('fm.config.widget.size')">
+        {{ $t('fm.config.widget.width') }} <el-input v-model.number="data.options.size.width" style="width: 90px;" type="number" />
+        {{ $t('fm.config.widget.height') }} <el-input v-model.number="data.options.size.height" style="width: 90px;" type="number" />
+      </el-form-item>
+      <!-- 占位内容 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('placeholder')>=0 && (data.type!='time' || data.type!='date')" :label="$t('fm.config.widget.placeholder')">
+        <el-input v-model="data.options.placeholder" />
+      </el-form-item>
+      <!-- 布局方式,块,行 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('inline')>=0" :label="$t('fm.config.widget.layout')">
+        <el-radio-group v-model="data.options.inline">
+          <el-radio-button :label="false">{{ $t('fm.config.widget.block') }}</el-radio-button>
+          <el-radio-button :label="true">{{ $t('fm.config.widget.inline') }}</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <!-- 显示输入框 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('showInput')>=0" :label="$t('fm.config.widget.showInput')">
+        <el-switch v-model="data.options.showInput" />
+      </el-form-item>
+      <!-- 最小值 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('min')>=0" :label="$t('fm.config.widget.min')">
+        <el-input-number v-model="data.options.min" :min="0" :max="100" :step="1" />
+      </el-form-item>
+      <!-- 最大值 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('max')>=0" :label="$t('fm.config.widget.max')">
+        <el-input-number v-model="data.options.max" :min="0" :max="100" :step="1" />
+      </el-form-item>
+      <!-- 步长 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('step')>=0" :label="$t('fm.config.widget.step')">
+        <el-input-number v-model="data.options.step" :min="0" :max="100" :step="1" />
+      </el-form-item>
+      <!-- 是否多选 -->
+      <el-form-item v-if="data.type=='select'" :label="$t('fm.config.widget.multiple')">
+        <el-switch v-model="data.options.multiple" @change="handleSelectMuliple" />
+      </el-form-item>
+      <!-- 是否可搜索 -->
+      <el-form-item v-if="data.type=='select'" :label="$t('fm.config.widget.filterable')">
+        <el-switch v-model="data.options.filterable" />
+      </el-form-item>
+      <!-- 允许半选 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('allowHalf')>=0" :label="$t('fm.config.widget.allowHalf')">
+        <el-switch
+          v-model="data.options.allowHalf"
+        />
+      </el-form-item>
+      <!-- 支持透明度选择 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('showAlpha')>=0" :label="$t('fm.config.widget.showAlpha')">
+        <el-switch
+          v-model="data.options.showAlpha"
+        />
+      </el-form-item>
+      <!-- 是否显示标签 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('showLabel')>=0" :label="$t('fm.config.widget.showLabel')">
+        <el-switch
+          v-model="data.options.showLabel"
+        />
+      </el-form-item>
+      <!-- 选项 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('options')>=0" :label="$t('fm.config.widget.option')">
+        <el-radio-group v-model="data.options.remote" size="mini" style="margin-bottom:10px;">
+          <el-radio-button :label="false">{{ $t('fm.config.widget.staticData') }}</el-radio-button>
+          <el-radio-button :label="true">{{ $t('fm.config.widget.remoteData') }}</el-radio-button>
+        </el-radio-group>
+        <template v-if="data.options.remote">
+          <div>
+            <el-input v-model="data.options.remoteFunc" size="mini" style="">
+              <template slot="prepend">{{ $t('fm.config.widget.remoteFunc') }}</template>
+            </el-input>
+            <el-input v-model="data.options.props.value" size="mini" style="">
+              <template slot="prepend">{{ $t('fm.config.widget.value') }}</template>
+            </el-input>
+            <el-input v-model="data.options.props.label" size="mini" style="">
+              <template slot="prepend">{{ $t('fm.config.widget.label') }}</template>
+            </el-input>
+            <el-input v-if="data.type === 'cascader'" v-model="data.options.props.children" size="mini" style="">
+              <template slot="prepend">{{ $t('fm.config.widget.childrenOption') }}</template>
+            </el-input>
+          </div>
+        </template>
+        <template v-else>
+          <template v-if="data.type=='radio' || (data.type=='select'&&!data.options.multiple)">
+            <el-radio-group v-model="data.options.defaultValue">
+              <draggable
+                tag="ul"
+                :list="data.options.options"
+                v-bind="{group:{ name:'options'}, ghostClass: 'ghost',handle: '.drag-item'}"
+                handle=".drag-item"
+              >
+                <li v-for="(item, index) in data.options.options" :key="index">
+                  <el-radio
+                    :label="item.value"
+                    style="margin-right: 5px;"
+                  >
+                    <el-input v-model="item.value" :style="{'width': data.options.showLabel? '90px': '180px' }" size="mini" />
+                    <el-input v-if="data.options.showLabel" v-model="item.label" style="width:90px;" size="mini" />
+                    <!-- <input v-model="item.value"/> -->
+                  </el-radio>
+                  <i class="drag-item" style="font-size: 16px;margin: 0 5px;cursor: move;"><i class="iconfont icon-icon_bars" /></i>
+                  <el-button circle plain type="danger" size="mini" icon="el-icon-minus" style="padding: 4px;margin-left: 5px;" @click="handleOptionsRemove(index)" />
+
+                </li>
+              </draggable>
+
+            </el-radio-group>
+          </template>
+
+          <template v-if="data.type=='checkbox' || (data.type=='select' && data.options.multiple)">
+            <el-checkbox-group v-model="data.options.defaultValue">
+
+              <draggable
+                tag="ul"
+                :list="data.options.options"
+                v-bind="{group:{ name:'options'}, ghostClass: 'ghost',handle: '.drag-item'}"
+                handle=".drag-item"
+              >
+                <li v-for="(item, index) in data.options.options" :key="index">
+                  <el-checkbox
+                    :label="item.value"
+                    style="margin-right: 5px;"
+                  >
+                    <el-input v-model="item.value" :style="{'width': data.options.showLabel? '90px': '180px' }" size="mini" />
+                    <el-input v-if="data.options.showLabel" v-model="item.label" style="width:90px;" size="mini" />
+                  </el-checkbox>
+                  <i class="drag-item" style="font-size: 16px;margin: 0 5px;cursor: move;"><i class="iconfont icon-icon_bars" /></i>
+                  <el-button circle plain type="danger" size="mini" icon="el-icon-minus" style="padding: 4px;margin-left: 5px;" @click="handleOptionsRemove(index)" />
+
+                </li>
+              </draggable>
+            </el-checkbox-group>
+          </template>
+
+          <template v-if="data.type === 'cascader'">
+            <el-tree
+              :data="data.options.options"
+              node-key="id"
+              default-expand-all
+              :expand-on-click-node="false"
+            >
+              <span slot-scope="{ node, data }" class="custom-tree-node">
+                <span style="font-size: 12px;">{{ node.label }}</span>
+                <span>
+                  <el-button
+                    type="text"
+                    size="mini"
+                    @click="() => appendCascaderDialog(data)"
+                  >
+                    <i class="el-icon-circle-plus" style="font-size: 15px;" />
+                  </el-button>
+                  <el-button
+                    type="text"
+                    size="mini"
+                    @click="() => editCascaderData(data)"
+                  >
+                    <i class="el-icon-edit-outline" style="font-size: 15px;" />
+                  </el-button>
+                  <el-button
+                    type="text"
+                    size="mini"
+                    @click="() => removeCascaderData(node, data)"
+                  >
+                    <i class="el-icon-remove" style="font-size: 15px; color: #f56c6c" />
+                  </el-button>
+                </span>
+              </span>
+            </el-tree>
+          </template>
+
+          <div :style="data.type === 'cascader'?{'margin-left': '5px'}: {'margin-left': '22px'}">
+            <el-button type="text" :style="data.type === 'cascader'?{'font-size': '13px'}:{}" @click="data.type === 'cascader'?handleAddCascaderTopDialog():handleAddOption()">
+              {{ $t('fm.actions.addOption') }}
+            </el-button>
+          </div>
+        </template>
+      </el-form-item>
+      <!-- 默认值 -->
+      <el-form-item v-if="Object.keys(data.options).indexOf('defaultValue')>=0 && (data.type == 'textarea' || data.type == 'input' || data.type=='rate' || data.type=='color' || data.type=='switch')" :label="$t('fm.config.widget.defaultValue')">
+        <el-input v-if="data.type=='textarea'" v-model="data.options.defaultValue" type="textarea" :rows="5" />
+        <el-input v-if="data.type=='input'" v-model="data.options.defaultValue" />
+        <el-rate v-if="data.type == 'rate'" v-model="data.options.defaultValue" style="display:inline-block;vertical-align: middle;" :max="data.options.max" :allow-half="data.options.allowHalf" />
+        <el-button v-if="data.type == 'rate'" type="text" style="display:inline-block;vertical-align: middle;margin-left: 10px;" @click="data.options.defaultValue=0">{{ $t('fm.actions.clear') }}</el-button>
+        <el-color-picker
+          v-if="data.type == 'color'"
+          v-model="data.options.defaultValue"
+          :show-alpha="data.options.showAlpha"
+        />
+        <el-switch v-if="data.type=='switch'" v-model="data.options.defaultValue" />
+      </el-form-item>
+      <!-- 显示类型 -->
+      <template v-if="data.type == 'time' || data.type == 'date'">
+        <el-form-item v-if="data.type == 'date'" :label="$t('fm.config.widget.showType')">
+          <el-select v-model="data.options.type">
+            <el-option value="year" />
+            <el-option value="month" />
+            <el-option value="date" />
+            <el-option value="dates" />
+            <!-- <el-option value="week"></el-option> -->
+            <el-option value="datetime" />
+            <el-option value="datetimerange" />
+            <el-option value="daterange" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="data.type == 'time'" :label="$t('fm.config.widget.isRange')">
+          <el-switch
+            v-model="data.options.isRange"
+          />
+        </el-form-item>
+        <el-form-item v-if="data.type == 'date'" :label="$t('fm.config.widget.isTimestamp')">
+          <el-switch
+            v-model="data.options.timestamp"
+          />
+        </el-form-item>
+        <el-form-item v-if="(!data.options.isRange && data.type == 'time') || (data.type != 'time' && data.options.type != 'datetimerange' && data.options.type != 'daterange')" :label="$t('fm.config.widget.placeholder')">
+          <el-input v-model="data.options.placeholder" />
+        </el-form-item>
+        <el-form-item v-if="(data.options.isRange) || data.options.type=='datetimerange' || data.options.type=='daterange'" :label="$t('fm.config.widget.startPlaceholder')">
+          <el-input v-model="data.options.startPlaceholder" />
+        </el-form-item>
+        <el-form-item v-if="data.options.isRange || data.options.type=='datetimerange' || data.options.type=='daterange'" :label="$t('fm.config.widget.endPlaceholder')">
+          <el-input v-model="data.options.endPlaceholder" />
+        </el-form-item>
+        <el-form-item :label="$t('fm.config.widget.format')">
+          <el-input v-model="data.options.format" />
+        </el-form-item>
+        <el-form-item v-if="data.type=='time' && Object.keys(data.options).indexOf('isRange')>=0" :label="$t('fm.config.widget.defaultValue')">
+          <el-time-picker
+            v-if="!data.options.isRange"
+            key="1"
+            v-model="data.options.defaultValue"
+            style="width: 100%;"
+            :arrow-control="data.options.arrowControl"
+            :value-format="data.options.format"
+          />
+          <el-time-picker
+            v-if="data.options.isRange"
+            key="2"
+            v-model="data.options.defaultValue"
+            style="width: 100%;"
+            is-range
+            :arrow-control="data.options.arrowControl"
+            :value-format="data.options.format"
+          />
+        </el-form-item>
+      </template>
+      <!-- 图片上传 -->
+      <template v-if="data.type=='imgupload' || data.type=='file'">
+
+        <el-form-item :label="$t('fm.config.widget.limit')">
+          <el-input v-model.number="data.options.length" type="number" />
+        </el-form-item>
+        <el-form-item v-if="Object.keys(data.options).indexOf('isQiniu')>0" :label="$t('fm.config.widget.isQiniu')">
+          <el-switch v-model="data.options.isQiniu" />
+        </el-form-item>
+        <template v-if="data.options.isQiniu">
+          <el-form-item label="Domain" :required="true">
+            <el-input v-model="data.options.domain" />
+          </el-form-item>
+          <el-form-item :label="$t('fm.config.widget.tokenFunc')" :required="true">
+            <el-input v-model="data.options.tokenFunc" />
+          </el-form-item>
+        </template>
+        <template v-else>
+          <el-form-item :label="$t('fm.config.widget.imageAction')" :required="true">
+            <el-input v-model="data.options.action" />
+          </el-form-item>
+          <el-form-item :label="$t('fm.config.widget.setHeaders')">
+            <el-row v-for="(uploadItem, uploadIndex) in headers" :key="uploadIndex">
+              <el-col :span="10">
+                <el-input
+                  v-model="uploadItem.key"
+                  type="textarea"
+                  :rows="1"
+                  placeholder="KEY"
+                />
+              </el-col>
+              <el-col :span="10" style="float: left; margin-left: 10px">
+                <el-input
+                  v-model="uploadItem.value"
+                  type="textarea"
+                  :rows="1"
+                  placeholder="VALUE"
+                />
+              </el-col>
+              <el-col :span="2">
+                <el-button
+                  type="danger"
+                  icon="el-icon-delete"
+                  plain
+                  circle
+                  style="padding: 4px; margin-left: 5px;"
+                  @click="handleDelHeader(uploadIndex)"
+                />
+              </el-col>
+            </el-row>
+            <el-button type="text" style="font-size: 12px; color: #1890ff" @click="handleAddHeader">添加</el-button>
+          </el-form-item>
+        </template>
+      </template>
+      <!-- 多行文本 -->
+      <template v-if="data.type==='text'">
+        <el-form-item label="文字内容">
+          <el-input v-model="data.options.defaultValue" placeholder="请输入文字内容" />
+        </el-form-item>
+        <el-form-item label="文字大小">
+          <el-input v-model="data.options.font_size" placeholder="请输入字体大小" />
+        </el-form-item>
+        <el-form-item label="文字颜色">
+          <el-input v-model="data.options.font_color" placeholder="请输入文字颜色" />
+        </el-form-item>
+        <el-form-item label="设置粗体">
+          <el-input v-model="data.options.font_weight" placeholder="请输入设置粗体" />
+        </el-form-item>
+        <el-form-item label="字体属性">
+          <el-input v-model="data.options.font_family" placeholder="请输入字体属性" />
+        </el-form-item>
+      </template>
+      <!-- 分割符 -->
+      <template v-if="data.type==='divider'">
+        <el-form-item label="文字内容">
+          <el-input v-model="data.options.defaultValue" placeholder="请输入文字内容" />
+        </el-form-item>
+        <el-form-item label="文字位置">
+          <el-radio-group v-model="data.options.content_position">
+            <el-radio-button label="left">左</el-radio-button>
+            <el-radio-button label="center">中间</el-radio-button>
+            <el-radio-button label="right">右</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="分割线方向">
+          <el-radio-group v-model="data.options.direction">
+            <el-radio-button label="horizontal">横</el-radio-button>
+            <el-radio-button label="vertical">竖</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="文字大小">
+          <el-input v-model="data.options.font_size" placeholder="请输入字体大小" />
+        </el-form-item>
+        <el-form-item label="文字颜色">
+          <el-input v-model="data.options.font_color" placeholder="请输入文字颜色" />
+        </el-form-item>
+        <el-form-item label="设置粗体">
+          <el-input v-model="data.options.font_weight" placeholder="请输入设置粗体" />
+        </el-form-item>
+        <el-form-item label="字体属性">
+          <el-input v-model="data.options.font_family" placeholder="请输入字体属性" />
+        </el-form-item>
+      </template>
+
+      <template v-if="data.type==='blank'">
+        <el-form-item :label="$t('fm.config.widget.defaultType')">
+          <el-select v-model="data.options.defaultType">
+            <el-option value="String" :label="$t('fm.config.widget.string')" />
+            <el-option value="Object" :label="$t('fm.config.widget.object')" />
+            <el-option value="Array" :label="$t('fm.config.widget.array')" />
+          </el-select>
+        </el-form-item>
+      </template>
+      <!-- 珊格 -->
+      <template v-if="data.type === 'grid'">
+        <el-form-item :label="$t('fm.config.widget.gutter')">
+          <el-input v-model.number="data.options.gutter" type="number" />
+        </el-form-item>
+        <el-form-item :label="$t('fm.config.widget.columnOption')">
+          <draggable
+            tag="ul"
+            :list="data.columns"
+            v-bind="{group:{ name:'options'}, ghostClass: 'ghost',handle: '.drag-item'}"
+            handle=".drag-item"
+          >
+            <li v-for="(item, index) in data.columns" :key="index">
+              <i class="drag-item" style="font-size: 16px;margin: 0 5px;cursor: move;"><i class="iconfont icon-icon_bars" /></i>
+              <el-input v-model.number="item.span" :placeholder="$t('fm.config.widget.span')" size="mini" style="width: 100px;" type="number" />
+
+              <el-button circle plain type="danger" size="mini" icon="el-icon-minus" style="padding: 4px;margin-left: 5px;" @click="handleOptionsRemove(index)" />
+
+            </li>
+          </draggable>
+          <div style="margin-left: 22px;">
+            <el-button type="text" @click="handleAddColumn">{{ $t('fm.actions.addColumn') }}</el-button>
+          </div>
+        </el-form-item>
+        <el-form-item :label="$t('fm.config.widget.justify')">
+          <el-select v-model="data.options.justify">
+            <el-option value="start" :label="$t('fm.config.widget.justifyStart')" />
+            <el-option value="end" :label="$t('fm.config.widget.justifyEnd')" />
+            <el-option value="center" :label="$t('fm.config.widget.justifyCenter')" />
+            <el-option value="space-around" :label="$t('fm.config.widget.justifySpaceAround')" />
+            <el-option value="space-between" :label="$t('fm.config.widget.justifySpaceBetween')" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="$t('fm.config.widget.align')">
+          <el-select v-model="data.options.align">
+            <el-option value="top" :label="$t('fm.config.widget.alignTop')" />
+            <el-option value="middle" :label="$t('fm.config.widget.alignMiddle')" />
+            <el-option value="bottom" :label="$t('fm.config.widget.alignBottom')" />
+          </el-select>
+        </el-form-item>
+      </template>
+      <!-- 非珊格 -->
+      <template v-if="data.type !== 'grid'">
+        <el-form-item :label="$t('fm.config.widget.attribute')">
+          <el-checkbox v-if="Object.keys(data.options).indexOf('readonly')>=0" v-model="data.options.readonly">{{ $t('fm.config.widget.readonly') }}</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('disabled')>=0" v-model="data.options.disabled">{{ $t('fm.config.widget.disabled') }}	</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('showPassword')>=0" v-model="data.options.showPassword">{{ $t('fm.config.widget.showPassword') }}	</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('editable')>=0" v-model="data.options.editable">{{ $t('fm.config.widget.editable') }}</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('clearable')>=0" v-model="data.options.clearable">{{ $t('fm.config.widget.clearable') }} </el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('arrowControl')>=0" v-model="data.options.arrowControl">{{ $t('fm.config.widget.arrowControl') }}</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('isDelete')>=0" v-model="data.options.isDelete">{{ $t('fm.config.widget.isDelete') }}</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('isEdit')>=0" v-model="data.options.isEdit">{{ $t('fm.config.widget.isEdit') }}</el-checkbox>
+          <el-checkbox v-if="Object.keys(data.options).indexOf('showAllLevels')>=0" v-model="data.options.showAllLevels">{{ $t('fm.config.widget.showAllLevels') }}</el-checkbox>
+        </el-form-item>
+        <el-form-item :label="$t('fm.config.widget.validate')">
+          <div v-if="Object.keys(data.options).indexOf('required')>=0">
+            <el-checkbox v-model="data.options.required">{{ $t('fm.config.widget.required') }}</el-checkbox>
+          </div>
+          <el-select v-if="Object.keys(data.options).indexOf('dataType')>=0" v-model="data.options.dataType" size="mini">
+            <el-option value="string" :label="$t('fm.config.widget.string')" />
+            <el-option value="number" :label="$t('fm.config.widget.number')" />
+            <el-option value="boolean" :label="$t('fm.config.widget.boolean')" />
+            <el-option value="integer" :label="$t('fm.config.widget.integer')" />
+            <el-option value="float" :label="$t('fm.config.widget.float')" />
+            <el-option value="url" :label="$t('fm.config.widget.url')" />
+            <el-option value="email" :label="$t('fm.config.widget.email')" />
+            <el-option value="hex" :label="$t('fm.config.widget.hex')" />
+          </el-select>
+
+          <div v-if="Object.keys(data.options).indexOf('pattern')>=0">
+            <el-input v-model.lazy="data.options.pattern" size="mini" style=" width: 240px;" :placeholder="$t('fm.config.widget.patternPlaceholder')" />
+          </div>
+        </el-form-item>
+      </template>
+      <el-form-item v-if="Object.keys(data.options).indexOf('displayVerifiy')>=0" :label="$t('fm.config.widget.displayVerifiy')">
+        <el-radio-group v-model="data.options.displayVerifiy.type">
+          <el-radio label="hide">不校验</el-radio>
+          <el-radio label="and">与</el-radio>
+          <el-radio label="or">或</el-radio>
+        </el-radio-group>
+        <div v-if="data.options.displayVerifiy.type !== 'hide'">
+          <template v-for="(item, index) in data.options.displayVerifiy.list">
+            <div :key="item.model">
+              <el-input v-model="item.model" size="mini" :placeholder="$t('fm.config.widget.displayVerifiyPlaceholderModel')" />
+              <el-input v-model="item.value" size="mini" :placeholder="$t('fm.config.widget.displayVerifiyPlaceholderValue')" />
+              <el-button v-if="index > 0" type="text" icon="el-icon-remove-outline" @click="delDisplayVerifiy(index)">删  除</el-button>
+              <hr v-if="data.options.displayVerifiy.list.length > 1" style="background-color: #dcdfe6; border:none; height:1px;">
+            </div>
+          </template>
+          <el-button type="text" icon="el-icon-circle-plus-outline" @click="addDisplayVerifiy">新  增</el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+    <el-dialog
+      title="提示"
+      :visible.sync="cascaderDialog"
+      width="30%"
+      append-to-body
+      :before-close="handleClose"
+    >
+      <div>
+        <el-form ref="addTreeData" :model="addTreeData" label-width="80px">
+          <el-form-item
+            prop="label"
+            label="Label"
+            :rules="{ required: true, message: 'Label不能为空', trigger: 'blur' }"
+          >
+            <el-input v-model="addTreeData.label" style="width: 95%" />
+          </el-form-item>
+          <el-form-item
+            prop="value"
+            label="Value"
+            :rules="{ required: true, message: 'Value不能为空', trigger: 'blur' }"
+          >
+            <el-input v-model="addTreeData.value" style="width: 95%" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="cascaderDialog = false">取 消</el-button>
+        <el-button type="primary" @click="operatingStatus==='add'?appendCascaderData():cascaderDialog = false">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Draggable from 'vuedraggable'
+
+export default {
+  components: {
+    Draggable
+  },
+  /* eslint-disable */
+  props: ['data'],
+  data() {
+    return {
+      selectTreeData: {},
+      addTreeData: {},
+      cascaderDialog: false,
+      operatingStatus: 'add',
+      validator: {
+        type: null,
+        required: null,
+        pattern: null,
+        range: null,
+        length: null
+      },
+      headers: []
+    }
+  },
+  computed: {
+    show() {
+      if (this.data && Object.keys(this.data).length > 0) {
+        return true
+      }
+      return false
+    }
+  },
+  watch: {
+    'data.options.isRange': function(val) {
+      if (typeof val !== 'undefined') {
+        if (val) {
+          this.data.options.defaultValue = null
+        } else {
+          if (Object.keys(this.data.options).indexOf('defaultValue') >= 0) { this.data.options.defaultValue = '' }
+        }
+      }
+    },
+    'data.options.required': function(val) {
+      this.validateRequired(val)
+    },
+    'data.options.dataType': function(val) {
+      this.validateDataType(val)
+    },
+    'data.options.pattern': function(val) {
+      this.valiatePattern(val)
+    },
+    'data.name': function(val) {
+      if (this.data.options) {
+        this.validateRequired(this.data.options.required)
+        this.validateDataType(this.data.options.dataType)
+        this.valiatePattern(this.data.options.pattern)
+      }
+    },
+    headers: {
+      handler: function (val) {
+        if (this.data.options) {
+          if (this.headers.length > 0) {
+            this.data.options.headers = {}
+            for (var headerValue of this.headers) {
+              this.data.options.headers[headerValue.key] = headerValue.value
+            }
+          } else {
+            this.data.options.headers = {}
+          }
+        }
+      },
+      deep: true
+    }
+  },
+  created() {
+    this.handleInitHeaders()
+    console.log(this.data)
+  },
+  methods: {
+    addDisplayVerifiy() {
+      this.data.options.displayVerifiy.list.push({
+        model: (new Date()).valueOf(),
+        value: '字段值'
+      })
+    },
+    delDisplayVerifiy(index) {
+      this.data.options.displayVerifiy.list.splice(index, 1)
+    },
+    // 级联选择器
+    handleAddCascaderTopDialog() {
+      this.selectTreeData = "top"
+      this.addTreeData = {}
+      this.cascaderDialog = true
+    },
+    handleClose() {},
+    appendCascaderDialog(val) {
+      this.operatingStatus = 'add'
+      this.addTreeData = {}
+      this.selectTreeData = val
+      this.cascaderDialog = true
+    },
+    appendCascaderData() {
+      this.$refs['addTreeData'].validate((valid) => {
+        if (valid) {
+          if (this.selectTreeData === 'top') {
+            this.data.options.options.push(this.addTreeData)
+          } else {
+            if (this.selectTreeData.children) {
+              this.selectTreeData.children.push(this.addTreeData);
+            } else {
+              this.$set(this.selectTreeData, 'children', [this.addTreeData]);
+            }
+          }
+          this.cascaderDialog = false
+        }
+      })
+    },
+    editCascaderData(val) {
+      this.operatingStatus = 'edit'
+      this.addTreeData = val
+      this.cascaderDialog = true
+    },
+    removeCascaderData(node, data) {
+      const parent = node.parent;
+      const children = parent.data.children || parent.data;
+      const index = children.findIndex(d => d.id === data.id);
+      children.splice(index, 1);
+    },
+
+    handleInitHeaders() {
+      if (this.data.options) {
+        for (var key in this.data.options.headers) {
+          this.headers.push({
+            key: key,
+            value: this.data.options.headers[key]
+          })
+        }
+      }
+    },
+    handleAddHeader() {
+      this.headers.push({
+        key: '',
+        value: ''
+      })
+    },
+    handleDelHeader(index) {
+      this.headers.splice(index, 1)
+    },
+    handleOptionsRemove(index) {
+      if (this.data.type === 'grid') {
+        this.data.columns.splice(index, 1)
+      } else {
+        this.data.options.options.splice(index, 1)
+      }
+    },
+    handleAddOption() {
+      if (this.data.options.showLabel) {
+        this.data.options.options.push({
+          value: this.$t('fm.config.widget.newOption'),
+          label: this.$t('fm.config.widget.newOption')
+        })
+      } else {
+        this.data.options.options.push({
+          value: this.$t('fm.config.widget.newOption')
+        })
+      }
+    },
+    handleAddColumn() {
+      this.data.columns.push({
+        span: '',
+        list: []
+      })
+    },
+    generateRule() {
+      this.data.rules = []
+      Object.keys(this.validator).forEach(key => {
+        if (this.validator[key]) {
+          this.data.rules.push(this.validator[key])
+        }
+      })
+    },
+    handleSelectMuliple(value) {
+      if (value) {
+        if (this.data.options.defaultValue) {
+          this.data.options.defaultValue = [this.data.options.defaultValue]
+        } else {
+          this.data.options.defaultValue = []
+        }
+      } else {
+        if (this.data.options.defaultValue.length > 0) {
+          this.data.options.defaultValue = this.data.options.defaultValue[0]
+        } else {
+          this.data.options.defaultValue = ''
+        }
+      }
+    },
+
+    validateRequired(val) {
+      if (val) {
+        this.validator.required = { required: true, message: `${this.data.name}${this.$t('fm.config.widget.validatorRequired')}` }
+      } else {
+        this.validator.required = null
+      }
+
+      this.$nextTick(() => {
+        this.generateRule()
+      })
+    },
+
+    validateDataType(val) {
+      if (!this.show) {
+        return false
+      }
+
+      if (val) {
+        this.validator.type = { type: val, message: this.data.name + this.$t('fm.config.widget.validatorType') }
+      } else {
+        this.validator.type = null
+      }
+
+      this.generateRule()
+    },
+    valiatePattern(val) {
+      if (!this.show) {
+        return false
+      }
+
+      if (val) {
+        this.validator.pattern = { pattern: val, message: this.data.name + this.$t('fm.config.widget.validatorPattern') }
+      } else {
+        this.validator.pattern = null
+      }
+
+      this.generateRule()
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .custom-tree-node {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    padding-right: 8px;
+  }
+</style>

+ 290 - 0
src/components/VueFormMaking/components/WidgetForm.vue

@@ -0,0 +1,290 @@
+<template>
+  <div class="widget-form-container">
+    <div v-if="data.list.length == 0" class="form-empty">{{ $t('fm.description.containerEmpty') }}</div>
+    <el-form :size="data.config.size" label-suffix=":" :label-position="data.config.labelPosition" :label-width="data.config.labelWidth + 'px'">
+
+      <draggable
+        v-model="data.list"
+        class=""
+        v-bind="{group:'people', ghostClass: 'ghost',animation: 200, handle: '.drag-widget'}"
+        @end="handleMoveEnd"
+        @add="handleWidgetAdd"
+      >
+
+        <transition-group name="fade" tag="div" class="widget-form-list">
+          <template v-for="(element, index) in data.list">
+            <!-- 珊格 -->
+            <template v-if="element.type == 'grid'">
+              <el-row
+                v-if="element && element.key"
+                :key="element.key"
+                class="widget-col widget-view"
+                type="flex"
+                :class="{active: selectWidget.key == element.key}"
+                :gutter="element.options.gutter ? element.options.gutter : 0"
+                :justify="element.options.justify"
+                :align="element.options.align"
+                @click.native="handleSelectWidget(index)"
+              >
+                <el-col v-for="(col, colIndex) in element.columns" :key="colIndex" :span="col.span ? col.span : 0">
+
+                  <draggable
+                    v-model="col.list"
+                    :no-transition-on-drag="true"
+                    v-bind="{group:'people', ghostClass: 'ghost',animation: 200, handle: '.drag-widget'}"
+                    @end="handleMoveEnd"
+                    @add="handleWidgetColAdd($event, element, colIndex)"
+                  >
+                    <transition-group name="fade" tag="div" class="widget-col-list">
+                      <template v-for="(el, i) in col.list">
+                        <widget-form-item
+                          v-if="el.key"
+                          :key="el.key"
+                          :element="el"
+                          :select.sync="selectWidget"
+                          :index="i"
+                          :data="col"
+                          :data-config="data"
+                        />
+                      </template>
+
+                    </transition-group>
+
+                  </draggable>
+                </el-col>
+                <div v-if="selectWidget.key == element.key" class="widget-view-action widget-col-action">
+
+                  <i class="iconfont icon-trash" @click.stop="handleWidgetDelete(index)" />
+                </div>
+
+                <div v-if="selectWidget.key == element.key" class="widget-view-drag widget-col-drag">
+                  <i class="iconfont icon-drag drag-widget" />
+                </div>
+              </el-row>
+            </template>
+            <!-- 子表单 -->
+            <template v-else-if="element.type == 'subform'">
+              <el-row
+                v-if="element && element.key"
+                :key="element.key"
+              >
+                <el-form-item
+                  class="widget-col widget-view"
+                  :label-width="element.options.labelWidthStatus?data.config.labelWidth + 'px': '0px'"
+                  :class="{active: selectWidget.key === element.key, 'is_req': element.options.required}"
+                  :label="element.options.labelWidthStatus?element.name:''"
+                  @click.native="handleSelectWidget(index)"
+                >
+                  <div
+                    type="flex"
+                    :class="{active: selectWidget.key == element.key}"
+                    :gutter="element.options.gutter ? element.options.gutter : 0"
+                    :justify="element.options.justify"
+                    :align="element.options.align"
+                  >
+                    <el-col v-for="(col, colIndex) in element.columns" :key="colIndex" :span="col.span ? col.span : 0">
+                      <draggable
+                        v-model="col.list"
+                        :no-transition-on-drag="true"
+                        v-bind="{group:'people', ghostClass: 'ghost',animation: 200, handle: '.drag-widget'}"
+                        @end="handleMoveEnd"
+                        @add="handleWidgetColAdd($event, element, colIndex)"
+                      >
+                        <transition-group
+                          name="fade"
+                          tag="div"
+                          class="widget-col-list"
+                          style="min-height: 131px;overflow-x: auto; white-space: nowrap;"
+                        >
+                          <template v-for="(el, i) in col.list">
+                            <div
+                              v-if="el && el.key"
+                              :key="el.key"
+                              @click="handleSelectWidget(i)"
+                            >
+                              <widget-form-item
+                                :element="el"
+                                :select.sync="selectWidget"
+                                :index="i"
+                                :data="col"
+                                :data-config="data"
+                                :is-label="true"
+                                :is-table="true"
+                              />
+                            </div>
+                          </template>
+                        </transition-group>
+                      </draggable>
+                    </el-col>
+
+                    <div v-if="selectWidget.key == element.key" class="widget-view-action widget-col-action">
+                      <i class="iconfont icon-trash" @click.stop="handleWidgetDelete(index)" />
+                    </div>
+
+                    <div v-if="selectWidget.key == element.key" class="widget-view-drag widget-col-drag">
+                      <i class="iconfont icon-drag drag-widget" />
+                    </div>
+                  </div>
+                </el-form-item>
+              </el-row>
+            </template>
+            <template v-else>
+              <widget-form-item
+                v-if="element && element.key"
+                :key="element.key"
+                :element="element"
+                :select.sync="selectWidget"
+                :index="index"
+                :data-config="data"
+                :data="data"
+              />
+            </template>
+          </template>
+        </transition-group>
+      </draggable>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import Draggable from 'vuedraggable'
+import WidgetFormItem from './WidgetFormItem'
+
+export default {
+  components: {
+    Draggable,
+    WidgetFormItem
+  },
+  /* eslint-disable */
+  props: ['data', 'select'],
+  data() {
+    return {
+      selectWidget: this.select
+    }
+  },
+  watch: {
+    select(val) {
+      this.selectWidget = val
+    },
+    selectWidget: {
+      handler(val) {
+        this.$emit('update:select', val)
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    document.body.ondrop = function(event) {
+      const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
+      if (isFirefox) {
+        event.preventDefault()
+        event.stopPropagation()
+      }
+    }
+  },
+  methods: {
+    handleMoveEnd({ newIndex, oldIndex }) {
+    },
+    handleSelectWidget(index) {
+      this.selectWidget = this.data.list[index]
+    },
+    handleWidgetAdd(evt) {
+      const newIndex = evt.newIndex
+      const to = evt.to
+
+      // 为拖拽到容器的元素添加唯一 key
+      const key = Date.parse(new Date()) + '_' + Math.ceil(Math.random() * 99999)
+      this.$set(this.data.list, newIndex, {
+        ...this.data.list[newIndex],
+        options: {
+          ...this.data.list[newIndex].options,
+          remoteFunc: 'func_' + key
+        },
+        key,
+        // 绑定键值
+        model: this.data.list[newIndex].type + '_' + key,
+        rules: []
+      })
+
+      if (this.data.list[newIndex].type === 'radio' || this.data.list[newIndex].type === 'checkbox' || this.data.list[newIndex].type === 'select') {
+        this.$set(this.data.list, newIndex, {
+          ...this.data.list[newIndex],
+          options: {
+            ...this.data.list[newIndex].options,
+            options: this.data.list[newIndex].options.options.map(item => ({
+              ...item
+            }))
+          }
+        })
+      }
+
+      if (this.data.list[newIndex].type === 'grid' || this.data.list[newIndex].type === 'subform') {
+        this.$set(this.data.list, newIndex, {
+          ...this.data.list[newIndex],
+          columns: this.data.list[newIndex].columns.map(item => ({ ...item }))
+        })
+      }
+
+      this.selectWidget = this.data.list[newIndex]
+    },
+    handleWidgetColAdd($event, row, colIndex) {
+      const newIndex = $event.newIndex
+      const oldIndex = $event.oldIndex
+      const item = $event.item
+
+      // 防止布局元素的嵌套拖拽
+      if (item.className.indexOf('data-grid') >= 0) {
+        // 如果是列表中拖拽的元素需要还原到原来位置
+        item.tagName === 'DIV' && this.data.list.splice(oldIndex, 0, row.columns[colIndex].list[newIndex])
+
+        row.columns[colIndex].list.splice(newIndex, 1)
+
+        return false
+      }
+
+      const key = Date.parse(new Date()) + '_' + Math.ceil(Math.random() * 99999)
+
+      this.$set(row.columns[colIndex].list, newIndex, {
+        ...row.columns[colIndex].list[newIndex],
+        options: {
+          ...row.columns[colIndex].list[newIndex].options,
+          remoteFunc: 'func_' + key
+        },
+        key,
+        // 绑定键值
+        model: row.columns[colIndex].list[newIndex].type + '_' + key,
+        rules: []
+      })
+
+      if (row.columns[colIndex].list[newIndex].type === 'radio' || row.columns[colIndex].list[newIndex].type === 'checkbox' || row.columns[colIndex].list[newIndex].type === 'select') {
+        this.$set(row.columns[colIndex].list, newIndex, {
+          ...row.columns[colIndex].list[newIndex],
+          options: {
+            ...row.columns[colIndex].list[newIndex].options,
+            options: row.columns[colIndex].list[newIndex].options.options.map(item => ({
+              ...item
+            }))
+          }
+        })
+      }
+
+      this.selectWidget = row.columns[colIndex].list[newIndex]
+    },
+    handleWidgetDelete(index) {
+      if (this.data.list.length - 1 === index) {
+        if (index === 0) {
+          this.selectWidget = {}
+        } else {
+          this.selectWidget = this.data.list[index - 1]
+        }
+      } else {
+        this.selectWidget = this.data.list[index + 1]
+      }
+
+      this.$nextTick(() => {
+        this.data.list.splice(index, 1)
+      })
+    }
+  }
+}
+</script>

+ 232 - 0
src/components/VueFormMaking/components/WidgetFormFields.vue

@@ -0,0 +1,232 @@
+<template>
+  <div>
+    <template v-if="element.type == 'input'">
+      <el-input
+        v-model="element.options.defaultValue"
+        :style="{width: element.options.width}"
+        :placeholder="element.options.placeholder"
+        :disabled="element.options.disabled"
+      />
+    </template>
+
+    <template v-if="element.type == 'textarea'">
+      <el-input
+        v-model="element.options.defaultValue"
+        type="textarea"
+        :rows="5"
+        :style="{width: element.options.width}"
+        :disabled="element.options.disabled"
+        :placeholder="element.options.placeholder"
+      />
+    </template>
+
+    <template v-if="element.type == 'number'">
+      <el-input-number
+        v-model="element.options.defaultValue"
+        :disabled="element.options.disabled"
+        :controls-position="element.options.controlsPosition"
+        :style="{width: element.options.width}"
+      />
+    </template>
+
+    <template v-if="element.type == 'radio'">
+      <el-radio-group
+        v-model="element.options.defaultValue"
+        :style="{width: element.options.width}"
+        :disabled="element.options.disabled"
+      >
+        <el-radio
+          v-for="(item, index2) in element.options.options"
+          :key="item.value + index2"
+          :style="{display: element.options.inline ? 'inline-block' : 'block'}"
+          :label="item.value"
+        >
+          {{ element.options.showLabel ? item.label : item.value }}
+        </el-radio>
+      </el-radio-group>
+    </template>
+
+    <template v-if="element.type == 'checkbox'">
+      <el-checkbox-group
+        v-model="element.options.defaultValue"
+        :style="{width: element.options.width}"
+        :disabled="element.options.disabled"
+      >
+        <el-checkbox
+          v-for="(item, index1) in element.options.options"
+          :key="item.value + index1"
+          :style="{display: element.options.inline ? 'inline-block' : 'block'}"
+          :label="item.value"
+        >
+          {{ element.options.showLabel ? item.label : item.value }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </template>
+
+    <template v-if="element.type == 'time'">
+      <el-time-picker
+        v-model="element.options.defaultValue"
+        :is-range="element.options.isRange"
+        :placeholder="element.options.placeholder"
+        :start-placeholder="element.options.startPlaceholder"
+        :end-placeholder="element.options.endPlaceholder"
+        :readonly="element.options.readonly"
+        :disabled="element.options.disabled"
+        :editable="element.options.editable"
+        :clearable="element.options.clearable"
+        :arrow-control="element.options.arrowControl"
+        :style="{width: element.options.width}"
+      />
+    </template>
+
+    <template v-if="element.type == 'date'">
+      <el-date-picker
+        v-model="element.options.defaultValue"
+        :type="element.options.type"
+        :is-range="element.options.isRange"
+        :placeholder="element.options.placeholder"
+        :start-placeholder="element.options.startPlaceholder"
+        :end-placeholder="element.options.endPlaceholder"
+        :readonly="element.options.readonly"
+        :disabled="element.options.disabled"
+        :editable="element.options.editable"
+        :clearable="element.options.clearable"
+        :style="{width: element.options.width}"
+      />
+    </template>
+
+    <template v-if="element.type == 'rate'">
+      <el-rate
+        v-model="element.options.defaultValue"
+        :max="element.options.max"
+        :disabled="element.options.disabled"
+        :allow-half="element.options.allowHalf"
+      />
+    </template>
+
+    <template v-if="element.type == 'color'">
+      <el-color-picker
+        v-model="element.options.defaultValue"
+        :disabled="element.options.disabled"
+        :show-alpha="element.options.showAlpha"
+      />
+    </template>
+
+    <template v-if="element.type == 'select'">
+      <el-select
+        v-model="element.options.defaultValue"
+        :disabled="element.options.disabled"
+        :multiple="element.options.multiple"
+        :clearable="element.options.clearable"
+        :placeholder="element.options.placeholder"
+        :style="{width: element.options.width}"
+      >
+        <el-option v-for="item in element.options.options" :key="item.value" :value="item.value" :label="element.options.showLabel?item.label:item.value" />
+      </el-select>
+    </template>
+
+    <template v-if="element.type=='switch'">
+      <el-switch
+        v-model="element.options.defaultValue"
+        :disabled="element.options.disabled"
+      />
+    </template>
+
+    <template v-if="element.type=='slider'">
+      <el-slider
+        v-model="element.options.defaultValue"
+        :min="element.options.min"
+        :max="element.options.max"
+        :disabled="element.options.disabled"
+        :step="element.options.step"
+        :show-input="element.options.showInput"
+        :range="element.options.range"
+        :style="{width: element.options.width}"
+      />
+    </template>
+
+    <template v-if="element.type=='imgupload'">
+      <fm-upload
+        v-model="element.options.defaultValue"
+        :disabled="element.options.disabled"
+        :style="{'width': element.options.width}"
+        :width="element.options.size.width"
+        :height="element.options.size.height"
+        token="xxx"
+        domain="xxx"
+      />
+    </template>
+
+    <template v-if="element.type=='file'">
+      <FileUpload :element="element" />
+    </template>
+
+    <template v-if="element.type == 'cascader'">
+      <el-cascader
+        v-model="element.options.defaultValue"
+        :disabled="element.options.disabled"
+        :show-all-levels="element.options.showAllLevels"
+        :clearable="element.options.clearable"
+        :placeholder="element.options.placeholder"
+        :style="{width: element.options.width}"
+        :options="element.options.remote?element.options.remoteOptions:element.options.options"
+      />
+    </template>
+
+    <template v-if="element.type == 'editor'">
+      <vue-editor
+        v-model="element.options.defaultValue"
+        :style="{width: element.options.width}"
+      />
+    </template>
+
+    <template v-if="element.type=='blank'">
+      <div style="height: 50px;color: #999;background: #eee;line-height:50px;text-align:center;">{{ $t('fm.components.fields.blank') }}</div>
+    </template>
+
+    <template v-if="element.type === 'text'">
+      <span
+        :style="{
+          'font-size': element.options.font_size,
+          'font-family': element.options.font_family,
+          'font-weight': element.options.font_weight,
+          'color': element.options.font_color
+        }"
+      >
+        {{ element.options.defaultValue }}
+      </span>
+    </template>
+
+    <template v-if="element.type === 'divider'">
+      <el-divider
+        :direction="element.options.direction"
+        :content-position="element.options.content_position"
+      >
+        <span
+          :style="{
+            'font-size': element.options.font_size,
+            'font-family': element.options.font_family,
+            'font-weight': element.options.font_weight,
+            'color': element.options.font_color
+          }"
+        >
+          {{ element.options.defaultValue }}
+        </span>
+      </el-divider>
+    </template>
+  </div>
+</template>
+
+<script>
+import FmUpload from './Upload'
+import FileUpload from './Upload/file'
+export default {
+  name: 'WidgetFormFields',
+  /* eslint-disable */ 
+  props: ['element'],
+  components: {
+    FmUpload,
+    FileUpload
+  }
+}
+</script>

Some files were not shown because too many files changed in this diff