瀏覽代碼

workflow_test

guojy 1 年之前
父節點
當前提交
333e1962ac
共有 100 個文件被更改,包括 15605 次插入4 次删除
  1. 103 0
      .history/build/webpack.base.conf_20240320145208.js
  2. 103 0
      .history/build/webpack.base.conf_20240320145214.js
  3. 95 0
      .history/build/webpack.base.conf_20240320145507.js
  4. 128 0
      .history/src/main_20240320144058.js
  5. 327 0
      .history/src/views/admin/FormProcessDesign_20240320143102.vue
  6. 327 0
      .history/src/views/admin/FormProcessDesign_20240320143954.vue
  7. 327 0
      .history/src/views/admin/FormProcessDesign_20240320144349.vue
  8. 327 0
      .history/src/views/admin/FormProcessDesign_20240320144420.vue
  9. 327 0
      .history/src/views/admin/FormProcessDesign_20240320144458.vue
  10. 327 0
      .history/src/views/admin/FormProcessDesign_20240320144513.vue
  11. 327 0
      .history/src/views/admin/FormProcessDesign_20240320144545.vue
  12. 327 0
      .history/src/views/admin/FormProcessDesign_20240320144558.vue
  13. 327 0
      .history/src/views/admin/FormProcessDesign_20240320145023.vue
  14. 327 0
      .history/src/views/admin/FormProcessDesign_20240320145038.vue
  15. 327 0
      .history/src/views/admin/FormProcessDesign_20240320145619.vue
  16. 327 0
      .history/src/views/admin/FormProcessDesign_20240320145951.vue
  17. 161 0
      .history/src/views/admin/LayoutHeader_20240320145850.vue
  18. 161 0
      .history/src/views/admin/LayoutHeader_20240320181810.vue
  19. 269 0
      .history/src/views/admin/layout/FormBaseSetting_20240320145626.vue
  20. 269 0
      .history/src/views/admin/layout/FormBaseSetting_20240320145948.vue
  21. 342 0
      .history/src/views/common/process/nodes/ConditionNode_20240320142911.vue
  22. 342 0
      .history/src/views/common/process/nodes/InclusiveNode_20240320142840.vue
  23. 4 1
      package.json
  24. 285 0
      src/api/design.js
  25. 32 0
      src/api/org.js
  26. 65 0
      src/api/request.js
  27. 268 0
      src/assets/iconfont/iconfont.json
  28. 54 0
      src/components/common/Ellipsis.vue
  29. 464 0
      src/components/common/OrgPicker.vue
  30. 36 0
      src/components/common/Tip.vue
  31. 119 0
      src/components/common/WDialog.vue
  32. 35 0
      src/components/common/route-tab.vue
  33. 13 3
      src/main.js
  34. 5 0
      src/router/index.js
  35. 22 0
      src/store/index.js
  36. 327 0
      src/views/admin/FormProcessDesign.vue
  37. 351 0
      src/views/admin/FormsPanel.vue
  38. 161 0
      src/views/admin/LayoutHeader.vue
  39. 269 0
      src/views/admin/layout/FormBaseSetting.vue
  40. 533 0
      src/views/admin/layout/FormDesign.vue
  41. 48 0
      src/views/admin/layout/FormProSetting.vue
  42. 103 0
      src/views/admin/layout/ProcessDesign.vue
  43. 89 0
      src/views/admin/layout/ProcessDiagramViewer.vue
  44. 53 0
      src/views/admin/layout/form/FormDesignRender.vue
  45. 168 0
      src/views/admin/layout/process/DefaultNodeProps.js
  46. 521 0
      src/views/admin/layout/process/ProcessTree.vue
  47. 449 0
      src/views/admin/layout/process/ProcessTreeViewer.vue
  48. 118 0
      src/views/common/InsertButton.vue
  49. 29 0
      src/views/common/form/ComponentExport.js
  50. 26 0
      src/views/common/form/ComponentMinxins.js
  51. 220 0
      src/views/common/form/ComponentsConfigExport.js
  52. 74 0
      src/views/common/form/FormComponentConfig.vue
  53. 103 0
      src/views/common/form/FormRender.vue
  54. 159 0
      src/views/common/form/components/AmountInput.vue
  55. 57 0
      src/views/common/form/components/DateTime.vue
  56. 122 0
      src/views/common/form/components/DateTimeRange.vue
  57. 65 0
      src/views/common/form/components/DeptPicker.vue
  58. 35 0
      src/views/common/form/components/Description.vue
  59. 125 0
      src/views/common/form/components/FileUpload.vue
  60. 163 0
      src/views/common/form/components/ImageUpload.vue
  61. 18 0
      src/views/common/form/components/Location.vue
  62. 18 0
      src/views/common/form/components/MoneyInput.vue
  63. 74 0
      src/views/common/form/components/MultipleSelect.vue
  64. 43 0
      src/views/common/form/components/NumberInput.vue
  65. 65 0
      src/views/common/form/components/SelectInput.vue
  66. 18 0
      src/views/common/form/components/SignPannel.vue
  67. 216 0
      src/views/common/form/components/SpanLayout.vue
  68. 314 0
      src/views/common/form/components/TableList.vue
  69. 43 0
      src/views/common/form/components/TextInput.vue
  70. 54 0
      src/views/common/form/components/TextareaInput.vue
  71. 77 0
      src/views/common/form/components/UserPicker.vue
  72. 37 0
      src/views/common/form/config/AmountInputConfig.vue
  73. 38 0
      src/views/common/form/config/DateTimeConfig.vue
  74. 42 0
      src/views/common/form/config/DateTimeRangeConfig.vue
  75. 33 0
      src/views/common/form/config/DescriptionConfig.vue
  76. 48 0
      src/views/common/form/config/FileUploadConfig.vue
  77. 43 0
      src/views/common/form/config/ImageUploadConfig.vue
  78. 18 0
      src/views/common/form/config/LocationConfig.vue
  79. 18 0
      src/views/common/form/config/MoneyInputConfig.vue
  80. 30 0
      src/views/common/form/config/NumberInputConfig.vue
  81. 34 0
      src/views/common/form/config/OrgPickerConfig.vue
  82. 97 0
      src/views/common/form/config/SelectInputConfig.vue
  83. 52 0
      src/views/common/form/config/TableListConfig.vue
  84. 30 0
      src/views/common/form/config/TextInputConfig.vue
  85. 30 0
      src/views/common/form/config/TextareaInputConfig.vue
  86. 48 0
      src/views/common/process/OrgItems.vue
  87. 299 0
      src/views/common/process/config/ApprovalNodeConfig.vue
  88. 69 0
      src/views/common/process/config/CcNodeConfig.vue
  89. 281 0
      src/views/common/process/config/ConditionGroupItemConfig.vue
  90. 135 0
      src/views/common/process/config/ConditionNodeConfig.vue
  91. 48 0
      src/views/common/process/config/DelayNodeConfig.vue
  92. 133 0
      src/views/common/process/config/FormAuthorityConfig.vue
  93. 281 0
      src/views/common/process/config/InclusiveGroupItemConfig.vue
  94. 135 0
      src/views/common/process/config/InclusiveNodeConfig.vue
  95. 74 0
      src/views/common/process/config/NodeConfig.vue
  96. 58 0
      src/views/common/process/config/RootNodeConfig.vue
  97. 255 0
      src/views/common/process/config/SubprocessNodeConfig.vue
  98. 298 0
      src/views/common/process/config/TaskNodeConfig.vue
  99. 193 0
      src/views/common/process/config/TriggerNodeConfig.vue
  100. 141 0
      src/views/common/process/nodes/ApprovalNode.vue

+ 103 - 0
.history/build/webpack.base.conf_20240320145208.js

@@ -0,0 +1,103 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const { VueLoaderPlugin } = require('vue-loader')
+const vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+module.exports = {
+  context: path.resolve(__dirname, '../'),
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    filename: '[name].js',
+    publicPath:
+      process.env.NODE_ENV === 'production'
+        ? config.build.assetsPublicPath
+        : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      '@': resolve('src')
+    }
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {  
+        test: /\.less$/,  
+        use: [  
+          'style-loader', // 将JS字符串生成为style节点  
+          'css-loader', // 将CSS转化成CommonJS模块  
+          'less-loader' // 将Less编译成CSS  
+        ]  
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader?cacheDirectory',
+        include: [
+          resolve('src'),
+          resolve('test'),
+          resolve('node_modules/webpack-dev-server/client')
+        ]
+      },
+      {
+        test: /\.svg$/,
+        loader: 'svg-sprite-loader',
+        include: [resolve('src/icons')],
+        options: {
+          symbolId: 'icon-[name]'
+        }
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        exclude: [resolve('src/icons')],
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('media/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  plugins: [new VueLoaderPlugin()],
+  node: {
+    // prevent webpack from injecting useless setImmediate polyfill because Vue
+    // source contains it (although only uses it if it's native).
+    setImmediate: false,
+    // prevent webpack from injecting mocks to Node native modules
+    // that does not make sense for the client
+    dgram: 'empty',
+    fs: 'empty',
+    net: 'empty',
+    tls: 'empty',
+    child_process: 'empty'
+  }
+}

+ 103 - 0
.history/build/webpack.base.conf_20240320145214.js

@@ -0,0 +1,103 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const { VueLoaderPlugin } = require('vue-loader')
+const vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+module.exports = {
+  context: path.resolve(__dirname, '../'),
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    filename: '[name].js',
+    publicPath:
+      process.env.NODE_ENV === 'production'
+        ? config.build.assetsPublicPath
+        : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      '@': resolve('src')
+    }
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {  
+        test: /\.less$/,  
+        use: [  
+          'style-loader', // 将JS字符串生成为style节点  
+          'css-loader', // 将CSS转化成CommonJS模块  
+          'less-loader' // 将Less编译成CSS  
+        ]  
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader?cacheDirectory',
+        include: [
+          resolve('src'),
+          resolve('test'),
+          resolve('node_modules/webpack-dev-server/client')
+        ]
+      },
+      {
+        test: /\.svg$/,
+        loader: 'svg-sprite-loader',
+        include: [resolve('src/icons')],
+        options: {
+          symbolId: 'icon-[name]'
+        }
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        exclude: [resolve('src/icons')],
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('media/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  plugins: [new VueLoaderPlugin()],
+  node: {
+    // prevent webpack from injecting useless setImmediate polyfill because Vue
+    // source contains it (although only uses it if it's native).
+    setImmediate: false,
+    // prevent webpack from injecting mocks to Node native modules
+    // that does not make sense for the client
+    dgram: 'empty',
+    fs: 'empty',
+    net: 'empty',
+    tls: 'empty',
+    child_process: 'empty'
+  }
+}

+ 95 - 0
.history/build/webpack.base.conf_20240320145507.js

@@ -0,0 +1,95 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const { VueLoaderPlugin } = require('vue-loader')
+const vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve(dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+module.exports = {
+  context: path.resolve(__dirname, '../'),
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    filename: '[name].js',
+    publicPath:
+      process.env.NODE_ENV === 'production'
+        ? config.build.assetsPublicPath
+        : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      '@': resolve('src')
+    }
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel-loader?cacheDirectory',
+        include: [
+          resolve('src'),
+          resolve('test'),
+          resolve('node_modules/webpack-dev-server/client')
+        ]
+      },
+      {
+        test: /\.svg$/,
+        loader: 'svg-sprite-loader',
+        include: [resolve('src/icons')],
+        options: {
+          symbolId: 'icon-[name]'
+        }
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        exclude: [resolve('src/icons')],
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('media/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  plugins: [new VueLoaderPlugin()],
+  node: {
+    // prevent webpack from injecting useless setImmediate polyfill because Vue
+    // source contains it (although only uses it if it's native).
+    setImmediate: false,
+    // prevent webpack from injecting mocks to Node native modules
+    // that does not make sense for the client
+    dgram: 'empty',
+    fs: 'empty',
+    net: 'empty',
+    tls: 'empty',
+    child_process: 'empty'
+  }
+}

+ 128 - 0
.history/src/main_20240320144058.js

@@ -0,0 +1,128 @@
+import Vue from 'vue'
+import 'normalize.css/normalize.css' // 用于加载页面时上面的加载动画条。
+import Element from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css';
+import '@/styles/index.scss'
+import moment from 'moment' // 时间库
+import App from './App'
+import router from './router'
+import store from './store'
+import {getDept,openUrl, getToken, getTyps, getTypsName, supremeAuthority, getUserData, getEmployeeMap,getEmployeeMapAll,getEmployeeMapItem,getIsIdentity,getCache,setCache,removeCache,returnDeptName,getIsAdministrator} from '@/utils/auth'
+import './performanceSet' // 绩效系统相关配置
+import {onFilePreView } from '@/okr/utils/auth';
+// import { VueOkrTree } from "vue-okr-tree";
+// import "vue-okr-tree/dist/vue-okr-tree.css";
+import 'shepherd.js/dist/css/shepherd.css';
+
+import './icons'
+import './permission'
+import * as filters from '@/utils/filters'
+import axiosKq from '@/utils/axiosKq'
+import axiosUser from '@/utils/axiosUser'
+import axios from '@/utils/axios'
+import * as socketApi from './api/websocket'
+import * as socketApiTow from './api/websocketTow'
+import ECharts from 'echarts';
+
+// 头像
+import UserImage from '@/components/UserImage'
+import noData from '@/components/noData'
+import BrawerBox from '@/performance/components/public/BrawerBox';
+import TaskItem from '@/okr/components/public/TaskItem'; //任务内容
+Vue.component('userImage', UserImage)
+Vue.component('NoData', noData)
+Vue.component('BrawerBox', BrawerBox)
+Vue.component('TaskItem', TaskItem)
+// Vue.component('VueOkrTree', VueOkrTree)
+Vue.use(Element, {
+  size: 'medium',
+})
+Vue.prototype.$echarts = ECharts
+Vue.prototype.$axios = axios
+Vue.prototype.$axiosUser = axiosUser
+Vue.prototype.$axiosKq = axiosKq
+Vue.prototype.$moment = moment
+Vue.prototype.$getToken = getToken
+Vue.prototype.$serveAd = process.env.SERVE_AD
+Vue.prototype.$serverdomain= process.env.BASE_API
+Vue.prototype.$getIsIdentity = getIsIdentity //在积分系统里 判断是否具备某项权限
+Vue.prototype.$supremeAuthority = supremeAuthority //返回积分系统的最高权限
+Vue.prototype.$getIsAdministrator = getIsAdministrator //最高权限  平台管理者
+Vue.prototype.$getTypsName = getTypsName
+Vue.prototype.$getTyps = getTyps
+Vue.prototype.$userInfo = getUserData
+Vue.prototype.$getEmployeeMap = getEmployeeMap  //人员列表
+Vue.prototype.$getEmployeeMapAll = getEmployeeMapAll  //人员列表(包含已删除的)
+Vue.prototype.$getEmployeeMapItem = getEmployeeMapItem  //查询人员信息(包含已删除的)
+Vue.prototype.$getCache = getCache
+Vue.prototype.$setCache = setCache
+Vue.prototype.$removeCache = removeCache
+Vue.prototype.$socketApi = socketApi   //长连接
+Vue.prototype.$socketApiTow = socketApiTow   //长连接
+Vue.prototype.$returnDeptName = returnDeptName
+Vue.prototype.$openUrl = openUrl
+Vue.prototype.$getDept = getDept
+// okr
+Vue.prototype.$onFilePreView = onFilePreView
+//工作流
+Vue.prototype.$isNotEmpty = function(obj){
+  return (obj !== undefined && obj !== null && obj !== '' && obj !== 'null')
+}
+
+Vue.prototype.$getDefalut = function(obj, key, df){
+  return (obj === undefined || key === undefined || !this.$isNotEmpty(obj[key])) ? df : obj[key];
+}
+
+Vue.prototype.$deepCopy = function (obj){return JSON.parse(JSON.stringify(obj))}
+
+// 上传相关
+Vue.prototype.$action = 'https://integralsys.oss-cn-shenzhen.aliyuncs.com'
+Vue.prototype.$acceptImg = 'image/jpeg,image/png'
+Vue.prototype.$acceptFile = 'application/pdf,application/vnd.ms-excel,application/msword,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+Vue.prototype.$acceptImgFile='image/jpeg,image/png,application/pdf,application/vnd.ms-excel,application/msword,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+Vue.prototype.$xtoken={ 'X-Token': getToken() };
+Vue.directive('dragscroll', function (el) {
+  el.onmousedown = function (ev) {
+    const disX = ev.clientX
+    const disY = ev.clientY
+    const originalScrollLeft = el.scrollLeft
+    const originalScrollTop = el.scrollTop
+    const originalScrollBehavior = el.style['scroll-behavior']
+    const originalPointerEvents = el.style['pointer-events']
+    // auto: 默认值,表示滚动框立即滚动到指定位置。
+    el.style['scroll-behavior'] = 'auto'
+    el.style['cursor'] = 'grabbing'
+    // 鼠标移动事件是监听的整个document,这样可以使鼠标能够在元素外部移动的时候也能实现拖动
+    document.onmousemove = function (ev) {
+      ev.preventDefault()
+      // 计算拖拽的偏移距离
+      const distanceX = ev.clientX - disX
+      const distanceY = ev.clientY - disY
+      el.scrollTo(originalScrollLeft - distanceX, originalScrollTop - distanceY)
+      // 在鼠标拖动的时候将点击事件屏蔽掉
+      // el.style['pointer-events'] = 'none'
+      // document.body.style['cursor'] = 'grabbing'
+    }
+    document.onmouseup = function () {
+      // el.style['scroll-behavior'] = originalScrollBehavior
+      // el.style['pointer-events'] = originalPointerEvents
+      // el.style['cursor'] = 'grab'
+      document.onmousemove = null
+      document.onmouseup = null
+    }
+  }
+})
+
+
+Object.keys(filters).forEach(key => {
+  Vue.filter(key, filters[key])
+})
+
+Vue.config.productionTip = false
+
+new Vue({
+  el: '#app',
+  router,
+  store,
+  render: h => h(App)
+})

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320143102.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320143954.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320144349.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design.vue'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting.vue'
+import FormDesign from '@/views/admin/layout/FormDesign.vue'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign.vue'
+import FormProSetting from '@/views/admin/layout/FormProSetting.vue'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320144420.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+// import {getFormDetail, createForm, updateFormDetail} from '@/api/design.vue'
+// import FormBaseSetting from '@/views/admin/layout/FormBaseSetting.vue'
+// import FormDesign from '@/views/admin/layout/FormDesign.vue'
+// import ProcessDesign from '@/views/admin/layout/ProcessDesign.vue'
+// import FormProSetting from '@/views/admin/layout/FormProSetting.vue'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320144458.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader.vue'
+// import {getFormDetail, createForm, updateFormDetail} from '@/api/design.vue'
+// import FormBaseSetting from '@/views/admin/layout/FormBaseSetting.vue'
+// import FormDesign from '@/views/admin/layout/FormDesign.vue'
+// import ProcessDesign from '@/views/admin/layout/ProcessDesign.vue'
+// import FormProSetting from '@/views/admin/layout/FormProSetting.vue'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320144513.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader.vue'
+// import {getFormDetail, createForm, updateFormDetail} from '@/api/design.vue'
+// import FormBaseSetting from '@/views/admin/layout/FormBaseSetting.vue'
+// import FormDesign from '@/views/admin/layout/FormDesign.vue'
+// import ProcessDesign from '@/views/admin/layout/ProcessDesign.vue'
+// import FormProSetting from '@/views/admin/layout/FormProSetting.vue'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320144545.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader.vue'
+// import {getFormDetail, createForm, updateFormDetail} from '@/api/design.vue'
+// import FormBaseSetting from '@/views/admin/layout/FormBaseSetting.vue'
+// import FormDesign from '@/views/admin/layout/FormDesign.vue'
+// import ProcessDesign from '@/views/admin/layout/ProcessDesign.vue'
+// import FormProSetting from '@/views/admin/layout/FormProSetting.vue'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320144558.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320145023.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320145038.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320145619.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="sass" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 327 - 0
.history/src/views/admin/FormProcessDesign_20240320145951.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 161 - 0
.history/src/views/admin/LayoutHeader_20240320145850.vue

@@ -0,0 +1,161 @@
+<template>
+  <div>
+    <div class="header">
+      <el-menu :default-active="value" active-text-color="#409eff" class="el-menu-demo" mode="horizontal"
+               @select="handleSelect">
+        <el-menu-item index="baseSetting" @click="to('baseSetting')">① 基础信息</el-menu-item>
+        <el-menu-item index="formSetting" @click="to('formSetting')">② 审批表单</el-menu-item>
+        <el-menu-item index="processDesign" @click="to('processDesign')">③ 审批流程
+        </el-menu-item>
+        <el-menu-item index="proSetting" @click="to('proSetting')">④ 扩展设置</el-menu-item>
+      </el-menu>
+      <div class="publish">
+        <el-button size="mini" @click="preview"><i class="el-icon-view"></i>预览</el-button>
+        <el-button size="mini" type="primary" @click="publish"><i class="el-icon-s-promotion"></i>发布</el-button>
+      </div>
+      <div class="back">
+        <el-button @click="exit" size="medium" icon="el-icon-arrow-left" circle></el-button>
+        <span>
+          <i :class="setup.logo.icon" :style="'background:' + setup.logo.background"></i>
+          <span>{{ setup.formName }}</span>
+        </span>
+      </div>
+    </div>
+
+    <el-dialog title="请使用手机扫码预览" :visible.sync="viewCode" width="300px" :close-on-click-modal="false" center>
+      <!-- <img src="../../assets/image/code.png" width="250" height="250"> -->
+      <span>请使用手机扫码预览</span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: "LayoutHeader",
+  props:{
+    value: {
+      type: String,
+      default: 'baseSetup'
+    }
+  },
+  data() {
+    return {
+      viewCode: false,
+    };
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    }
+  },
+  created() {
+    this.check()
+  },
+  mounted() {
+    console.log(document.body.offsetWidth)
+    if (document.body.offsetWidth <= 970) {
+      this.$msgbox.alert("本设计器未适配中小屏幕,建议您在PC电脑端浏览器进行操作")
+    }
+    this.listener()
+  },
+  methods: {
+    publish() {
+      this.$emit('publish')
+    },
+    preview() {
+      this.$emit('preview')
+      //this.
+      this.viewCode = true;
+    },
+    valid() {
+      if (!this.$isNotEmpty(this.setup.group)) {
+        this.$message.warning('请选择分组')
+        this.$router.push('/layout/baseSetup')
+        return false;
+      }
+      return true;
+    },
+    exit() {
+      this.$confirm('未发布的内容将不会被保存,是否直接退出 ?', '提示', {
+        confirmButtonText: '退出',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        //window.location.reload()
+        //this.$store.commit('clearTemplate')
+        this.$router.push('/formsPanel')
+      })
+    },
+    to(path) {
+      this.$emit('input', path)
+    },
+    handleSelect(key, keyPath) {
+      console.log(key, keyPath);
+    },
+    listener() {
+      window.onunload = this.closeBefore()
+      window.onbeforeunload = this.closeBefore()
+      //window.on('beforeunload',this.closeBefore())
+    },
+    closeBefore() {
+      //alert("您将要离开本页")
+      return false
+    },
+    check() {
+      if (this.$store.state.isEdit === null) {
+        //this.$router.push("/workPanel");
+      }
+    }
+  }
+}
+</script>
+<style lang="sass" scoped>
+
+/deep/ .header {
+  min-width: 980px;
+  position: relative;
+
+  .el-menu {
+    top: 0;
+    z-index: 999;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+  }
+
+  .publish {
+    position: absolute;
+    top: 15px;
+    right: 20px;
+    z-index: 1000;
+
+    i {
+      margin-right: 6px;
+    }
+
+    button {
+      border-radius: 15px;
+    }
+  }
+
+  .back {
+    position: absolute;
+    z-index: 1000;
+    top: 10px;
+    left: 20px;
+    font-size: small;
+
+    span {
+      i {
+        border-radius: 10px;
+        padding: 7.8px;
+        font-size: 20px;
+        color: #ffffff;
+        margin: 0 10px;
+      }
+    }
+
+  }
+}
+</style>

+ 161 - 0
.history/src/views/admin/LayoutHeader_20240320181810.vue

@@ -0,0 +1,161 @@
+<template>
+  <div>
+    <div class="header">
+      <el-menu :default-active="value" active-text-color="#409eff" class="el-menu-demo" mode="horizontal"
+               @select="handleSelect">
+        <el-menu-item index="baseSetting" @click="to('baseSetting')">① 基础信息</el-menu-item>
+        <el-menu-item index="formSetting" @click="to('formSetting')">② 审批表单</el-menu-item>
+        <el-menu-item index="processDesign" @click="to('processDesign')">③ 审批流程
+        </el-menu-item>
+        <el-menu-item index="proSetting" @click="to('proSetting')">④ 扩展设置</el-menu-item>
+      </el-menu>
+      <div class="publish">
+        <el-button size="mini" @click="preview"><i class="el-icon-view"></i>预览</el-button>
+        <el-button size="mini" type="primary" @click="publish"><i class="el-icon-s-promotion"></i>发布</el-button>
+      </div>
+      <div class="back">
+        <el-button @click="exit" size="medium" icon="el-icon-arrow-left" circle></el-button>
+        <span>
+          <i :class="setup.logo.icon" :style="'background:' + setup.logo.background"></i>
+          <span>{{ setup.formName }}</span>
+        </span>
+      </div>
+    </div>
+
+    <el-dialog title="请使用手机扫码预览" :visible.sync="viewCode" width="300px" :close-on-click-modal="false" center>
+      <!-- <img src="../../assets/image/code.png" width="250" height="250"> -->
+      <span>请使用手机扫码预览</span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: "LayoutHeader",
+  props:{
+    value: {
+      type: String,
+      default: 'baseSetup'
+    }
+  },
+  data() {
+    return {
+      viewCode: false,
+    };
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    }
+  },
+  created() {
+    this.check()
+  },
+  mounted() {
+    console.log(document.body.offsetWidth)
+    if (document.body.offsetWidth <= 970) {
+      this.$msgbox.alert("本设计器未适配中小屏幕,建议您在PC电脑端浏览器进行操作")
+    }
+    this.listener()
+  },
+  methods: {
+    publish() {
+      this.$emit('publish')
+    },
+    preview() {
+      this.$emit('preview')
+      //this.
+      this.viewCode = true;
+    },
+    valid() {
+      if (!this.$isNotEmpty(this.setup.group)) {
+        this.$message.warning('请选择分组')
+        this.$router.push('/layout/baseSetup')
+        return false;
+      }
+      return true;
+    },
+    exit() {
+      this.$confirm('未发布的内容将不会被保存,是否直接退出 ?', '提示', {
+        confirmButtonText: '退出',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        //window.location.reload()
+        //this.$store.commit('clearTemplate')
+        this.$router.push('/formsPanel')
+      })
+    },
+    to(path) {
+      this.$emit('input', path)
+    },
+    handleSelect(key, keyPath) {
+      console.log(key, keyPath);
+    },
+    listener() {
+      window.onunload = this.closeBefore()
+      window.onbeforeunload = this.closeBefore()
+      //window.on('beforeunload',this.closeBefore())
+    },
+    closeBefore() {
+      //alert("您将要离开本页")
+      return false
+    },
+    check() {
+      if (this.$store.state.isEdit === null) {
+        //this.$router.push("/workPanel");
+      }
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+
+.header {
+  min-width: 980px;
+  position: relative;
+
+  .el-menu {
+    top: 0;
+    z-index: 999;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+  }
+
+  .publish {
+    position: absolute;
+    top: 15px;
+    right: 20px;
+    z-index: 1000;
+
+    i {
+      margin-right: 6px;
+    }
+
+    button {
+      border-radius: 15px;
+    }
+  }
+
+  .back {
+    position: absolute;
+    z-index: 1000;
+    top: 10px;
+    left: 20px;
+    font-size: small;
+
+    span {
+      i {
+        border-radius: 10px;
+        padding: 7.8px;
+        font-size: 20px;
+        color: #ffffff;
+        margin: 0 10px;
+      }
+    }
+
+  }
+}
+</style>

+ 269 - 0
.history/src/views/admin/layout/FormBaseSetting_20240320145626.vue

@@ -0,0 +1,269 @@
+<template>
+  <div class="base-setup" @click="showIconSelect = false">
+    <el-form ref="baseSetting" :model="setup" label-position="top" label-width="80px">
+      <el-form-item label="表单图标">
+        <i :class="setup.logo.icon" :style="'background:' + setup.logo.background"></i>
+        <span class="change-icon">
+					<span>
+						<span>选择背景色</span>
+						<el-color-picker v-model="setup.logo.background" show-alpha size="small" :predefine="colors"></el-color-picker>
+					</span>
+					<span>
+						<span>选择图标</span>
+						<el-popover placement="bottom-start" width="390" trigger="click">
+							<div class="icon-select">
+								<i :class="i" v-for="(i, id) in icons" :key="id" @click="setup.logo.icon = i"></i>
+							</div>
+              <i slot="reference" :class="setup.logo.icon"></i>
+            </el-popover>
+            <!--<i :class="setup.icon" @click.stop="showIconSelect = true"></i>-->
+					</span>
+				</span>
+      </el-form-item>
+      <el-form-item label="表单名称" :rules="getRule('请输入表单名称')" prop="formName">
+        <el-input v-model="setup.formName" size="medium"></el-input>
+      </el-form-item>
+      <el-form-item label="所在分组" :rules="getRule('请选择表单分组')" class="group" prop="groupId">
+        <el-select v-model="setup.groupId" placeholder="请选择分组" size="medium">
+          <el-option v-for="(op, index) in fromGroup" :key="index" v-show="op.id > 1"
+                     :label="op.name" :value="op.id"></el-option>
+        </el-select>
+        <el-popover placement="bottom-end" title="新建表单分组" width="300" trigger="click">
+          <el-input size="medium" v-model="newGroup" placeholder="请输入新的分组名">
+            <el-button slot="append" size="medium" type="primary" @click="addGroup">提交</el-button>
+          </el-input>
+          <el-button icon="el-icon-plus" slot="reference" size="medium" type="primary">新建分组</el-button>
+        </el-popover>
+      </el-form-item>
+      <el-form-item label="表单说明">
+        <el-input placeholder="请输入表单说明" v-model="setup.remark" type="textarea" show-word-limit
+                  :autosize="{ minRows: 2, maxRows: 5}" maxlength="500"></el-input>
+      </el-form-item>
+      <el-form-item label="消息通知方式" :rules="getRule('请选择消息通知方式')">
+        <el-select v-model="setup.settings.notify.types" value-key="name"
+                   placeholder="选择消息通知方式" style="width: 30%;"
+                   size="medium" clearable multiple collapse-tags>
+          <el-option v-for="(wc, index) in notifyTypes" :label="wc.name" :key="index" :value="wc"></el-option>
+        </el-select>
+        <el-input size="medium" v-model="setup.settings.notify.title" style="width: 68%; float:right;" placeholder="消息通知标题"></el-input>
+      </el-form-item>
+      <el-form-item label="谁可以管理此表单">
+        <el-select v-model="setup.settings.admin" @click.native="selectUser('admin')" value-key="name"
+                   class="select-u" placeholder="请选择可以管理此表单的人员"
+                   size="medium" clearable multiple>
+          <el-option v-for="(wc, index) in setup.settings.admin" :label="wc.name" :key="index" :value="wc"></el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <org-picker title="请选择可以管理此表单的人员" multiple ref="orgPicker" :selected="select" @ok="selected"></org-picker>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import {getFormGroups, updateGroup} from '@/api/design'
+import iconfont from '@/assets/iconfont/iconfont.json'
+
+export default {
+  name: "FormBaseSetting",
+  components: {OrgPicker},
+  data() {
+    return {
+      nowUserSelect: null,
+      showIconSelect: false,
+      select: [],
+      newGroup: '',
+      fromGroup: [],
+      notifyTypes:[
+        {type:'APP',name:'应用内通知'},
+        {type:'EMAIL',name:'邮件通知'},
+        {type:'SMS',name:'短信通知'},
+        {type:'WX',name:'微信通知'},
+        {type:'DING',name:'钉钉通知'},
+      ],
+      colors: [
+        '#ff4500',
+        '#ff8c00',
+        '#ffd700',
+        '#90ee90',
+        '#00ced1',
+        '#1e90ff',
+        '#c71585',
+        'rgba(255, 69, 0, 0.68)',
+        'rgb(255, 120, 0)',
+        'hsl(181, 100%, 37%)',
+        'hsla(209, 100%, 56%, 0.73)',
+        '#c7158577'
+      ],
+      icons: [
+        'el-icon-delete-solid',
+        'el-icon-s-tools',
+        'el-icon-s-goods',
+        'el-icon-warning',
+        'el-icon-circle-plus',
+        'el-icon-camera-solid',
+        'el-icon-s-promotion',
+        'el-icon-s-cooperation',
+        'el-icon-s-platform',
+        'el-icon-s-custom',
+        'el-icon-s-data',
+        'el-icon-s-check',
+        'el-icon-s-claim',
+      ],
+      rules:{
+        formName:[{}],
+        groupId: [],
+      }
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design;
+    }
+  },
+  created() {
+    this.loadIconfont()
+  },
+  mounted(){
+    this.getGroups()
+  },
+  methods: {
+    getRule(msg){
+      return [{ required: true, message: msg, trigger: 'blur' }]
+    },
+    loadIconfont(){
+      if (iconfont && iconfont.id){
+        iconfont.glyphs.forEach(icon => {
+          this.icons.push(`${iconfont.font_family} ${iconfont.css_prefix_text}${icon.font_class}`)
+        })
+      }
+    },
+    getGroups(){
+      getFormGroups().then(rsp => {
+        this.fromGroup = rsp.data
+      }).catch(err => this.$message.error('获取分组异常'))
+    },
+    addGroup() {
+      if (this.newGroup.trim() !== '') {
+        updateGroup({name: this.newGroup.trim()}, 'post').then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+        }).catch(err => this.$message.error(err.response.data))
+      }
+    },
+    selected(select) {
+      this.$set(this.setup.settings, this.nowUserSelect, select)
+      //this.setup[this.nowUserSelect] = select
+    },
+    selectUser(key) {
+      this.select = this.setup.settings[key]
+      this.nowUserSelect = key
+      this.$refs.orgPicker.show()
+    },
+    validate(){
+      this.$refs.baseSetting.validate()
+      let err = []
+      if (!this.$isNotEmpty(this.setup.formName)){
+        err.push('表单名称未设置')
+      }
+      if (!this.$isNotEmpty(this.setup.groupId)){
+        err.push('表单分组未设置')
+      }
+      if (this.setup.settings.notify.types.length === 0){
+        err.push('审批消息通知方式未设置')
+      }
+      return err
+    }
+  }
+}
+</script>
+
+<style lang="sass" scoped>
+/deep/ .el-select-dropdown {
+  display: none;
+}
+
+.icon-select {
+  display: flex;
+  flex-wrap: wrap;
+  i {
+    cursor: pointer;
+    font-size: large;
+    padding: 10px;
+    max-width: 38px !important;
+    &:hover {
+      box-shadow: 0 0 10px 2px #C2C2C2;
+    }
+  }
+}
+
+/deep/ .select-u {
+  width: 100%;
+}
+
+.base-setup {
+  overflow: auto;
+  margin: 0 auto;
+  width: 600px;
+  height: calc(100vh - 105px);
+  background: #ffffff;
+  margin-top: 10px;
+  padding: 15px 20px;
+
+  i:first-child {
+    position: relative;
+    cursor: pointer;
+    font-size: xx-large;
+    color: #ffffff;
+    border-radius: 10px;
+    padding: 10px;
+  }
+
+  .change-icon {
+    margin-left: 20px;
+
+    span {
+      font-size: small;
+      color: #7a7a7a;
+      margin-right: 15px;
+    }
+
+    i {
+      cursor: pointer;
+      color: #7a7a7a;
+      font-size: x-large;
+    }
+  }
+
+  /deep/ .el-form-item__label {
+    padding: 0;
+    font-weight: bold;
+  }
+
+  /deep/ .el-form-item {
+    margin-bottom: 5px;
+  }
+}
+
+/deep/ .group {
+  .el-select {
+    width: calc(100% - 130px);
+  }
+
+  .el-button {
+    margin-left: 10px;
+    width: 120px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 4px;
+  height: 4px;
+  background-color: #f8f8f8;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+</style>

+ 269 - 0
.history/src/views/admin/layout/FormBaseSetting_20240320145948.vue

@@ -0,0 +1,269 @@
+<template>
+  <div class="base-setup" @click="showIconSelect = false">
+    <el-form ref="baseSetting" :model="setup" label-position="top" label-width="80px">
+      <el-form-item label="表单图标">
+        <i :class="setup.logo.icon" :style="'background:' + setup.logo.background"></i>
+        <span class="change-icon">
+					<span>
+						<span>选择背景色</span>
+						<el-color-picker v-model="setup.logo.background" show-alpha size="small" :predefine="colors"></el-color-picker>
+					</span>
+					<span>
+						<span>选择图标</span>
+						<el-popover placement="bottom-start" width="390" trigger="click">
+							<div class="icon-select">
+								<i :class="i" v-for="(i, id) in icons" :key="id" @click="setup.logo.icon = i"></i>
+							</div>
+              <i slot="reference" :class="setup.logo.icon"></i>
+            </el-popover>
+            <!--<i :class="setup.icon" @click.stop="showIconSelect = true"></i>-->
+					</span>
+				</span>
+      </el-form-item>
+      <el-form-item label="表单名称" :rules="getRule('请输入表单名称')" prop="formName">
+        <el-input v-model="setup.formName" size="medium"></el-input>
+      </el-form-item>
+      <el-form-item label="所在分组" :rules="getRule('请选择表单分组')" class="group" prop="groupId">
+        <el-select v-model="setup.groupId" placeholder="请选择分组" size="medium">
+          <el-option v-for="(op, index) in fromGroup" :key="index" v-show="op.id > 1"
+                     :label="op.name" :value="op.id"></el-option>
+        </el-select>
+        <el-popover placement="bottom-end" title="新建表单分组" width="300" trigger="click">
+          <el-input size="medium" v-model="newGroup" placeholder="请输入新的分组名">
+            <el-button slot="append" size="medium" type="primary" @click="addGroup">提交</el-button>
+          </el-input>
+          <el-button icon="el-icon-plus" slot="reference" size="medium" type="primary">新建分组</el-button>
+        </el-popover>
+      </el-form-item>
+      <el-form-item label="表单说明">
+        <el-input placeholder="请输入表单说明" v-model="setup.remark" type="textarea" show-word-limit
+                  :autosize="{ minRows: 2, maxRows: 5}" maxlength="500"></el-input>
+      </el-form-item>
+      <el-form-item label="消息通知方式" :rules="getRule('请选择消息通知方式')">
+        <el-select v-model="setup.settings.notify.types" value-key="name"
+                   placeholder="选择消息通知方式" style="width: 30%;"
+                   size="medium" clearable multiple collapse-tags>
+          <el-option v-for="(wc, index) in notifyTypes" :label="wc.name" :key="index" :value="wc"></el-option>
+        </el-select>
+        <el-input size="medium" v-model="setup.settings.notify.title" style="width: 68%; float:right;" placeholder="消息通知标题"></el-input>
+      </el-form-item>
+      <el-form-item label="谁可以管理此表单">
+        <el-select v-model="setup.settings.admin" @click.native="selectUser('admin')" value-key="name"
+                   class="select-u" placeholder="请选择可以管理此表单的人员"
+                   size="medium" clearable multiple>
+          <el-option v-for="(wc, index) in setup.settings.admin" :label="wc.name" :key="index" :value="wc"></el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <org-picker title="请选择可以管理此表单的人员" multiple ref="orgPicker" :selected="select" @ok="selected"></org-picker>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import {getFormGroups, updateGroup} from '@/api/design'
+import iconfont from '@/assets/iconfont/iconfont.json'
+
+export default {
+  name: "FormBaseSetting",
+  components: {OrgPicker},
+  data() {
+    return {
+      nowUserSelect: null,
+      showIconSelect: false,
+      select: [],
+      newGroup: '',
+      fromGroup: [],
+      notifyTypes:[
+        {type:'APP',name:'应用内通知'},
+        {type:'EMAIL',name:'邮件通知'},
+        {type:'SMS',name:'短信通知'},
+        {type:'WX',name:'微信通知'},
+        {type:'DING',name:'钉钉通知'},
+      ],
+      colors: [
+        '#ff4500',
+        '#ff8c00',
+        '#ffd700',
+        '#90ee90',
+        '#00ced1',
+        '#1e90ff',
+        '#c71585',
+        'rgba(255, 69, 0, 0.68)',
+        'rgb(255, 120, 0)',
+        'hsl(181, 100%, 37%)',
+        'hsla(209, 100%, 56%, 0.73)',
+        '#c7158577'
+      ],
+      icons: [
+        'el-icon-delete-solid',
+        'el-icon-s-tools',
+        'el-icon-s-goods',
+        'el-icon-warning',
+        'el-icon-circle-plus',
+        'el-icon-camera-solid',
+        'el-icon-s-promotion',
+        'el-icon-s-cooperation',
+        'el-icon-s-platform',
+        'el-icon-s-custom',
+        'el-icon-s-data',
+        'el-icon-s-check',
+        'el-icon-s-claim',
+      ],
+      rules:{
+        formName:[{}],
+        groupId: [],
+      }
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design;
+    }
+  },
+  created() {
+    this.loadIconfont()
+  },
+  mounted(){
+    this.getGroups()
+  },
+  methods: {
+    getRule(msg){
+      return [{ required: true, message: msg, trigger: 'blur' }]
+    },
+    loadIconfont(){
+      if (iconfont && iconfont.id){
+        iconfont.glyphs.forEach(icon => {
+          this.icons.push(`${iconfont.font_family} ${iconfont.css_prefix_text}${icon.font_class}`)
+        })
+      }
+    },
+    getGroups(){
+      getFormGroups().then(rsp => {
+        this.fromGroup = rsp.data
+      }).catch(err => this.$message.error('获取分组异常'))
+    },
+    addGroup() {
+      if (this.newGroup.trim() !== '') {
+        updateGroup({name: this.newGroup.trim()}, 'post').then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+        }).catch(err => this.$message.error(err.response.data))
+      }
+    },
+    selected(select) {
+      this.$set(this.setup.settings, this.nowUserSelect, select)
+      //this.setup[this.nowUserSelect] = select
+    },
+    selectUser(key) {
+      this.select = this.setup.settings[key]
+      this.nowUserSelect = key
+      this.$refs.orgPicker.show()
+    },
+    validate(){
+      this.$refs.baseSetting.validate()
+      let err = []
+      if (!this.$isNotEmpty(this.setup.formName)){
+        err.push('表单名称未设置')
+      }
+      if (!this.$isNotEmpty(this.setup.groupId)){
+        err.push('表单分组未设置')
+      }
+      if (this.setup.settings.notify.types.length === 0){
+        err.push('审批消息通知方式未设置')
+      }
+      return err
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+/deep/ .el-select-dropdown {
+  display: none;
+}
+
+.icon-select {
+  display: flex;
+  flex-wrap: wrap;
+  i {
+    cursor: pointer;
+    font-size: large;
+    padding: 10px;
+    max-width: 38px !important;
+    &:hover {
+      box-shadow: 0 0 10px 2px #C2C2C2;
+    }
+  }
+}
+
+/deep/ .select-u {
+  width: 100%;
+}
+
+.base-setup {
+  overflow: auto;
+  margin: 0 auto;
+  width: 600px;
+  height: calc(100vh - 105px);
+  background: #ffffff;
+  margin-top: 10px;
+  padding: 15px 20px;
+
+  i:first-child {
+    position: relative;
+    cursor: pointer;
+    font-size: xx-large;
+    color: #ffffff;
+    border-radius: 10px;
+    padding: 10px;
+  }
+
+  .change-icon {
+    margin-left: 20px;
+
+    span {
+      font-size: small;
+      color: #7a7a7a;
+      margin-right: 15px;
+    }
+
+    i {
+      cursor: pointer;
+      color: #7a7a7a;
+      font-size: x-large;
+    }
+  }
+
+  /deep/ .el-form-item__label {
+    padding: 0;
+    font-weight: bold;
+  }
+
+  /deep/ .el-form-item {
+    margin-bottom: 5px;
+  }
+}
+
+/deep/ .group {
+  .el-select {
+    width: calc(100% - 130px);
+  }
+
+  .el-button {
+    margin-left: 10px;
+    width: 120px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 4px;
+  height: 4px;
+  background-color: #f8f8f8;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+</style>

+ 342 - 0
.history/src/views/common/process/nodes/ConditionNode_20240320142911.vue

@@ -0,0 +1,342 @@
+<template>
+  <div :class="{'node': true, 'node-error-state': showError}">
+    <div :class="{'node-body': true, 'error': showError}">
+      <div class="node-body-left" @click="$emit('leftMove')" v-if="level > 1 && $store.state.diagramMode !== 'viewer'">
+        <i class="el-icon-arrow-left"></i>
+      </div>
+      <div class="node-body-main" @click="$emit('selected')">
+        <div class="node-body-main-header">
+          <ellipsis class="title" hover-tip :content="config.name ? config.name : ('条件' + level)"/>
+          <span class="level">优先级{{ level }}</span>
+          <span class="option" v-if="$store.state.diagramMode !== 'viewer'">
+            <el-tooltip effect="dark" content="复制条件" placement="top">
+              <i class="el-icon-copy-document" @click.stop="$emit('copy')"></i>
+            </el-tooltip>
+            <i class="el-icon-close" @click.stop="$emit('delNode')"></i>
+          </span>
+        </div>
+        <div class="node-body-main-content">
+          <span class="placeholder" v-if="(content || '').trim() === ''">{{
+            level == size && size != 0 ? "其他条件进入此流程" : placeholder
+          }}</span>
+          <ellipsis hoverTip :row="4" :content="content" v-else/>
+        </div>
+      </div>
+      <div class="node-body-right" @click="$emit('rightMove')"
+           v-if="level < size && $store.state.diagramMode !== 'viewer'">
+        <i class="el-icon-arrow-right"></i>
+      </div>
+      <div class="node-error" v-if="showError">
+        <el-tooltip effect="dark" :content="errorInfo" placement="top-start">
+          <i class="el-icon-warning-outline"></i>
+        </el-tooltip>
+      </div>
+    </div>
+    <div class="node-footer">
+      <div class="btn">
+        <insert-button v-if="$store.state.diagramMode !== 'viewer'"
+                       @insertNode="type => $emit('insertNode', type)"></insert-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import InsertButton from '@/views/common/InsertButton.vue'
+import {ValueType} from '@/views/common/form/ComponentsConfigExport'
+
+const groupNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
+export default {
+  name: "ConditionNode",
+  components: {InsertButton},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    //索引位置
+    level: {
+      type: Number,
+      default: 1
+    },
+    //条件数
+    size: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      ValueType,
+      groupNames,
+      placeholder: '请设置条件',
+      errorInfo: '',
+      showError: false
+    }
+  },
+  computed: {
+    content() {
+      const groups = this.config.props.groups
+      let confitions = []
+      groups.forEach(group => {
+        let subConditions = []
+        group.conditions.forEach(subCondition => {
+          let subConditionStr = ''
+          switch (subCondition.valueType) {
+            case ValueType.dept:
+            case ValueType.user:
+              subConditionStr = `${subCondition.title}属于[${String(subCondition.value.map(u => u.name)).replaceAll(',', '. ')}]之一`
+              break;
+            case ValueType.number:
+            case ValueType.string:
+              subConditionStr = this.getOrdinaryConditionContent(subCondition)
+              break;
+          }
+          subConditions.push(subConditionStr)
+        })
+        //根据子条件关系构建描述
+        let subConditionsStr = String(subConditions)
+            .replaceAll(',', subConditions.length > 1 ?
+                (group.groupType === 'AND' ? ') 且 (' : ') 或 (') :
+                (group.groupType === 'AND' ? ' 且 ' : ' 或 '))
+        confitions.push(subConditions.length > 1 ? `(${subConditionsStr})` : subConditionsStr)
+      })
+      //构建最终描述
+      return String(confitions).replaceAll(',', (this.config.props.groupsType === 'AND' ? ' 且 ' : ' 或 '))
+    }
+  },
+  methods: {
+    getDefault(val, df) {
+      return val && val !== '' ? val : df;
+    },
+    getOrdinaryConditionContent(subCondition) {
+      switch (subCondition.compare) {
+        case 'IN':
+          return `${subCondition.title}为[${String(subCondition.value).replaceAll(',', '、')}]中之一`
+        case 'B':
+          return `${subCondition.value[0]} < ${subCondition.title} < ${subCondition.value[1]}`
+        case 'AB':
+          return `${subCondition.value[0]} ≤ ${subCondition.title} < ${subCondition.value[1]}`
+        case 'BA':
+          return `${subCondition.value[0]} < ${subCondition.title} ≤ ${subCondition.value[1]}`
+        case 'ABA':
+          return `${subCondition.value[0]} ≤ ${subCondition.title} ≤ ${subCondition.value[1]}`
+        case '<=':
+          return `${subCondition.title} ≤ ${this.getDefault(subCondition.value[0], ' ?')}`
+        case '>=':
+          return `${subCondition.title} ≥ ${this.getDefault(subCondition.value[0], ' ?')}`
+        default:
+          return `${subCondition.title}${subCondition.compare}${this.getDefault(subCondition.value[0], ' ?')}`
+      }
+    },
+    //校验数据配置的合法性
+    validate(err) {
+      console.log('condition children', this.config.children)
+      if (!(this.level == this.size && this.size != 0) && this.config.children && !this.config.children.id) {
+        this.showError = true
+        this.errorInfo = '条件分支后不能为空'
+        err.push(`条件分支后不能为空`)
+        return !this.showError
+      }
+
+      const props = this.config.props
+      if (props.groups.length <= 0){
+        this.showError = true
+        this.errorInfo = '请设置分支条件'
+        err.push(`${this.config.name} 未设置条件`)
+      }else {
+        if (!(this.level == this.size && this.size != 0)) {
+          for (let i = 0; i < props.groups.length; i++) {
+            if (props.groups[i].cids.length === 0){
+              this.showError = true
+              this.errorInfo = `请设置条件组${this.groupNames[i]}内的条件`
+              err.push(`条件 ${this.config.name} 条件组${this.groupNames[i]}内未设置条件`)
+              break
+            }else {
+              let conditions = props.groups[i].conditions
+              for (let ci = 0; ci < conditions.length; ci++) {
+                let subc = conditions[ci]
+                if (subc.value.length === 0){
+                  this.showError = true
+                }else {
+                  this.showError = false
+                }
+                if (this.showError){
+                  this.errorInfo = `请完善条件组${this.groupNames[i]}内的${subc.title}条件`
+                  err.push(`条件 ${this.config.name} 条件组${this.groupNames[i]}内${subc.title}条件未完善`)
+                  return false
+                }
+              }
+            }
+          }
+        }
+      }
+      return !this.showError;
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+
+
+.node-error-state {
+  .node-body {
+    box-shadow: 0px 0px 5px 0px #F56C6C !important;
+  }
+}
+
+.node {
+  padding: 30px 55px 0;
+  width: 220px;
+
+  .node-body {
+    cursor: pointer;
+    min-height: 80px;
+    max-height: 120px;
+    position: relative;
+    border-radius: 5px;
+    background-color: white;
+    box-shadow: 0px 0px 5px 0px #d8d8d8;
+
+    &:hover {
+      .node-body-left, .node-body-right {
+        i {
+          display: block !important;
+        }
+      }
+
+      .node-body-main {
+        .level {
+          display: none !important;
+        }
+
+        .option {
+          display: inline-block !important;
+        }
+      }
+
+      box-shadow: 0px 0px 3px 0px @theme-primary;
+    }
+
+    .node-body-left, .node-body-right {
+      display: flex;
+      align-items: center;
+      position: absolute;
+      height: 100%;
+
+      i {
+        display: none;
+      }
+
+      &:hover {
+        background-color: #ececec;
+      }
+    }
+
+    .node-body-left {
+      left: 0;
+    }
+
+    .node-body-right {
+      right: 0;
+      top: 0;
+    }
+
+    .node-body-main {
+      //position: absolute;
+      width: 188px;
+      margin-left: 17px;
+      display: inline-block;
+
+      .node-body-main-header {
+        padding: 10px 0px 5px;
+        font-size: xx-small;
+        position: relative;
+
+        .title {
+          color: #15bca3;
+          display: inline-block;
+          height: 14px;
+          width: 125px;
+        }
+
+        .level {
+          position: absolute;
+          right: 15px;
+          color: #888888;
+        }
+
+        .option {
+          position: absolute;
+          right: 0;
+          display: none;
+          font-size: medium;
+
+          i {
+            color: #888888;
+            padding: 0 3px;
+          }
+        }
+      }
+
+      .node-body-main-content {
+        padding: 6px;
+        color: #656363;
+        font-size: 14px;
+
+        i {
+          position: absolute;
+          top: 55%;
+          right: 10px;
+          font-size: medium;
+        }
+
+        .placeholder {
+          color: #8c8c8c;
+        }
+      }
+    }
+
+    .node-error {
+      position: absolute;
+      right: -40px;
+      top: 20px;
+      font-size: 25px;
+      color: #F56C6C;
+    }
+  }
+
+  .node-footer {
+    position: relative;
+
+    .btn {
+      width: 100%;
+      display: flex;
+      height: 70px;
+      padding: 20px 0 32px;
+      justify-content: center;
+    }
+
+    /deep/ .el-button {
+      height: 32px;
+    }
+
+    &::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: -1;
+      margin: auto;
+      width: 2px;
+      height: 100%;
+      background-color: #CACACA;
+    }
+  }
+}
+</style>

+ 342 - 0
.history/src/views/common/process/nodes/InclusiveNode_20240320142840.vue

@@ -0,0 +1,342 @@
+<template>
+  <div :class="{'node': true, 'node-error-state': showError}">
+    <div :class="{'node-body': true, 'error': showError}">
+      <div class="node-body-left" @click="$emit('leftMove')" v-if="level > 1 && $store.state.diagramMode !== 'viewer'">
+        <i class="el-icon-arrow-left"></i>
+      </div>
+      <div class="node-body-main" @click="$emit('selected')">
+        <div class="node-body-main-header">
+          <ellipsis class="title" hover-tip :content="config.name ? config.name : ('条件' + level)"/>
+          <span class="level">优先级{{ level }}</span>
+          <span class="option" v-if="$store.state.diagramMode !== 'viewer'">
+            <el-tooltip effect="dark" content="复制条件" placement="top">
+              <i class="el-icon-copy-document" @click.stop="$emit('copy')"></i>
+            </el-tooltip>
+            <i class="el-icon-close" @click.stop="$emit('delNode')"></i>
+          </span>
+        </div>
+        <div class="node-body-main-content">
+          <span class="placeholder" v-if="(content || '').trim() === ''">{{
+            level == size && size != 0 ? "其他条件进入此流程" : placeholder
+          }}</span>
+          <ellipsis hoverTip :row="4" :content="content" v-else/>
+        </div>
+      </div>
+      <div class="node-body-right" @click="$emit('rightMove')"
+           v-if="level < size && $store.state.diagramMode !== 'viewer'">
+        <i class="el-icon-arrow-right"></i>
+      </div>
+      <div class="node-error" v-if="showError">
+        <el-tooltip effect="dark" :content="errorInfo" placement="top-start">
+          <i class="el-icon-warning-outline"></i>
+        </el-tooltip>
+      </div>
+    </div>
+    <div class="node-footer">
+      <div class="btn">
+        <insert-button v-if="$store.state.diagramMode !== 'viewer'"
+                       @insertNode="type => $emit('insertNode', type)"></insert-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import InsertButton from '@/views/common/InsertButton.vue'
+import {ValueType} from '@/views/common/form/ComponentsConfigExport'
+
+const groupNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
+export default {
+  name: "InclusiveNode",
+  components: {InsertButton},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    //索引位置
+    level: {
+      type: Number,
+      default: 1
+    },
+    //条件数
+    size: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      ValueType,
+      groupNames,
+      placeholder: '请设置条件',
+      errorInfo: '',
+      showError: false
+    }
+  },
+  computed: {
+    content() {
+      const groups = this.config.props.groups
+      let confitions = []
+      groups.forEach(group => {
+        let subConditions = []
+        group.conditions.forEach(subCondition => {
+          let subConditionStr = ''
+          switch (subCondition.valueType) {
+            case ValueType.dept:
+            case ValueType.user:
+              subConditionStr = `${subCondition.title}属于[${String(subCondition.value.map(u => u.name)).replaceAll(',', '. ')}]之一`
+              break;
+            case ValueType.number:
+            case ValueType.string:
+              subConditionStr = this.getOrdinaryConditionContent(subCondition)
+              break;
+          }
+          subConditions.push(subConditionStr)
+        })
+        //根据子条件关系构建描述
+        let subConditionsStr = String(subConditions)
+            .replaceAll(',', subConditions.length > 1 ?
+                (group.groupType === 'AND' ? ') 且 (' : ') 或 (') :
+                (group.groupType === 'AND' ? ' 且 ' : ' 或 '))
+        confitions.push(subConditions.length > 1 ? `(${subConditionsStr})` : subConditionsStr)
+      })
+      //构建最终描述
+      return String(confitions).replaceAll(',', (this.config.props.groupsType === 'AND' ? ' 且 ' : ' 或 '))
+    }
+  },
+  methods: {
+    getDefault(val, df) {
+      return val && val !== '' ? val : df;
+    },
+    getOrdinaryConditionContent(subCondition) {
+      switch (subCondition.compare) {
+        case 'IN':
+          return `${subCondition.title}为[${String(subCondition.value).replaceAll(',', '、')}]中之一`
+        case 'B':
+          return `${subCondition.value[0]} < ${subCondition.title} < ${subCondition.value[1]}`
+        case 'AB':
+          return `${subCondition.value[0]} ≤ ${subCondition.title} < ${subCondition.value[1]}`
+        case 'BA':
+          return `${subCondition.value[0]} < ${subCondition.title} ≤ ${subCondition.value[1]}`
+        case 'ABA':
+          return `${subCondition.value[0]} ≤ ${subCondition.title} ≤ ${subCondition.value[1]}`
+        case '<=':
+          return `${subCondition.title} ≤ ${this.getDefault(subCondition.value[0], ' ?')}`
+        case '>=':
+          return `${subCondition.title} ≥ ${this.getDefault(subCondition.value[0], ' ?')}`
+        default:
+          return `${subCondition.title}${subCondition.compare}${this.getDefault(subCondition.value[0], ' ?')}`
+      }
+    },
+    //校验数据配置的合法性
+    validate(err) {
+      console.log('inclusive children', this.config.children)
+      if (!(this.level == this.size && this.size != 0) && this.config.children && !this.config.children.id) {
+        this.showError = true
+        this.errorInfo = '条件分支后不能为空'
+        err.push(`条件分支后不能为空`)
+        return !this.showError
+      }
+
+      const props = this.config.props
+      if (props.groups.length <= 0){
+        this.showError = true
+        this.errorInfo = '请设置分支条件'
+        err.push(`${this.config.name} 未设置条件`)
+      }else {
+        if (!(this.level == this.size && this.size != 0)) {
+          for (let i = 0; i < props.groups.length; i++) {
+            if (props.groups[i].cids.length === 0){
+              this.showError = true
+              this.errorInfo = `请设置条件组${this.groupNames[i]}内的条件`
+              err.push(`条件 ${this.config.name} 条件组${this.groupNames[i]}内未设置条件`)
+              break
+            }else {
+              let conditions = props.groups[i].conditions
+              for (let ci = 0; ci < conditions.length; ci++) {
+                let subc = conditions[ci]
+                if (subc.value.length === 0){
+                  this.showError = true
+                }else {
+                  this.showError = false
+                }
+                if (this.showError){
+                  this.errorInfo = `请完善条件组${this.groupNames[i]}内的${subc.title}条件`
+                  err.push(`条件 ${this.config.name} 条件组${this.groupNames[i]}内${subc.title}条件未完善`)
+                  return false
+                }
+              }
+            }
+          }
+        }
+      }
+      return !this.showError;
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+
+
+.node-error-state {
+  .node-body {
+    box-shadow: 0px 0px 5px 0px #F56C6C !important;
+  }
+}
+
+.node {
+  padding: 30px 55px 0;
+  width: 220px;
+
+  .node-body {
+    cursor: pointer;
+    min-height: 80px;
+    max-height: 120px;
+    position: relative;
+    border-radius: 5px;
+    background-color: white;
+    box-shadow: 0px 0px 5px 0px #d8d8d8;
+
+    &:hover {
+      .node-body-left, .node-body-right {
+        i {
+          display: block !important;
+        }
+      }
+
+      .node-body-main {
+        .level {
+          display: none !important;
+        }
+
+        .option {
+          display: inline-block !important;
+        }
+      }
+
+      box-shadow: 0px 0px 3px 0px @theme-primary;
+    }
+
+    .node-body-left, .node-body-right {
+      display: flex;
+      align-items: center;
+      position: absolute;
+      height: 100%;
+
+      i {
+        display: none;
+      }
+
+      &:hover {
+        background-color: #ececec;
+      }
+    }
+
+    .node-body-left {
+      left: 0;
+    }
+
+    .node-body-right {
+      right: 0;
+      top: 0;
+    }
+
+    .node-body-main {
+      //position: absolute;
+      width: 188px;
+      margin-left: 17px;
+      display: inline-block;
+
+      .node-body-main-header {
+        padding: 10px 0px 5px;
+        font-size: xx-small;
+        position: relative;
+
+        .title {
+          color: #425c9d;
+          display: inline-block;
+          height: 14px;
+          width: 125px;
+        }
+
+        .level {
+          position: absolute;
+          right: 15px;
+          color: #888888;
+        }
+
+        .option {
+          position: absolute;
+          right: 0;
+          display: none;
+          font-size: medium;
+
+          i {
+            color: #888888;
+            padding: 0 3px;
+          }
+        }
+      }
+
+      .node-body-main-content {
+        padding: 6px;
+        color: #656363;
+        font-size: 14px;
+
+        i {
+          position: absolute;
+          top: 55%;
+          right: 10px;
+          font-size: medium;
+        }
+
+        .placeholder {
+          color: #8c8c8c;
+        }
+      }
+    }
+
+    .node-error {
+      position: absolute;
+      right: -40px;
+      top: 20px;
+      font-size: 25px;
+      color: #F56C6C;
+    }
+  }
+
+  .node-footer {
+    position: relative;
+
+    .btn {
+      width: 100%;
+      display: flex;
+      height: 70px;
+      padding: 20px 0 32px;
+      justify-content: center;
+    }
+
+    /deep/ .el-button {
+      height: 32px;
+    }
+
+    &::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: -1;
+      margin: auto;
+      width: 2px;
+      height: 100%;
+      background-color: #CACACA;
+    }
+  }
+}
+</style>

+ 4 - 1
package.json

@@ -58,6 +58,7 @@
     "vue-router-storage": "^1.0.9",
     "vue-seamless-scroll": "^1.1.23",
     "vue-shepherd": "^3.0.0",
+    "vuedraggable": "^2.24.3",
     "vuex": "^3.6.2",
     "xlsx": "^0.17.5"
   },
@@ -88,6 +89,8 @@
     "hash-sum": "^1.0.2",
     "html-webpack-plugin": "4.0.0-alpha",
     "husky": "0.14.3",
+    "less": "^4.2.0",
+    "less-loader": "^12.2.0",
     "lint-staged": "7.2.2",
     "mini-css-extract-plugin": "0.4.1",
     "node-notifier": "5.2.1",
@@ -108,7 +111,7 @@
     "uglifyjs-webpack-plugin": "1.2.7",
     "url-loader": "1.0.1",
     "vue-loader": "15.3.0",
-    "vue-okr-tree": "^1.0.12",
+    "vue-okr-tree": "^1.0.17",
     "vue-style-loader": "4.1.2",
     "vue-template-compiler": "^2.7.15",
     "webpack": "4.16.5",

+ 285 - 0
src/api/design.js

@@ -0,0 +1,285 @@
+import request from '@/api/request.js';
+
+// 查询表单组
+export function getFormGroups(param) {
+  return request({
+    url: 'admin/form/group',
+    method: 'get',
+    params: param,
+  });
+}
+
+// 表单排序
+export function groupItemsSort(param) {
+  return request({
+    url: 'admin/form/group/sort',
+    method: 'put',
+    data: param,
+  });
+}
+
+// 更新表单组
+export function updateGroup(param, method) {
+  return request({
+    url: 'admin/form/group',
+    method: method,
+    params: param,
+  });
+}
+
+// 获取表单分组
+export function getGroup() {
+  return request({
+    url: 'admin/form/group/list',
+    method: 'get',
+  });
+}
+
+// 更新表单
+export function updateForm(param) {
+  console.log(JSON.stringify(param));
+  // return;
+  return request({
+    url: 'admin/form',
+    method: 'put',
+    params: param,
+  });
+}
+
+export function createForm(param) {
+  console.log(JSON.stringify(param));
+  // return;
+  return request({
+    url: 'admin/form',
+    method: 'post',
+    data: param,
+  });
+}
+
+// 查询表单详情
+export function getFormDetail(id) {
+  return request({
+    url: 'admin/form/detail/' + id,
+    method: 'get',
+  });
+}
+
+export function getFormDetailV2(templateId) {
+  return request({
+    url: 'workspace/process/detail',
+    method: 'get',
+    params: {
+      templateId,
+    },
+  });
+}
+
+// 更新表单详情
+export function updateFormDetail(param) {
+  console.log(JSON.stringify(param));
+  // return;
+  return request({
+    url: 'admin/form/detail',
+    method: 'put',
+    data: param,
+  });
+}
+
+// 发起流程
+export function startProcess(param) {
+  return request({
+    url: 'workspace/process/start',
+    method: 'POST',
+    data: param,
+  });
+}
+
+// 查询我发起的
+export function applyList(data) {
+  return request({
+    url: 'workspace/process/applyList',
+    method: 'POST',
+    data,
+  });
+}
+
+// 查看我的待办
+export function todoList(data) {
+  return request({
+    url: 'workspace/process/toDoList',
+    method: 'POST',
+    data,
+  });
+}
+export function ccList(data) {
+  return request({
+    url: 'workspace/process/ccList',
+    method: 'POST',
+    data,
+  });
+}
+
+export function submitedTaskList(data) {
+  return request({
+    url: 'workspace/process/submitedTaskList',
+    method: 'POST',
+    data,
+  });
+}
+
+export function deleteProcessInstance(data) {
+  return request({
+    url: 'workspace/process/deleteProcessInstance',
+    method: 'POST',
+    data : { data}
+  });
+}
+
+// 查看我的已办
+export function doneList(data) {
+  return request({
+    url: 'workspace/process/doneList',
+    method: 'POST',
+    data,
+  });
+}
+// 查询流程详情
+export function getProcessInstanceInfo(processInstanceId, taskId) {
+  return request({
+    url: 'workspace/process/instanceInfo',
+    method: 'POST',
+    data: { processInstanceId, taskId },
+  });
+}
+
+// 同意
+export function agree(data) {
+  return request({
+    url: 'workspace/agree',
+    method: 'POST',
+    data: data,
+  });
+}
+// 委派人
+export function delegateTask(data) {
+  return request({
+    url: 'workspace/delegateTask',
+    method: 'POST',
+    data: data,
+  });
+}
+// 委派人完成的按钮
+export function resolveTask(data) {
+  return request({
+    url: 'workspace/resolveTask',
+    method: 'POST',
+    data: data,
+  });
+}
+
+//  拒绝,驳回
+export function refuse(data) {
+  return request({
+    url: 'workspace/refuse',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 撤回
+export function revoke(data) {
+  return request({
+    url: 'workspace/revoke',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 转办
+export function assignee(data) {
+  return request({
+    url: 'workspace/assignee',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 回退
+export function rollback(data) {
+  return request({
+    url: 'workspace/rollback',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 加签
+export function addMulti(data) {
+  return request({
+    url: 'workspace/addMulti',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 查询加签人信息
+export function queryMultiUsersInfo(data) {
+  return request({
+    url: 'workspace/queryMultiUsersInfo',
+    method: 'POST',
+    data: data,
+  });
+}
+// 减签
+export function deleteMulti(data) {
+  return request({
+    url: 'workspace/deleteMulti',
+    method: 'POST',
+    data: data,
+  });
+}
+// 评论
+export function comments(data) {
+  return request({
+    url: 'workspace/comments',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 获取历史任务信息列表
+export function historyTaskList(param) {
+  return request({
+    url: 'workspace/process/historyTaskList',
+    method: 'GET',
+    params: param,
+  });
+}
+
+// 上传文件
+export function upLoadFileApi(data) {
+  return request({
+    url: 'workspace/upLoadFile',
+    method: 'POST',
+    data: data,
+  });
+}
+
+// 下载文件
+export function downLoadFileApi(data) {
+  return request({
+    url: 'workspace/downLoadFile',
+    method: 'POST',
+    data: data,
+    responseType: 'blob', //必须加,否则可能出现乱码或者文件错误,导致文件无法打开
+  });
+}
+
+// 查询可退回的节点
+export function getRollbackNodes(data) {
+  return request({
+    url: 'workspace/rollbackNodes',
+    method: 'POST',
+    data,
+  });
+}

+ 32 - 0
src/api/org.js

@@ -0,0 +1,32 @@
+import request from '@/api/request.js'
+
+
+// 查询组织架构树
+export function getOrgTree(param) {
+  return request({
+    url: 'oa/org/tree',
+    method: 'get',
+    params: param
+  })
+}
+
+// 查询系统角色
+export function getRole() {
+  return request({
+    url: 'oa/org/role',
+    method: 'get'
+  })
+}
+
+// 搜索人员
+export function getUserByName(param) {
+  return request({
+    url: 'oa/org/tree/user/search',
+    method: 'get',
+    params: param
+  })
+}
+
+export default {
+  getOrgTree, getUserByName, getRole
+}

+ 65 - 0
src/api/request.js

@@ -0,0 +1,65 @@
+import Vue from "vue";
+import axios from "axios";
+
+import { Notification, MessageBox, Message } from "element-ui";
+
+// 第三方插件
+import "element-ui/lib/theme-chalk/index.css";
+
+Vue.prototype.$axios = axios;
+// 字体图标
+
+const service = axios.create({
+	baseURL: Vue.prototype.BASE_URL + ":9998",
+	timeout: 50000
+});
+
+service.defaults.withCredentials = true; // 让ajax携带cookie
+service.interceptors.request.use(
+	// 每次请求都自动携带Cookie
+	config => {
+		//config.headers.Cookie = document.cookie
+
+		return config;
+	},
+	// eslint-disable-next-line handle-callback-err
+	error => {
+		return Promise.reject(error);
+	}
+);
+
+service.interceptors.response.use(
+	rsp => {
+
+		return rsp;
+	},
+	// 拦截异常的响应
+	err => {
+		console.log("请求", err);
+		switch (err.response.status) {
+			case 401:
+				MessageBox.alert("登陆已过期,请关闭当前窗口重新进入-能臣工作台");
+				break;
+			case 403:
+				//Message.warning("抱歉,您无权访问!")
+				break;
+			case 500:
+				Notification.error({ title: "提醒", message: "服务器出了点小错误" });
+				break;
+			case 404:
+				Notification({
+					title: "提醒",
+					message: "未找到,检查参数",
+					type: "warning"
+				});
+				break;
+			default:
+				//throw 'request error'
+				return Promise.reject(err);
+		}
+		//throw 'request error'
+		return Promise.reject(err);
+	}
+);
+
+export default service;

+ 268 - 0
src/assets/iconfont/iconfont.json

@@ -0,0 +1,268 @@
+{
+  "id": "3538338",
+  "name": "wflow",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "807897",
+      "name": "iconfont-kefu",
+      "font_class": "iconfontkefu",
+      "unicode": "e61c",
+      "unicode_decimal": 58908
+    },
+    {
+      "icon_id": "1313126",
+      "name": "BBD密码",
+      "font_class": "mima",
+      "unicode": "e648",
+      "unicode_decimal": 58952
+    },
+    {
+      "icon_id": "2131309",
+      "name": "人力社保",
+      "font_class": "renlishebao",
+      "unicode": "e636",
+      "unicode_decimal": 58934
+    },
+    {
+      "icon_id": "4774868",
+      "name": "部门",
+      "font_class": "bumen",
+      "unicode": "e758",
+      "unicode_decimal": 59224
+    },
+    {
+      "icon_id": "6337457",
+      "name": "插入图片",
+      "font_class": "charutupian",
+      "unicode": "ec7f",
+      "unicode_decimal": 60543
+    },
+    {
+      "icon_id": "2958951",
+      "name": "考勤管理",
+      "font_class": "kaoqinguanli",
+      "unicode": "e610",
+      "unicode_decimal": 58896
+    },
+    {
+      "icon_id": "3007689",
+      "name": "身份证",
+      "font_class": "shenfenzheng",
+      "unicode": "e614",
+      "unicode_decimal": 58900
+    },
+    {
+      "icon_id": "5121522",
+      "name": "位置",
+      "font_class": "weizhi",
+      "unicode": "e64b",
+      "unicode_decimal": 58955
+    },
+    {
+      "icon_id": "7568869",
+      "name": "24gf-phoneBubble",
+      "font_class": "24gf-phoneBubble",
+      "unicode": "e966",
+      "unicode_decimal": 59750
+    },
+    {
+      "icon_id": "11134714",
+      "name": "考勤",
+      "font_class": "kaoqin",
+      "unicode": "e643",
+      "unicode_decimal": 58947
+    },
+    {
+      "icon_id": "15972093",
+      "name": "会议",
+      "font_class": "huiyi",
+      "unicode": "e61b",
+      "unicode_decimal": 58907
+    },
+    {
+      "icon_id": "19883444",
+      "name": "加班",
+      "font_class": "jiaban",
+      "unicode": "e637",
+      "unicode_decimal": 58935
+    },
+    {
+      "icon_id": "1392555",
+      "name": "表格",
+      "font_class": "biaoge",
+      "unicode": "e665",
+      "unicode_decimal": 58981
+    },
+    {
+      "icon_id": "3868276",
+      "name": "使用文档",
+      "font_class": "shiyongwendang",
+      "unicode": "eb66",
+      "unicode_decimal": 60262
+    },
+    {
+      "icon_id": "5881147",
+      "name": "多选框",
+      "font_class": "duoxuankuang",
+      "unicode": "e62e",
+      "unicode_decimal": 58926
+    },
+    {
+      "icon_id": "26323690",
+      "name": "单选",
+      "font_class": "danxuan",
+      "unicode": "e751",
+      "unicode_decimal": 59217
+    },
+    {
+      "icon_id": "5032",
+      "name": "出租",
+      "font_class": "chuzu",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "1079372",
+      "name": "招聘",
+      "font_class": "zhaopin",
+      "unicode": "e647",
+      "unicode_decimal": 58951
+    },
+    {
+      "icon_id": "1183143",
+      "name": "财务",
+      "font_class": "caiwu",
+      "unicode": "e67d",
+      "unicode_decimal": 59005
+    },
+    {
+      "icon_id": "1727267",
+      "name": "05采购",
+      "font_class": "caigou",
+      "unicode": "e887",
+      "unicode_decimal": 59527
+    },
+    {
+      "icon_id": "1876349",
+      "name": "我的产品",
+      "font_class": "wodechanpin",
+      "unicode": "e679",
+      "unicode_decimal": 59001
+    },
+    {
+      "icon_id": "1977843",
+      "name": "发票管理",
+      "font_class": "fapiaoguanli",
+      "unicode": "e63b",
+      "unicode_decimal": 58939
+    },
+    {
+      "icon_id": "7790995",
+      "name": "工资",
+      "font_class": "gongzi",
+      "unicode": "e7e9",
+      "unicode_decimal": 59369
+    },
+    {
+      "icon_id": "10120009",
+      "name": "住房补贴账户",
+      "font_class": "zhufangbutiezhanghu",
+      "unicode": "e60c",
+      "unicode_decimal": 58892
+    },
+    {
+      "icon_id": "11435446",
+      "name": "维修",
+      "font_class": "weixiu",
+      "unicode": "e613",
+      "unicode_decimal": 58899
+    },
+    {
+      "icon_id": "11435453",
+      "name": "员工离职",
+      "font_class": "yuangonglizhi",
+      "unicode": "e615",
+      "unicode_decimal": 58901
+    },
+    {
+      "icon_id": "11435456",
+      "name": "招聘管理",
+      "font_class": "zhaopinguanli",
+      "unicode": "e616",
+      "unicode_decimal": 58902
+    },
+    {
+      "icon_id": "12911861",
+      "name": "财务",
+      "font_class": "caiwu1",
+      "unicode": "e603",
+      "unicode_decimal": 58883
+    },
+    {
+      "icon_id": "14443545",
+      "name": "请假申请",
+      "font_class": "qingjiashenqing",
+      "unicode": "e60d",
+      "unicode_decimal": 58893
+    },
+    {
+      "icon_id": "14947326",
+      "name": "出差",
+      "font_class": "ziyuan207",
+      "unicode": "e722",
+      "unicode_decimal": 59170
+    },
+    {
+      "icon_id": "17187052",
+      "name": "用餐就餐",
+      "font_class": "yongcanjiucan",
+      "unicode": "e67e",
+      "unicode_decimal": 59006
+    },
+    {
+      "icon_id": "18170995",
+      "name": "地图组织站点,层级,下级,组织架构布局",
+      "font_class": "map-site",
+      "unicode": "ea00",
+      "unicode_decimal": 59904
+    },
+    {
+      "icon_id": "21053836",
+      "name": "合同",
+      "font_class": "hetong",
+      "unicode": "e68a",
+      "unicode_decimal": 59018
+    },
+    {
+      "icon_id": "21159370",
+      "name": "补卡",
+      "font_class": "buka",
+      "unicode": "e6ca",
+      "unicode_decimal": 59082
+    },
+    {
+      "icon_id": "24080655",
+      "name": "出差",
+      "font_class": "chucha",
+      "unicode": "e6c7",
+      "unicode_decimal": 59079
+    },
+    {
+      "icon_id": "24283254",
+      "name": "报销申请-费用报销申请-02",
+      "font_class": "baoxiaoshenqing-feiyongbaoxiaoshenqing-02",
+      "unicode": "e726",
+      "unicode_decimal": 59174
+    },
+    {
+      "icon_id": "29522596",
+      "name": "11C分组,组织树",
+      "font_class": "a-11Cfenzuzuzhishu",
+      "unicode": "e676",
+      "unicode_decimal": 58998
+    }
+  ]
+}

+ 54 - 0
src/components/common/Ellipsis.vue

@@ -0,0 +1,54 @@
+<template>
+  <div :class="{'line': row === 1, 'lines': row > 1}"
+       :title="hoverTip ? content: null"
+       :style="{'--row':row}">
+    <slot name="pre"></slot>
+    {{content}}
+  </div>
+</template>
+
+<script>
+//超出指定行数自动隐藏文字
+export default {
+  name: "Ellipsis",
+  install(Vue){
+    Vue.component('ellipsis', this)
+  },
+  components: {},
+  props:{
+    row: {
+      type: Number,
+      default: 1
+    },
+    hoverTip:{
+      type: Boolean,
+      default: false
+    },
+    content:{
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+
+.line{
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.lines{
+  display: -webkit-box;
+  word-break: break-all;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: var(--row);
+  -webkit-box-orient: vertical;
+}
+</style>

+ 464 - 0
src/components/common/OrgPicker.vue

@@ -0,0 +1,464 @@
+<template>
+  <w-dialog :border="false" closeFree width="600px" @ok="selectOk" :title="title" v-model="visible">
+    <div class="picker">
+      <div class="candidate" v-loading="loading">
+        <div v-if="type !== 'role'">
+          <el-input v-model="search" @input="searchUser" style="width: 95%;" size="small"
+                    clearable placeholder="搜索人员,支持拼音、姓名" prefix-icon="el-icon-search"/>
+          <div v-show="!showUsers">
+            <ellipsis hoverTip style="height: 18px; color: #8c8c8c; padding: 5px 0 0" :row="1" :content="deptStackStr">
+              <i slot="pre" class="el-icon-office-building"></i>
+            </ellipsis>
+            <div style="margin-top: 5px">
+              <el-checkbox v-model="checkAll" @change="handleCheckAllChange" :disabled="!multiple">全选</el-checkbox>
+              <span v-show="deptStack.length > 0" class="top-dept" @click="beforeNode">上一级</span>
+            </div>
+          </div>
+        </div>
+        <div class="role-header" v-else>
+          <div>系统角色</div>
+        </div>
+        <div class="org-items" :style="type === 'role' ? 'height: 350px':''">
+          <el-empty :image-size="100" description="似乎没有数据" v-show="orgs.length === 0"/>
+          <div v-for="(org, index) in orgs" :key="index" :class="orgItemClass(org)" @click="selectChange(org)">
+            <el-checkbox v-model="org.selected" :disabled="disableDept(org)"></el-checkbox>
+            <div v-if="org.type === 'dept'">
+              <i class="el-icon-folder-opened"></i>
+              <span class="name">{{ org.name }}</span>
+              <span @click.stop="nextNode(org)" :class="`next-dept${org.selected ? '-disable':''}`">
+                <i class="iconfont icon-map-site"></i>下级
+              </span>
+            </div>
+            <div v-else-if="org.type === 'user'" style="display: flex; align-items: center">
+              <el-avatar :size="35" :src="org.avatar" v-if="$isNotEmpty(org.avatar)"/>
+              <span v-else class="avatar">{{getShortName(org.name)}}</span>
+              <span class="name">{{ org.name }}</span>
+            </div>
+            <div style="display: inline-block" v-else>
+              <i class="iconfont icon-bumen"></i>
+              <span class="name">{{ org.name }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="selected">
+        <div class="count">
+          <span>已选 {{ select.length }} 项</span>
+          <span @click="clearSelected">清空</span>
+        </div>
+        <div class="org-items" style="height: 350px;">
+          <el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="select.length === 0"/>
+          <div v-for="(org, index) in select" :key="index" :class="orgItemClass(org)" >
+            <div v-if="org.type === 'dept'">
+              <i class="el-icon-folder-opened"></i>
+              <span style="position: static" class="name">{{ org.name }}</span>
+            </div>
+            <div v-else-if="org.type === 'user'" style="display: flex; align-items: center">
+              <el-avatar :size="35" :src="org.avatar" v-if="$isNotEmpty(org.avatar)"/>
+              <span v-else class="avatar">{{getShortName(org.name)}}</span>
+              <span class="name">{{ org.name }}</span>
+            </div>
+            <div v-else>
+              <i class="iconfont icon-bumen"></i>
+              <span class="name">{{ org.name }}</span>
+            </div>
+            <i class="el-icon-close" @click="noSelected(index)"></i>
+          </div>
+        </div>
+      </div>
+    </div>
+  </w-dialog>
+</template>
+
+<script>
+import {getOrgTree, getUserByName} from '@/api/org'
+
+export default {
+  name: "OrgPicker",
+  components: {},
+  props: {
+    title: {
+      default: '请选择',
+      type: String
+    },
+    type: {
+      default: 'org', //org选择部门/人员  user-选人  dept-选部门 role-选角色
+      type: String
+    },
+    multiple: { //是否多选
+      default: false,
+      type: Boolean
+    },
+    selected: {
+      default: () => {
+        return []
+      },
+      type: Array
+    },
+  },
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      checkAll: false,
+      nowDeptId: null,
+      isIndeterminate: false,
+      searchUsers: [],
+      nodes: [],
+      select: [],
+      search: '',
+      deptStack: []
+    }
+  },
+  computed: {
+    deptStackStr() {
+      return String(this.deptStack.map(v => v.name)).replaceAll(',', ' > ')
+    },
+    orgs() {
+      return !this.search || this.search.trim() === '' ? this.nodes : this.searchUsers
+    },
+    showUsers(){
+      return this.search || this.search.trim() !== ''
+    }
+  },
+  methods: {
+    show() {
+      this.visible = true
+      this.init()
+      this.getOrgList()
+    },
+    orgItemClass(org){
+      return {
+        'org-item': true,
+        'org-dept-item': org.type === 'dept',
+        'org-user-item': org.type === 'user',
+        'org-role-item': org.type === 'role'
+      }
+    },
+    disableDept(node) {
+      return this.type === 'user' && 'dept' === node.type
+    },
+    getOrgList() {
+      this.loading = true
+      getOrgTree({deptId: this.nowDeptId, type: this.type}).then(rsp => {
+        this.loading = false
+        this.nodes = rsp.data
+        this.selectToLeft()
+      }).catch(err => {
+        this.loading = false
+        this.$message.error(err.response.data)
+      })
+    },
+    getShortName(name) {
+      if (name) {
+        return name.length > 2 ? name.substring(1, 3) : name;
+      }
+      return '**'
+    },
+    searchUser() {
+      let userName = this.search.trim()
+      this.searchUsers = []
+      this.loading = true
+      getUserByName({userName: userName}).then(rsp => {
+        this.loading = false
+        this.searchUsers = rsp.data
+        this.selectToLeft()
+      }).catch(err => {
+        this.loading = false
+        this.$message.error("接口异常")
+      })
+    },
+    selectToLeft() {
+      let nodes = this.search.trim() === '' ? this.nodes : this.searchUsers;
+      nodes.forEach(node => {
+        for (let i = 0; i < this.select.length; i++) {
+          if (this.select[i].id === node.id) {
+            node.selected = true;
+            break;
+          } else {
+            node.selected = false;
+          }
+        }
+      })
+    },
+    selectChange(node) {
+      if (node.selected) {
+        this.checkAll = false;
+        for (let i = 0; i < this.select.length; i++) {
+          if (this.select[i].id === node.id) {
+            this.select.splice(i, 1);
+            break;
+          }
+        }
+        node.selected = false;
+      } else if (!this.disableDept(node)) {
+        node.selected = true
+        let nodes = this.search.trim() === '' ? this.nodes : this.searchUsers;
+        if (!this.multiple) {
+          nodes.forEach(nd => {
+            if (node.id !== nd.id) {
+              nd.selected = false
+            }
+          })
+        }
+        if (node.type === 'dept') {
+          if (!this.multiple) {
+            this.select = [node]
+          } else {
+            this.select.unshift(node);
+          }
+        } else {
+          if (!this.multiple) {
+            this.select = [node]
+          } else {
+            this.select.push(node);
+          }
+        }
+      }
+    },
+    noSelected(index) {
+      let nodes = this.nodes;
+      for (let f = 0; f < 2; f++) {
+        for (let i = 0; i < nodes.length; i++) {
+          if (nodes[i].id === this.select[index].id) {
+            nodes[i].selected = false;
+            this.checkAll = false;
+            break;
+          }
+        }
+        nodes = this.searchUsers;
+      }
+      this.select.splice(index, 1)
+    },
+    handleCheckAllChange() {
+      this.nodes.forEach(node => {
+        if (this.checkAll) {
+          if (!node.selected && !this.disableDept(node)) {
+            node.selected = true
+            this.select.push(node)
+          }
+        } else {
+          node.selected = false;
+          for (let i = 0; i < this.select.length; i++) {
+            if (this.select[i].id === node.id) {
+              this.select.splice(i, 1);
+              break;
+            }
+          }
+        }
+      })
+    },
+    nextNode(node) {
+      this.nowDeptId = node.id
+      this.deptStack.push(node)
+      this.getOrgList()
+    },
+    beforeNode() {
+      if (this.deptStack.length === 0) {
+        return;
+      }
+      if (this.deptStack.length < 2) {
+        this.nowDeptId = null
+      } else {
+        this.nowDeptId = this.deptStack[this.deptStack.length - 2].id
+      }
+      this.deptStack.splice(this.deptStack.length - 1, 1);
+      this.getOrgList()
+    },
+    recover() {
+      this.select = []
+      this.nodes.forEach(nd => nd.selected = false)
+    },
+    selectOk() {
+      this.$emit('ok', Object.assign([], this.select.map(v => {
+        v.avatar = undefined
+        return v
+      })))
+      this.visible = false
+      this.recover()
+    },
+    clearSelected(){
+      this.$confirm('您确定要清空已选中的项?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.recover()
+      })
+    },
+    close() {
+      this.$emit('close')
+      this.recover()
+    },
+    init() {
+      this.checkAll = false;
+      this.nowDeptId = null;
+      this.deptStack = [];
+      this.nodes = []
+      this.select = Object.assign([], this.selected)
+      this.selectToLeft()
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+@containWidth: 278px;
+
+.candidate, .selected {
+  position: absolute;
+  display: inline-block;
+  width: @containWidth;
+  height: 400px;
+  border: 1px solid #e8e8e8;
+}
+
+.picker {
+  height: 402px;
+  position: relative;
+  text-align: left;
+  .candidate {
+    left: 0;
+    top: 0;
+
+    .role-header{
+      padding: 10px !important;
+      margin-bottom: 5px;
+      border-bottom: 1px solid #e8e8e8;
+    }
+
+    .top-dept{
+      margin-left: 20px;
+      cursor: pointer;
+      color:#38adff;
+    }
+    .next-dept {
+      float: right;
+      color: @theme-primary;
+      cursor: pointer;
+    }
+
+    .next-dept-disable {
+      float: right;
+      color: #8c8c8c;
+      cursor: not-allowed;
+    }
+
+    & > div:first-child {
+      padding: 5px 10px;
+    }
+
+  }
+
+  .selected {
+    right: 0;
+    top: 0;
+  }
+
+  .org-items {
+    overflow-y: auto;
+    height: 310px;
+
+    .el-icon-close {
+      position: absolute;
+      right: 5px;
+      cursor: pointer;
+      font-size: larger;
+    }
+    .org-dept-item {
+      padding: 10px 5px;
+
+      & > div {
+        display: inline-block;
+
+        & > span:last-child {
+          position: absolute;
+          right: 5px;
+        }
+      }
+    }
+
+    .org-role-item {
+      display: flex;
+      align-items: center;
+      padding: 10px 5px;
+    }
+
+    /deep/ .org-user-item {
+      display: flex;
+      align-items: center;
+      padding: 5px;
+
+      & > div {
+        display: inline-block;
+      }
+
+      .avatar {
+        width: 35px;
+        text-align: center;
+        line-height: 35px;
+        background: @theme-primary;
+        color: white;
+        border-radius: 50%;
+      }
+    }
+
+    /deep/ .org-item {
+      margin: 0 5px;
+      border-radius: 5px;
+      position: relative;
+
+      .el-checkbox {
+        margin-right: 10px;
+      }
+
+      .name {
+        margin-left: 5px;
+      }
+
+      &:hover {
+        background: #f1f1f1;
+      }
+    }
+  }
+}
+
+.selected {
+  border-left: none;
+
+  .count {
+    width: calc(@containWidth - 20px);
+    padding: 10px;
+    display: inline-block;
+    border-bottom: 1px solid #e8e8e8;
+    margin-bottom: 5px;
+    & > span:nth-child(2) {
+      float: right;
+      color: #c75450;
+      cursor: pointer;
+    }
+  }
+}
+
+/deep/ .el-dialog__body {
+  padding: 10px 20px;
+}
+
+.disabled{
+  cursor: not-allowed !important;
+  color: #8c8c8c !important;
+}
+
+::-webkit-scrollbar {
+  float: right;
+  width: 4px;
+  height: 4px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #efefef;
+}
+</style>

+ 36 - 0
src/components/common/Tip.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-tooltip :effect="isDark ? 'dark':'light'" :content="content" placement="top-start">
+    <div>
+      <slot></slot>
+      <i class="el-icon-question" style="margin: 0 0px"></i>
+    </div>
+  </el-tooltip>
+</template>
+
+<script>
+export default {
+  install(Vue) {
+    Vue.component('Tip', this)
+  },
+  name: "Tip",
+  components: {},
+  props:{
+    isDark:{
+      type: Boolean,
+      default: false
+    },
+    content:{
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 119 - 0
src/components/common/WDialog.vue

@@ -0,0 +1,119 @@
+<template>
+  <el-dialog
+    custom-class="custom-dialog"
+    class="border"
+    :width="width"
+    :title="title"
+    append-to-body
+    :close-on-click-modal="clickClose"
+    :destroy-on-close="closeFree"
+    :visible.sync="_value"
+  >
+    <slot></slot>
+    <div slot="footer" v-if="showFooter">
+      <el-button
+        size="mini"
+        @click="
+          _value = false;
+          $emit('cancel');
+        "
+        >{{ cancelText }}</el-button
+      >
+      <el-button size="mini" type="primary" @click="$emit('ok')">{{
+        okText
+      }}</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: "WDialog",
+  install(Vue) {
+    Vue.component("WDialog", this);
+  },
+  components: {},
+  props: {
+    title: {
+      type: String,
+      default: "",
+    },
+    width: {
+      type: String,
+      default: "50%",
+    },
+    value: {
+      type: Boolean,
+      default: false,
+    },
+    clickClose: {
+      type: Boolean,
+      default: false,
+    },
+    closeFree: {
+      type: Boolean,
+      default: false,
+    },
+    showFooter: {
+      type: Boolean,
+      default: true,
+    },
+    cancelText: {
+      type: String,
+      default: "取 消",
+    },
+    okText: {
+      type: String,
+      default: "确 定",
+    },
+    border: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  computed: {
+    _value: {
+      get() {
+        return this.value;
+      },
+      set(val) {
+        this.$emit("input", val);
+      },
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="less" scoped>
+/deep/ .custom-dialog {
+  .el-dialog__header {
+    padding: 10px 20px;
+    .el-dialog__title {
+      font-size: 17px;
+    }
+    .el-dialog__headerbtn {
+      top: 15px;
+      .i {
+        font-size: large;
+      }
+    }
+  }
+
+  .el-dialog__footer {
+    padding: 10px 20px;
+  }
+}
+
+.border {
+  /deep/ .el-dialog__header {
+    border-bottom: 1px solid #e8e8e8;
+  }
+  /deep/ .el-dialog__footer {
+    border-top: 1px solid #e8e8e8;
+  }
+}
+</style>

+ 35 - 0
src/components/common/route-tab.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="route-tab">
+    <el-tabs v-bind="$attrs" :value="$route.path" @tab-click="handleClick">
+      <el-tab-pane
+        v-for="route in routes"
+        :key="route.path"
+        :label="route.label"
+        :name="route.path"
+        :disabled="route.disabled"
+        :closable="route.closable"
+      >
+        <slot v-if="route.path === $route.path"></slot>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'RouteTab',
+  props: {
+    routes: {
+      type: Array,
+      default: () => []
+    },
+  },
+  methods: {
+    handleClick(tab) {
+      this.$router.push({
+        path: tab.name,
+      })
+    },
+  },
+}
+</script>

+ 13 - 3
src/main.js

@@ -10,8 +10,8 @@ import store from './store'
 import {getDept,openUrl, getToken, getTyps, getTypsName, supremeAuthority, getUserData, getEmployeeMap,getEmployeeMapAll,getEmployeeMapItem,getIsIdentity,getCache,setCache,removeCache,returnDeptName,getIsAdministrator} from '@/utils/auth'
 import './performanceSet' // 绩效系统相关配置
 import {onFilePreView } from '@/okr/utils/auth';
-import { VueOkrTree } from "vue-okr-tree";
-import "vue-okr-tree/dist/vue-okr-tree.css";
+// import { VueOkrTree } from "vue-okr-tree";
+// import "vue-okr-tree/dist/vue-okr-tree.css";
 import 'shepherd.js/dist/css/shepherd.css';
 
 import './icons'
@@ -33,7 +33,7 @@ Vue.component('userImage', UserImage)
 Vue.component('NoData', noData)
 Vue.component('BrawerBox', BrawerBox)
 Vue.component('TaskItem', TaskItem)
-Vue.component('VueOkrTree', VueOkrTree)
+// Vue.component('VueOkrTree', VueOkrTree)
 Vue.use(Element, {
   size: 'medium',
 })
@@ -64,6 +64,16 @@ Vue.prototype.$openUrl = openUrl
 Vue.prototype.$getDept = getDept
 // okr
 Vue.prototype.$onFilePreView = onFilePreView
+//工作流
+Vue.prototype.$isNotEmpty = function(obj){
+  return (obj !== undefined && obj !== null && obj !== '' && obj !== 'null')
+}
+
+Vue.prototype.$getDefalut = function(obj, key, df){
+  return (obj === undefined || key === undefined || !this.$isNotEmpty(obj[key])) ? df : obj[key];
+}
+
+Vue.prototype.$deepCopy = function (obj){return JSON.parse(JSON.stringify(obj))}
 
 // 上传相关
 Vue.prototype.$action = 'https://integralsys.oss-cn-shenzhen.aliyuncs.com'

+ 5 - 0
src/router/index.js

@@ -414,6 +414,11 @@ const constantRouterMap = [
         name: '设置',
         component: () => import( /* webpackChunkName: "system" */ '@/views/system'),
       },
+      {
+        path: '/admin/design',
+        name: 'design',
+        component: () => import( /* webpackChunkName: "design" */ '@/views/admin/FormProcessDesign.vue'),
+      },
     ]
   },
 

+ 22 - 0
src/store/index.js

@@ -10,6 +10,28 @@ const store = new Vuex.Store({
     user,
     okrStore
   },
+  state: {
+    nodeMap: new Map(),
+    isEdit: null,
+    selectedNode: {},
+    selectFormItem: null,
+    design: {},
+    runningList: [],
+    noTakeList: [],
+    endList: [],
+    diagramMode: 'design',
+  },
+  mutations: {
+    selectedNode(state, val) {
+      state.selectedNode = val
+    },
+    loadForm(state, val) {
+      state.design = val
+    },
+    setIsEdit(state, val) {
+      state.isEdit = val
+    }
+  },
   getters
 })
 

+ 327 - 0
src/views/admin/FormProcessDesign.vue

@@ -0,0 +1,327 @@
+<template>
+  <el-container>
+    <el-header style="background: white">
+      <layout-header v-model="activeSelect" @publish="publishProcess" @preview="preview"></layout-header>
+    </el-header>
+    <div class="layout-body">
+      <form-base-setting ref="baseSetting" v-show="activeSelect === 'baseSetting'"/>
+      <form-design ref="formSetting" v-show="activeSelect === 'formSetting'"/>
+      <process-design ref="processDesign" v-show="activeSelect === 'processDesign'"/>
+      <form-pro-setting ref="proSetting" v-show="activeSelect === 'proSetting'"/>
+    </div>
+    <w-dialog :showFooter="false" v-model="validVisible" title="设置项检查">
+      <el-steps align-center :active="validStep" finish-status="success">
+        <el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
+                 :icon="step.icon" :status="step.status" :description="step.description"/>
+      </el-steps>
+      <el-result :icon="validIcon" :title="errTitle" :subTitle="validResult.desc">
+        <i slot="icon" style="font-size: 30px" v-if="!validResult.finished" class="el-icon-loading"></i>
+        <div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
+          <ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
+            <i slot="pre" class="el-icon-warning-outline"></i>
+          </ellipsis>
+        </div>
+        <template slot="extra">
+          <el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
+            {{ validResult.action }}
+          </el-button>
+        </template>
+      </el-result>
+    </w-dialog>
+  </el-container>
+
+</template>
+
+<script>
+import LayoutHeader from './LayoutHeader'
+import {getFormDetail, createForm, updateFormDetail} from '@/api/design'
+import FormBaseSetting from '@/views/admin/layout/FormBaseSetting'
+import FormDesign from '@/views/admin/layout/FormDesign'
+import ProcessDesign from '@/views/admin/layout/ProcessDesign'
+import FormProSetting from '@/views/admin/layout/FormProSetting'
+
+export default {
+  name: "FormProcessDesign",
+  components: {LayoutHeader, FormBaseSetting, FormDesign, ProcessDesign, FormProSetting},
+  data() {
+    return {
+      isNew: true,
+      validStep: 0,
+      timer: null,
+      activeSelect: 'baseSetting',
+      validVisible: false,
+      validResult: {},
+      validOptions: [
+        {title: '基础信息', description: '', icon: '', status: ''},
+        {title: '审批表单', description: '', icon: '', status: ''},
+        {title: '审批流程', description: '', icon: '', status: ''},
+        {title: '扩展设置', description: '', icon: '', status: ''}
+      ],
+      validComponents: ['baseSetting', 'formSetting', 'processDesign', 'proSetting'],
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    },
+    errTitle(){
+      if (this.validResult.finished && !this.validResult.success){
+        return this.validResult.title + ` (${this.validResult.errs.length}项错误) 😥`
+      }
+      return this.validResult.title
+    },
+    validIcon() {
+      if (!this.validResult.finished) {
+        return 'el-icon-loading'
+      } else if (this.validResult.success) {
+        return 'success'
+      } else {
+        return 'warning'
+      }
+    }
+  },
+  created() {
+    this.showValiding()
+    let formId = this.$route.query.code
+    //判断传参,决定是新建还是加载原始数据
+    this.loadInitFrom()
+    if (this.$isNotEmpty(formId)) {
+      this.isNew = false
+      this.loadFormInfo(formId)
+    }
+    let group = this.$route.query.group
+    this.setup.groupId = this.$isNotEmpty(group) ? parseInt(group) : null
+  },
+  beforeDestroy() {
+    this.stopTimer()
+  },
+  methods: {
+    loadFormInfo(formId) {
+      getFormDetail(formId).then(rsp => {
+        console.log(rsp.data)
+        let form = rsp.data;
+        form.logo = JSON.parse(form.logo)
+        form.settings = JSON.parse(form.settings)
+        form.formItems = JSON.parse(form.formItems)
+        form.process = JSON.parse(form.process)
+        this.$store.commit('loadForm', form)
+      }).catch(err => {
+        this.$message.error(err)
+      })
+    },
+    loadInitFrom() {
+      this.$store.commit('loadForm', {
+        formId: null,
+        formName: "未命名表单",
+        logo: {
+          icon: "el-icon-eleme",
+          background: "#1e90ff"
+        },
+        settings: {
+          commiter: [],
+          admin: [],
+          sign: false,
+          notify: {
+            types: ["APP"],
+            title: "消息通知标题"
+          }
+        },
+        groupId: undefined,
+        formItems: [],
+        process: {
+          id: "root",
+          parentId: null,
+          type: "ROOT",
+          name: "发起人",
+          desc: "任何人",
+          props: {
+            assignedUser: [],
+            formPerms: []
+          },
+          children: {}
+        },
+        remark: "备注说明"
+      })
+    },
+    validateDesign() {
+      console.log('000')
+      this.validVisible = true
+      this.validStep = 0
+      this.showValiding()
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.validResult.errs = this.$refs[this.validComponents[this.validStep]].validate()
+        if (Array.isArray(this.validResult.errs) && this.validResult.errs.length === 0) {
+          this.validStep++;
+          if (this.validStep >= this.validOptions.length) {
+            this.stopTimer()
+            this.showValidFinish(true)
+          }
+        } else {
+          this.stopTimer()
+          this.validOptions[this.validStep].status = 'error'
+          this.showValidFinish(false, this.getDefaultValidErr())
+        }
+      }, 300)
+    },
+    getDefaultValidErr() {
+      switch (this.validStep) {
+        case 0:
+          return '请检查基础设置项';
+        case 1:
+          return '请检查审批表单相关设置'
+        case 2:
+          return '请检查审批流程,查看对应标注节点错误信息'
+        case 3:
+          return '请检查扩展设置'
+        default:
+          return '未知错误'
+      }
+    },
+    showValidFinish(success, err) {
+      this.validResult.success = success
+      this.validResult.finished = true
+      this.validResult.title = success ? '校验完成 😀' : '校验失败 '
+      this.validResult.desc = success ? '设置项校验成功,是否提交?' : err
+      this.validResult.action = success ? '提 交' : '去修改'
+    },
+    showValiding() {
+      this.validResult = {
+        errs: [],
+        finished: false,
+        success: false,
+        title: '检查中...',
+        action: '处理',
+        desc: '正在检查设置项'
+      }
+      this.validStep = 0
+      this.validOptions.forEach(op => {
+        op.status = ''
+        op.icon = ''
+        op.description = ''
+      })
+    },
+    doAfter() {
+      if (this.validResult.success) {
+        this.doPublish()
+      } else {
+        this.activeSelect = this.validComponents[this.validStep]
+        this.validVisible = false
+      }
+    },
+    stopTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+      }
+    },
+    preview() {
+      this.validateDesign()
+    },
+    publishProcess() {
+      this.validateDesign()
+    },
+    doPublish() {
+      this.$confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
+        confirmButtonText: '发布',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        console.log(this.setup)
+        let processNew =  JSON.parse(JSON.stringify(this.setup.process));
+
+        //判断条件分支
+        this.conditionRecursion(processNew);
+
+        let template = {
+          formId: this.setup.formId,
+          formName: this.setup.formName,
+          logo: JSON.stringify(this.setup.logo),
+          settings: JSON.stringify(this.setup.settings),
+          groupId: this.setup.groupId,
+          formItems: JSON.stringify(this.setup.formItems),
+          process: JSON.stringify(processNew),
+          remark: this.setup.remark
+        }
+        console.log(template);
+        // return;
+        if (this.isNew || !this.$isNotEmpty(this.setup.formId)) {
+          createForm(template).then(rsp => {
+            this.$message.success("创建表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        } else {
+          updateFormDetail(template).then(rsp => {
+            this.$message.success("更新表单成功")
+            this.$router.push("/formsPanel")
+          }).catch(err => {
+            this.$message.error(err)
+          })
+        }
+      })
+    },
+    conditionRecursion(process){
+      if(null != process && undefined != process){
+        if(null != process.branchs && undefined != process.branchs){
+          process.branchs.map((item, i) => {
+            if (i == process.branchs.length - 1) {
+              item.typeElse = true;
+            } else {
+              item.typeElse = false;
+            }
+            if(null != item.children && undefined != item.children){
+              this.conditionRecursion(item.children)
+            }else{
+              return item;
+            }
+          });
+        }
+        this.conditionRecursion(process.children)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+.layout-body {
+  min-width: 980px;
+}
+
+/deep/ .el-step {
+  .is-success {
+    color: #2a99ff;
+    border-color: #2a99ff;
+  }
+}
+
+.err-info{
+  max-height: 180px;
+  overflow-y: auto;
+  & > div{
+    padding: 5px;
+    margin: 2px 0;
+    width: 220px;
+    text-align: left;
+    border-radius: 3px;
+    background: rgb(242 242 242);
+  }
+  i{
+    margin: 0 5px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 2px;
+  height: 2px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+
+</style>

+ 351 - 0
src/views/admin/FormsPanel.vue

@@ -0,0 +1,351 @@
+<template>
+  <div class="from-panel" ref="panel">
+    <div class="from-title">
+      <el-button icon="el-icon-back" type="info" size="mini" circle plain style="margin-right: 15px" @click="$router.push('/')"></el-button>
+      <span>流程面板</span>
+      <span style="color: #c75450; margin-left: 20px">📢 大家要体验的话,尽量使用已有的分组和流程进行编辑,不要随意新建,数据有点乱哈,谢谢了❤</span>
+      <div>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="newProcess">新建表单</el-button>
+        <el-button icon="el-icon-plus" @click="addGroup" size="mini">新建分组</el-button>
+      </div>
+    </div>
+    <draggable :list="groups" group="group" handle=".el-icon-rank" filter=".undrag" @start="groupsSort = true"
+               :options="{animation: 300 ,sort:true, scroll: true, chosenClass:'choose'}" @end="groupSort">
+      <div :class="{'form-group':true, 'undrag': (group.id === 0 || group.id === undefined)}"
+           v-show="group.id > 1 || group.items.length > 0 " v-for="(group, gidx) in groups" :key="gidx">
+        <div class="form-group-title">
+          <span>{{group.name}}</span>
+          <span>({{group.items.length}})</span>
+          <i class="el-icon-rank" title="长按拖动可对分组排序"></i>
+          <div v-if="!(group.id === 0 || group.id === undefined)">
+            <el-dropdown>
+              <el-button type="text" icon="el-icon-setting">编辑分组</el-button>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item icon="el-icon-edit-outline" @click.native="editGroup(group)">修改名称</el-dropdown-item>
+                <el-dropdown-item icon="el-icon-delete" @click.native="delGroup(group)">删除分组</el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </div>
+        </div>
+        <draggable style="width: 100%; min-height:25px" :list="group.items" group="from" @end="groupSort" v-show="!groupsSort" filter=".undrag"
+                   :options="{animation: 300, delay: 200, chosenClass:'choose', scroll: true, sort:true}">
+          <div :class="{'form-group-item':true, 'undrag': item.isStop}" v-for="(item, index) in group.items"
+               :key="index" title="长按0.5S后可拖拽表单进行排序">
+            <div>
+              <i :class="item.logo.icon" :style="'background: '+item.logo.background"></i>
+              <span>{{item.formName}}</span><br>
+            </div>
+            <div class="desp">{{item.remark}}</div>
+            <div>
+              <span>最后更新时间:{{item.updated}}</span>
+            </div>
+            <div>
+              <el-button type="text" icon="el-icon-edit-outline" size="mini" @click="editFrom(item, group)">编辑</el-button>
+              <el-button type="text" :icon="item.isStop ? 'el-icon-check':'el-icon-close'" size="mini" @click="stopFrom(item)">
+                {{item.isStop ? '启用' : '停用'}}
+              </el-button>
+
+              <el-button type="text" icon="el-icon-delete" size="mini" @click="moveFrom(item)" v-if="item.isStop">删除
+              </el-button>
+              <el-popover placement="left" trigger="click" width="400" style="margin-left: 10px" @show="moveSelect === null" v-else>
+                <el-radio-group v-model="moveSelect" size="mini">
+                  <el-radio :label="g.id" border v-for="g in groups" :key="g.id" v-show="g.id > 1"
+                            :disabled="g.id === group.id" style="margin: 10px;">{{g.name}}</el-radio>
+                </el-radio-group>
+                <div style="text-align: right; margin: 0">
+                  <el-button type="primary" size="mini" @click="moveFrom(item)">提交</el-button>
+                </div>
+                <el-button slot="reference" type="text" icon="el-icon-s-promotion" size="mini" >移动</el-button>
+              </el-popover>
+            </div>
+          </div>
+        </draggable>
+        <div style="text-align: center" v-if="group.items === undefined || group.items.length === 0">
+          <el-button style="padding-top: 0" type="text" icon="el-icon-plus" @click="newProcess(group.id)">创建新表单</el-button>
+        </div>
+      </div>
+    </draggable>
+  </div>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import {
+  getFormGroups, groupItemsSort,
+  getFormDetail, updateGroup, updateForm
+} from '@/api/design'
+
+export default {
+  name: "FormsPanel",
+  components: {draggable},
+  data() {
+    return {
+      moveSelect: '',
+      visible: false,
+      groupsSort: false,
+      groups: []
+    }
+  },
+  mounted() {
+    this.getGroups()
+  },
+  methods: {
+    getGroups() {
+      getFormGroups().then(rsp => {
+        this.groups = rsp.data
+      }).catch(err => this.$message.error('获取分组异常'))
+    },
+    newProcess(groupId) {
+      this.$store.commit("setTemplate", this.getTemplateData());
+      this.$store.commit("setIsEdit", false);
+      this.$router.push("/admin/design?groupId=" + groupId);
+    },
+    groupSort() {
+      this.groupsSort = false
+      groupItemsSort(this.groups).then(rsp => {
+        this.$message.success(rsp.data)
+        this.getGroups()
+      }).catch(err => {
+        this.getGroups()
+        this.$message.error(err.response.data)
+      })
+    },
+    addGroup() {
+      this.$prompt('请输入要添加的组名', '新的分组名', {
+        confirmButtonText: '提交',
+        cancelButtonText: '取消',
+        inputPattern: /^[\u4E00-\u9FA5A-Za-z0-9\\-]{1,30}$/,
+        inputErrorMessage: '分组名不能为空且长度小于30',
+        inputPlaceholder: '请输入分组名'
+      }).then(({value}) => {
+        updateGroup({name: value}, 'post').then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+        }).catch(err => this.$message.error(err.response.data))
+      })
+    },
+    delGroup(group) {
+      this.$confirm('删除分组并不会删除表单,表单将会被转移到 “其他” 分组,确定要删除分组 ' + group.name + '?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        updateGroup({id: group.id}, 'delete').then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+        }).catch(err => this.$message.error(err.response.data))
+      })
+    },
+    editGroup(group) {
+      this.$prompt('请输入新的组名', '修改分组名', {
+        confirmButtonText: '提交',
+        cancelButtonText: '取消',
+        inputPattern: /^[\u4E00-\u9FA5A-Za-z0-9\\-]{1,30}$/,
+        inputErrorMessage: '分组名不能为空且长度小于30',
+        inputPlaceholder: '请输入分组名',
+        inputValue: group.name
+      }).then(({value}) => {
+        updateGroup({id: group.id, name: value}, 'put').then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+        }).catch(err => this.$message.error(err.response.data))
+      })
+    },
+    updateForm(item, type) {
+      updateForm({templateId: item.templateId, type: type}).then(rsp => {
+        this.$message.success(rsp.data)
+        this.getGroups()
+      }).catch(err => this.$message.error(err.response.data))
+    },
+    getTemplateData(data, group){
+      return data
+    },
+    editFrom(item, group) {
+      this.$router.push("/admin/design?code=" + item.formId);
+    },
+    stopFrom(item) {
+      console.log(item)
+      let tip = item.isStop ? ' 启用后将会进入 “其他” 分组,是否继续?' : ' 停用后将会被转移到 “已停用” 分组,您可以再次启用或者删除它,是否继续?';
+      this.$confirm(`<span style="font-weight: bold">${item.formName}</span>` + tip, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        dangerouslyUseHTMLString: true,
+      }).then(() => {
+        this.updateForm(item, (item.isStop ? 'using' : 'stop'));
+      })
+    },
+    moveFrom(item) {
+      if (item.isStop) {
+        this.$confirm(`您确定要删除表单 <span style="font-weight: bold">${item.formName}</span>> 吗,删除后无法恢复,是否继续?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+          dangerouslyUseHTMLString: true,
+        }).then(() => {
+          this.updateForm(item, 'delete');
+        })
+      } else {
+        if (this.moveSelect === null || this.moveSelect === ''){
+          this.$message.error('请选择分组')
+          return;
+        }
+        updateForm({templateId: item.templateId, type: 'move', groupId: this.moveSelect}).then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+          this.moveSelect = null
+        }).catch(err => this.$message.error(err.response.data))
+      }
+    },
+  }
+}
+</script>
+
+<style lang="less" scoped>
+body {
+  background: #ffffff !important;
+}
+
+.undrag {
+  background: #ebecee !important;
+}
+
+.from-panel {
+  padding: 50px 100px;
+  min-width: 500px;
+  background: #ffffff;
+
+  /deep/ .from-title {
+    div {
+      float: right;
+
+      .el-button {
+        border-radius: 15px;
+      }
+    }
+  }
+
+  //height: 100vh;
+}
+
+.choose {
+  background: #e9ebee;
+}
+
+.form-group {
+  margin: 20px 0;
+  padding: 5px 0px;
+  border-radius: 10px;
+  //border: 1px solid #d3d3d3;
+  box-shadow: 1px 1px 10px 0 #d2d2d2;
+
+  &:hover {
+    box-shadow: 1px 1px 12px 0 #b3b3b3;
+  }
+
+  .form-group-title {
+    padding: 5px 20px;
+    height: 40px;
+    line-height: 40px;
+    border-bottom: 1px solid #d3d3d3;
+
+    .el-icon-rank {
+      display: none;
+      cursor: move;
+    }
+
+    &:hover {
+      .el-icon-rank {
+        display: inline-block;
+      }
+    }
+
+    div {
+      display: inline-block;
+      float: right;
+    }
+
+    span:first-child {
+      margin-right: 5px;
+      font-size: 15px;
+      font-weight: bold;
+    }
+
+    span:nth-child(2) {
+      color: #656565;
+      font-size: small;
+      margin-right: 10px;
+    }
+
+    /deep/ .el-button {
+      color: #404040;
+      margin-left: 20px;
+
+      &:hover {
+        color: #2b2b2b;
+      }
+    }
+  }
+
+  .form-group-item:nth-child(1) {
+    border-top: none !important;
+  }
+
+  .form-group-item {
+    color: #3e3e3e;
+    font-size: small;
+    padding: 10px 0;
+    margin: 0 20px;
+    height: 40px;
+    position: relative;
+    line-height: 40px;
+    border-top: 1px solid #d3d3d3;
+
+    div {
+      display: inline-block;
+    }
+
+    i {
+      border-radius: 10px;
+      padding: 7px;
+      font-size: 20px;
+      color: #ffffff;
+      margin-right: 10px;
+    }
+
+    div:nth-child(1) {
+      float: left;
+    }
+
+    div:nth-child(2) {
+      position: absolute;
+      color: #7a7a7a;
+      font-size: 12px;
+      left: 200px;
+      max-width: 300px;
+      overflow: hidden;
+    }
+
+    div:nth-child(3) {
+      position: absolute;
+      right: 30%;
+    }
+
+    div:nth-child(4) {
+      float: right;
+    }
+  }
+}
+
+@media screen and (max-width: 1000px) {
+  .desp {
+    display: none !important;
+  }
+}
+
+@media screen and (max-width: 800px) {
+  .from-panel{
+    padding: 50px 10px;
+  }
+}
+</style>

+ 161 - 0
src/views/admin/LayoutHeader.vue

@@ -0,0 +1,161 @@
+<template>
+  <div>
+    <div class="header">
+      <el-menu :default-active="value" active-text-color="#409eff" class="el-menu-demo" mode="horizontal"
+               @select="handleSelect">
+        <el-menu-item index="baseSetting" @click="to('baseSetting')">① 基础信息</el-menu-item>
+        <el-menu-item index="formSetting" @click="to('formSetting')">② 审批表单</el-menu-item>
+        <el-menu-item index="processDesign" @click="to('processDesign')">③ 审批流程
+        </el-menu-item>
+        <el-menu-item index="proSetting" @click="to('proSetting')">④ 扩展设置</el-menu-item>
+      </el-menu>
+      <div class="publish">
+        <el-button size="mini" @click="preview"><i class="el-icon-view"></i>预览</el-button>
+        <el-button size="mini" type="primary" @click="publish"><i class="el-icon-s-promotion"></i>发布</el-button>
+      </div>
+      <div class="back">
+        <el-button @click="exit" size="medium" icon="el-icon-arrow-left" circle></el-button>
+        <span>
+          <i :class="setup.logo.icon" :style="'background:' + setup.logo.background"></i>
+          <span>{{ setup.formName }}</span>
+        </span>
+      </div>
+    </div>
+
+    <el-dialog title="请使用手机扫码预览" :visible.sync="viewCode" width="300px" :close-on-click-modal="false" center>
+      <!-- <img src="../../assets/image/code.png" width="250" height="250"> -->
+      <span>请使用手机扫码预览</span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: "LayoutHeader",
+  props:{
+    value: {
+      type: String,
+      default: 'baseSetup'
+    }
+  },
+  data() {
+    return {
+      viewCode: false,
+    };
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design
+    }
+  },
+  created() {
+    this.check()
+  },
+  mounted() {
+    console.log(document.body.offsetWidth)
+    if (document.body.offsetWidth <= 970) {
+      this.$msgbox.alert("本设计器未适配中小屏幕,建议您在PC电脑端浏览器进行操作")
+    }
+    this.listener()
+  },
+  methods: {
+    publish() {
+      this.$emit('publish')
+    },
+    preview() {
+      this.$emit('preview')
+      //this.
+      this.viewCode = true;
+    },
+    valid() {
+      if (!this.$isNotEmpty(this.setup.group)) {
+        this.$message.warning('请选择分组')
+        this.$router.push('/layout/baseSetup')
+        return false;
+      }
+      return true;
+    },
+    exit() {
+      this.$confirm('未发布的内容将不会被保存,是否直接退出 ?', '提示', {
+        confirmButtonText: '退出',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        //window.location.reload()
+        //this.$store.commit('clearTemplate')
+        this.$router.push('/formsPanel')
+      })
+    },
+    to(path) {
+      this.$emit('input', path)
+    },
+    handleSelect(key, keyPath) {
+      console.log(key, keyPath);
+    },
+    listener() {
+      window.onunload = this.closeBefore()
+      window.onbeforeunload = this.closeBefore()
+      //window.on('beforeunload',this.closeBefore())
+    },
+    closeBefore() {
+      //alert("您将要离开本页")
+      return false
+    },
+    check() {
+      if (this.$store.state.isEdit === null) {
+        //this.$router.push("/workPanel");
+      }
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+
+.header {
+  min-width: 980px;
+  position: relative;
+
+  .el-menu {
+    top: 0;
+    z-index: 999;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+  }
+
+  .publish {
+    position: absolute;
+    top: 15px;
+    right: 20px;
+    z-index: 1000;
+
+    i {
+      margin-right: 6px;
+    }
+
+    button {
+      border-radius: 15px;
+    }
+  }
+
+  .back {
+    position: absolute;
+    z-index: 1000;
+    top: 10px;
+    left: 20px;
+    font-size: small;
+
+    span {
+      i {
+        border-radius: 10px;
+        padding: 7.8px;
+        font-size: 20px;
+        color: #ffffff;
+        margin: 0 10px;
+      }
+    }
+
+  }
+}
+</style>

+ 269 - 0
src/views/admin/layout/FormBaseSetting.vue

@@ -0,0 +1,269 @@
+<template>
+  <div class="base-setup" @click="showIconSelect = false">
+    <el-form ref="baseSetting" :model="setup" label-position="top" label-width="80px">
+      <el-form-item label="表单图标">
+        <i :class="setup.logo.icon" :style="'background:' + setup.logo.background"></i>
+        <span class="change-icon">
+					<span>
+						<span>选择背景色</span>
+						<el-color-picker v-model="setup.logo.background" show-alpha size="small" :predefine="colors"></el-color-picker>
+					</span>
+					<span>
+						<span>选择图标</span>
+						<el-popover placement="bottom-start" width="390" trigger="click">
+							<div class="icon-select">
+								<i :class="i" v-for="(i, id) in icons" :key="id" @click="setup.logo.icon = i"></i>
+							</div>
+              <i slot="reference" :class="setup.logo.icon"></i>
+            </el-popover>
+            <!--<i :class="setup.icon" @click.stop="showIconSelect = true"></i>-->
+					</span>
+				</span>
+      </el-form-item>
+      <el-form-item label="表单名称" :rules="getRule('请输入表单名称')" prop="formName">
+        <el-input v-model="setup.formName" size="medium"></el-input>
+      </el-form-item>
+      <el-form-item label="所在分组" :rules="getRule('请选择表单分组')" class="group" prop="groupId">
+        <el-select v-model="setup.groupId" placeholder="请选择分组" size="medium">
+          <el-option v-for="(op, index) in fromGroup" :key="index" v-show="op.id > 1"
+                     :label="op.name" :value="op.id"></el-option>
+        </el-select>
+        <el-popover placement="bottom-end" title="新建表单分组" width="300" trigger="click">
+          <el-input size="medium" v-model="newGroup" placeholder="请输入新的分组名">
+            <el-button slot="append" size="medium" type="primary" @click="addGroup">提交</el-button>
+          </el-input>
+          <el-button icon="el-icon-plus" slot="reference" size="medium" type="primary">新建分组</el-button>
+        </el-popover>
+      </el-form-item>
+      <el-form-item label="表单说明">
+        <el-input placeholder="请输入表单说明" v-model="setup.remark" type="textarea" show-word-limit
+                  :autosize="{ minRows: 2, maxRows: 5}" maxlength="500"></el-input>
+      </el-form-item>
+      <el-form-item label="消息通知方式" :rules="getRule('请选择消息通知方式')">
+        <el-select v-model="setup.settings.notify.types" value-key="name"
+                   placeholder="选择消息通知方式" style="width: 30%;"
+                   size="medium" clearable multiple collapse-tags>
+          <el-option v-for="(wc, index) in notifyTypes" :label="wc.name" :key="index" :value="wc"></el-option>
+        </el-select>
+        <el-input size="medium" v-model="setup.settings.notify.title" style="width: 68%; float:right;" placeholder="消息通知标题"></el-input>
+      </el-form-item>
+      <el-form-item label="谁可以管理此表单">
+        <el-select v-model="setup.settings.admin" @click.native="selectUser('admin')" value-key="name"
+                   class="select-u" placeholder="请选择可以管理此表单的人员"
+                   size="medium" clearable multiple>
+          <el-option v-for="(wc, index) in setup.settings.admin" :label="wc.name" :key="index" :value="wc"></el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <org-picker title="请选择可以管理此表单的人员" multiple ref="orgPicker" :selected="select" @ok="selected"></org-picker>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import {getFormGroups, updateGroup} from '@/api/design'
+import iconfont from '@/assets/iconfont/iconfont.json'
+
+export default {
+  name: "FormBaseSetting",
+  components: {OrgPicker},
+  data() {
+    return {
+      nowUserSelect: null,
+      showIconSelect: false,
+      select: [],
+      newGroup: '',
+      fromGroup: [],
+      notifyTypes:[
+        {type:'APP',name:'应用内通知'},
+        {type:'EMAIL',name:'邮件通知'},
+        {type:'SMS',name:'短信通知'},
+        {type:'WX',name:'微信通知'},
+        {type:'DING',name:'钉钉通知'},
+      ],
+      colors: [
+        '#ff4500',
+        '#ff8c00',
+        '#ffd700',
+        '#90ee90',
+        '#00ced1',
+        '#1e90ff',
+        '#c71585',
+        'rgba(255, 69, 0, 0.68)',
+        'rgb(255, 120, 0)',
+        'hsl(181, 100%, 37%)',
+        'hsla(209, 100%, 56%, 0.73)',
+        '#c7158577'
+      ],
+      icons: [
+        'el-icon-delete-solid',
+        'el-icon-s-tools',
+        'el-icon-s-goods',
+        'el-icon-warning',
+        'el-icon-circle-plus',
+        'el-icon-camera-solid',
+        'el-icon-s-promotion',
+        'el-icon-s-cooperation',
+        'el-icon-s-platform',
+        'el-icon-s-custom',
+        'el-icon-s-data',
+        'el-icon-s-check',
+        'el-icon-s-claim',
+      ],
+      rules:{
+        formName:[{}],
+        groupId: [],
+      }
+    }
+  },
+  computed: {
+    setup() {
+      return this.$store.state.design;
+    }
+  },
+  created() {
+    this.loadIconfont()
+  },
+  mounted(){
+    this.getGroups()
+  },
+  methods: {
+    getRule(msg){
+      return [{ required: true, message: msg, trigger: 'blur' }]
+    },
+    loadIconfont(){
+      if (iconfont && iconfont.id){
+        iconfont.glyphs.forEach(icon => {
+          this.icons.push(`${iconfont.font_family} ${iconfont.css_prefix_text}${icon.font_class}`)
+        })
+      }
+    },
+    getGroups(){
+      getFormGroups().then(rsp => {
+        this.fromGroup = rsp.data
+      }).catch(err => this.$message.error('获取分组异常'))
+    },
+    addGroup() {
+      if (this.newGroup.trim() !== '') {
+        updateGroup({name: this.newGroup.trim()}, 'post').then(rsp => {
+          this.$message.success(rsp.data)
+          this.getGroups()
+        }).catch(err => this.$message.error(err.response.data))
+      }
+    },
+    selected(select) {
+      this.$set(this.setup.settings, this.nowUserSelect, select)
+      //this.setup[this.nowUserSelect] = select
+    },
+    selectUser(key) {
+      this.select = this.setup.settings[key]
+      this.nowUserSelect = key
+      this.$refs.orgPicker.show()
+    },
+    validate(){
+      this.$refs.baseSetting.validate()
+      let err = []
+      if (!this.$isNotEmpty(this.setup.formName)){
+        err.push('表单名称未设置')
+      }
+      if (!this.$isNotEmpty(this.setup.groupId)){
+        err.push('表单分组未设置')
+      }
+      if (this.setup.settings.notify.types.length === 0){
+        err.push('审批消息通知方式未设置')
+      }
+      return err
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+/deep/ .el-select-dropdown {
+  display: none;
+}
+
+.icon-select {
+  display: flex;
+  flex-wrap: wrap;
+  i {
+    cursor: pointer;
+    font-size: large;
+    padding: 10px;
+    max-width: 38px !important;
+    &:hover {
+      box-shadow: 0 0 10px 2px #C2C2C2;
+    }
+  }
+}
+
+/deep/ .select-u {
+  width: 100%;
+}
+
+.base-setup {
+  overflow: auto;
+  margin: 0 auto;
+  width: 600px;
+  height: calc(100vh - 105px);
+  background: #ffffff;
+  margin-top: 10px;
+  padding: 15px 20px;
+
+  i:first-child {
+    position: relative;
+    cursor: pointer;
+    font-size: xx-large;
+    color: #ffffff;
+    border-radius: 10px;
+    padding: 10px;
+  }
+
+  .change-icon {
+    margin-left: 20px;
+
+    span {
+      font-size: small;
+      color: #7a7a7a;
+      margin-right: 15px;
+    }
+
+    i {
+      cursor: pointer;
+      color: #7a7a7a;
+      font-size: x-large;
+    }
+  }
+
+  /deep/ .el-form-item__label {
+    padding: 0;
+    font-weight: bold;
+  }
+
+  /deep/ .el-form-item {
+    margin-bottom: 5px;
+  }
+}
+
+/deep/ .group {
+  .el-select {
+    width: calc(100% - 130px);
+  }
+
+  .el-button {
+    margin-left: 10px;
+    width: 120px;
+  }
+}
+
+::-webkit-scrollbar {
+  width: 4px;
+  height: 4px;
+  background-color: #f8f8f8;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+</style>

+ 533 - 0
src/views/admin/layout/FormDesign.vue

@@ -0,0 +1,533 @@
+<template>
+  <el-container style="height: calc(100vh - 65px);">
+    <el-aside>
+      <div class="components-nav">
+        <span @click="libSelect = 0">组件库</span>
+      </div>
+      <div>
+        <div class="components" v-for="(group, i) in baseComponents" :key="i">
+          <p>{{group.name}}</p>
+          <ul>
+            <draggable class="drag" :list="group.components" :options="{sort: false}"
+                       :group="{ name: 'form', pull: 'clone', put: false }"
+                       @start="isStart = true" @end="isStart = false" :clone="clone">
+              <li v-for="(cp, id) in group.components" :key="id">
+                <i :class="cp.icon"></i>
+                <span>{{ cp.title }}</span>
+              </li>
+            </draggable>
+          </ul>
+        </div>
+      </div>
+
+    </el-aside>
+
+    <el-main class="layout-main">
+      <div class="tool-nav">
+        <div>
+          <el-tooltip class="item" effect="dark" content="撤销" placement="bottom-start">
+            <i class="el-icon-refresh-left"></i>
+          </el-tooltip>
+          <el-tooltip class="item" effect="dark" content="恢复" placement="bottom-start">
+            <i class="el-icon-refresh-right"></i>
+          </el-tooltip>
+        </div>
+        <div>
+          <el-tooltip class="item" effect="dark" content="预览表单" placement="bottom-start">
+            <i class="el-icon-view" @click="viewForms"></i>
+          </el-tooltip>
+          <el-tooltip class="item" effect="dark" content="移动端" placement="bottom-start">
+            <i :class="{'el-icon-mobile':true, 'select': showMobile}" @click="showMobile = true"></i>
+          </el-tooltip>
+          <el-tooltip class="item" effect="dark" content="PC端" placement="bottom-start">
+            <i :class="{'el-icon-monitor':true, 'select': !showMobile}" @click="showMobile = false"></i>
+          </el-tooltip>
+        </div>
+      </div>
+      <div class="work-form">
+        <div :class="{'mobile': showMobile, 'pc': !showMobile}">
+          <div :class="{'bd': showMobile}">
+            <div :class="{'form-content': showMobile}">
+              <div class="form">
+                <div class="tip" v-show="forms.length === 0 && !isStart">👈 请在左侧选择控件并拖至此处</div>
+                <draggable class="drag-from" :list="forms" group="form"
+                           :options="{animation: 300, chosenClass:'choose', sort:true}"
+                           @start="drag = true; selectFormItem = null" @end="drag = false">
+
+                  <div v-for="(cp, id) in forms" :key="id" class="form-item" @click="selectItem(cp)" :style="getSelectedClass(cp)">
+                    <div class="form-header">
+                      <p><span v-if="cp.props.required">*</span>{{ cp.title }}</p>
+                      <div class="option">
+                      <!--<i class="el-icon-copy-document" @click="copy"></i>-->
+                        <i class="el-icon-close" @click="del(id)"></i>
+                      </div>
+                      <form-design-render :config="cp"/>
+                    </div>
+                  </div>
+                </draggable>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-main>
+
+    <el-aside class="layout-param">
+      <div class="tool-nav-r" v-if="selectFormItem">
+        <i :class="selectFormItem.icon" style="margin-right: 5px; font-size: medium"></i>
+        <span>{{ selectFormItem.title }}</span>
+      </div>
+      <div v-if="!selectFormItem || forms.length === 0" class="tip">
+        😀 选中控件后在这里进行编辑
+      </div>
+      <div style="text-align:left; padding: 10px" v-else>
+        <form-component-config />
+      </div>
+    </el-aside>
+    <w-dialog clickClose closeFree width="800px" :showFooter="false" :border="false" title="表单预览" v-model="viewFormVisible">
+      <form-render ref="form" :forms="forms" v-model="formData"/>
+    </w-dialog>
+  </el-container>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import FormRender from '@/views/common/form/FormRender'
+import FormDesignRender from '@/views/admin/layout/form/FormDesignRender'
+import FormComponentConfig from '@/views/common/form/FormComponentConfig'
+import {baseComponents} from '@/views/common/form/ComponentsConfigExport'
+
+export default {
+  name: "FormDesign",
+  components: {draggable, FormComponentConfig, FormDesignRender, FormRender},
+  data() {
+    return {
+      formData:{},
+      libSelect: 0,
+      viewFormVisible: false,
+      isStart: false,
+      showMobile: true,
+      baseComponents,
+      select: null,
+      drag: false,
+    }
+  },
+  computed: {
+    forms() {
+      return this.$store.state.design.formItems;
+    },
+    selectFormItem: {
+      get(){
+        return this.$store.state.selectFormItem
+      },
+      set(val){
+        this.$store.state.selectFormItem = val
+      },
+    },
+    nodeMap(){
+      return this.$store.state.nodeMap
+    }
+  },
+  methods: {
+    copy(node, index) {
+      this.form.splice(index + 1, 0, Object.assign({}, node))
+    },
+    getId() {
+      return 'field' + (Math.floor(Math.random() * (99999 - 10000)) + 10000).toString()
+          + new Date().getTime().toString().substring(5);
+    },
+    del(index) {
+      this.$confirm('删除组件将会连带删除包含该组件的条件以及相关设置,是否继续?', '提示', {
+        confirmButtonText: '确 定',
+        cancelButtonText: '取 消',
+        type: 'warning'
+      }).then(() => {
+        if (this.forms[index].name === 'SpanLayout'){
+          //删除的是分栏则遍历删除分栏内所有子组件
+          this.forms[index].props.items.forEach(item => {
+            this.removeFormItemAbout(item)
+          })
+          this.forms[index].props.items.length = 0
+        }else {
+          this.removeFormItemAbout(this.forms[index])
+        }
+        this.forms.splice(index, 1)
+      })
+    },
+    async removeFormItemAbout(item){
+      this.nodeMap.forEach(node => {
+        //搜寻条件,进行移除
+        if (node.type === 'CONDITION'){
+          node.props.groups.forEach(group => {
+            let i = group.cids.remove(item.id)
+            if (i > -1){
+              //从子条件移除
+              group.conditions.splice(i, 1)
+            }
+          })
+        }
+        //搜寻权限,进行移除
+        if (node.type === 'ROOT' || node.type === 'APPROVAL' || node.type === 'CC'){
+          node.props.formPerms.removeByKey('id', item.id)
+          if (node.props.formUser === item.id){
+            node.props.formUser = ''
+          }
+        }
+      })
+    },
+    clone(obj) {
+      obj.id = this.getId()
+      return JSON.parse(JSON.stringify(obj));
+    },
+    viewForms(){
+      this.viewFormVisible = true
+    },
+    selectItem(cp){
+      this.selectFormItem = cp
+    },
+    getSelectedClass(cp){
+      return this.selectFormItem && this.selectFormItem.id === cp.id ?
+          'border-left: 4px solid #409eff':''
+    },
+    validateItem(err, titleSet, item){
+      if (titleSet.has(item.title) && item.name !== 'SpanLayout'){
+        err.push(`表单 ${item.title} 名称重复`)
+      }
+      titleSet.add(item.title)
+      if (item.name === 'SelectInput' || item.name === 'MultipleSelect'){
+        if (item.props.options.length === 0){
+          err.push(`${item.title} 未设置选项`)
+        }
+      }else if (item.name === 'TableList'){
+        if (item.props.columns.length === 0){
+          err.push(`明细表 ${item.title} 内未添加组件`)
+        }
+      }else if (item.name === 'SpanLayout'){
+        if (item.props.items.length === 0){
+          err.push('分栏内未添加组件')
+        }else {
+          item.props.items.forEach(sub => this.validateItem(err, titleSet, sub))
+        }
+      }
+    },
+    validate(){
+      let err = []
+      if (this.forms.length > 0){
+        let titleSet = new Set()
+        this.forms.forEach(item => {
+          //主要校验表格及分栏/选择器/表单名称/是否设置
+          this.validateItem(err, titleSet, item)
+        })
+      }else {
+        err.push('表单为空,请添加组件')
+      }
+      return err
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+
+.choose {
+  border: 1px dashed @theme-primary !important;
+}
+
+.process-form{
+  /deep/ .el-form-item__label{
+    padding: 0 0;
+  }
+}
+
+.components-nav {
+  box-sizing: content-box;
+  display: flex;
+  align-items: center;
+  margin: 12px 12px 0;
+  height: 28px;
+  box-shadow: 0 2px 4px 0 rgba(17, 31, 44, 0.04);
+  border: 1px solid #ecedef;
+  border-radius: 16px;
+  background-color: #fff;
+
+  .selected {
+    color: @theme-primary;
+  }
+
+  .border {
+    border-left: 1px solid #f5f6f6;
+    border-right: 1px solid #f5f6f6;
+  }
+
+  span {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    font-size: 12px;
+    color: rgba(17, 31, 44, 0.72);
+    cursor: pointer;
+
+    &:hover {
+      color: @theme-primary;
+    }
+  }
+}
+
+.components {
+  overflow-x: hidden;
+  overflow-y: scroll;
+  //margin-top: 20px;
+  //padding: 0 20px;
+  font-size: 12px;
+  width: 100%;
+  color: rgba(17, 31, 44, 0.85);
+  &>p{
+    padding: 0 20px;
+  }
+  .drag {
+    margin-left: 20px;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+
+    li {
+      text-align: center;
+      display: flex;
+      align-items: center;
+      width: 124px;
+      height: 38px;
+      margin-bottom: 12px;
+      border: 1px solid transparent;
+      border-radius: 8px;
+      cursor: grab;
+      background-color: #fff;
+
+      &:hover {
+        border: 1px solid @theme-primary;
+        color: @theme-primary;
+      }
+
+      i {
+        margin: 0 12px;
+      }
+    }
+
+    li:nth-child(odd) {
+      margin-right: 8px;
+    }
+  }
+}
+
+/deep/ .el-main {
+  padding: 0;
+}
+
+.layout-main {
+  background-color: #feffff;
+
+  .tool-nav {
+    font-size: medium;
+    padding: 8px 20px;
+    background: #fafafb;
+    border-bottom: 1px solid #ebecee;
+
+    div:first-child {
+      display: inline-block;
+      text-align: left;
+
+      i {
+        margin-right: 10px
+      }
+    }
+
+    div:last-child {
+      float: right;
+
+      i {
+        margin-left: 10px
+      }
+    }
+
+    i {
+      color: #7a7a7a;
+      cursor: pointer;
+
+      &:hover {
+        color: #4b4b4b;
+      }
+    }
+  }
+
+  .work-form {
+    margin: 0 auto;
+    height: calc(100% - 38px);
+    overflow-y: auto;
+    background: rgb(245, 246, 246);
+    border-left: 1px solid rgb(235, 236, 238);
+    border-right: 1px solid rgb(235, 236, 238);
+
+    .pc {
+      margin-top: 4%;
+
+      .drag-from {
+        height: calc(100vh - 190px);
+        background-color: rgb(245, 246, 246);
+
+        .form-item, li {
+          cursor: grab;
+          background: #ffffff;
+          padding: 10px;
+          border: 1px solid #ebecee;
+          margin: 5px 0;
+        }
+      }
+    }
+
+    .mobile {
+      margin-left: auto;
+      margin-right: auto;
+      width: 360px;
+      max-height: 640px;
+      margin-top: 4%;
+      border-radius: 24px;
+      box-shadow: 0 8px 40px 0 rgba(17, 31, 44, 0.12);
+
+      .bd {
+        border: 1px solid rgba(17, 31, 44, 0.08);
+        border-radius: 24px;
+        padding: 10px 10px;
+        background-color: #ffffff;
+
+        .form-content {
+          padding: 3px 2px;
+          border-radius: 14px;
+          background-color: #f2f4f5;
+
+          .drag-from {
+            width: 100%;
+            height: calc(100vh - 190px);
+            min-height: 200px;
+            max-height: 600px;
+          }
+
+          .form {
+            overflow-y: auto;
+            width: 100%;
+            display: inline-block;
+            max-height: 640px;
+
+            .form-item, li {
+              border: 1px solid #ffffff;
+              list-style: none;
+              background: #ffffff;
+              padding: 10px;
+              margin: 5px 0;
+              cursor: grab;
+            }
+          }
+        }
+      }
+    }
+
+    .tip {
+      //float: left;
+      margin: 0 auto;
+      width: 65%;
+      max-width: 400px;
+      padding: 35px 20px;
+      border-radius: 10px;
+      border: 1px dashed rgba(25, 31, 37, 0.12);
+      margin-top: 50px;
+      text-align: center;
+      font-size: 14px;
+      color: rgb(122, 122, 122);
+      z-index: 9999;
+
+      &:hover {
+        border: 1px dashed @theme-primary;
+      }
+    }
+  }
+
+}
+
+.layout-param {
+  text-align: center;
+  font-size: 14px;
+  color: rgb(122, 122, 122);
+
+  .tool-nav-r {
+    text-align: left;
+    font-size: small;
+    border-left: 1px solid #ebecee;
+    padding: 10px 20px;
+    background: #fafafb;
+    border-bottom: 1px solid #ebecee;
+  }
+
+  .tip {
+    margin-top: 150px;
+  }
+}
+
+.flip-list-move {
+  transition: transform 0.5s;
+}
+
+.no-move {
+  transition: transform 0s;
+}
+
+.select {
+  color: #4b4b4b !important;
+}
+
+.form-header {
+  font-size: small;
+  color: #818181;
+  text-align: left;
+  position: relative;
+  background-color: #fff;
+
+  p {
+    position: relative;
+    margin: 0 0 10px 0;
+
+    span {
+      position: absolute;
+      left: -8px;
+      top: 3px;
+      color: rgb(217, 0, 19);
+    }
+  }
+
+  .option {
+    position: absolute;
+    top: -10px;
+    right: -10px;
+    i {
+      font-size: large;
+      cursor: pointer;
+      color: #8c8c8c;
+      padding: 5px;
+      &:hover{
+        color: #f56c6c;
+      }
+    }
+  }
+}
+
+::-webkit-scrollbar {
+  width: 4px;
+  height: 4px;
+  background-color: #f8f8f8;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #e8e8e8;
+}
+</style>

+ 48 - 0
src/views/admin/layout/FormProSetting.vue

@@ -0,0 +1,48 @@
+<template>
+  <div class="senior-setup">
+    <el-form label-position="top" label-width="80px">
+      <el-form-item label="审批同意时是否签字">
+        <el-switch inactive-text="无需签字" active-text="需要签字"
+                   v-model="setup.sign"></el-switch>
+        <div class="sign-tip">如果此处设置为 <b>需要签字</b>,则所有审批人“同意时” <b>必须签字</b></div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "FormProSetting",
+  computed: {
+    setup() {
+      return this.$store.state.design.settings;
+    }
+  },
+  data(){
+    return{}
+  },
+  methods:{
+    validate(){
+      return []
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.senior-setup {
+  overflow: auto;
+  margin: 0 auto;
+  width: 600px;
+  height: calc(100vh - 105px);
+  background: #ffffff;
+  margin-top: 10px;
+  padding: 15px 20px;
+
+  .sign-tip {
+    color: #949495;
+    font-size: small;
+    margin-left: 20px;
+  }
+}
+</style>

+ 103 - 0
src/views/admin/layout/ProcessDesign.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-main>
+    <div class="scale">
+      <el-button icon="el-icon-plus" size="small" @click="scale += 10" :disabled="scale >= 150" circle></el-button>
+      <span>{{ scale }}%</span>
+      <el-button icon="el-icon-minus" size="small" @click="scale -= 10" :disabled="scale <= 40" circle></el-button>
+<!--      <el-button @click="validate">校验流程</el-button>-->
+    </div>
+    <div class="design" :style="'transform: scale('+ scale / 100 +');'">
+      <process-tree ref="process-tree" @selectedNode="nodeSelected"/>
+    </div>
+    <el-drawer :title="selectedNode.name" :visible.sync="showConfig"
+               :modal-append-to-body="false"
+               :size="selectedNode.type === 'CONDITION' ? '600px':'500px'"
+               direction="rtl" :modal="false" destroy-on-close>
+      <div slot="title">
+        <el-input v-model="selectedNode.name" size="medium" v-show="showInput"
+                  style="width: 300px" @blur="showInput = false"></el-input>
+        <el-link v-show="!showInput" @click="showInput = true" style="font-size: medium">
+          <i class="el-icon-edit" style="margin-right: 10px"></i>
+          {{selectedNode.name}}
+        </el-link>
+      </div>
+      <div class="node-config-content">
+        <node-config/>
+      </div>
+    </el-drawer>
+  </el-main>
+</template>
+
+<script>
+import ProcessTree from './process/ProcessTree.vue'
+import NodeConfig from '../../common/process/config/NodeConfig'
+
+export default {
+  name: "ProcessDesign",
+  components: {ProcessTree, NodeConfig},
+  data() {
+    return {
+      scale: 100,
+      selected: {},
+      showInput: false,
+      showConfig: false
+    }
+  },
+  computed:{
+    selectedNode(){
+      return this.$store.state.selectedNode
+    }
+  },
+  mounted() {
+
+  },
+  methods: {
+    validate(){
+      return this.$refs["process-tree"].validateProcess()
+    },
+    nodeSelected(node){
+      console.log('配置节点', node)
+      this.showConfig = true
+    }
+  },
+  watch:{
+    /*selectedNode:{
+      deep: true,
+      handler(node){
+        console.log("更新")
+        this.$refs["process-tree"].nodeDomUpdate(node)
+      }
+    }*/
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.design {
+  margin-top: 100px;
+  display: flex;
+  transform-origin: 50% 0px 0px;
+}
+
+.scale {
+  z-index: 999;
+  position: fixed;
+  top: 80px;
+  right: 40px;
+
+  span {
+    margin: 0 10px;
+    font-size: 15px;
+    color: #7a7a7a;
+    width: 50px;
+  }
+}
+
+.node-config-content{
+  padding: 0 20px 20px;
+}
+
+/deep/ .el-drawer__body{
+  overflow-y: auto;
+}
+</style>

+ 89 - 0
src/views/admin/layout/ProcessDiagramViewer.vue

@@ -0,0 +1,89 @@
+<template>
+  <el-main>
+    <div class="scale">
+      <el-button icon="el-icon-plus" size="small" @click="scale += 10" :disabled="scale >= 150" circle></el-button>
+      <span>{{ scale }}%</span>
+      <el-button icon="el-icon-minus" size="small" @click="scale -= 10" :disabled="scale <= 40" circle></el-button>
+    </div>
+    <div class="design" :style="'transform: scale('+ scale / 100 +');'">
+      <process-tree-viewer ref="process-tree" @selectedNode="nodeSelected"/>
+    </div>
+  </el-main>
+</template>
+
+<script>
+import NodeConfig from '../../common/process/config/NodeConfig'
+import ProcessTreeViewer from "@/views/admin/layout/process/ProcessTreeViewer";
+
+export default {
+  name: "ProcessDesign",
+  components: { ProcessTreeViewer, NodeConfig },
+  data() {
+    return {
+      scale: 100,
+      selected: {},
+      showInput: false,
+      showConfig: false
+    }
+  },
+  computed:{
+    selectedNode(){
+      return this.$store.state.selectedNode
+    }
+  },
+  created() {
+    this.$store.state.diagramMode = 'viewer'
+  },
+  beforeDestroy() {
+    this.$store.state.diagramMode = 'design'
+  },
+  methods: {
+    validate(){
+      return this.$refs["process-tree"].validateProcess()
+    },
+    nodeSelected(node){
+      console.log('配置节点', node)
+      this.showConfig = true
+    }
+  },
+  watch:{
+    /*selectedNode:{
+      deep: true,
+      handler(node){
+        console.log("更新")
+        this.$refs["process-tree"].nodeDomUpdate(node)
+      }
+    }*/
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.design {
+  margin-top: 100px;
+  display: flex;
+  transform-origin: 50% 0px 0px;
+}
+
+.scale {
+  z-index: 999;
+  position: fixed;
+  top: 80px;
+  right: 40px;
+
+  span {
+    margin: 0 10px;
+    font-size: 15px;
+    color: #7a7a7a;
+    width: 50px;
+  }
+}
+
+.node-config-content{
+  padding: 0 20px 20px;
+}
+
+/deep/ .el-drawer__body{
+  overflow-y: auto;
+}
+</style>

+ 53 - 0
src/views/admin/layout/form/FormDesignRender.vue

@@ -0,0 +1,53 @@
+<template>
+  <component ref="form" :is="config.name" :mode="mode" v-bind="config.props" v-model="_value" />
+</template>
+<script>
+
+import components from '@/views/common/form/ComponentExport'
+
+export default {
+  name: "FormRender",
+  components: components,
+  props:{
+    mode:{
+      type: String,
+      default: 'DESIGN'
+    },
+    value: {
+      default: undefined
+    },
+    config:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  computed: {
+    _value: {
+      get() {
+        const valueType = this.config.valueType
+        const value = valueType === "Number" && this.value ? Number(this.value) : this.value;
+        return value;
+      },
+      set(val) {
+        const valueType = this.config.valueType
+        const value = valueType === "Number" ? Number(val) : val
+        this.$emit("input", value);
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    validate(call){
+      this.$refs.form.validate(call)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+</style>

+ 168 - 0
src/views/admin/layout/process/DefaultNodeProps.js

@@ -0,0 +1,168 @@
+//审批节点默认属性
+export const APPROVAL_PROPS = {
+  assignedType: "ASSIGN_USER",
+  mode: "AND",
+  sign: false,
+  nobody: {
+    handler: "TO_PASS",
+    assignedUser:[]
+  },
+  timeLimit:{
+    timeout:{
+      unit: "H",
+      value: 0
+    },
+    handler:{
+      type: "REFUSE",
+      notify:{
+        once: true,
+        hour: 1
+      }
+    }
+  },
+  assignedUser:[],
+  formPerms:[],
+  selfSelect: {
+    multiple: false
+  },
+  leaderTop: {
+    endCondition: "TOP",
+    endLevel: 1,
+  },
+  leader:{
+    level: 1
+  },
+  role:[],
+  refuse: {
+    type: 'TO_END', //驳回规则 TO_END  TO_NODE  TO_BEFORE
+    target: '' //驳回到指定ID的节点
+  },
+  formUser: ''
+}
+
+//根节点默认属性
+export const ROOT_PROPS = {
+  assignedUser: [],
+  formPerms:[]
+}
+
+//条件节点默认属性
+export const CONDITION_PROPS = {
+  groupsType:"OR", //条件组逻辑关系 OR、AND
+  groups:[
+    {
+      groupType:"AND", //条件组内条件关系 OR、AND
+      cids:[], //条件ID集合
+      conditions:[] //组内子条件
+    }
+  ],
+  expression: "" //自定义表达式,灵活构建逻辑关系
+}
+
+//抄送节点默认属性
+export const CC_PROPS = {
+  shouldAdd: false,
+  assignedUser: [],
+  formPerms:[]
+}
+
+//触发器节点默认属性
+export const TRIGGER_PROPS = {
+  type: 'WEBHOOK',
+  http:{
+    method: 'GET', //请求方法 支持GET/POST
+    url: '', //URL地址,可以直接带参数
+    headers: [ //http header
+      {
+        name: '',
+        isField: true,
+        value: '' //支持表达式 ${xxx} xxx为表单字段名称
+      }
+    ],
+    contentType: 'FORM', //请求参数类型
+    params:[ //请求参数
+      {
+        name: '',
+        isField: true, //是表单字段还是自定义
+        value: '' //支持表达式 ${xxx} xxx为表单字段名称
+      }
+    ],
+    retry: 1,
+    handlerByScript: false,
+    success: 'function handlerOk(res) {\n  return true;\n}',
+    fail: 'function handlerFail(res) {\n  return true;\n}'
+  },
+  email:{
+    subject: '',
+    to: [],
+    content: ''
+  }
+}
+
+//延时节点默认属性
+export const DELAY_PROPS = {
+  type: "FIXED", //延时类型 FIXED:到达当前节点后延时固定时长 、AUTO:延时到 dateTime设置的时间
+  time: 0, //延时时间
+  unit: "M", //时间单位 D天 H小时 M分钟
+  dateTime: "" //如果当天没有超过设置的此时间点,就延时到这个指定的时间,到了就直接跳过不延时
+}
+
+//办理人节点默认属性
+export const TASK_PROPS = {
+  assignedType: "ASSIGN_USER",
+  mode: "AND",
+  sign: false,
+  nobody: {
+    handler: "TO_PASS",
+    assignedUser:[]
+  },
+  timeLimit:{
+    timeout:{
+      unit: "H",
+      value: 0
+    },
+    handler:{
+      type: "REFUSE",
+      notify:{
+        once: true,
+        hour: 1
+      }
+    }
+  },
+  assignedUser:[],
+  formPerms:[],
+  selfSelect: {
+    multiple: false
+  },
+  leaderTop: {
+    endCondition: "TOP",
+    endLevel: 1,
+  },
+  leader:{
+    level: 1
+  },
+  role:[],
+  refuse: {
+    type: 'TO_END', //驳回规则 TO_END  TO_NODE  TO_BEFORE
+    target: '' //驳回到指定ID的节点
+  },
+  formUser: ''
+}
+
+//包容节点默认属性
+export const INCLUSIVE_PROPS = {
+  groupsType:"OR", //条件组逻辑关系 OR、AND
+  groups:[
+    {
+      groupType:"AND", //条件组内条件关系 OR、AND
+      cids:[], //条件ID集合
+      conditions:[] //组内子条件
+    }
+  ],
+  expression: "" //自定义表达式,灵活构建逻辑关系
+}
+
+
+export default {
+  APPROVAL_PROPS, TASK_PROPS, CC_PROPS, DELAY_PROPS, CONDITION_PROPS, ROOT_PROPS, TRIGGER_PROPS, INCLUSIVE_PROPS
+}

+ 521 - 0
src/views/admin/layout/process/ProcessTree.vue

@@ -0,0 +1,521 @@
+<script>
+//导入所有节点组件
+import Approval from '@/views/common/process/nodes/ApprovalNode.vue'
+import Task from '@/views/common/process/nodes/TaskNode.vue'
+import Cc from '@/views/common/process/nodes/CcNode.vue'
+import Concurrent from '@/views/common/process/nodes/ConcurrentNode.vue'
+import Condition from '@/views/common/process/nodes/ConditionNode.vue'
+import Inclusive from '@/views/common/process/nodes/InclusiveNode.vue'
+import Trigger from '@/views/common/process/nodes/TriggerNode.vue'
+import Delay from '@/views/common/process/nodes/DelayNode.vue'
+import Empty from '@/views/common/process/nodes/EmptyNode.vue'
+import Root from '@/views/common/process/nodes/RootNode.vue'
+import Node from '@/views/common/process/nodes/Node.vue'
+import Subprocess from '@/views/common/process/nodes/SubprocessNode.vue'
+
+import DefaultProps from "./DefaultNodeProps"
+
+export default {
+  name: "ProcessTree",
+  components: {Node, Root, Approval, Task, Cc, Trigger, Concurrent, Condition, Inclusive, Delay, Empty, Subprocess},
+  data() {
+    return {
+      valid: true
+    }
+  },
+  computed:{
+    nodeMap(){
+      return this.$store.state.nodeMap;
+    },
+    dom(){
+      return this.$store.state.design.process;
+    }
+  },
+  render(h) {
+    console.log("渲染流程树")
+    this.nodeMap.clear()
+    console.log('DOM',this.dom)
+    let processTrees = this.getDomTree(h, this.dom)
+    console.log('nodeMap',this.nodeMap)
+    //插入末端节点
+    processTrees.push(h('div', {style:{'text-align': 'center'}}, [
+      h('div', {class:{'process-end': true}, domProps: {innerHTML:'流程结束'}})
+    ]))
+    console.log(processTrees)
+    return h('div', {class:{'_root': true}, ref:'_root'}, processTrees)
+  },
+  methods: {
+    getDomTree(h, node) {
+      this.toMapping(node);
+      if (this.isPrimaryNode(node)){
+        //普通业务节点
+        let childDoms = this.getDomTree(h, node.children)
+        this.decodeAppendDom(h, node, childDoms)
+        return [h('div', {'class':{'primary-node': true}}, childDoms)];
+      }else if (this.isBranchNode(node)){
+        let index = 0;
+        //遍历分支节点,包含并行及条件节点
+        let branchItems = node.branchs.map(branchNode => {
+          //处理每个分支内子节点
+          this.toMapping(branchNode);
+          let childDoms = this.getDomTree(h, branchNode.children)
+          this.decodeAppendDom(h, branchNode, childDoms, {level: index + 1, size: node.branchs.length})
+          //插入4条横线,遮挡掉条件节点左右半边线条
+          this.insertCoverLine(h, index, childDoms, node.branchs)
+          //遍历子分支尾部分支
+          index++;
+          return h('div', {'class':{'branch-node-item': true}}, childDoms);
+        })
+        //插入添加分支/条件的按钮
+        branchItems.unshift(h('div',{'class':{'add-branch-btn': true}}, [
+          h('el-button', {
+           'class':{'add-branch-btn-el': true},
+            props: {size: 'small', round: true},
+            on:{click: () => this.addBranchNode(node)},
+            domProps: {innerHTML: `添加${this.isConditionNode(node)?'条件':'分支'}`},
+          }, [])
+        ]));
+        let bchDom = [h('div', {'class':{'branch-node': true}}, branchItems)]
+        //继续遍历分支后的节点
+        let afterChildDoms = this.getDomTree(h, node.children)
+        return [h('div', {}, [bchDom, afterChildDoms])]
+      }else if (this.isEmptyNode(node)){
+        //空节点,存在于分支尾部
+        let childDoms = this.getDomTree(h, node.children)
+        this.decodeAppendDom(h, node, childDoms)
+        return [h('div', {'class':{'empty-node': true}}, childDoms)];
+      }else {
+        //遍历到了末端,无子节点
+        return [];
+      }
+    },
+    //解码渲染的时候插入dom到同级
+    decodeAppendDom(h, node, dom, props = {}){
+      props.config = node
+      dom.unshift(h(node.type.toLowerCase(), {
+        props: props,
+        ref: node.id,
+        key: node.id,
+        //定义事件,插入节点,删除节点,选中节点,复制/移动
+        on:{
+          insertNode: type => this.insertNode(type, node),
+          delNode: () => this.delNode(node),
+          selected: () => this.selectNode(node),
+          copy:() => this.copyBranch(node),
+          leftMove: () => this.branchMove(node, -1),
+          rightMove: () => this.branchMove(node, 1)
+        }
+      }, []))
+    },
+    //id映射到map,用来向上遍历
+    toMapping(node){
+      if (node && node.id){
+        //console.log("node=> " + node.id + " name:" + node.name + " type:" + node.type)
+        this.nodeMap.set(node.id, node)
+      }
+    },
+    insertCoverLine(h, index, doms, branchs){
+      if (index === 0){
+        //最左侧分支
+        doms.unshift(h('div', {'class':{'line-top-left': true}}, []))
+        doms.unshift(h('div', {'class':{'line-bot-left': true}}, []))
+      }else if (index === branchs.length - 1){
+        //最右侧分支
+        doms.unshift(h('div', {'class':{'line-top-right': true}}, []))
+        doms.unshift(h('div', {'class':{'line-bot-right': true}}, []))
+      }
+    },
+    copyBranch(node){
+      let parentNode = this.nodeMap.get(node.parentId)
+      let branchNode = this.$deepCopy(node)
+      branchNode.name = branchNode.name + '-copy'
+      this.forEachNode(parentNode, branchNode, (parent, node) => {
+        let id = this.getRandomId()
+        console.log(node, '新id =>'+ id, '老nodeId:' + node.id )
+        node.id = id
+        node.parentId = parent.id
+      })
+      parentNode.branchs.splice(parentNode.branchs.indexOf(node), 0, branchNode)
+      this.$forceUpdate()
+    },
+    branchMove(node, offset){
+      let parentNode = this.nodeMap.get(node.parentId)
+      let index = parentNode.branchs.indexOf(node)
+      let branch = parentNode.branchs[index + offset]
+      parentNode.branchs[index + offset] = parentNode.branchs[index]
+      parentNode.branchs[index] = branch
+      this.$forceUpdate()
+    },
+    //判断是否为主要业务节点
+    isPrimaryNode(node){
+      return node &&
+          (node.type === 'ROOT' || node.type === 'APPROVAL' || node.type === 'TASK'
+          || node.type === 'CC' || node.type === 'DELAY' || node.type === 'SUBPROCESS'
+              || node.type === 'TRIGGER');
+    },
+    isBranchNode(node){
+      return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS' || node.type === 'INCLUSIVES');
+    },
+    isEmptyNode(node){
+      return node && (node.type === 'EMPTY')
+    },
+    //是分支节点
+    isConditionNode(node){
+      return node.type === 'CONDITIONS';
+    },
+    //是分支节点
+    isBranchSubNode(node){
+      return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT' || node.type === 'INCLUSIVE');
+    },
+    isInclusiveNode(node){
+      return node.type === 'INCLUSIVES'
+    },
+    isConcurrentNode(node){
+      return node.type === 'CONCURRENTS'
+    },
+    getRandomId(){
+      return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random()*9000+1000)}`
+    },
+    //选中一个节点
+    selectNode(node){
+      this.$store.commit('selectedNode', node)
+      this.$emit('selectedNode', node)
+    },
+    //处理节点插入逻辑
+    insertNode(type, parentNode){
+      this.$refs['_root'].click()
+      //缓存一下后面的节点
+      let afterNode = parentNode.children
+      //插入新节点
+      parentNode.children = {
+        id: this.getRandomId(),
+        parentId: parentNode.id,
+        props: {},
+        type: type,
+      }
+      switch (type){
+        case 'APPROVAL': this.insertApprovalNode(parentNode, afterNode); break;
+        case 'SUBPROCESS' : this.insertApprovalNode(parentNode, afterNode); break;
+        case 'TASK': this.insertTaskNode(parentNode); break;
+        case 'CC': this.insertCcNode(parentNode); break;
+        case 'DELAY': this.insertDelayNode(parentNode); break;
+        case 'TRIGGER': this.insertTriggerNode(parentNode); break;
+        case 'CONDITIONS': this.insertConditionsNode(parentNode); break;
+        case 'INCLUSIVES': this.insertInclusiveNode(parentNode); break;
+        case 'CONCURRENTS': this.insertConcurrentsNode(parentNode); break;
+        default: break;
+      }
+      //拼接后续节点
+      if (this.isBranchNode({type: type})){
+        if (afterNode && afterNode.id){
+          afterNode.parentId = parentNode.children.children.id
+        }
+        this.$set(parentNode.children.children, 'children', afterNode)
+      }else {
+        if (afterNode && afterNode.id){
+          afterNode.parentId = parentNode.children.id
+        }
+        this.$set(parentNode.children, 'children', afterNode)
+      }
+      this.$forceUpdate()
+    },
+    insertApprovalNode(parentNode){
+      this.$set(parentNode.children, "name", "审批人")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.APPROVAL_PROPS))
+    },
+    insertTaskNode(parentNode){
+      this.$set(parentNode.children, "name", "办理人")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.TASK_PROPS))
+    },
+    insertCcNode(parentNode){
+      this.$set(parentNode.children, "name", "抄送人")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.CC_PROPS))
+    },
+    insertDelayNode(parentNode){
+      this.$set(parentNode.children, "name", "延时处理")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.DELAY_PROPS))
+    },
+    insertTriggerNode(parentNode){
+      this.$set(parentNode.children, "name", "触发器")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.TRIGGER_PROPS))
+    },
+    insertConditionsNode(parentNode){
+      this.$set(parentNode.children, "name", "条件分支")
+      this.$set(parentNode.children, 'children', {
+        id: this.getRandomId(),
+        parentId: parentNode.children.id,
+        type: "EMPTY"
+      })
+      this.$set(parentNode.children, "branchs", [
+        {
+          id: this.getRandomId(),
+          parentId: parentNode.children.id,
+          type: "CONDITION",
+          props: this.$deepCopy(DefaultProps.CONDITION_PROPS),
+          name: "条件1",
+          children:{}
+        },{
+          id: this.getRandomId(),
+          parentId: parentNode.children.id,
+          type: "CONDITION",
+          props: this.$deepCopy(DefaultProps.CONDITION_PROPS),
+          name: "条件2",
+          children:{}
+        }
+      ])
+    },
+    insertInclusiveNode(parentNode){
+      this.$set(parentNode.children, "name", "包容分支")
+      this.$set(parentNode.children, 'children', {
+        id: this.getRandomId(),
+        parentId: parentNode.children.id,
+        type: "EMPTY"
+      })
+      this.$set(parentNode.children, "branchs", [
+        {
+          id: this.getRandomId(),
+          parentId: parentNode.children.id,
+          type: "INCLUSIVE",
+          props: this.$deepCopy(DefaultProps.INCLUSIVE_PROPS),
+          name: "包容条件1",
+          children:{}
+        },{
+          id: this.getRandomId(),
+          parentId: parentNode.children.id,
+          type: "INCLUSIVE",
+          props: this.$deepCopy(DefaultProps.INCLUSIVE_PROPS),
+          name: "包容条件2",
+          children:{}
+        }
+      ])
+    },
+    insertConcurrentsNode(parentNode){
+      this.$set(parentNode.children, "name", "并行分支")
+      this.$set(parentNode.children, 'children',{
+        id: this.getRandomId(),
+        parentId: parentNode.children.id,
+        type: "EMPTY"
+      })
+      this.$set(parentNode.children, "branchs", [
+        {
+          id: this.getRandomId(),
+          name: "分支1",
+          parentId: parentNode.children.id,
+          type: "CONCURRENT",
+          props: {},
+          children:{}
+        },{
+          id: this.getRandomId(),
+          name: "分支2",
+          parentId: parentNode.children.id,
+          type: "CONCURRENT",
+          props: {},
+          children:{}
+        }
+      ])
+    },
+    getBranchEndNode(conditionNode){
+      if (!conditionNode.children || !conditionNode.children.id){
+        return conditionNode;
+      }
+      return this.getBranchEndNode(conditionNode.children);
+    },
+    addBranchNode(node){
+      if (node.branchs.length < 8){
+        node.branchs.push({
+          id: this.getRandomId(),
+          parentId: node.id,
+          name: (this.isConditionNode(node) ? '条件':this.isInclusiveNode(node) ? '包容条件':'分支') + (node.branchs.length + 1),
+          props: this.isConditionNode(node) ? this.$deepCopy(DefaultProps.CONDITION_PROPS) : this.isInclusiveNode(node) ? this.$deepCopy(DefaultProps.INCLUSIVE_PROPS):{},
+          type: this.isConditionNode(node) ? "CONDITION": this.isInclusiveNode(node) ? "INCLUSIVE":"CONCURRENT",
+          children:{}
+        })
+      }else {
+        this.$message.warning("最多只能添加 8 项😥")
+      }
+    },
+    //删除当前节点
+    delNode(node){
+      console.log("删除节点", node)
+      //获取该节点的父节点
+      let parentNode = this.nodeMap.get(node.parentId)
+      if (parentNode){
+        //判断该节点的父节点是不是分支节点
+        if (this.isBranchNode(parentNode)){
+          //移除该分支
+          parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
+          //处理只剩1个分支的情况
+          if (parentNode.branchs.length < 2){
+            //获取条件组的父节点
+            let ppNode = this.nodeMap.get(parentNode.parentId)
+            //判断唯一分支是否存在业务节点
+            if (parentNode.branchs[0].children && parentNode.branchs[0].children.id){
+              //将剩下的唯一分支头部合并到主干
+              ppNode.children = parentNode.branchs[0].children
+              ppNode.children.parentId = ppNode.id
+              //搜索唯一分支末端最后一个节点
+              let endNode = this.getBranchEndNode(parentNode.branchs[0])
+              //后续节点进行拼接, 这里要取EMPTY后的节点
+              endNode.children = parentNode.children.children
+              if (endNode.children && endNode.children.id){
+                endNode.children.parentId = endNode.id
+              }
+            }else {
+              //直接合并分支后面的节点,这里要取EMPTY后的节点
+              ppNode.children = parentNode.children.children
+              if (ppNode.children && ppNode.children.id){
+                ppNode.children.parentId = ppNode.id
+              }
+            }
+          }
+        }else {
+          //不是的话就直接删除
+          if (node.children && node.children.id) {
+            node.children.parentId = parentNode.id
+          }
+          parentNode.children = node.children
+        }
+        this.$forceUpdate()
+      }else {
+        this.$message.warning("出现错误,找不到上级节点😥")
+      }
+    },
+    validateProcess(){
+      this.valid = true
+      let err = []
+      this.validate(err, this.dom)
+      return err
+    },
+    validateNode(err, node){
+      if (this.$refs[node.id].validate){
+        this.valid = this.$refs[node.id].validate(err)
+      }
+    },
+    //更新指定节点的dom
+    nodeDomUpdate(node){
+      this.$refs[node.id].$forceUpdate()
+    },
+    //给定一个起始节点,遍历内部所有节点
+    forEachNode(parent, node, callback){
+      if (this.isBranchNode(node)){
+        callback(parent, node)
+        this.forEachNode(node, node.children, callback)
+        node.branchs.map(branchNode => {
+          callback(node, branchNode)
+          this.forEachNode(branchNode, branchNode.children, callback)
+        })
+      }else if (this.isPrimaryNode(node) || this.isEmptyNode(node) || this.isBranchSubNode(node)){
+        callback(parent, node)
+        this.forEachNode(node, node.children, callback)
+      }
+    },
+    //校验所有节点设置
+    validate(err, node){
+      if (this.isPrimaryNode(node)){
+        this.validateNode(err, node)
+        this.validate(err, node.children)
+      }else if (this.isBranchNode(node)){
+        //校验每个分支
+        node.branchs.map(branchNode => {
+          //校验条件节点
+          this.validateNode(err, branchNode)
+          //校验条件节点后面的节点
+          this.validate(err, branchNode.children)
+        })
+        this.validate(err, node.children)
+      }else if (this.isEmptyNode(node)){
+        this.validate(err, node.children)
+      }
+
+    }
+  },
+  watch:{
+
+  }
+}
+</script>
+
+<style lang="less" scoped>
+._root{
+ margin: 0 auto;
+}
+.process-end{
+  width: 60px;
+  margin: 0 auto;
+  margin-bottom: 20px;
+  border-radius: 15px;
+  padding: 5px 10px;
+  font-size: small;
+  color: #747474;
+  background-color: #f2f2f2;
+  box-shadow: 0 0 10px 0 #bcbcbc;
+}
+.primary-node{
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+}
+.branch-node{
+  display: flex;
+  justify-content: center;
+  /*border-top: 2px solid #cccccc;
+  border-bottom: 2px solid #cccccc;*/
+}
+.branch-node-item{
+  position: relative;
+  display: flex;
+  background: #f5f6f6;
+  flex-direction: column;
+  align-items: center;
+  border-top: 2px solid #cccccc;
+  border-bottom: 2px solid #cccccc;
+  &:before{
+    content: "";
+    position: absolute;
+    top: 0;
+    left: calc(50% - 1px);
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #CACACA;
+  }
+  .line-top-left, .line-top-right, .line-bot-left, .line-bot-right{
+    position: absolute;
+    width: 50%;
+    height: 4px;
+    background-color: #f5f6f6;
+  }
+  .line-top-left{
+    top: -2px;
+    left: -1px;
+  }
+  .line-top-right{
+    top: -2px;
+    right: -1px;
+  }
+  .line-bot-left{
+    bottom: -2px;
+    left: -1px;
+  }
+  .line-bot-right{
+    bottom: -2px;
+    right: -1px;
+  }
+}
+.add-branch-btn{
+  position: absolute;
+  width: 80px;
+  .add-branch-btn-el{
+    z-index: 999;
+    position: absolute;
+    top: -15px;
+  }
+}
+
+.empty-node{
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  align-items: center;
+}
+</style>

+ 449 - 0
src/views/admin/layout/process/ProcessTreeViewer.vue

@@ -0,0 +1,449 @@
+<script>
+//导入所有节点组件
+import Approval from '@/views/common/process/nodes/ApprovalNode.vue'
+import Task from '@/views/common/process/nodes/TaskNode.vue'
+import Cc from '@/views/common/process/nodes/CcNode.vue'
+import Concurrent from '@/views/common/process/nodes/ConcurrentNode.vue'
+import Condition from '@/views/common/process/nodes/ConditionNode.vue'
+import Inclusive from '@/views/common/process/nodes/InclusiveNode.vue'
+import Trigger from '@/views/common/process/nodes/TriggerNode.vue'
+import Delay from '@/views/common/process/nodes/DelayNode.vue'
+import Empty from '@/views/common/process/nodes/EmptyNode.vue'
+import Root from '@/views/common/process/nodes/RootNode.vue'
+import Node from '@/views/common/process/nodes/Node.vue'
+
+import DefaultProps from "./DefaultNodeProps"
+
+export default {
+  name: "ProcessTreeViewer",
+  components: {Node, Root, Approval, Task , Cc, Trigger, Concurrent, Condition, Inclusive, Delay, Empty},
+  data() {
+    return {
+      valid: true
+    }
+  },
+  computed:{
+    nodeMap(){
+      return this.$store.state.nodeMap;
+    },
+    dom(){
+      return this.$store.state.design.process;
+    }
+  },
+  render(h, ctx) {
+    this.nodeMap.clear()
+    let processTrees = this.getDomTree(h, this.dom)
+    //插入末端节点
+    processTrees.push(h('div', {style:{'text-align': 'center'}}, [
+      h('div', {class:{'process-end': true}, domProps: {innerHTML:'流程结束'}})
+    ]))
+    return h('div', {class:{'_root': true}, ref:'_root'}, processTrees)
+  },
+  methods: {
+    getDomTree(h, node) {
+      this.toMapping(node);
+      if (this.isPrimaryNode(node)){
+        //普通业务节点
+        let childDoms = this.getDomTree(h, node.children)
+        let headerBgc = '#ff943e'
+        if (this.$store.state.runningList.includes(node.id)) {
+          headerBgc = '#1e90ff'
+        }
+        else if (this.$store.state.endList.includes(node.id)) {
+          headerBgc = '#20b2aa'
+        }
+        else if (this.$store.state.noTakeList.includes(node.id)) {
+          headerBgc = '#909399'
+        }
+        node.props.headerBgc = headerBgc
+
+        this.decodeAppendDom(h, node, childDoms)
+        return [h('div', {'class':{'primary-node': true}}, childDoms)];
+      }
+      else if (this.isBranchNode(node)) {
+        let index = 0;
+        //遍历分支节点,包含并行及条件节点
+        let branchItems = node.branchs.map(branchNode => {
+          //处理每个分支内子节点
+          this.toMapping(branchNode);
+          let childDoms = this.getDomTree(h, branchNode.children)
+          this.decodeAppendDom(h, branchNode, childDoms, {level: index + 1, size: node.branchs.length})
+          //插入4条横线,遮挡掉条件节点左右半边线条
+          this.insertCoverLine(h, index, childDoms, node.branchs)
+          //遍历子分支尾部分支
+          index++;
+          return h('div', {'class':{'branch-node-item': true}}, childDoms);
+        })
+
+        let bchDom = [h('div', {'class':{'branch-node': true}}, branchItems)]
+        //继续遍历分支后的节点
+        let afterChildDoms = this.getDomTree(h, node.children)
+        return [h('div', {}, [bchDom, afterChildDoms])]
+      }
+      else if (this.isEmptyNode(node)) {
+        //空节点,存在于分支尾部
+        let childDoms = this.getDomTree(h, node.children)
+        this.decodeAppendDom(h, node, childDoms)
+        return [h('div', {'class':{'empty-node': true}}, childDoms)];
+      }
+      else {
+        //遍历到了末端,无子节点
+        return [];
+      }
+    },
+    //解码渲染的时候插入dom到同级
+    decodeAppendDom(h, node, dom, props = {}){
+      props.config = node
+      dom.unshift(h(node.type.toLowerCase(), {
+        props: props,
+        ref: node.id,
+        key: node.id,
+      }, []))
+    },
+    //id映射到map,用来向上遍历
+    toMapping(node){
+      if (node && node.id){
+        //console.log("node=> " + node.id + " name:" + node.name + " type:" + node.type)
+        this.nodeMap.set(node.id, node)
+      }
+    },
+    insertCoverLine(h, index, doms, branchs){
+      if (index === 0){
+        //最左侧分支
+        doms.unshift(h('div', {'class':{'line-top-left': true}}, []))
+        doms.unshift(h('div', {'class':{'line-bot-left': true}}, []))
+      }
+      else if (index === branchs.length - 1){
+        //最右侧分支
+        doms.unshift(h('div', {'class':{'line-top-right': true}}, []))
+        doms.unshift(h('div', {'class':{'line-bot-right': true}}, []))
+      }
+    },
+    copyBranch(node){
+      let parentNode = this.nodeMap.get(node.parentId)
+      let branchNode = this.$deepCopy(node)
+      branchNode.name = branchNode.name + '-copy'
+      this.forEachNode(parentNode, branchNode, (parent, node) => {
+        let id = this.getRandomId()
+        console.log(node, '新id =>'+ id, '老nodeId:' + node.id )
+        node.id = id
+        node.parentId = parent.id
+      })
+      parentNode.branchs.splice(parentNode.branchs.indexOf(node), 0, branchNode)
+      this.$forceUpdate()
+    },
+    branchMove(node, offset){
+      let parentNode = this.nodeMap.get(node.parentId)
+      let index = parentNode.branchs.indexOf(node)
+      let branch = parentNode.branchs[index + offset]
+      parentNode.branchs[index + offset] = parentNode.branchs[index]
+      parentNode.branchs[index] = branch
+      this.$forceUpdate()
+    },
+    //判断是否为主要业务节点
+    isPrimaryNode(node){
+      return node &&
+          (node.type === 'ROOT' || node.type === 'APPROVAL' || node.type === 'TASK'
+          || node.type === 'CC' || node.type === 'DELAY'
+              || node.type === 'TRIGGER');
+    },
+    isBranchNode(node){
+      return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS' || node.type === 'INCLUSIVES');
+    },
+    isEmptyNode(node){
+      return node && (node.type === 'EMPTY')
+    },
+    //是分支节点
+    isConditionNode(node){
+      return node.type === 'CONDITIONS';
+    },
+    //是分支节点
+    isBranchSubNode(node){
+      return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT' || node.type === 'INCLUSIVE');
+    },
+    isInclusiveNode(node){
+      return node.type === 'INCLUSIVES'
+    },
+    isConcurrentNode(node){
+      return node.type === 'CONCURRENTS'
+    },
+    getRandomId(){
+      return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random()*9000+1000)}`
+    },
+    //选中一个节点
+    selectNode(node){
+      this.$store.commit('selectedNode', node)
+      this.$emit('selectedNode', node)
+    },
+    //处理节点插入逻辑
+    insertApprovalNode(parentNode){
+      this.$set(parentNode.children, "name", "审批人")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.APPROVAL_PROPS))
+    },
+    insertCcNode(parentNode){
+      this.$set(parentNode.children, "name", "抄送人")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.CC_PROPS))
+    },
+    insertDelayNode(parentNode){
+      this.$set(parentNode.children, "name", "延时处理")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.DELAY_PROPS))
+    },
+    insertTriggerNode(parentNode){
+      this.$set(parentNode.children, "name", "触发器")
+      this.$set(parentNode.children, "props", this.$deepCopy(DefaultProps.TRIGGER_PROPS))
+    },
+    insertConditionsNode(parentNode){
+      this.$set(parentNode.children, "name", "条件分支")
+      this.$set(parentNode.children, 'children', {
+        id: this.getRandomId(),
+        parentId: parentNode.children.id,
+        type: "EMPTY"
+      })
+      this.$set(parentNode.children, "branchs", [
+        {
+          id: this.getRandomId(),
+          parentId: parentNode.children.id,
+          type: "CONDITION",
+          props: this.$deepCopy(DefaultProps.CONDITION_PROPS),
+          name: "条件1",
+          children:{}
+        },{
+          id: this.getRandomId(),
+          parentId: parentNode.children.id,
+          type: "CONDITION",
+          props: this.$deepCopy(DefaultProps.CONDITION_PROPS),
+          name: "条件2",
+          children:{}
+        }
+      ])
+    },
+    insertConcurrentsNode(parentNode){
+      this.$set(parentNode.children, "name", "并行分支")
+      this.$set(parentNode.children, 'children',{
+        id: this.getRandomId(),
+        parentId: parentNode.children.id,
+        type: "EMPTY"
+      })
+      this.$set(parentNode.children, "branchs", [
+        {
+          id: this.getRandomId(),
+          name: "分支1",
+          parentId: parentNode.children.id,
+          type: "CONCURRENT",
+          props: {},
+          children:{}
+        },{
+          id: this.getRandomId(),
+          name: "分支2",
+          parentId: parentNode.children.id,
+          type: "CONCURRENT",
+          props: {},
+          children:{}
+        }
+      ])
+    },
+    getBranchEndNode(conditionNode){
+      if (!conditionNode.children || !conditionNode.children.id){
+        return conditionNode;
+      }
+      return this.getBranchEndNode(conditionNode.children);
+    },
+    addBranchNode(node){
+      if (node.branchs.length < 8){
+        node.branchs.push({
+          id: this.getRandomId(),
+          parentId: node.id,
+          name: (this.isConditionNode(node) ? '条件':'分支') + (node.branchs.length + 1),
+          props: this.isConditionNode(node) ? this.$deepCopy(DefaultProps.CONDITION_PROPS):{},
+          type: this.isConditionNode(node) ? "CONDITION":"CONCURRENT",
+          children:{}
+        })
+      }else {
+        this.$message.warning("最多只能添加 8 项😥")
+      }
+    },
+    //删除当前节点
+    delNode(node){
+      console.log("删除节点", node)
+      //获取该节点的父节点
+      let parentNode = this.nodeMap.get(node.parentId)
+      if (parentNode){
+        //判断该节点的父节点是不是分支节点
+        if (this.isBranchNode(parentNode)){
+          //移除该分支
+          parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
+          //处理只剩1个分支的情况
+          if (parentNode.branchs.length < 2){
+            //获取条件组的父节点
+            let ppNode = this.nodeMap.get(parentNode.parentId)
+            //判断唯一分支是否存在业务节点
+            if (parentNode.branchs[0].children && parentNode.branchs[0].children.id){
+              //将剩下的唯一分支头部合并到主干
+              ppNode.children = parentNode.branchs[0].children
+              ppNode.children.parentId = ppNode.id
+              //搜索唯一分支末端最后一个节点
+              let endNode = this.getBranchEndNode(parentNode.branchs[0])
+              //后续节点进行拼接, 这里要取EMPTY后的节点
+              endNode.children = parentNode.children.children
+              if (endNode.children && endNode.children.id){
+                endNode.children.parentId = endNode.id
+              }
+            }else {
+              //直接合并分支后面的节点,这里要取EMPTY后的节点
+              ppNode.children = parentNode.children.children
+              if (ppNode.children && ppNode.children.id){
+                ppNode.children.parentId = ppNode.id
+              }
+            }
+          }
+        }else {
+          //不是的话就直接删除
+          if (node.children && node.children.id) {
+            node.children.parentId = parentNode.id
+          }
+          parentNode.children = node.children
+        }
+        this.$forceUpdate()
+      }else {
+        this.$message.warning("出现错误,找不到上级节点😥")
+      }
+    },
+    validateProcess(){
+      this.valid = true
+      let err = []
+      this.validate(err, this.dom)
+      return err
+    },
+    validateNode(err, node){
+      if (this.$refs[node.id].validate){
+        this.valid = this.$refs[node.id].validate(err)
+      }
+    },
+    //更新指定节点的dom
+    nodeDomUpdate(node){
+      this.$refs[node.id].$forceUpdate()
+    },
+    //给定一个起始节点,遍历内部所有节点
+    forEachNode(parent, node, callback){
+      if (this.isBranchNode(node)){
+        callback(parent, node)
+        this.forEachNode(node, node.children, callback)
+        node.branchs.map(branchNode => {
+          callback(node, branchNode)
+          this.forEachNode(branchNode, branchNode.children, callback)
+        })
+      }else if (this.isPrimaryNode(node) || this.isEmptyNode(node) || this.isBranchSubNode(node)){
+        callback(parent, node)
+        this.forEachNode(node, node.children, callback)
+      }
+    },
+    //校验所有节点设置
+    validate(err, node){
+      if (this.isPrimaryNode(node)){
+        this.validateNode(err, node)
+        this.validate(err, node.children)
+      }else if (this.isBranchNode(node)){
+        //校验每个分支
+        node.branchs.map(branchNode => {
+          //校验条件节点
+          this.validateNode(err, branchNode)
+          //校验条件节点后面的节点
+          this.validate(err, branchNode.children)
+        })
+        this.validate(err, node.children)
+      }else if (this.isEmptyNode(node)){
+        this.validate(err, node.children)
+      }
+
+    }
+  },
+  watch:{
+
+  }
+}
+</script>
+
+<style lang="less" scoped>
+._root{
+ margin: 0 auto;
+}
+.process-end{
+  width: 60px;
+  margin: 0 auto;
+  margin-bottom: 20px;
+  border-radius: 15px;
+  padding: 5px 10px;
+  font-size: small;
+  color: #747474;
+  background-color: #f2f2f2;
+  box-shadow: 0 0 10px 0 #bcbcbc;
+}
+.primary-node{
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+}
+.branch-node{
+  display: flex;
+  justify-content: center;
+  /*border-top: 2px solid #cccccc;
+  border-bottom: 2px solid #cccccc;*/
+}
+.branch-node-item{
+  position: relative;
+  display: flex;
+  background: #f5f6f6;
+  flex-direction: column;
+  align-items: center;
+  border-top: 2px solid #cccccc;
+  border-bottom: 2px solid #cccccc;
+  &:before{
+    content: "";
+    position: absolute;
+    top: 0;
+    left: calc(50% - 1px);
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #CACACA;
+  }
+  .line-top-left, .line-top-right, .line-bot-left, .line-bot-right{
+    position: absolute;
+    width: 50%;
+    height: 4px;
+    background-color: #f5f6f6;
+  }
+  .line-top-left{
+    top: -2px;
+    left: -1px;
+  }
+  .line-top-right{
+    top: -2px;
+    right: -1px;
+  }
+  .line-bot-left{
+    bottom: -2px;
+    left: -1px;
+  }
+  .line-bot-right{
+    bottom: -2px;
+    right: -1px;
+  }
+}
+.add-branch-btn{
+  position: absolute;
+  width: 80px;
+  .add-branch-btn-el{
+    z-index: 999;
+    position: absolute;
+    top: -15px;
+  }
+}
+
+.empty-node{
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  align-items: center;
+}
+</style>

+ 118 - 0
src/views/common/InsertButton.vue

@@ -0,0 +1,118 @@
+<template>
+  <el-popover placement="bottom-start" title="添加流程节点" width="350" trigger="click">
+    <div class="node-select">
+      <div @click="addApprovalNode">
+        <i class="el-icon-s-check" style="color:rgb(255, 148, 62);"></i>
+        <span>审批人</span>
+      </div>
+      <div @click="addTaskNode">
+        <i class="el-icon-s-check" style="color:rgb(230, 176, 57);"></i>
+        <span>办理人</span>
+      </div>
+      <div @click="addCcNode">
+        <i class="el-icon-s-promotion" style="color:rgb(50, 150, 250);"></i>
+        <span>抄送人</span>
+      </div>
+      <div @click="addConditionsNode">
+        <i class="el-icon-share" style="color:rgb(21, 188, 131);"></i>
+        <span>条件分支</span>
+      </div>
+      <div @click="addConcurrentsNode">
+        <i class="el-icon-s-operation" style="color:#718dff;"></i>
+        <span>并行分支</span>
+      </div>
+      <div @click="addInclusivesNode">
+        <i class="el-icon-s-operation" style="color:#718dff;"></i>
+        <span>包容分支</span>
+      </div>
+      <div @click="addDelayNode">
+        <i class="el-icon-time" style="color:#f25643;"></i>
+        <span>延迟等待</span>
+      </div>
+      <div @click="addTriggerNode">
+        <i class="el-icon-set-up" style="color:#15BC83;"></i>
+        <span>触发器</span>
+      </div>
+      <div @click="addSubprocessNode">
+        <i class="el-icon-set-up" style="color:#15BC93;"></i>
+        <span>子流程</span>
+      </div>
+    </div>
+    <el-button icon="el-icon-plus" slot="reference" type="primary" size="small" circle></el-button>
+  </el-popover>
+</template>
+
+<script>
+export default {
+  name: "InsertButton",
+  components: {},
+  data() {
+    return {}
+  },
+  computed:{
+    selectedNode(){
+      this.$store.state.selectedNode
+    }
+  },
+  methods: {
+    addApprovalNode(){
+      this.$emit('insertNode', "APPROVAL")
+    },
+    addTaskNode(){
+      this.$emit('insertNode', "TASK")
+    },
+    addCcNode(){
+      this.$emit('insertNode', "CC")
+    },
+    addDelayNode(){
+      this.$emit('insertNode', "DELAY")
+    },
+    addConditionsNode(){
+      this.$emit('insertNode', "CONDITIONS")
+    },
+    addConcurrentsNode(){
+      this.$emit('insertNode', "CONCURRENTS")
+    },
+    addInclusivesNode(){
+      this.$emit('insertNode', "INCLUSIVES")
+    },
+    addTriggerNode(){
+      this.$emit('insertNode', "TRIGGER")
+    },
+    addSubprocessNode(){
+      this.$emit('insertNode', "SUBPROCESS")
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.node-select{
+  div{
+    display: inline-block;
+    margin: 5px 5px;
+    cursor: pointer;
+    padding: 10px 15px;
+    border: 1px solid #F8F9F9;
+    background-color: #F8F9F9;
+    border-radius: 10px;
+    width: 130px;
+    position: relative;
+    span{
+      position: absolute;
+      left: 65px;
+      top: 18px;
+    }
+    &:hover{
+      background-color: #fff;
+      box-shadow: 0 0 8px 2px #d6d6d6;
+    }
+    i{
+      font-size: 25px;
+      padding: 5px;
+      border: 1px solid #dedfdf;
+      border-radius: 14px;
+    }
+  }
+}
+</style>

+ 29 - 0
src/views/common/form/ComponentExport.js

@@ -0,0 +1,29 @@
+let TextInput = () => import('./components/TextInput.vue')
+let NumberInput = () => import('./components/NumberInput.vue')
+let AmountInput = () => import('./components/AmountInput.vue')
+let TextareaInput = () => import('./components/TextareaInput.vue')
+let SelectInput = () => import('./components/SelectInput.vue')
+let MultipleSelect = () => import('./components/MultipleSelect.vue')
+let DateTime = () => import('./components/DateTime.vue')
+let DateTimeRange = () => import('./components/DateTimeRange.vue')
+
+let Description = () => import('./components/Description.vue')
+let ImageUpload = () => import('./components/ImageUpload.vue')
+let FileUpload = () => import('./components/FileUpload.vue')
+let Location = () => import('./components/Location.vue')
+let MoneyInput = () => import('./components/MoneyInput.vue')
+let DeptPicker = () => import('./components/DeptPicker.vue')
+let UserPicker = () => import('./components/UserPicker.vue')
+let SignPanel = () => import('./components/SignPannel.vue')
+
+let SpanLayout = () => import('./components/SpanLayout.vue')
+let TableList = () => import('./components/TableList.vue')
+
+export default {
+  //基础组件
+  TextInput, NumberInput, AmountInput, TextareaInput, SelectInput, MultipleSelect,
+  DateTime, DateTimeRange, UserPicker, DeptPicker,
+  //高级组件
+  Description, FileUpload, ImageUpload, MoneyInput, Location, SignPanel,
+  SpanLayout, TableList
+}

+ 26 - 0
src/views/common/form/ComponentMinxins.js

@@ -0,0 +1,26 @@
+//混入组件数据
+export default{
+  props:{
+    mode:{
+      type: String,
+      default: 'DESIGN'
+    },
+    required:{
+      type: Boolean,
+      default: false
+    },
+  },
+  data(){
+    return {}
+  },
+  computed: {
+    _value: {
+      get() {
+        return this.value;
+      },
+      set(val) {
+        this.$emit("input", val);
+      }
+    }
+  },
+}

+ 220 - 0
src/views/common/form/ComponentsConfigExport.js

@@ -0,0 +1,220 @@
+export const ValueType = {
+  string: 'String',
+  object: 'Object',
+  array: 'Array',
+  number: 'Number',
+  date: 'Date',
+  user: 'User',
+  dept: 'Dept',
+  dateRange: 'DateRange'
+}
+
+export const baseComponents = [
+  {
+    name: '布局',
+    components: [
+      {
+        title: '分栏布局',
+        name: 'SpanLayout',
+        icon: 'el-icon-c-scale-to-original',
+        value: [],
+        valueType: ValueType.array,
+        props: {
+          items:[]
+        }
+      }
+    ]
+  }, {
+    name: '基础组件',
+    components: [
+      {
+        title: '单行文本输入',
+        name: 'TextInput',
+        icon: 'el-icon-edit',
+        value: '',
+        valueType: ValueType.string,
+        props: {
+          required: false,
+          enablePrint: true
+        }
+      },
+      {
+        title: '多行文本输入',
+        name: 'TextareaInput',
+        icon: 'el-icon-more-outline',
+        value: '',
+        valueType: ValueType.string,
+        props: {
+          required: false,
+          enablePrint: true
+        }
+      },
+      {
+        title: '数字输入框',
+        name: 'NumberInput',
+        icon: 'el-icon-edit-outline',
+        value: '',
+        valueType: ValueType.number,
+        props: {
+          required: false,
+          enablePrint: true,
+        }
+      },
+      {
+        title: '金额输入框',
+        name: 'AmountInput',
+        icon: 'iconfont icon-zhufangbutiezhanghu',
+        value: '',
+        valueType: ValueType.number,
+        props: {
+          required: false,
+          enablePrint: true,
+          showChinese: true
+        }
+      },
+      {
+        title: '单选框',
+        name: 'SelectInput',
+        icon: 'el-icon-circle-check',
+        value: '',
+        valueType: ValueType.string,
+        props: {
+          required: false,
+          enablePrint: true,
+          expanding: false,
+          options: ['选项1', '选项2']
+        }
+      },
+      {
+        title: '多选框',
+        name: 'MultipleSelect',
+        icon: 'iconfont icon-duoxuankuang',
+        value: [],
+        valueType: ValueType.array,
+        props: {
+          required: false,
+          enablePrint: true,
+          expanding: false,
+          options: ['选项1', '选项2']
+        }
+      },
+      {
+        title: '日期时间点',
+        name: 'DateTime',
+        icon: 'el-icon-date',
+        value: '',
+        valueType: ValueType.date,
+        props: {
+          required: false,
+          enablePrint: true,
+          format: 'yyyy-MM-dd HH:mm',
+        }
+      },
+      {
+        title: '日期时间区间',
+        name: 'DateTimeRange',
+        icon: 'iconfont icon-kaoqin',
+        valueType: ValueType.dateRange,
+        props: {
+          required: false,
+          enablePrint: true,
+          placeholder: ['开始时间', '结束时间'],
+          format: 'yyyy-MM-dd HH:mm',
+          showLength: false
+        }
+      },
+      {
+        title: '上传图片',
+        name: 'ImageUpload',
+        icon: 'el-icon-picture-outline',
+        value: [],
+        valueType: ValueType.array,
+        props: {
+          required: false,
+          enablePrint: true,
+          maxSize: 5, //图片最大大小MB
+          maxNumber: 10, //最大上传数量
+          enableZip: true //图片压缩后再上传
+        }
+      },
+      {
+        title: '上传附件',
+        name: 'FileUpload',
+        icon: 'el-icon-folder-opened',
+        value: [],
+        valueType: ValueType.array,
+        props: {
+          required: false,
+          enablePrint: true,
+          onlyRead: false, //是否只读,false只能在线预览,true可以下载
+          maxSize: 100, //文件最大大小MB
+          maxNumber: 10, //最大上传数量
+          fileTypes: [] //限制文件上传类型
+        }
+      },
+      {
+        title: '人员选择',
+        name: 'UserPicker',
+        icon: 'el-icon-user',
+        value: [],
+        valueType: ValueType.user,
+        props: {
+          required: false,
+          enablePrint: true,
+          multiple: false
+        }
+      },
+      {
+        title: '部门选择',
+        name: 'DeptPicker',
+        icon: 'iconfont icon-map-site',
+        value: [],
+        valueType: ValueType.dept,
+        props: {
+          required: false,
+          enablePrint: true,
+          multiple: false
+        }
+      },
+      {
+        title: '说明文字',
+        name: 'Description',
+        icon: 'el-icon-warning-outline',
+        value: '',
+        valueType: ValueType.string,
+        props: {
+          required: false,
+          enablePrint: true
+        }
+      },
+    ]
+  }, {
+    name: '扩展组件',
+    components: [
+      {
+        title: '明细表',
+        name: 'TableList',
+        icon: 'el-icon-tickets',
+        value: [],
+        valueType: ValueType.array,
+        props: {
+          required: false,
+          enablePrint: true,
+          showBorder: true,
+          rowLayout: true,
+          showSummary: false,
+          summaryColumns: [],
+          maxSize: 0, //最大条数,为0则不限制
+          columns:[] //列设置
+        }
+      }
+    ]
+  }
+]
+
+
+
+export default {
+  baseComponents
+}
+

+ 74 - 0
src/views/common/form/FormComponentConfig.vue

@@ -0,0 +1,74 @@
+<template>
+  <div>
+    <el-form label-width="90px" v-if="form.name !== 'SpanLayout'">
+      <el-form-item label="表单id">
+        <el-input size="small" readonly="true"  v-model="form.id"/>
+      </el-form-item>
+      <el-form-item label="表单名称">
+        <el-input size="small" clearable v-model="form.title"/>
+      </el-form-item>
+      <component :is="form.name" v-model="form.props"/>
+      <el-form-item label="必填项">
+        <el-switch v-model="form.props.required"></el-switch>
+      </el-form-item>
+      <el-form-item label="可打印">
+        <el-switch v-model="form.props.enablePrint"></el-switch>
+      </el-form-item>
+    </el-form>
+    <el-empty v-else description="当前组件不支持配置"></el-empty>
+  </div>
+
+</template>
+
+<script>
+import TextInput from './config/TextInputConfig.vue'
+import NumberInput from './config/NumberInputConfig.vue'
+import AmountInput from './config/AmountInputConfig.vue'
+import TextareaInput from './config/TextareaInputConfig.vue'
+import MultipleSelect from './config/SelectInputConfig.vue'
+import SelectInput from './config/SelectInputConfig.vue'
+import DateTime from './config/DateTimeConfig.vue'
+import DateTimeRange from './config/DateTimeRangeConfig.vue'
+import ImageUpload from './config/ImageUploadConfig.vue'
+import FileUpload from './config/FileUploadConfig.vue'
+import Description from './config/DescriptionConfig.vue'
+import MoneyInput from './config/MoneyInputConfig.vue'
+import DeptPicker from './config/OrgPickerConfig.vue'
+import UserPicker from './config/OrgPickerConfig.vue'
+import TableList from './config/TableListConfig.vue'
+
+export default {
+  name: "FormComponentConfig",
+  components: {
+    TextInput,
+    NumberInput,
+    AmountInput,
+    TextareaInput,
+    SelectInput,
+    MultipleSelect,
+    DateTime,
+    DateTimeRange,
+    ImageUpload,
+    FileUpload,
+    Description,
+    MoneyInput,
+    DeptPicker,
+    UserPicker,
+    TableList
+  },
+  props:{},
+  computed:{
+    form(){
+      return this.$store.state.selectFormItem
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 103 - 0
src/views/common/form/FormRender.vue

@@ -0,0 +1,103 @@
+<template>
+  <!--渲染表单-->
+  <el-form ref="form" class="process-form" label-position="top" :rules="rules" :model="_value">
+    <template v-for="(item, index) in forms" >
+      <el-form-item :key="item.name + index" v-if="item.name !== 'SpanLayout' && item.name !== 'Description'" :prop="item.id" :label="item.title">
+        <form-design-render :ref="`sub-item_${item.id}`" v-model="_value[item.id]" mode="PC" :config="item"/>
+      </el-form-item>
+      <template v-else>
+        <form-design-render :key="index" ref="span-layout"  v-model="_value" mode="PC" :config="item"/>
+      </template>
+    </template>
+  </el-form>
+</template>
+
+<script>
+import FormDesignRender from '@/views/admin/layout/form/FormDesignRender'
+
+export default {
+  name: "FormRender",
+  components: {FormDesignRender},
+  props:{
+    forms: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    value: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      rules: {},
+    }
+  },
+  created() {
+    this.loadFormConfig(this.forms)
+  },
+  computed: {
+    _value:{
+      get(){
+        return this.value
+      },
+      set(val){
+        this.$emit('input', val)
+      }
+    }
+  },
+  methods: {
+    validate(call) {
+      let success = true
+      this.$refs.form.validate(valid => {
+        success = valid
+        if(valid){
+          //校验成功再校验内部
+          for (let i = 0; i < this.forms.length; i++) {
+            if (this.forms[i].name === 'TableList'){
+              let formRef = this.$refs[`sub-item_${this.forms[i].id}`]
+              if (formRef && Array.isArray(formRef) && formRef.length > 0){
+                formRef[0].validate(subValid => {
+                  success = subValid
+                })
+                if (!success){
+                  break
+                }
+              }
+            }
+          }
+        }
+        call(success)
+      });
+    },
+    loadFormConfig(forms){
+      forms.forEach(item => {
+        if (item.name === 'SpanLayout'){
+          this.loadFormConfig(item.props.items)
+        }else {
+          this.$set(this._value, item.id, this.value[item.id])
+          if(item.props.required){
+            this.$set(this.rules, item.id, [{
+              type: item.valueType === 'Array' ? 'array': undefined,
+              required: true,
+              message: `请填写${item.title}`, trigger: 'blur'
+            }])
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.process-form {
+  /deep/ .el-form-item__label {
+    padding: 0 0;
+  }
+}
+</style>

+ 159 - 0
src/views/common/form/components/AmountInput.vue

@@ -0,0 +1,159 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-input size="medium" disabled :placeholder="placeholder"/>
+      <div style="margin-top: 15px" v-show="showChinese">
+        <span>大写:</span>
+        <span class="chinese">{{chinese}}</span>
+      </div>
+
+    </div>
+    <div v-else>
+      <el-input-number v-if="!readerMode" style="width: 100%;" :min="0" controls-position="right" :precision="precision" size="medium" clearable v-model="_value" :placeholder="placeholder"/>
+      <div v-else v-text="_value"></div>
+      <div v-show="showChinese">
+        <span>大写:</span>
+        <span class="chinese">{{chinese}}</span>
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "AmountInput",
+  components: {},
+  props: {
+    value: {
+      default: null
+    },
+    placeholder: {
+      type: String,
+      default: '请输入金额'
+    },
+    showChinese: {
+      type: Boolean,
+      default: true
+    },
+    precision: {
+      type: Number,
+      default: 0
+    },
+    readerMode: {
+      type: Boolean,
+      default: false
+    },
+  },
+  computed:{
+    chinese(){
+      return this.convertCurrency(this.value)
+    },
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    convertCurrency(money) {
+      //汉字的数字
+      const cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
+      //基本单位
+      const cnIntRadice = ['', '拾', '佰', '仟'];
+      //对应整数部分扩展单位
+      const cnIntUnits = ['', '万', '亿', '兆'];
+      //对应小数部分单位
+      const cnDecUnits = ['角', '分', '毫', '厘'];
+      //整数金额时后面跟的字符
+      const cnInteger = '整';
+      //整型完以后的单位
+      const cnIntLast = '元';
+      //最大处理的数字
+      let maxNum = 999999999999999.9999;
+      //金额整数部分
+      let integerNum;
+      //金额小数部分
+      let decimalNum;
+      //输出的中文金额字符串
+      let chineseStr = '';
+      //分离金额后用的数组,预定义
+      let parts;
+      if (money === '') {
+        return '';
+      }
+      money = parseFloat(money);
+      if (money >= maxNum) {
+        //超出最大处理数字
+        return '';
+      }
+      if (money === 0) {
+        chineseStr = cnNums[0] + cnIntLast + cnInteger;
+        return chineseStr;
+      }
+      //转换为字符串
+      money = money.toString();
+      if (money.indexOf('.') === -1) {
+        integerNum = money;
+        decimalNum = '';
+      } else {
+        parts = money.split('.');
+        integerNum = parts[0];
+        decimalNum = parts[1].substr(0, 4);
+      }
+      //获取整型部分转换
+      if (parseInt(integerNum, 10) > 0) {
+        var zeroCount = 0;
+        var IntLen = integerNum.length;
+        for (let i = 0; i < IntLen; i++) {
+          let n = integerNum.substr(i, 1);
+          let p = IntLen - i - 1;
+          let q = p / 4;
+          let m = p % 4;
+          if (n == '0') {
+            zeroCount++;
+          } else {
+            if (zeroCount > 0) {
+              chineseStr += cnNums[0];
+            }
+            //归零
+            zeroCount = 0;
+            chineseStr += cnNums[parseInt(n)] + cnIntRadice[m];
+          }
+          if (m == 0 && zeroCount < 4) {
+            chineseStr += cnIntUnits[q];
+          }
+        }
+        chineseStr += cnIntLast;
+      }
+      //小数部分
+      if (decimalNum !== '') {
+        let decLen = decimalNum.length;
+        for (let i = 0; i < decLen; i++) {
+          let n = decimalNum.substr(i, 1);
+          if (n !== '0') {
+            chineseStr += cnNums[Number(n)] + cnDecUnits[i];
+          }
+        }
+      }
+      if (chineseStr === '') {
+        chineseStr += cnNums[0] + cnIntLast + cnInteger;
+      } else if (decimalNum === '') {
+        chineseStr += cnInteger;
+      }
+      return chineseStr;
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.chinese{
+  color: #afadad;
+  font-size: smaller;
+}
+ /deep/ .el-input__inner{
+   text-align: left;
+ }
+</style>

+ 57 - 0
src/views/common/form/components/DateTime.vue

@@ -0,0 +1,57 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'" >
+      <el-date-picker size="medium" disabled :type="type" :placeholder="placeholder"></el-date-picker>
+    </div>
+    <div v-else>
+      <el-date-picker v-if="!readerMode" v-model="_value" :value-format="format" size="medium" clearable :type="type" :placeholder="placeholder"></el-date-picker>
+      <div v-else v-text="value"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "DateTime",
+  components: {},
+  props:{
+    value: {
+      default: null
+    },
+    format:{
+      type: String,
+      default: 'yyyy-MM-dd HH:mm'
+    },
+    placeholder:{
+      type: String,
+      default: '请选择日期时间'
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed:{
+    type(){
+      switch (this.format){
+        case 'yyyy': return 'year';
+        case 'yyyy-MM': return 'month';
+        case 'yyyy-MM-dd': return 'date';
+        case 'yyyy-MM-dd HH:mm': return 'datetime';
+        default: return 'datetime';
+      }
+    },
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 122 - 0
src/views/common/form/components/DateTimeRange.vue

@@ -0,0 +1,122 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-date-picker size="medium" v-model="_value" disabled :type="type" :start-placeholder="placeholder[0]" :end-placeholder="placeholder[1]"/>
+      <div v-if="showLength" class="length">
+        <span>时长:</span>
+        <span>{{timeLength}}</span>
+      </div>
+    </div>
+    <div v-else>
+      <template>
+        <el-date-picker v-if="!readerMode" v-model="_value" size="medium" clearable :value-format="format" :type="type" :start-placeholder="placeholder[0]" :end-placeholder="placeholder[1]"/>
+        <div v-else v-text="value[0] + ' ~ ' + value[1]"></div>
+      </template>
+
+      <div v-if="showLength" class="length">
+        <span>时长:</span>
+        <span>{{timeLength}}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+import moment from 'moment';
+
+export default {
+  mixins: [componentMinxins],
+  name: "DateTimeRange",
+  components: {},
+  props:{
+    value: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    format:{
+      type: String,
+      default: 'yyyy-MM-dd HH:mm'
+    },
+    placeholder:{
+      type: Array,
+      default: ()=>{
+        return ['开始时间', '结束时间']
+      }
+    },
+    showLength:{
+      type: Boolean,
+      default: false,
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    }
+  },
+  computed:{
+    type(){
+      switch (this.format){
+        case 'yyyy-MM-dd': return 'daterange';
+        case 'yyyy-MM-dd HH:mm': return 'datetimerange';
+        default: return 'daterange';
+      }
+    },
+    timeLength(){
+      //求时长算法
+      if (Array.isArray(this.value)){
+        let start = moment(this.value[0]).format(this.format.replaceAll('dd', 'DD'))
+        let end = moment(this.value[1]).format(this.format.replaceAll('dd', 'DD'))
+        if (start === end){
+          return '0 (时长为0,请确认)'
+        }
+        let mstart = moment(start);
+        let mend = moment(end)
+        let years = mend.diff(start, 'years')
+        let months = mend.diff(start, 'months')
+        let days = mend.diff(start, 'days')
+        let hours = mend.diff(start, 'hours')
+        let minutes = mend.diff(start, 'minutes')
+        minutes = minutes % 60
+        hours = hours % 24
+        months = months % 12
+        //因为每月天不固定,所以天要特殊动态处理
+        if(mstart.date() < mend.date()){
+          days = mend.date() - mstart.date()
+          if (minutes > 0 || hours > 0){
+            days --;
+          }
+        }
+        //处理超过俩月且天超过31
+        if (days > 31 && mend.month() - mstart.month() >= 2){
+          //将日期推至上月求差
+          days = mend.diff(mstart.add(mend.month() - mstart.month() - 1, 'month'), 'days')
+        }
+        return `${years > 0 ? years + '年 ': ' '}${months > 0 ? months + '个月 ': ' '}
+                ${days > 0 ? days + '天 ': ' '}${hours > 0 ? hours + '小时 ': ' '}
+                ${minutes > 0 ? minutes + '分钟 ': ' '}`
+      } else {
+        return '先选择时间哦'
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+.length{
+  margin-top: 5px;
+}
+.length:nth-child(2){
+  color: #8c8c8c;
+}
+/deep/ .el-date-editor--datetimerange.el-input__inner{
+  width: 100%;
+  max-width: 400px;
+}
+</style>

+ 65 - 0
src/views/common/form/components/DeptPicker.vue

@@ -0,0 +1,65 @@
+<template>
+  <div style="max-width: 350px">
+    <div v-if="mode === 'DESIGN'">
+      <el-button disabled icon="iconfont icon-map-site" type="primary" size="mini" round> 选择部门</el-button>
+      <span class="placeholder"> {{placeholder}}</span>
+    </div>
+    <div v-else>
+      <el-button icon="iconfont icon-map-site" type="primary" size="mini" round @click="$refs.orgPicker.show()"> 选择部门</el-button>
+      <org-picker type="dept" :multiple="multiple" ref="orgPicker" :selected="_value" @ok="selected"/>
+      <span class="placeholder"> {{placeholder}}</span>
+      <div style="margin-top: 5px">
+        <el-tag size="mini" style="margin: 5px" closable v-for="(dept, i) in _value" :key="i" @close="delDept(i)">{{dept.name}}</el-tag>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+import OrgPicker from "@/components/common/OrgPicker";
+
+export default {
+  mixins: [componentMinxins],
+  name: "DeptPicker",
+  components: {OrgPicker},
+  props: {
+    value:{
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    placeholder: {
+      type: String,
+      default: '请选择部门'
+    },
+    multiple:{
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      showOrgSelect: false
+    }
+  },
+  methods: {
+    selected(values){
+      this.showOrgSelect = false
+      this._value = values
+    },
+    delDept(i){
+      this._value.splice(i, 1)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.placeholder{
+  margin-left: 10px;
+  color: #adabab;
+  font-size: smaller;
+}
+</style>

+ 35 - 0
src/views/common/form/components/Description.vue

@@ -0,0 +1,35 @@
+<template>
+  <div :style="{'color': color}">
+    <el-icon class="el-icon-warning-outline"></el-icon>
+    <span> {{placeholder}}</span>
+  </div>
+
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "Description",
+  components: {},
+  props: {
+    color:{
+      type: String,
+      default: '#868686'
+    },
+    placeholder: {
+      type: String,
+      default: '只是一段说明文字'
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 125 - 0
src/views/common/form/components/FileUpload.vue

@@ -0,0 +1,125 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-button size="small" icon="el-icon-paperclip" round>选择文件</el-button>
+      <ellipsis :row="1" :content="placeholder + sizeTip" hoverTip slot="tip" class="el-upload__tip" />
+    </div>
+    <div v-else>
+      <el-upload
+        :file-list="_value"
+        action="#"
+        :limit="maxSize"
+        with-credentials
+        :multiple="maxSize > 0"
+        :data="uploadParams"
+        :auto-upload="false"
+        :before-upload="beforeUpload"
+        :http-request="requestUpload"
+      >
+        <el-button size="small" icon="el-icon-paperclip" round>选择文件</el-button>
+        <ellipsis :row="1" :content="placeholder + sizeTip" hoverTip slot="tip" class="el-upload__tip" />
+
+      </el-upload>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins';
+import { upLoadFileApi, downLoadFileApi } from '@/api/design';
+import {  downloadFileBlob } from '@/utils/index';
+export default {
+  mixins: [componentMinxins],
+  name: 'ImageUpload',
+  components: {},
+  props: {
+    placeholder: {
+      type: String,
+      default: '请选择附件',
+    },
+    value: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+    maxSize: {
+      type: Number,
+      default: 5,
+    },
+    maxNumber: {
+      type: Number,
+      default: 10,
+    },
+    fileTypes: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+  },
+  computed: {
+    sizeTip() {
+      if (this.fileTypes.length > 0) {
+        return ` | 只允许上传[${String(this.fileTypes).replaceAll(',', '、')}]格式的文件,且单个附件不超过${this.maxSize}MB`;
+      }
+      return this.maxSize > 0 ? ` | 单个附件不超过${this.maxSize}MB` : '';
+    },
+  },
+  data() {
+    return {
+      disabled: false,
+      uploadParams: {},
+    };
+  },
+  methods: {
+           // 覆盖默认的上传行为
+           requestUpload() {
+            
+           },
+    beforeUpload(file) {
+
+      // const alows = [];
+      // if (alows.indexOf(file.type) === -1) {
+      //   this.$message.warning('存在不支持的图片格式');
+      // } else 
+      if (this.maxSize > 0 && file.size / 1024 / 1024 > this.maxSize) {
+        this.$message.warning(`单张图片最大不超过 ${this.maxSize}MB`);
+      } else {
+         //上传文件的需要formdata类型;所以要转
+         let FormDatas = new FormData();
+        FormDatas.append('file', file);
+        upLoadFileApi(FormDatas).then(res => {
+        console.log('uploadFile', res);
+
+        if (res.data.result) {
+            this._value.push(res.data.result); //成功过后手动将文件添加到展示列表里
+            this.$emit('input', this._value);
+          }
+        });
+
+        return true;
+      }
+      return false;
+    },
+    handleRemove(file, fileList) {
+      console.log(file, fileList);
+    },
+    handlePictureCardPreview(file) {
+      console.log(file);
+    },
+    handleDownload(file) {
+        //上传文件的需要formdata类型;所以要转
+      let FormDatas = new FormData();
+      FormDatas.append('name', file.name);
+      downLoadFileApi(FormDatas).then(res => {
+        if (res.data) {
+          downloadFileBlob(res.data,file.name)
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 163 - 0
src/views/common/form/components/ImageUpload.vue

@@ -0,0 +1,163 @@
+<template>
+  <div>
+
+    <div v-if="mode === 'DESIGN'">
+      <div class="design">
+        <i class="el-icon-plus"></i>
+      </div>
+      <p>{{ placeholder }} {{ sizeTip }}</p>
+    </div>
+ 
+    <div v-else>
+      <el-upload
+        :file-list="_value"
+        action="#"
+        :limit="maxSize"
+        with-credentials
+        :multiple="maxSize > 0"
+        :data="uploadParams"
+        list-type="picture-card"
+
+        :before-upload="beforeUpload"
+
+      >
+  
+        <i slot="default" class="el-icon-plus"></i>
+        <div slot="file" slot-scope="{ file }">
+          <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+          <span class="el-upload-list__item-actions">
+            <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+              <i class="el-icon-zoom-in"></i>
+            </span>
+            <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)">
+              <i class="el-icon-download"></i>
+            </span>
+            <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+              <i class="el-icon-delete"></i>
+            </span>
+          </span>
+        </div>
+
+        <div slot="tip" class="el-upload__tip">{{ placeholder }} {{ sizeTip }}</div>
+      </el-upload>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins';
+import { upLoadFileApi, downLoadFileApi } from '@/api/design';
+import {  downloadFileBlob } from '@/utils/index';
+
+export default {
+  mixins: [componentMinxins],
+  name: 'ImageUpload',
+  components: {},
+  props: {
+    value: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+    placeholder: {
+      type: String,
+      default: '请选择图片',
+    },
+    maxSize: {
+      type: Number,
+      default: 5,
+    },
+    maxNumber: {
+      type: Number,
+      default: 10,
+    },
+    enableZip: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  computed: {
+    sizeTip() {
+      return this.maxSize > 0 ? `| 每张图不超过${this.maxSize}MB` : '';
+    },
+  },
+  data() {
+    return {
+      disabled: false,
+      uploadParams: {},
+    };
+  },
+  methods: {
+       // 覆盖默认的上传行为
+       requestUpload() {
+
+       },
+    beforeUpload(file) {
+      const alows = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
+      if (alows.indexOf(file.type) === -1) {
+        this.$message.warning('存在不支持的图片格式');
+      } else if (this.maxSize > 0 && file.size / 1024 / 1024 > this.maxSize) {
+        this.$message.warning(`单张图片最大不超过 ${this.maxSize}MB`);
+      } else {
+         //上传文件的需要formdata类型;所以要转
+        let FormDatas = new FormData();
+        FormDatas.append('file', file);
+        upLoadFileApi(FormDatas).then(res => {
+        console.log('uploadFile', res);
+
+        if (res.data.result) {
+            
+            this._value.push(res.data.result); //成功过后手动将文件添加到展示列表里
+            console.log("   {{_value}}",this._value)
+            this.$emit('input', this._value);
+          }
+        });
+        return true;
+      }
+      return false;
+    },
+    handleRemove(file, fileList) {
+      console.log(file, fileList);
+    },
+    handlePictureCardPreview(file) {
+      console.log(file);
+    },
+    handleDownload(file) {
+        //上传文件的需要formdata类型;所以要转
+      let FormDatas = new FormData();
+      FormDatas.append('name', file.name);
+      downLoadFileApi(FormDatas).then(res => {
+        if (res.data) {
+          downloadFileBlob(res.data,file.name)
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.design {
+  i {
+    padding: 10px;
+    font-size: xx-large;
+    background: white;
+    border: 1px dashed #8c8c8c;
+  }
+}
+/deep/ .el-upload--picture-card {
+  width: 80px;
+  height: 80px;
+  line-height: 87px;
+}
+/deep/ .el-upload-list__item {
+  width: 80px;
+  height: 80px;
+  .el-upload-list__item-actions {
+    & > span + span {
+      margin: 1px;
+    }
+  }
+}
+</style>

+ 18 - 0
src/views/common/form/components/Location.vue

@@ -0,0 +1,18 @@
+<template>
+
+</template>
+
+<script>
+export default {
+  name: "Location",
+  components: {},
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 18 - 0
src/views/common/form/components/MoneyInput.vue

@@ -0,0 +1,18 @@
+<template>
+
+</template>
+
+<script>
+export default {
+  name: "MoneyInput",
+  components: {},
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 74 - 0
src/views/common/form/components/MultipleSelect.vue

@@ -0,0 +1,74 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-select class="max-fill" v-if="!expanding" size="medium" multiple v-model="_value" disabled :placeholder="placeholder"/>
+      <el-checkbox-group v-else v-model="_value">
+        <el-checkbox disabled v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-checkbox>
+      </el-checkbox-group>
+    </div>
+    <div v-else>
+      <template v-if="!readerMode">
+        <el-select class="max-fill" v-if="!expanding" v-model="_value" multiple size="medium" clearable :placeholder="placeholder">
+          <el-option v-for="(op, index) in options" :key="index" :value="op" :label="op"></el-option>
+        </el-select>
+        <el-checkbox-group v-else v-model="_value">
+          <el-checkbox v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-checkbox>
+        </el-checkbox-group>
+      </template>
+      <template v-else>
+        <span>{{ readerValue }}</span>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "MultipleSelect",
+  components: {},
+  props:{
+    placeholder:{
+      type: String,
+      default: '请选择选项'
+    },
+    value:{
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    expanding:{
+      type: Boolean,
+      default: false
+    },
+    options:{
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed:{
+    readerValue() {
+      return this.value.join(", ")
+    }
+  },
+  data() {
+    return {
+      checks:[]
+    }
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 43 - 0
src/views/common/form/components/NumberInput.vue

@@ -0,0 +1,43 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-input size="medium" disabled :placeholder="placeholder" type="number"/>
+    </div>
+    <div v-else>
+      <el-input v-if="!readerMode" v-model="_value" size="medium" clearable :placeholder="placeholder" type="number"/>
+      <div v-else v-text="_value"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "NumberInput",
+  components: {},
+  props:{
+    value:{
+      type: Number,
+      default: null
+    },
+    placeholder:{
+      type: String,
+      default: '请输入数值'
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 65 - 0
src/views/common/form/components/SelectInput.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-select class="max-fill" v-if="!expanding" size="medium" v-model="_value" disabled :placeholder="placeholder"/>
+      <el-radio-group v-model="_value" v-else>
+        <el-radio disabled v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-radio>
+      </el-radio-group>
+    </div>
+    <div v-else>
+      <template v-if="!readerMode">
+        <el-select class="max-fill" v-if="!expanding" v-model="_value" size="medium" clearable :placeholder="placeholder">
+          <el-option v-for="(op, index) in options" :key="index" :value="op" :label="op"></el-option>
+        </el-select>
+        <el-radio-group v-model="_value" v-else>
+          <el-radio v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-radio>
+        </el-radio-group>
+      </template>
+      <template v-else>
+        <div v-text="_value"></div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "SelectInput",
+  components: {},
+  props:{
+    value:{
+      type: String,
+      default: null
+    },
+    placeholder:{
+      type: String,
+      default: '请选择选项'
+    },
+    expanding:{
+      type: Boolean,
+      default: false
+    },
+    options:{
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    readerMode: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 18 - 0
src/views/common/form/components/SignPannel.vue

@@ -0,0 +1,18 @@
+<template>
+
+</template>
+
+<script>
+export default {
+  name: "SignPannel",
+  components: {},
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 216 - 0
src/views/common/form/components/SpanLayout.vue

@@ -0,0 +1,216 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <draggable class="l-drag-from" :list="_items" group="form"
+                 :options="{animation: 300, chosenClass:'choose', sort:true}"
+                 @start="drag = true; selectFormItem = null" @end="drag = false">
+        <div v-for="(cp, id) in _items" :key="id" class="l-form-item" @click.stop="selectItem(cp)"
+             :style="getSelectedClass(cp)">
+          <div class="l-form-header">
+            <p><span v-if="cp.props.required">*</span>{{ cp.title }}</p>
+            <div class="l-option">
+              <!--<i class="el-icon-copy-document" @click="copy"></i>-->
+              <i class="el-icon-close" @click="delItem(id)"></i>
+            </div>
+            <form-design-render :config="cp"/>
+          </div>
+        </div>
+      </draggable>
+      <div style="color: #c0bebe;text-align: center; width: 90%; padding: 5px;">☝ 拖拽控件到布局容器内部</div>
+    </div>
+    <div v-else>
+      <el-row :gutter="20" v-for="(rows, rsi) in __items" :key="rsi + '_rows'">
+        <el-col :span="24 / rows.length" v-for="(item, ri) in rows" :key="ri + '_row'">
+          <el-form-item v-if="item.name !== 'SpanLayout' && item.name !== 'Description'" :prop="item.id"
+                        :label="item.title" :key="item.name + ri">
+            <form-design-render v-model="_value[item.id]" :mode="mode" :config="item"/>
+          </el-form-item>
+          <form-design-render v-else v-model="_value" :mode="mode" :config="item"/>
+        </el-col>
+      </el-row>
+
+    </div>
+
+  </div>
+
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import FormDesignRender from '@/views/admin/layout/form/FormDesignRender'
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "SpanLayout",
+  components: {draggable, FormDesignRender},
+  props: {
+    value:{
+      default: null
+    },
+    items: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    }
+  },
+  computed: {
+    _items: {
+      get() {
+        return this.items;
+      },
+      set(val) {
+        this.items = val;
+      }
+    },
+    __items() {
+      let result = []
+      for (let i = 0; i < this.items.length; i++) {
+        if (i > 0 && i % 2 > 0) {
+          result.push([this.items[i - 1], this.items[i]])
+        }
+      }
+      if (result.length * 2 < this.items.length) {
+        result.push([this.items[this.items.length - 1]])
+      }
+      return result
+    },
+    selectFormItem: {
+      get() {
+        return this.$store.state.selectFormItem
+      },
+      set(val) {
+        this.$store.state.selectFormItem = val
+      },
+    },
+    nodeMap() {
+      return this.$store.state.nodeMap
+    }
+  },
+  data() {
+    return {
+      select: null,
+      drag: false,
+      formConfig: {
+        //数据字段
+        data: {},
+        //校验规则
+        rules: {}
+      },
+      form: {
+        formId: '',
+        formName: "",
+        logo: {},
+        formItems: [],
+        process: {},
+        remark: ""
+      }
+    }
+  },
+  methods: {
+    selectItem(cp) {
+      this.selectFormItem = cp
+    },
+    getSelectedClass(cp) {
+      return this.selectFormItem && this.selectFormItem.id === cp.id ?
+          'border-left: 4px solid #f56c6c' : ''
+    },
+    delItem(index) {
+      this.$confirm('删除组件将会连带删除包含该组件的条件以及相关设置,是否继续?', '提示', {
+        confirmButtonText: '确 定',
+        cancelButtonText: '取 消',
+        type: 'warning'
+      }).then(() => {
+        if (this._items[index].name === 'SpanLayout') {
+          //删除的是分栏则遍历删除分栏内所有子组件
+          this._items[index].props.items.forEach(item => {
+            this.removeFormItemAbout(item)
+          })
+          this._items[index].props.items.length = 0
+        } else {
+          this.removeFormItemAbout(this._items[index])
+        }
+        this._items.splice(index, 1)
+      })
+    },
+    async removeFormItemAbout(item) {
+      this.nodeMap.forEach(node => {
+        //搜寻条件,进行移除
+        if (node.type === 'CONDITION') {
+          node.props.groups.forEach(group => {
+            let i = group.cids.remove(item.id)
+            if (i > -1) {
+              //从子条件移除
+              group.conditions.splice(i, 1)
+            }
+          })
+        }
+        //搜寻权限,进行移除
+        if (node.type === 'ROOT' || node.type === 'APPROVAL' || node.type === 'CC') {
+          node.props.formPerms.removeByKey('id', item.id)
+        }
+      })
+    },
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+
+.choose {
+  border: 1px dashed @theme-primary !important;
+}
+
+.l-drag-from {
+  min-height: 50px;
+  background-color: rgb(245, 246, 246);
+
+  .l-form-item, li {
+    cursor: grab;
+    background: #ffffff;
+    padding: 10px;
+    border: 1px solid #ebecee;
+    margin: 5px 0;
+  }
+}
+
+.l-form-header {
+  font-size: small;
+  color: #818181;
+  text-align: left;
+  position: relative;
+  background-color: #fff;
+
+  p {
+    position: relative;
+    margin: 0 0 10px 0;
+
+    span {
+      position: absolute;
+      left: -8px;
+      top: 3px;
+      color: rgb(217, 0, 19);
+    }
+  }
+
+  .l-option {
+    position: absolute;
+    top: -10px;
+    right: -10px;
+
+    i {
+      font-size: large;
+      cursor: pointer;
+      color: #8c8c8c;
+      padding: 5px;
+
+      &:hover {
+        color: @theme-primary;
+      }
+    }
+  }
+}
+
+</style>

+ 314 - 0
src/views/common/form/components/TableList.vue

@@ -0,0 +1,314 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <draggable class="l-drag-from" :list="_columns" group="form"
+                 :options="{animation: 300, chosenClass:'choose', sort:true}"
+                 @start="drag = true; selectFormItem = null" @end="drag = false">
+        <div v-for="(cp, id) in _columns" :key="id" class="l-form-item" @click.stop="selectItem(cp)" :style="getSelectedClass(cp)">
+          <div class="l-form-header">
+            <p><span v-if="cp.props.required">*</span>{{ cp.title }}</p>
+            <div class="l-option">
+              <i class="el-icon-close" @click="delItem(id)"></i>
+            </div>
+            <form-design-render :config="cp"/>
+          </div>
+        </div>
+      </draggable>
+      <div style="color: #c0bebe;text-align: center; width: 90%; padding: 5px;">☝ 拖拽控件到表格内部</div>
+    </div>
+    <div v-else>
+      <div v-if="rowLayout">
+        <el-table size="medium" :header-cell-style="{background:'#f5f7fa', padding:'3px 0'}" :border="showBorder" :data="_value" style="width: 100%">
+          <el-table-column fixed type="index" label="序号" width="50"></el-table-column>
+          <el-table-column :min-width="getMinWidth(column)" v-for="(column, index) in _columns" :key="index" :prop="column.id" :label="column.title">
+            <template slot-scope="scope">
+              <form-design-render :class="{'valid-error': showError(column, _value[scope.$index][column.id])}" v-model="_value[scope.$index][column.id]" :mode="mode" :config="column"/>
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" min-width="90" label="操作">
+            <template slot-scope="scope">
+              <el-button size="mini" type="text" @click="copyData(scope.$index, scope.row)">复制</el-button>
+              <el-button size="mini" type="text" @click="delRow(scope.$index, scope.row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-button size="small" icon="el-icon-plus" @click="addRow">{{placeholder}}</el-button>
+      </div>
+      <div v-else>
+        <el-form :rules="rules" :model="row" :ref="`table-form-${i}`" class="table-column" v-for="(row, i) in _value" :key="i">
+          <div class="table-column-action">
+            <span>第 {{i + 1}} 项</span>
+            <i class="el-icon-close" @click="delRow(i, row)"></i>
+          </div>
+          <el-form-item v-for="(column, index) in _columns" :key="'column_' + index" :prop="column.id" :label="column.title">
+            <form-design-render v-model="row[column.id]" :mode="mode" :config="column"/>
+          </el-form-item>
+        </el-form>
+        <el-button size="small" icon="el-icon-plus" @click="addRow">{{placeholder}}</el-button>
+      </div>
+    </div>
+  </div>
+
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import {ValueType} from "../ComponentsConfigExport";
+import FormDesignRender from '@/views/admin/layout/form/FormDesignRender'
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "TableList",
+  components: {draggable, FormDesignRender},
+  props: {
+    value:{
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    placeholder: {
+      type: String,
+      default: '添加数据'
+    },
+    columns: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    showBorder: {
+      type: Boolean,
+      default: true
+    },
+    maxSize: {
+      type: Number,
+      default: 0
+    },
+    rowLayout: {
+      type: Boolean,
+      default: true
+    },
+  },
+  created() {
+    if (!Array.isArray(this.value)){
+      this._value = []
+    }
+  },
+  computed: {
+    rules(){
+      const rules = {}
+      this.columns.forEach(col => {
+        if (col.props.required){
+          rules[col.id] = [{
+            type: col.valueType === 'Array' ? 'array':undefined,
+            required: true,
+            message: `请填写${col.title}`, trigger: 'blur'
+          }]
+        }
+      })
+      return rules
+    },
+    _columns: {
+      get() {
+        return this.columns;
+      },
+      set(val) {
+        this.columns = val;
+      }
+    },
+    selectFormItem: {
+      get() {
+        return this.$store.state.selectFormItem
+      },
+      set(val) {
+        this.$store.state.selectFormItem = val
+      },
+    }
+  },
+  data() {
+    return {
+      select: null,
+      drag: false,
+      ValueType
+    }
+  },
+  methods: {
+    getMinWidth(col){
+      switch (col.name){
+        case 'DateTime': return '250px'
+        case 'DateTimeRange': return '280px'
+        case 'MultipleSelect': return '200px'
+        default: return '150px'
+      }
+    },
+    showError(col, val){
+      if (col.props.required){
+        switch (col.valueType){
+          case ValueType.dept:
+          case ValueType.user:
+          case ValueType.dateRange:
+          case ValueType.array: return !(Array.isArray(val) && val.length > 0)
+          default: return !this.$isNotEmpty(val)
+        }
+      }
+      return false
+    },
+    copyData(i, row){
+      this._value.push(this.$deepCopy(row))
+    },
+    delRow(i, row){
+      this._value.splice(i, 1)
+    },
+    addRow(){
+      if (this.maxSize > 0 && this._value.length >= this.maxSize){
+        this.$message.warning(`最多只能添加${this.maxSize}行`)
+      }else {
+        let row = {}
+        this.columns.forEach(col => this.$set(row, col.id, undefined))
+        this._value.push(row)
+        this.$set(this, '_value', this._value)
+      }
+    },
+    delItem(id) {
+      this._columns.splice(id, 1)
+    },
+    selectItem(cp) {
+      this.selectFormItem = cp
+    },
+    getSelectedClass(cp) {
+      return this.selectFormItem && this.selectFormItem.id === cp.id ? 'border-left: 4px solid #f56c6c' : ''
+    },
+    validate(call){
+      if (this.rowLayout){
+        let result = true
+        for (let i = 0; i < this.columns.length; i++) {
+          if (this.columns[i].props.required){
+            for (let j = 0; j < this._value.length; j++) {
+              result = !this.showError(this.columns[i], this._value[j][this.columns[i].id])
+              if (!result){
+                call(false)
+                return
+              }
+            }
+          }
+        }
+        call(result)
+      }else {
+        let success = 0
+        this._value.forEach((v, i) => {
+          let formRef = this.$refs[`table-form-${i}`]
+          if (formRef && Array.isArray(formRef) && formRef.length > 0){
+            formRef[0].validate(valid => {
+              if (valid){
+                success ++;
+              }
+            })
+          }
+        })
+        if (success === this._value.length){
+          call(true)
+        }else {
+          call(false)
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+/deep/ .valid-error {
+  .el-input__inner{
+    border-color: #F56C6C;
+  }
+}
+
+.choose {
+  border: 1px dashed @theme-primary !important;
+}
+
+.table-column {
+  padding: 5px;
+  margin-bottom: 10px;
+  border-left: 3px solid #409eff;
+  border-radius: 5px;
+  background: #fafafa;
+  /deep/ .el-form-item{
+    margin-bottom: 0;
+    .el-form-item__label{
+      height: 25px;
+    }
+  }
+  .table-column-action{
+    float: right;
+    span{
+      color: #afafaf;
+      margin-right: 10px;
+      font-size: 13px;
+    }
+    i{
+      color: #afafaf;
+      padding: 5px;
+      font-size: large;
+      cursor: pointer;
+      &:hover{
+        color: #666666;
+      }
+    }
+  }
+}
+
+.l-drag-from {
+  min-height: 50px;
+  background-color: rgb(245, 246, 246);
+
+  .l-form-item, li {
+    cursor: grab;
+    background: #ffffff;
+    padding: 10px;
+    border: 1px solid #ebecee;
+    margin: 5px 0;
+  }
+}
+
+.l-form-header {
+  font-size: small;
+  color: #818181;
+  text-align: left;
+  position: relative;
+  background-color: #fff;
+
+  p {
+    position: relative;
+    margin: 0 0 10px 0;
+
+    span {
+      position: absolute;
+      left: -8px;
+      top: 3px;
+      color: rgb(217, 0, 19);
+    }
+  }
+
+  .l-option {
+    position: absolute;
+    top: -10px;
+    right: -10px;
+
+    i {
+      font-size: large;
+      cursor: pointer;
+      color: #8c8c8c;
+      padding: 5px;
+
+      &:hover {
+        color: @theme-primary;
+      }
+    }
+  }
+}
+
+</style>

+ 43 - 0
src/views/common/form/components/TextInput.vue

@@ -0,0 +1,43 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-input size="medium" disabled :placeholder="placeholder" />
+    </div>
+    <div v-else>
+      <el-input v-if="!readerMode" size="medium" clearable v-model="_value" :placeholder="placeholder" />
+      <div v-else v-text="_value"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "TextInput",
+  components: {},
+  props: {
+    value: {
+      type: String,
+      default: null
+    },
+    placeholder: {
+      type: String,
+      default: '请输入内容'
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 54 - 0
src/views/common/form/components/TextareaInput.vue

@@ -0,0 +1,54 @@
+<template>
+  <div>
+    <div v-if="mode === 'DESIGN'">
+      <el-input size="medium" disabled :placeholder="placeholder" show-word-limit :rows="2" type="textarea" />
+    </div>
+    <div v-else>
+      <el-input v-if="!readerMode" size="medium" v-model="_value" clearable :maxlength="255" :placeholder="placeholder" show-word-limit :rows="3" type="textarea" />
+      <div v-else v-text="_value"></div>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+
+export default {
+  mixins: [componentMinxins],
+  name: "TextareaInput",
+  components: {},
+  props:{
+    value: {
+      type: String,
+      default: null
+    },
+    placeholder:{
+      type: String,
+      default: '请输入内容'
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed: {
+    _value: {
+      get() {
+        return this.value;
+      },
+      set(val) {
+        this.$emit("input", val);
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 77 - 0
src/views/common/form/components/UserPicker.vue

@@ -0,0 +1,77 @@
+<template>
+  <div style="max-width: 350px">
+    <div v-if="mode === 'DESIGN'">
+      <el-button disabled icon="el-icon-user" type="primary" size="mini" round>选择人员</el-button>
+      <span class="placeholder"> {{placeholder}}</span>
+    </div>
+    <div v-else>
+      <template v-if="!readerMode">
+        <el-button icon="el-icon-user" type="primary" size="mini" round @click="$refs.orgPicker.show()">选择人员</el-button>
+        <org-picker type="user" :multiple="multiple" ref="orgPicker" :selected="_value" @ok="selected"/>
+        <span class="placeholder"> {{placeholder}}</span>
+        <div style="margin-top: 5px">
+          <el-tag v-for="(dept, i) in _value" :key="i" size="mini" style="margin: 5px" closable  @close="delDept(i)">{{dept.name}}</el-tag>
+        </div>
+      </template>
+      <template v-else>
+        <div>
+          <el-tag size="mini" style="margin: 5px" v-for="(dept, i) in _value" :key="i">{{dept.name}}</el-tag>
+        </div>
+      </template>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import componentMinxins from '../ComponentMinxins'
+import OrgPicker from "@/components/common/OrgPicker";
+
+export default {
+  mixins: [componentMinxins],
+  name: "DeptPicker",
+  components: {OrgPicker},
+  props: {
+    value:{
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    placeholder: {
+      type: String,
+      default: '请选择人员'
+    },
+    multiple:{
+      type: Boolean,
+      default: false
+    },
+    readerMode: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      showOrgSelect: false
+    }
+  },
+  methods: {
+    selected(values){
+      this.showOrgSelect = false
+      this._value = values
+    },
+    delDept(i){
+      this._value.splice(i, 1)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.placeholder{
+  margin-left: 10px;
+  color: #adabab;
+  font-size: smaller;
+}
+</style>

+ 37 - 0
src/views/common/form/config/AmountInputConfig.vue

@@ -0,0 +1,37 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+    <el-form-item label="保留小数">
+      <el-input-number controls-position="right" :precision="0" :max="3" :min="0" size="small" v-model="value.precision"  placeholder="小数位数"/>
+       位
+    </el-form-item>
+    <el-form-item label="展示大写">
+      <el-switch v-model="value.showChinese"></el-switch>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "AmountInputConfig",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 38 - 0
src/views/common/form/config/DateTimeConfig.vue

@@ -0,0 +1,38 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置日期提示"/>
+    </el-form-item>
+    <el-form-item label="日期格式">
+      <el-select size="small" v-model="value.format">
+        <el-option value="yyyy" label="年"></el-option>
+        <el-option value="yyyy-MM" label="年-月"></el-option>
+        <el-option value="yyyy-MM-dd" label="年-月-日"></el-option>
+        <el-option value="yyyy-MM-dd HH:mm" label="年-月-日 时:分"></el-option>
+      </el-select>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "DateTime",
+  components: {},
+  props: {
+    value: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 42 - 0
src/views/common/form/config/DateTimeRangeConfig.vue

@@ -0,0 +1,42 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder[0]" placeholder="开始日期提示"/>
+      <el-input size="small" v-model="value.placeholder[1]" placeholder="结束日期提示"/>
+    </el-form-item>
+    <el-form-item label="日期格式">
+      <el-select size="small" v-model="value.format">
+        <el-option value="yyyy" label="年"></el-option>
+        <el-option value="yyyy-MM" label="年-月"></el-option>
+        <el-option value="yyyy-MM-dd" label="年-月-日"></el-option>
+        <el-option value="yyyy-MM-dd HH:mm" label="年-月-日 时:分"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="展示时长">
+      <el-switch v-model="value.showLength"></el-switch>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "DateTimeRangeConfig",
+  components: {},
+  props: {
+    value: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 33 - 0
src/views/common/form/config/DescriptionConfig.vue

@@ -0,0 +1,33 @@
+<template>
+  <div>
+    <el-form-item label="提示内容">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示内容"/>
+    </el-form-item>
+    <el-form-item label="文字颜色">
+      <el-color-picker v-model="value.color" size="medium"></el-color-picker>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "Description",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 48 - 0
src/views/common/form/config/FileUploadConfig.vue

@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+    <el-form-item label="数量限制">
+      <tip slot="label" content="限制最大上传图片数量(为0则不限制)">数量限制</tip>
+      <el-input-number class="max-fill" controls-position="right" :precision="0" size="small" v-model="value.maxNumber" placeholder="最多上传几张图片"/>
+    </el-form-item>
+    <el-form-item label="大小限制">
+      <tip slot="label" content="限制单个文件最大大小-MB(为0则不限制)">大小限制</tip>
+      <el-input-number class="max-fill" controls-position="right" :precision="1" size="small" v-model="value.maxSize" placeholder="单个文件最大大小"/>
+    </el-form-item>
+    <el-form-item label="类型限制">
+      <tip slot="label" content="限制上传文件的后缀类型">类型限制</tip>
+      <el-select size="small" style="width: 100%;" v-model="value.fileTypes" multiple
+                 filterable allow-create default-first-option clearable placeholder="允许上传文件的后缀格式,可设置多种"/>
+    </el-form-item>
+    <el-form-item label="不可下载">
+      <el-switch v-model="value.onlyRead"></el-switch>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "FileUploadConfig",
+  components: {},
+  props: {
+    value: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+/deep/ .el-form-item__label{
+  padding: 0 12px 0 0;
+}
+</style>

+ 43 - 0
src/views/common/form/config/ImageUploadConfig.vue

@@ -0,0 +1,43 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+    <el-form-item label="数量限制">
+      <tip slot="label" content="限制最大上传图片数量(为0则不限制)">数量限制</tip>
+      <el-input-number class="max-fill" controls-position="right" :precision="0" size="small" v-model="value.maxNumber" placeholder="最多上传几张图片"/>
+    </el-form-item>
+    <el-form-item label="大小限制">
+      <tip slot="label" content="限制单个图片最大大小-MB(为0则不限制)">大小限制</tip>
+      <el-input-number class="max-fill" controls-position="right" :precision="1" size="small" v-model="value.maxSize" placeholder="单个文件最大大小"/>
+    </el-form-item>
+    <el-form-item label="图片压缩">
+      <el-switch v-model="value.enableZip"></el-switch>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ImageUploadConfig",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+/deep/ .el-form-item__label{
+  padding: 0 12px 0 0;
+}
+</style>

+ 18 - 0
src/views/common/form/config/LocationConfig.vue

@@ -0,0 +1,18 @@
+<template>
+
+</template>
+
+<script>
+export default {
+  name: "Location",
+  components: {},
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 18 - 0
src/views/common/form/config/MoneyInputConfig.vue

@@ -0,0 +1,18 @@
+<template>
+
+</template>
+
+<script>
+export default {
+  name: "MoneyInput",
+  components: {},
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 30 - 0
src/views/common/form/config/NumberInputConfig.vue

@@ -0,0 +1,30 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "NumberInput",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 34 - 0
src/views/common/form/config/OrgPickerConfig.vue

@@ -0,0 +1,34 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+    <el-form-item label="是否多选">
+      <el-switch v-model="value.multiple"></el-switch>
+    </el-form-item>
+  </div>
+
+</template>
+
+<script>
+export default {
+  name: "OrgPicker",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 97 - 0
src/views/common/form/config/SelectInputConfig.vue

@@ -0,0 +1,97 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+    <el-form label-position="top">
+      <el-form-item label="选项设置" class="options">
+        <div slot="label" class="option-item-label">
+          <span>选项设置</span>
+          <el-button icon="el-icon-plus" type="text" size="mini"
+                     @click="value.options.push('新选项')">新增选项</el-button>
+        </div>
+        <draggable :list="value.options" group="option" handler=".el-icon-rank" :options="dragOption">
+          <div v-for="(op, index) in value.options" :key="index" class="option-item">
+            <i class="el-icon-rank"></i>
+            <el-input v-model="value.options[index]" size="medium" placeholder="请设置选项值" clearable>
+              <el-button icon="el-icon-delete" slot="append" type="danger" size="medium"
+                         @click="value.options.splice(index, 1)"></el-button>
+            </el-input>
+          </div>
+        </draggable>
+
+      </el-form-item>
+    </el-form>
+    <el-form-item label="选项展开">
+      <el-switch v-model="value.expanding"></el-switch>
+    </el-form-item>
+
+  </div>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+
+export default {
+  name: "SelectInputConfig",
+  components: {draggable},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      dragOption:{
+        animation: 300,
+        sort: true
+      }
+    }
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+/deep/ .options{
+  .el-form-item__label{
+    display: block;
+    width: 100%;
+    text-align: left;
+  }
+  .el-icon-rank{
+    padding-right: 5px;
+    cursor: move;
+  }
+  .option-item{
+    .el-input{
+      width: 250px;
+      float:right;
+    }
+  }
+}
+
+.option-item-label{
+  height: 30px;
+  line-height: 30px;
+  button{
+    float:right;
+  }
+}
+
+/*/deep/ .el-form-item {
+  margin-bottom: 10px;
+
+  .el-form-item__label {
+    padding: 0;
+  }
+  .options{
+    .el-icon-rank{
+      cursor: move;
+    }
+  }
+}*/
+</style>

+ 52 - 0
src/views/common/form/config/TableListConfig.vue

@@ -0,0 +1,52 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="提醒添加记录的提示"/>
+    </el-form-item>
+    <el-form-item label="最大行数">
+      <tip slot="label" content="允许添加多少条记录(为0则不限制)">最大行数</tip>
+      <el-input-number controls-position="right" :precision="0" :max="100" :min="0" size="small" v-model="value.maxSize"  placeholder="限制条数"/>
+    </el-form-item>
+    <el-form-item label="布局方式">
+      <el-radio name="layout" :label="true" v-model="value.rowLayout">按表格</el-radio>
+      <el-radio name="layout" :label="false" v-model="value.rowLayout">按表单</el-radio>
+    </el-form-item>
+    <el-form-item label="展示合计">
+      <el-switch v-model="value.showSummary"></el-switch>
+      <el-select v-if="value.showSummary" style="width: 100%;" size="small" v-model="value.summaryColumns" multiple clearable placeholder="请选择合计项">
+        <el-option :label="column.title" :value="column.id" v-for="column in columns" :key="column.id"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="展示边框">
+      <el-switch v-model="value.showBorder"></el-switch>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "TableListConfig",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  computed:{
+    columns(){
+      return this.value.columns.filter(c => c.valueType === 'Number')
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 30 - 0
src/views/common/form/config/TextInputConfig.vue

@@ -0,0 +1,30 @@
+<template>
+  <el-form-item label="提示文字">
+    <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+  </el-form-item>
+</template>
+
+<script>
+export default {
+  name: "TextInput",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+
+    }
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 30 - 0
src/views/common/form/config/TextareaInputConfig.vue

@@ -0,0 +1,30 @@
+<template>
+  <div>
+    <el-form-item label="提示文字">
+      <el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "TextareaInput",
+  components: {},
+  props:{
+    value:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 48 - 0
src/views/common/process/OrgItems.vue

@@ -0,0 +1,48 @@
+<template>
+  <div style="margin-top: 10px">
+    <el-tag class="org-item" :type="org.type === 'dept'?'':'info'"
+            v-for="(org, index) in _value" :key="index + '_org'"
+            closable size="mini" @close="removeOrgItem(index)">
+      {{ org.name }}
+    </el-tag>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "OrgItems",
+  components: {},
+  props: {
+    value: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    }
+  },
+  computed: {
+    _value: {
+      get() {
+        return this.value;
+      },
+      set(val) {
+        this.$emit("input", val);
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    removeOrgItem(index) {
+      this._value.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.org-item{
+  margin: 5px;
+}
+</style>

+ 299 - 0
src/views/common/process/config/ApprovalNodeConfig.vue

@@ -0,0 +1,299 @@
+<template>
+  <div>
+    <el-form label-position="top" label-width="90px">
+      <el-form-item label="⚙ 选择审批对象" prop="text" class="user-type">
+        <el-radio-group v-model="nodeProps.assignedType">
+          <el-radio v-for="t in approvalTypes" :label="t.type" :key="t.type">{{ t.name }}</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.assignedType === 'ASSIGN_USER'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser" round>选择人员</el-button>
+          <org-items v-model="nodeProps.assignedUser"/>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'SELF_SELECT'">
+          <el-radio-group size="mini" v-model="nodeProps.selfSelect.multiple">
+            <el-radio-button :label="false">自选一个人</el-radio-button>
+            <el-radio-button :label="true">自选多个人</el-radio-button>
+          </el-radio-group>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'LEADER_TOP'">
+          <el-divider/>
+          <el-form-item label="审批终点" prop="text" class="approve-end">
+            <el-radio-group v-model="nodeProps.leaderTop.endCondition">
+              <el-radio label="TOP">直到最上层主管</el-radio>
+              <el-radio label="LEAVE">不超过发起人的</el-radio>
+            </el-radio-group>
+            <div class="approve-end-leave" v-if="nodeProps.leaderTop.endCondition === 'LEAVE'">
+              <span>第 </span>
+              <el-input-number :min="1" :max="20" :step="1" size="mini" v-model="nodeProps.leaderTop.level"/>
+              <span> 级主管</span>
+            </div>
+          </el-form-item>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'LEADER'">
+          <el-divider/>
+          <el-form-item label="指定主管" prop="text">
+            <span>发起人的第 </span>
+            <el-input-number :min="1" :max="20" :step="1" size="mini"
+                             v-model="nodeProps.leader.level"></el-input-number>
+            <span> 级主管</span>
+            <div style="color: #409EFF; font-size: small;">👉 直接主管为 第 1 级主管</div>
+          </el-form-item>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'ROLE'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectRole" round>选择系统角色111</el-button>
+          <org-items v-model="nodeProps.role"/>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'FORM_USER'">
+          <el-form-item label="选择表单联系人项" prop="text" class="approve-end">
+            <el-select style="width: 80%;" size="small" v-model="nodeProps.formUser" placeholder="请选择包含联系人的表单项">
+              <el-option v-for="op in forms" :label="op.title" :value="op.id" :key="op.id"></el-option>
+            </el-select>
+          </el-form-item>
+        </div>
+        <div v-else>
+          <span class="item-desc">发起人自己作为审批人进行审批</span>
+        </div>
+
+      </el-form-item>
+
+      <el-divider></el-divider>
+      <el-form-item label="👤 审批人为空时" prop="text" class="line-mode">
+        <el-radio-group v-model="nodeProps.nobody.handler">
+          <el-radio label="TO_PASS">自动通过</el-radio>
+          <el-radio label="TO_REFUSE">自动驳回</el-radio>
+          <el-radio label="TO_ADMIN">转交审批管理员</el-radio>
+          <el-radio label="TO_USER">转交到指定人员</el-radio>
+        </el-radio-group>
+
+        <div style="margin-top: 10px" v-if="nodeProps.nobody.handler === 'TO_USER'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectNoSetUser" round>选择人员</el-button>
+          <org-items v-model="nodeProps.nobody.assignedUser"/>
+        </div>
+
+      </el-form-item>
+
+      <div v-if="showMode">
+        <el-divider/>
+        <el-form-item label="👩‍👦‍👦 多人审批时审批方式" prop="text" class="approve-mode">
+          <el-radio-group v-model="nodeProps.mode">
+            <el-radio label="NEXT">会签 (按选择顺序审批,每个人必须同意)</el-radio>
+            <el-radio label="AND">会签(可同时审批,每个人必须同意)</el-radio>
+            <el-radio label="OR">或签(有一人同意即可)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </div>
+
+      <el-divider>高级设置</el-divider>
+      <el-form-item label="✍ 审批同意时是否需要签字" prop="text">
+        <el-switch inactive-text="不用" active-text="需要" v-model="nodeProps.sign"></el-switch>
+        <el-tooltip class="item" effect="dark" content="如果全局设置了需要签字,则此处不生效" placement="top-start">
+          <i class="el-icon-question" style="margin-left: 10px; font-size: medium; color: #b0b0b1"></i>
+        </el-tooltip>
+      </el-form-item>
+      <el-form-item label="⏱ 审批期限(为 0 则不生效)" prop="timeLimit">
+        <el-input style="width: 180px;" placeholder="时长" size="small" type="number"
+                  v-model="nodeProps.timeLimit.timeout.value">
+          <el-select style="width: 75px;" v-model="nodeProps.timeLimit.timeout.unit" slot="append" placeholder="请选择">
+            <el-option label="天" value="D"></el-option>
+            <el-option label="小时" value="H"></el-option>
+          </el-select>
+        </el-input>
+      </el-form-item>
+      <el-form-item label="审批期限超时后执行" prop="level" v-if="nodeProps.timeLimit.timeout.value > 0">
+        <el-radio-group v-model="nodeProps.timeLimit.handler.type">
+          <el-radio label="PASS">自动通过</el-radio>
+          <el-radio label="REFUSE">自动驳回</el-radio>
+          <el-radio label="NOTIFY">发送提醒</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.timeLimit.handler.type === 'NOTIFY'">
+          <div style="color:#409EEF; font-size: small">默认提醒当前审批人</div>
+          <el-switch inactive-text="循环" active-text="一次" v-model="nodeProps.timeLimit.handler.notify.once"></el-switch>
+          <span style="margin-left: 20px" v-if="!nodeProps.timeLimit.handler.notify.once">
+							每隔
+							<el-input-number :min="0" :max="10000" :step="1" size="mini"
+                               v-model="nodeProps.timeLimit.handler.notify.hour"/>
+							小时提醒一次
+						</span>
+        </div>
+      </el-form-item>
+      <el-form-item label="🙅‍ 如果审批被驳回 👇">
+        <el-radio-group v-model="nodeProps.refuse.type">
+          <el-radio label="TO_END">直接结束流程</el-radio>
+          <el-radio label="TO_BEFORE">驳回到上级审批节点</el-radio>
+          <el-radio label="TO_NODE">驳回到指定节点</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.refuse.type === 'TO_NODE'">
+          <span>指定节点:</span>
+          <el-select style="margin-left: 10px; width: 150px;" placeholder="选择跳转步骤" size="small" v-model="nodeProps.refuse.target">
+            <el-option v-for="(node, i) in nodeOptions" :key="i" :label="node.name" :value="node.id"></el-option>
+          </el-select>
+        </div>
+
+      </el-form-item>
+    </el-form>
+    <org-picker :title="pickerTitle" multiple :type="orgPickerType" ref="orgPicker" :selected="orgPickerSelected"
+                @ok="selected"/>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from "../OrgItems";
+
+export default {
+  name: "ApprovalNodeConfig",
+  components: {OrgPicker, OrgItems},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      showOrgSelect: false,
+      orgPickerSelected: [],
+      orgPickerType: 'user',
+      approvalTypes: [
+        {name: '指定人员', type: 'ASSIGN_USER'},
+        {name: '发起人自选', type: 'SELF_SELECT'},
+        {name: '连续多级主管', type: 'LEADER_TOP'},
+        {name: '主管', type: 'LEADER'},
+        {name: '角色', type: 'ROLE'},
+        {name: '发起人自己', type: 'SELF'},
+        {name: '表单内联系人', type: 'FORM_USER'}
+      ]
+    }
+  },
+  computed: {
+    nodeProps() {
+      return this.$store.state.selectedNode.props
+    },
+    select() {
+      return this.config.assignedUser || []
+    },
+    forms() {
+      return this.$store.state.design.formItems.filter(f => {
+        return f.name === 'UserPicker'
+      })
+    },
+    pickerTitle() {
+      switch (this.orgPickerType) {
+        case 'user':
+          return '请选择人员';
+        case 'role':
+          return '请选择系统角色aaa';
+        default:
+          return null;
+      }
+    },
+    nodeOptions() {
+      let values = []
+      const excType = ['ROOT', 'EMPTY', "CONDITION", "CONDITIONS", "CONCURRENT", "CONCURRENTS"]
+      this.$store.state.nodeMap.forEach((v) => {
+        if (excType.indexOf(v.type) === -1) {
+          values.push({id: v.id, name: v.name})
+        }
+      })
+      return values
+    },
+    showMode() {
+      switch (this.nodeProps.assignedType) {
+        case "ASSIGN_USER":
+          return this.nodeProps.assignedUser.length > 0;
+        case "SELF_SELECT":
+          return this.nodeProps.selfSelect.multiple;
+        case "LEADER_TOP":
+          return this.nodeProps.formUser !== '';
+        case "FORM_USER":
+          return true;
+        case "ROLE":
+          return true;
+        default:
+          return false;
+      }
+    }
+  },
+  methods: {
+    selectUser() {
+      this.orgPickerSelected = this.select
+      this.orgPickerType = 'user'
+      this.$refs.orgPicker.show()
+    },
+    selectNoSetUser() {
+      this.orgPickerSelected = this.config.nobody.assignedUser
+      this.orgPickerType = 'user'
+      this.$refs.orgPicker.show()
+    },
+    selectRole() {
+      this.orgPickerSelected = this.select
+      this.orgPickerType = 'role'
+      this.$refs.orgPicker.show()
+      console.log(this.select)
+    },
+    selected(select) {
+      console.log(select)
+      this.orgPickerSelected.length = 0
+      select.forEach(val => this.orgPickerSelected.push(val))
+    },
+    removeOrgItem(index) {
+      this.select.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.user-type {
+  /deep/ .el-radio {
+    width: 110px;
+    margin-top: 10px;
+    margin-bottom: 20px;
+  }
+}
+
+/deep/ .line-mode {
+  .el-radio {
+    width: 150px;
+    margin: 5px;
+  }
+}
+
+/deep/ .el-form-item__label {
+  line-height: 25px;
+}
+
+/deep/ .approve-mode {
+  .el-radio {
+    float: left;
+    width: 100%;
+    display: block;
+    margin-top: 15px;
+  }
+}
+
+/deep/ .approve-end {
+  position: relative;
+
+  .el-radio-group {
+    width: 160px;
+  }
+
+  .el-radio {
+    margin-bottom: 5px;
+    width: 100%;
+  }
+
+  .approve-end-leave {
+    position: absolute;
+    bottom: -5px;
+    left: 150px;
+  }
+}
+
+/deep/ .el-divider--horizontal {
+  margin: 10px 0;
+}
+</style>

+ 69 - 0
src/views/common/process/config/CcNodeConfig.vue

@@ -0,0 +1,69 @@
+<template>
+  <div>
+    <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectOrg" round>选择抄送人</el-button>
+    <div class="option">
+      <el-checkbox label="允许发起人添加抄送人" v-model="config.shouldAdd"></el-checkbox>
+    </div>
+    <org-items v-model="select"/>
+    <org-picker multiple ref="orgPicker" :selected="select" @ok="selected"/>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from "../OrgItems";
+
+export default {
+  name: "CcNodeConfig.vue",
+  components: {OrgPicker, OrgItems},
+  props:{
+    config:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  computed:{
+    select: {
+      get(){
+        return this.config.assignedUser || []
+      },
+      set(val){
+        this.config.assignedUser = val
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    selectOrg() {
+      this.$refs.orgPicker.show()
+    },
+    selected(select) {
+      console.log(select)
+      this.select = Object.assign([], select)
+    },
+    removeOrgItem(index){
+      this.select.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.option{
+  color: #606266;
+  margin-top: 20px;
+  font-size: small;
+}
+
+.desc{
+  font-size: small;
+  color: #8c8c8c;
+}
+.org-item{
+  margin: 5px;
+}
+</style>

+ 281 - 0
src/views/common/process/config/ConditionGroupItemConfig.vue

@@ -0,0 +1,281 @@
+<template>
+  <div>
+    <div v-for="(group, index) in selectedNode.props.groups" :key="index + '_g'" class="group">
+      <div class="group-header">
+        <span class="group-name">条件组 {{ groupNames[index] }}</span>
+        <div class="group-cp">
+          <span>组内条件关系:</span>
+          <el-switch v-model="group.groupType" active-color="#409EFF"
+                     inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
+                     active-text="且" inactive-text="或"/>
+        </div>
+        <div class="group-operation">
+          <el-popover placement="bottom" title="选择审批条件" width="300" trigger="click">
+            <!-- <div>以下条件将决定具体的审批流程</div>-->
+            <el-checkbox-group v-model="group.cids" value-key="id">
+              <el-checkbox :label="condition.id" v-for="(condition, cindex) in conditionList" :key="condition.id" @change="conditionChange(cindex, group)">
+                {{ condition.title }}
+              </el-checkbox>
+            </el-checkbox-group>
+            <i class="el-icon-plus" slot="reference"></i>
+          </el-popover>
+          <i class="el-icon-delete" @click="delGroup(index)"></i>
+        </div>
+      </div>
+      <div class="group-content">
+        <p v-if="group.conditions.length === 0">点击右上角 + 为本条件组添加条件 ☝</p>
+        <div v-else>
+          <el-form ref="condition-form" label-width="100px">
+            <!--构建表达式-->
+            <el-form-item  v-for="(condition, cindex) in group.conditions" :key="condition.id + '_' + cindex" >
+              <ellipsis slot="label" hover-tip :content="condition.title"/>
+               <span v-if="condition.valueType === ValueType.string">
+                <el-select size="small" placeholder="判断符" style="width: 120px;" v-model="condition.compare" @change="condition.value = []">
+                  <el-option label="等于" value="="></el-option>
+                  <el-option label="包含在" value="IN"></el-option>
+                </el-select>
+                 <span v-if="isSelect(condition.id)" style="margin-left: 10px">
+                   <el-select v-if="condition.compare === 'IN'" style="width: 280px;" clearable multiple size="small" v-model="condition.value" placeholder="选择值">
+                     <el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option" :value="option"></el-option>
+                   </el-select>
+                   <el-select v-else style="width: 280px;" clearable size="small" v-model="condition.value[0]" placeholder="选择值">
+                     <el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option" :value="option"></el-option>
+                   </el-select>
+                 </span>
+                 <span v-else style="margin-left: 10px">
+                   <el-input v-if="condition.compare === '='" style="width: 280px;" placeholder="输入比较值" size="small" v-model="condition.value[0]"/>
+                   <el-select v-else style="width: 280px;" multiple clearable filterable allow-create size="small" v-model="condition.value" placeholder="输入可能包含的值"></el-select>
+                 </span>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.number">
+                <el-select size="small" placeholder="判断符" style="width: 120px;" v-model="condition.compare">
+                  <el-option :label="exp.label" :value="exp.value" :key="exp.value" v-for="exp in explains"></el-option>
+                </el-select>
+                <span style="margin-left: 10px">
+                  <el-input style="width: 280px;" v-if="conditionValType(condition.compare) === 0" size="small" placeholder="输入比较值" type="number" v-model="condition.value[0]"/>
+                  <el-select style="width: 280px;" multiple filterable allow-create v-else-if="conditionValType(condition.compare) === 1" size="small" v-model="condition.value" placeholder="输入可能包含的值"></el-select>
+                  <span v-else>
+                    <el-input style="width: 130px;" size="small" type="number" placeholder="输入比较值" v-model="condition.value[0]"/>
+                    <span> ~
+                      <el-input size="small" style="width: 130px;" type="number" placeholder="输入比较值" v-model="condition.value[1]"/>
+                    </span>
+                  </span>
+                </span>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.user">
+                <span class="item-desc" style="margin-right: 20px">属于某部门 / 为某些人其中之一</span>
+                <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser(condition.value, 'user')" round>选择人员/部门</el-button>
+                <org-items v-model="condition.value"/>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.dept">
+                <span class="item-desc" style="margin-right: 20px">为某部门 / 某部门下的部门</span>
+                <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser(condition.value, 'dept')" round>选择部门</el-button>
+                <org-items v-model="condition.value"/>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.date"></span>
+              <i class="el-icon-delete" @click="rmSubCondition(group, cindex)"></i>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+    </div>
+    <org-picker :type="orgType" multiple ref="orgPicker" :selected="users" @ok="selected"></org-picker>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from '../OrgItems'
+import {ValueType} from '@/views/common/form/ComponentsConfigExport'
+
+export default {
+  name: "ConditionGroupItemConfig",
+  components: {OrgPicker, OrgItems},
+  data() {
+    return {
+      ValueType,
+      users: [],
+      orgType: 'user',
+      showOrgSelect: false,
+      //groupConditions: [],
+      groupNames: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
+      supportTypes:[ValueType.number, ValueType.string, ValueType.date, ValueType.dept, ValueType.user],
+      explains:[
+        {label: '等于', value:'='},
+        {label: '大于', value:'>'},
+        {label: '大于等于', value:'>='},
+        {label: '小于', value:'<'},
+        {label: '小于等于', value:'<='},
+        {label: '包含在', value:'IN'},
+        {label: 'x < 值 < x', value:'B'},
+        {label: 'x ≤ 值 < x', value:'AB'},
+        {label: 'x < 值 ≤ x', value:'BA'},
+        {label: 'x ≤ 值 ≤ x', value:'ABA'},
+      ]
+    }
+  },
+  computed: {
+    selectedNode() {
+      return this.$store.state.selectedNode
+    },
+    select() {
+      return this.selectedNode.props.assignedUser || []
+    },
+    formItems(){
+      return this.$store.state.design.formItems
+    },
+    formMap(){
+      const map = new Map();
+      this.formItems.forEach(item => this.itemToMap(map, item))
+      return map
+    },
+    conditionList() {
+      //构造可用条件选项
+      const conditionItems = []
+      this.formItems.forEach(item => this.filterCondition(item, conditionItems))
+      if (conditionItems.length === 0 || conditionItems[0].id !== 'root'){
+        conditionItems.unshift({id: 'root', title: '发起人', valueType: 'User'})
+      }
+      return conditionItems
+    }
+  },
+  methods: {
+    itemToMap(map, item){
+      map.set(item.id, item)
+      if (item.name === 'SpanLayout'){
+        item.props.items.forEach(sub => this.itemToMap(map, sub))
+      }
+    },
+    isSelect(formId){
+      let form = this.formMap.get(formId)
+      if (form && (form.name === 'SelectInput' || form.name === 'MultipleSelect')){
+        return true
+      }
+      return false
+    },
+    getOptions(formId){
+      return this.formMap.get(formId).props.options || []
+    },
+    conditionValType(type){
+      switch (type){
+        case '=':
+        case '>':
+        case '>=':
+        case '<':
+        case '<=': return 0;
+        case 'IN': return 1;
+        default: return 2;
+      }
+    },
+    selectUser(value, orgType) {
+      this.orgType = orgType
+      this.users = value
+      this.$refs.orgPicker.show()
+    },
+    filterCondition(item, list){
+      if (item.name === 'SpanLayout'){
+        item.props.items.forEach(sub => this.filterCondition(sub, list))
+      }else if (this.supportTypes.indexOf(item.valueType) > -1 && item.props.required){
+        list.push({title: item.title, id: item.id, valueType: item.valueType})
+      }
+    },
+    selected(select) {
+      console.log(select)
+      this.users.length = 0
+      select.forEach(u => this.users.push(u))
+    },
+    delGroup(index) {
+      this.selectedNode.props.groups.splice(index, 1)
+    },
+    rmSubCondition(group, index){
+      group.cids.splice(index, 1)
+      group.conditions.splice(index, 1)
+    },
+    conditionChange(index, group) {
+      //判断新增的
+      group.cids.forEach(cid => {
+        if (0 > group.conditions.findIndex(cd => cd.id === cid)){
+          //新增条件
+          let condition = {...this.conditionList[index]}
+          console.log(condition, this.conditionList, index)
+          condition.compare = '';
+          condition.value = []
+          group.conditions.push(condition)
+        }
+      })
+      for (let i = 0; i < group.conditions.length; i++) {
+        //去除没有选中的
+        if (group.cids.indexOf(group.conditions[i].id) < 0){
+          group.conditions.splice(i, 1)
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.group {
+  margin-bottom: 20px;
+  color: #5e5e5e;
+  overflow: hidden;
+  border-radius: 6px;
+  border: 1px solid #e3e3e3;
+
+  .group-header {
+    padding: 5px 10px;
+    background: #e3e3e3;
+    position: relative;
+
+    div {
+      display: inline-block;
+    }
+
+    .group-name {
+      font-size: small;
+    }
+
+    .group-cp {
+      font-size: small;
+      position: absolute;
+      left: 100px;
+      display: flex;
+      top: 5px;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .group-operation {
+      position: absolute;
+      right: 10px;
+
+      i {
+        padding: 0 10px;
+
+        &:hover {
+          cursor: pointer;
+        }
+      }
+    }
+  }
+
+  .group-content{
+    padding: 10px 5px;
+    p{
+      text-align: center;
+      font-size: small;
+    }
+    .el-icon-delete{
+      position: absolute;
+      cursor: pointer;
+      top: 12px;
+      right: 0;
+    }
+  }
+
+  .condition-title{
+    display: block;
+    width: 100px;
+  }
+}
+</style>

+ 135 - 0
src/views/common/process/config/ConditionNodeConfig.vue

@@ -0,0 +1,135 @@
+<template>
+  <div>
+    <el-form inline label-width="100px">
+      <el-form-item label="调整优先级" prop="level">
+        <el-popover placement="right" title="拖拽条件调整优先级顺序" width="250" trigger="click">
+          <draggable style="width: 100%; min-height:25px" :list="prioritySortList" group="from" :options="sortOption">
+            <div :class="{'drag-no-choose': true, 'drag-hover': cd.id === selectedNode.id}"
+                 v-for="(cd, index) in prioritySortList">
+              <ellipsis style="width: 160px;" hover-tip :content="cd.name"/>
+              <div>优先级 {{ index + 1 }}</div>
+            </div>
+          </draggable>
+          <el-button icon="el-icon-sort" size="small" slot="reference">第{{ nowNodeLeave + 1 }}级</el-button>
+        </el-popover>
+      </el-form-item>
+      <el-form-item label="条件组关系" label-width="150px">
+        <el-switch v-model="config.groupsType" active-color="#409EFF"
+                   inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
+                   active-text="且" inactive-text="或">
+        </el-switch>
+      </el-form-item>
+      <el-form-item label="条件组表达式">
+        <el-input size="mini" v-model="config.expression" placeholder="输入条件组关系表达式  &为与,|为或"/>
+        <span class="item-desc">使用表达式构建复杂逻辑,例如: (A & B) | C</span>
+      </el-form-item>
+    </el-form>
+    <div>
+      <el-button type="primary" size="mini" icon="el-icon-plus" style="margin: 0 15px 15px 0" round @click="addConditionGroup">
+        添加条件组
+      </el-button>
+      <span class="item-desc">只有必填选项才能作为审批条件</span>
+    </div>
+    <group-item/>
+  </div>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import GroupItem from "./ConditionGroupItemConfig.vue"
+
+export default {
+  name: "ConditionNodeConfig",
+  components: {draggable, GroupItem},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  computed: {
+    selectedNode() {
+      return this.$store.state.selectedNode
+    },
+    select() {
+      return this.config.assignedUser || []
+    },
+    nowNodeLeave() {
+      return this.prioritySortList.indexOf(this.selectedNode)
+    },
+    //条件节点
+    prioritySortList() {
+      let node = this.$store.state.nodeMap.get(this.selectedNode.parentId)
+      console.log(this.selectedNode.id, node)
+      if (node) {
+        return node.branchs || []
+      }
+      return []
+    }
+  },
+  data() {
+    return {
+      sortOption: {
+        animation: 300,
+        chosenClass: 'choose',
+        scroll: true,
+        sort: true
+      }
+    }
+  },
+  methods: {
+    addConditionGroup() {
+      this.config.groups.push({
+        cids:[],
+        groupType: "OR",
+        conditions:[]
+      })
+    },
+    selectUser() {
+      this.showOrgSelect = true
+    },
+    selected(select) {
+      console.log(select)
+      this.showOrgSelect = false
+      select.forEach(val => this.select.push(val))
+    },
+    removeOrgItem(index) {
+      this.select.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.choose {
+  border-radius: 5px;
+  margin-top: 2px;
+  background: #f4f4f4;
+  border: 1px dashed #1890FF !important;
+}
+
+.drag-hover {
+  color: #1890FF
+}
+
+.drag-no-choose {
+  cursor: move;
+  background: #f8f8f8;
+  border-radius: 5px;
+  margin: 5px 0;
+  height: 25px;
+  line-height: 25px;
+  padding: 5px 10px;
+  border: 1px solid #ffffff;
+  div{
+    display: inline-block;
+    font-size: small !important;
+  }
+
+  div:nth-child(2) {
+    float: right !important;
+  }
+}
+</style>

+ 48 - 0
src/views/common/process/config/DelayNodeConfig.vue

@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <div style="margin-bottom: 20px">
+      <p class="item-desc">延时方式</p>
+      <el-radio-group v-model="config.type" size="small">
+        <el-radio-button label="FIXED">固定时长</el-radio-button>
+        <el-radio-button label="AUTO">自动计算</el-radio-button>
+      </el-radio-group>
+    </div>
+    <div v-if="config.type === 'FIXED'">
+      <el-input style="width: 180px;" placeholder="时间单位" size="small" type="number" v-model="config.time">
+        <el-select style="width: 75px;" v-model="config.unit" slot="append" placeholder="请选择">
+          <el-option label="天" value="D"></el-option>
+          <el-option label="小时" value="H"></el-option>
+          <el-option label="分钟" value="M"></el-option>
+        </el-select>
+      </el-input>
+      <span class="item-desc"> 后进入下一步</span>
+    </div>
+    <div class="item-desc" v-else>
+      <el-time-picker value-format="HH:mm:ss" style="width: 150px;" size="small" v-model="config.dateTime" placeholder="任意时间点"></el-time-picker>
+      <span class="item-desc"> 后进入下一步</span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "DelayNodeConfig",
+  components: {},
+  props:{
+    config:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 133 - 0
src/views/common/process/config/FormAuthorityConfig.vue

@@ -0,0 +1,133 @@
+<template>
+  <div>
+    <el-table :header-cell-style="{background:'#f5f6f6'}" :data="formPerms" border style="width: 100%">
+      <el-table-column prop="title" show-overflow-tooltip label="表单字段">
+        <template slot-scope="scope">
+           <span v-if="scope.row.required" style="color: #c75450"> * </span>
+          <span>{{ scope.row.title }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="readOnly" label="只读" width="80">
+        <template slot="header" slot-scope="scope">
+          <el-radio label="R" v-model="permSelect" @change="allSelect('R')">只读</el-radio>
+        </template>
+        <template slot-scope="scope">
+          <el-radio v-model="scope.row.perm" label="R" :name="scope.row.id"></el-radio>
+        </template>
+      </el-table-column>
+      <el-table-column prop="editable" label="可编辑" width="90" v-if="nowNode.type !== 'CC'">
+        <template slot="header" slot-scope="scope">
+          <el-radio label="E" v-model="permSelect" @change="allSelect('E')">可编辑</el-radio>
+        </template>
+        <template slot-scope="scope">
+          <el-radio v-model="scope.row.perm" label="E" :name="scope.row.id"></el-radio>
+        </template>
+      </el-table-column>
+      <el-table-column prop="hide" label="隐藏" width="80">
+        <template slot="header" slot-scope="scope">
+          <el-radio label="H" v-model="permSelect" @change="allSelect('H')">隐藏</el-radio>
+        </template>
+        <template slot-scope="scope">
+          <el-radio v-model="scope.row.perm" label="H" :name="scope.row.id"></el-radio>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "FormAuthorityConfig",
+  components: {},
+  data() {
+    return {
+      tableData: [],
+      isIndeterminate: false,
+      permSelect: '',
+      checkStatus: {
+        readOnly: true,
+        editable: false,
+        hide: false
+      }
+    }
+  },
+  created() {
+    //备份
+    let oldPermMap = this.formPerms.toMap('id')
+    //重新清空,按顺序加载权限
+    this.formPerms.length = 0;
+    this.formPermsLoad(oldPermMap, this.formData)
+  },
+  computed: {
+    nowNode(){
+      return this.$store.state.selectedNode
+    },
+    formData() {
+      return this.$store.state.design.formItems
+    },
+    formPerms() {
+      return this.$store.state.selectedNode.props.formPerms
+    }
+  },
+  methods: {
+    allSelect(type) {
+      this.permSelect = type
+      this.formPerms.forEach(f => f.perm = type)
+    },
+    formPermsLoad(oldPermMap, forms) {
+      forms.forEach(form => {
+        if (form.name === 'SpanLayout') {
+          this.formPermsLoad(oldPermMap, form.props.items)
+        } else {
+          //刷新名称
+          let old = oldPermMap.get(form.id)
+          if (old){
+            old.title = form.title
+            old.required = form.props.required
+            this.formPerms.push(old)
+          }else {
+            this.formPerms.push({
+              id: form.id,
+              title: form.title,
+              required: form.props.required,
+              perm: this.$store.state.selectedNode.type === 'ROOT' ? 'E' : 'R'
+            })
+          }
+        }
+      })
+    },
+    handleCheckAllChange() {
+
+    }
+  },
+  watch: {
+    formPerms: {
+      deep: true,
+      handler() {
+        const set = new Set(this.formPerms.map(f => f.perm))
+        this.permSelect = set.size === 1 ? set.values()[0] : ''
+      }
+    },
+
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+/deep/ .el-table__row {
+  & > td:first-child {
+    .cell {
+      text-align: left;
+    }
+  }
+
+  .cell {
+    text-align: center;
+  }
+
+  .el-radio__label {
+    display: none;
+  }
+}
+</style>

+ 281 - 0
src/views/common/process/config/InclusiveGroupItemConfig.vue

@@ -0,0 +1,281 @@
+<template>
+  <div>
+    <div v-for="(group, index) in selectedNode.props.groups" :key="index + '_g'" class="group">
+      <div class="group-header">
+        <span class="group-name">条件组 {{ groupNames[index] }}</span>
+        <div class="group-cp">
+          <span>组内条件关系:</span>
+          <el-switch v-model="group.groupType" active-color="#409EFF"
+                     inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
+                     active-text="且" inactive-text="或"/>
+        </div>
+        <div class="group-operation">
+          <el-popover placement="bottom" title="选择审批条件" width="300" trigger="click">
+            <!-- <div>以下条件将决定具体的审批流程</div>-->
+            <el-checkbox-group v-model="group.cids" value-key="id">
+              <el-checkbox :label="condition.id" v-for="(condition, cindex) in conditionList" :key="condition.id" @change="conditionChange(cindex, group)">
+                {{ condition.title }}
+              </el-checkbox>
+            </el-checkbox-group>
+            <i class="el-icon-plus" slot="reference"></i>
+          </el-popover>
+          <i class="el-icon-delete" @click="delGroup(index)"></i>
+        </div>
+      </div>
+      <div class="group-content">
+        <p v-if="group.conditions.length === 0">点击右上角 + 为本条件组添加条件 ☝</p>
+        <div v-else>
+          <el-form ref="condition-form" label-width="100px">
+            <!--构建表达式-->
+            <el-form-item  v-for="(condition, cindex) in group.conditions" :key="condition.id + '_' + cindex" >
+              <ellipsis slot="label" hover-tip :content="condition.title"/>
+               <span v-if="condition.valueType === ValueType.string">
+                <el-select size="small" placeholder="判断符" style="width: 120px;" v-model="condition.compare" @change="condition.value = []">
+                  <el-option label="等于" value="="></el-option>
+                  <el-option label="包含在" value="IN"></el-option>
+                </el-select>
+                 <span v-if="isSelect(condition.id)" style="margin-left: 10px">
+                   <el-select v-if="condition.compare === 'IN'" style="width: 280px;" clearable multiple size="small" v-model="condition.value" placeholder="选择值">
+                     <el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option" :value="option"></el-option>
+                   </el-select>
+                   <el-select v-else style="width: 280px;" clearable size="small" v-model="condition.value[0]" placeholder="选择值">
+                     <el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option" :value="option"></el-option>
+                   </el-select>
+                 </span>
+                 <span v-else style="margin-left: 10px">
+                   <el-input v-if="condition.compare === '='" style="width: 280px;" placeholder="输入比较值" size="small" v-model="condition.value[0]"/>
+                   <el-select v-else style="width: 280px;" multiple clearable filterable allow-create size="small" v-model="condition.value" placeholder="输入可能包含的值"></el-select>
+                 </span>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.number">
+                <el-select size="small" placeholder="判断符" style="width: 120px;" v-model="condition.compare">
+                  <el-option :label="exp.label" :value="exp.value" :key="exp.value" v-for="exp in explains"></el-option>
+                </el-select>
+                <span style="margin-left: 10px">
+                  <el-input style="width: 280px;" v-if="conditionValType(condition.compare) === 0" size="small" placeholder="输入比较值" type="number" v-model="condition.value[0]"/>
+                  <el-select style="width: 280px;" multiple filterable allow-create v-else-if="conditionValType(condition.compare) === 1" size="small" v-model="condition.value" placeholder="输入可能包含的值"></el-select>
+                  <span v-else>
+                    <el-input style="width: 130px;" size="small" type="number" placeholder="输入比较值" v-model="condition.value[0]"/>
+                    <span> ~
+                      <el-input size="small" style="width: 130px;" type="number" placeholder="输入比较值" v-model="condition.value[1]"/>
+                    </span>
+                  </span>
+                </span>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.user">
+                <span class="item-desc" style="margin-right: 20px">属于某部门 / 为某些人其中之一</span>
+                <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser(condition.value, 'user')" round>选择人员/部门</el-button>
+                <org-items v-model="condition.value"/>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.dept">
+                <span class="item-desc" style="margin-right: 20px">为某部门 / 某部门下的部门</span>
+                <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser(condition.value, 'dept')" round>选择部门</el-button>
+                <org-items v-model="condition.value"/>
+              </span>
+              <span v-else-if="condition.valueType === ValueType.date"></span>
+              <i class="el-icon-delete" @click="rmSubCondition(group, cindex)"></i>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+    </div>
+    <org-picker :type="orgType" multiple ref="orgPicker" :selected="users" @ok="selected"></org-picker>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from '../OrgItems'
+import {ValueType} from '@/views/common/form/ComponentsConfigExport'
+
+export default {
+  name: "InclusiveGroupItemConfig",
+  components: {OrgPicker, OrgItems},
+  data() {
+    return {
+      ValueType,
+      users: [],
+      orgType: 'user',
+      showOrgSelect: false,
+      //groupConditions: [],
+      groupNames: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
+      supportTypes:[ValueType.number, ValueType.string, ValueType.date, ValueType.dept, ValueType.user],
+      explains:[
+        {label: '等于', value:'='},
+        {label: '大于', value:'>'},
+        {label: '大于等于', value:'>='},
+        {label: '小于', value:'<'},
+        {label: '小于等于', value:'<='},
+        {label: '包含在', value:'IN'},
+        {label: 'x < 值 < x', value:'B'},
+        {label: 'x ≤ 值 < x', value:'AB'},
+        {label: 'x < 值 ≤ x', value:'BA'},
+        {label: 'x ≤ 值 ≤ x', value:'ABA'},
+      ]
+    }
+  },
+  computed: {
+    selectedNode() {
+      return this.$store.state.selectedNode
+    },
+    select() {
+      return this.selectedNode.props.assignedUser || []
+    },
+    formItems(){
+      return this.$store.state.design.formItems
+    },
+    formMap(){
+      const map = new Map();
+      this.formItems.forEach(item => this.itemToMap(map, item))
+      return map
+    },
+    conditionList() {
+      //构造可用条件选项
+      const conditionItems = []
+      this.formItems.forEach(item => this.filterCondition(item, conditionItems))
+      if (conditionItems.length === 0 || conditionItems[0].id !== 'root'){
+        conditionItems.unshift({id: 'root', title: '发起人', valueType: 'User'})
+      }
+      return conditionItems
+    }
+  },
+  methods: {
+    itemToMap(map, item){
+      map.set(item.id, item)
+      if (item.name === 'SpanLayout'){
+        item.props.items.forEach(sub => this.itemToMap(map, sub))
+      }
+    },
+    isSelect(formId){
+      let form = this.formMap.get(formId)
+      if (form && (form.name === 'SelectInput' || form.name === 'MultipleSelect')){
+        return true
+      }
+      return false
+    },
+    getOptions(formId){
+      return this.formMap.get(formId).props.options || []
+    },
+    conditionValType(type){
+      switch (type){
+        case '=':
+        case '>':
+        case '>=':
+        case '<':
+        case '<=': return 0;
+        case 'IN': return 1;
+        default: return 2;
+      }
+    },
+    selectUser(value, orgType) {
+      this.orgType = orgType
+      this.users = value
+      this.$refs.orgPicker.show()
+    },
+    filterCondition(item, list){
+      if (item.name === 'SpanLayout'){
+        item.props.items.forEach(sub => this.filterCondition(sub, list))
+      }else if (this.supportTypes.indexOf(item.valueType) > -1 && item.props.required){
+        list.push({title: item.title, id: item.id, valueType: item.valueType})
+      }
+    },
+    selected(select) {
+      console.log(select)
+      this.users.length = 0
+      select.forEach(u => this.users.push(u))
+    },
+    delGroup(index) {
+      this.selectedNode.props.groups.splice(index, 1)
+    },
+    rmSubCondition(group, index){
+      group.cids.splice(index, 1)
+      group.conditions.splice(index, 1)
+    },
+    conditionChange(index, group) {
+      //判断新增的
+      group.cids.forEach(cid => {
+        if (0 > group.conditions.findIndex(cd => cd.id === cid)){
+          //新增条件
+          let condition = {...this.conditionList[index]}
+          console.log(condition, this.conditionList, index)
+          condition.compare = '';
+          condition.value = []
+          group.conditions.push(condition)
+        }
+      })
+      for (let i = 0; i < group.conditions.length; i++) {
+        //去除没有选中的
+        if (group.cids.indexOf(group.conditions[i].id) < 0){
+          group.conditions.splice(i, 1)
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.group {
+  margin-bottom: 20px;
+  color: #5e5e5e;
+  overflow: hidden;
+  border-radius: 6px;
+  border: 1px solid #e3e3e3;
+
+  .group-header {
+    padding: 5px 10px;
+    background: #e3e3e3;
+    position: relative;
+
+    div {
+      display: inline-block;
+    }
+
+    .group-name {
+      font-size: small;
+    }
+
+    .group-cp {
+      font-size: small;
+      position: absolute;
+      left: 100px;
+      display: flex;
+      top: 5px;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .group-operation {
+      position: absolute;
+      right: 10px;
+
+      i {
+        padding: 0 10px;
+
+        &:hover {
+          cursor: pointer;
+        }
+      }
+    }
+  }
+
+  .group-content{
+    padding: 10px 5px;
+    p{
+      text-align: center;
+      font-size: small;
+    }
+    .el-icon-delete{
+      position: absolute;
+      cursor: pointer;
+      top: 12px;
+      right: 0;
+    }
+  }
+
+  .condition-title{
+    display: block;
+    width: 100px;
+  }
+}
+</style>

+ 135 - 0
src/views/common/process/config/InclusiveNodeConfig.vue

@@ -0,0 +1,135 @@
+<template>
+  <div>
+    <el-form inline label-width="100px">
+      <el-form-item label="调整优先级" prop="level">
+        <el-popover placement="right" title="拖拽条件调整优先级顺序" width="250" trigger="click">
+          <draggable style="width: 100%; min-height:25px" :list="prioritySortList" group="from" :options="sortOption">
+            <div :class="{'drag-no-choose': true, 'drag-hover': cd.id === selectedNode.id}"
+                 v-for="(cd, index) in prioritySortList">
+              <ellipsis style="width: 160px;" hover-tip :content="cd.name"/>
+              <div>优先级 {{ index + 1 }}</div>
+            </div>
+          </draggable>
+          <el-button icon="el-icon-sort" size="small" slot="reference">第{{ nowNodeLeave + 1 }}级</el-button>
+        </el-popover>
+      </el-form-item>
+      <el-form-item label="条件组关系" label-width="150px">
+        <el-switch v-model="config.groupsType" active-color="#409EFF"
+                   inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
+                   active-text="且" inactive-text="或">
+        </el-switch>
+      </el-form-item>
+      <el-form-item label="条件组表达式">
+        <el-input size="mini" v-model="config.expression" placeholder="输入条件组关系表达式  &为与,|为或"/>
+        <span class="item-desc">使用表达式构建复杂逻辑,例如: (A & B) | C</span>
+      </el-form-item>
+    </el-form>
+    <div>
+      <el-button type="primary" size="mini" icon="el-icon-plus" style="margin: 0 15px 15px 0" round @click="addConditionGroup">
+        添加条件组
+      </el-button>
+      <span class="item-desc">只有必填选项才能作为审批条件</span>
+    </div>
+    <group-item/>
+  </div>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import GroupItem from "./InclusiveGroupItemConfig.vue"
+
+export default {
+  name: "InclusiveNodeConfig",
+  components: {draggable, GroupItem},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  computed: {
+    selectedNode() {
+      return this.$store.state.selectedNode
+    },
+    select() {
+      return this.config.assignedUser || []
+    },
+    nowNodeLeave() {
+      return this.prioritySortList.indexOf(this.selectedNode)
+    },
+    //条件节点
+    prioritySortList() {
+      let node = this.$store.state.nodeMap.get(this.selectedNode.parentId)
+      console.log(this.selectedNode.id, node)
+      if (node) {
+        return node.branchs || []
+      }
+      return []
+    }
+  },
+  data() {
+    return {
+      sortOption: {
+        animation: 300,
+        chosenClass: 'choose',
+        scroll: true,
+        sort: true
+      }
+    }
+  },
+  methods: {
+    addConditionGroup() {
+      this.config.groups.push({
+        cids:[],
+        groupType: "OR",
+        conditions:[]
+      })
+    },
+    selectUser() {
+      this.showOrgSelect = true
+    },
+    selected(select) {
+      console.log(select)
+      this.showOrgSelect = false
+      select.forEach(val => this.select.push(val))
+    },
+    removeOrgItem(index) {
+      this.select.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.choose {
+  border-radius: 5px;
+  margin-top: 2px;
+  background: #f4f4f4;
+  border: 1px dashed #1890FF !important;
+}
+
+.drag-hover {
+  color: #1890FF
+}
+
+.drag-no-choose {
+  cursor: move;
+  background: #f8f8f8;
+  border-radius: 5px;
+  margin: 5px 0;
+  height: 25px;
+  line-height: 25px;
+  padding: 5px 10px;
+  border: 1px solid #ffffff;
+  div{
+    display: inline-block;
+    font-size: small !important;
+  }
+
+  div:nth-child(2) {
+    float: right !important;
+  }
+}
+</style>

+ 74 - 0
src/views/common/process/config/NodeConfig.vue

@@ -0,0 +1,74 @@
+<template>
+  <div>
+    <el-tabs v-model="active" v-if="name && formConfig.length > 0">
+      <el-tab-pane :label="name" name="properties">
+        <component :is="(selectNode.type||'').toLowerCase()" :config="selectNode.props"/>
+      </el-tab-pane>
+      <el-tab-pane label="表单权限设置" name="permissions">
+        <form-authority-config/>
+      </el-tab-pane>
+    </el-tabs>
+    <component :is="(selectNode.type||'').toLowerCase()" v-else :config="selectNode.props"/>
+  </div>
+</template>
+
+<script>
+import Approval from './ApprovalNodeConfig.vue'
+import Task from './TaskNodeConfig.vue'
+import Condition from './ConditionNodeConfig.vue'
+import Inclusive from './InclusiveNodeConfig.vue'
+import Delay from './DelayNodeConfig.vue'
+import Cc from './CcNodeConfig.vue'
+import Trigger from './TriggerNodeConfig.vue'
+import FormAuthorityConfig from './FormAuthorityConfig.vue'
+import Root from './RootNodeConfig.vue'
+import Subprocess from './SubprocessNodeConfig.vue'
+
+export default {
+  name: "NodeConfig",
+  components: {
+    Approval,
+    Task,
+    Condition,
+    Inclusive,
+    Trigger,
+    Delay,
+    Root,
+    Cc,
+    Subprocess,
+    FormAuthorityConfig
+  },
+  data() {
+    return {
+      active: 'properties',
+    }
+  },
+  computed: {
+    selectNode() {
+      return this.$store.state.selectedNode
+    },
+    formConfig() {
+      return this.$store.state.design.formItems
+    },
+    name() {
+      switch (this.selectNode.type) {
+        case 'ROOT':
+          return '设置发起人';
+        case 'APPROVAL':
+          return '设置审批人';
+        case 'TASK':
+          return '设置办理人';
+        case 'CC':
+          return '设置抄送人';
+        default:
+          return null;
+      }
+    }
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+
+</style>

+ 58 - 0
src/views/common/process/config/RootNodeConfig.vue

@@ -0,0 +1,58 @@
+<template>
+  <div>
+    <p class="desc">选择能发起该审批的人员/部门,不选则默认开放给所有人</p>
+    <el-button size="mini" @click="selectOrg" icon="el-icon-plus" type="primary" round>请选择</el-button>
+    <org-items v-model="select"/>
+    <org-picker title="请选择可发起本审批的人员/部门" multiple ref="orgPicker" :selected="select" @ok="selected"/>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from "../OrgItems";
+
+export default {
+  name: "RootConfig",
+  components: {OrgPicker, OrgItems},
+  props:{
+    config:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      showOrgSelect: false
+    }
+  },
+  computed:{
+    select(){
+      return this.config.assignedUser
+    }
+  },
+  methods: {
+    selectOrg() {
+      this.$refs.orgPicker.show()
+    },
+    selected(select) {
+      this.select.length = 0
+      select.forEach(val => this.select.push(val))
+    },
+    removeOrgItem(index){
+      this.select.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.desc{
+  font-size: small;
+  color: #8c8c8c;
+}
+.org-item{
+  margin: 5px;
+}
+</style>

+ 255 - 0
src/views/common/process/config/SubprocessNodeConfig.vue

@@ -0,0 +1,255 @@
+<template>
+  <div>
+    <el-form label-position="top" label-width="90px">
+      <el-form-item label="⚙ 选择审批对象" prop="text" class="user-type">
+        <el-radio-group v-model="nodeProps.assignedType">
+          <el-radio v-for="t in approvalTypes" :label="t.type" :key="t.type">{{ t.name }}</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.assignedType === 'ASSIGN_USER'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser" round>选择人员</el-button>
+          <org-items v-model="nodeProps.assignedUser"/>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'SELF_SELECT'">
+          <el-radio-group size="mini" v-model="nodeProps.selfSelect.multiple">
+            <el-radio-button :label="false">自选一个人</el-radio-button>
+            <el-radio-button :label="true">自选多个人</el-radio-button>
+          </el-radio-group>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'LEADER_TOP'">
+          <el-divider/>
+          <el-form-item label="审批终点" prop="text" class="approve-end">
+            <el-radio-group v-model="nodeProps.leaderTop.endCondition">
+              <el-radio label="TOP">直到最上层主管</el-radio>
+              <el-radio label="LEAVE">不超过发起人的</el-radio>
+            </el-radio-group>
+            <div class="approve-end-leave" v-if="nodeProps.leaderTop.endCondition === 'LEAVE'">
+              <span>第 </span>
+              <el-input-number :min="1" :max="20" :step="1" size="mini" v-model="nodeProps.leaderTop.level"/>
+              <span> 级主管</span>
+            </div>
+          </el-form-item>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'LEADER'">
+          <el-divider/>
+          <el-form-item label="指定主管" prop="text">
+            <span>发起人的第 </span>
+            <el-input-number :min="1" :max="20" :step="1" size="mini"
+                             v-model="nodeProps.leader.level"></el-input-number>
+            <span> 级主管</span>
+            <div style="color: #409EFF; font-size: small;">👉 直接主管为 第 1 级主管</div>
+          </el-form-item>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'ROLE'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectRole" round>选择系统角色</el-button>
+          <org-items v-model="nodeProps.role"/>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'FORM_USER'">
+          <el-form-item label="选择表单联系人项" prop="text" class="approve-end">
+            <el-select style="width: 80%;" size="small" v-model="nodeProps.formUser" placeholder="请选择包含联系人的表单项">
+              <el-option v-for="op in forms" :label="op.title" :value="op.id" :key="op.id"></el-option>
+            </el-select>
+          </el-form-item>
+        </div>
+        <div v-else>
+          <span class="item-desc">发起人自己作为审批人进行审批</span>
+        </div>
+      </el-form-item>
+
+      <el-divider></el-divider>
+      <el-form-item label="⚙子流程表单" prop="text" class="line-mode">
+        <el-cascader  style="width: 80%;" size="small" v-model="nodeProps.subprocessId" placeholder="请选择子流程表单" :options="fromGroup" :props="{ expandTrigger: 'hover'}" :show-all-levels="false"
+        @change="handleChange">
+        </el-cascader>
+      </el-form-item>
+    </el-form>
+    <org-picker :title="pickerTitle" multiple :type="orgPickerType" ref="orgPicker" :selected="orgPickerSelected"
+                @ok="selected"/>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from "../OrgItems";
+import {getFormGroups} from '@/api/design'
+
+export default {
+  name: "SubprocessNodeConfig",
+  components: {OrgPicker, OrgItems},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      showOrgSelect: false,
+      orgPickerSelected: [],
+      fromGroup: [],
+      orgPickerType: 'user',
+      approvalTypes: [
+        {name: '指定人员', type: 'ASSIGN_USER'},
+        {name: '发起人自选', type: 'SELF_SELECT'},
+        {name: '连续多级主管', type: 'LEADER_TOP'},
+        {name: '主管', type: 'LEADER'},
+        {name: '角色', type: 'ROLE'},
+        {name: '发起人自己', type: 'SELF'},
+        {name: '表单内联系人', type: 'FORM_USER'}
+      ]
+    }
+  },
+  computed: {
+    nodeProps() {
+      return this.$store.state.selectedNode.props
+    },
+    select() {
+      return this.config.assignedUser || []
+    },
+    forms() {
+      return this.$store.state.design.formItems.filter(f => {
+        return f.name === 'UserPicker'
+      })
+    },
+    pickerTitle() {
+      switch (this.orgPickerType) {
+        case 'user':
+          return '请选择人员';
+        case 'role':
+          return '请选择系统角色bbb';
+        default:
+          return null;
+      }
+    },
+    nodeOptions() {
+      let values = []
+      const excType = ['ROOT', 'EMPTY', "CONDITION", "CONDITIONS", "CONCURRENT", "CONCURRENTS"]
+      this.$store.state.nodeMap.forEach((v) => {
+        if (excType.indexOf(v.type) === -1) {
+          values.push({id: v.id, name: v.name})
+        }
+      })
+      return values
+    },
+    showMode() {
+      switch (this.nodeProps.assignedType) {
+        case "ASSIGN_USER":
+          return this.nodeProps.assignedUser.length > 0;
+        case "SELF_SELECT":
+          return this.nodeProps.selfSelect.multiple;
+        case "LEADER_TOP":
+          return this.nodeProps.formUser !== '';
+        case "FORM_USER":
+          return true;
+        case "ROLE":
+          return true;
+        default:
+          return false;
+      }
+    }
+  },
+  mounted(){
+    this.getGroups()
+  },
+  methods: {
+    selectUser() {
+      this.orgPickerSelected = this.select
+      this.orgPickerType = 'user'
+      this.$refs.orgPicker.show()
+    },
+    selectNoSetUser() {
+      this.orgPickerSelected = this.config.nobody.assignedUser
+      this.orgPickerType = 'user'
+      this.$refs.orgPicker.show()
+    },
+    selectRole() {
+      this.orgPickerSelected = this.select
+      this.orgPickerType = 'role'
+      this.$refs.orgPicker.show()
+    },
+    selected(select) {
+      console.log(select)
+      this.orgPickerSelected.length = 0
+      select.forEach(val => this.orgPickerSelected.push(val))
+    },
+    removeOrgItem(index) {
+      this.select.splice(index, 1)
+    },
+    getGroups(){
+      // 简便  不使用迭代方法处理
+      getFormGroups().then(rsp => {
+        var data = rsp.data
+        this.fromGroup = data.map(group => {
+          return {
+            value: group.id,
+            label: group.name,
+            children: group.items.map(item => {
+              return {
+                value: item.formId,
+                label: item.formName,
+              };
+            })
+          };
+        });
+      }).catch(err => this.$message.error('获取分组异常'))
+    },
+    handleChange(key){
+       // 对值进行处理
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.user-type {
+  /deep/ .el-radio {
+    width: 110px;
+    margin-top: 10px;
+    margin-bottom: 20px;
+  }
+}
+
+/deep/ .line-mode {
+  .el-radio {
+    width: 150px;
+    margin: 5px;
+  }
+}
+
+/deep/ .el-form-item__label {
+  line-height: 25px;
+}
+
+/deep/ .approve-mode {
+  .el-radio {
+    float: left;
+    width: 100%;
+    display: block;
+    margin-top: 15px;
+  }
+}
+
+/deep/ .approve-end {
+  position: relative;
+
+  .el-radio-group {
+    width: 160px;
+  }
+
+  .el-radio {
+    margin-bottom: 5px;
+    width: 100%;
+  }
+
+  .approve-end-leave {
+    position: absolute;
+    bottom: -5px;
+    left: 150px;
+  }
+}
+
+/deep/ .el-divider--horizontal {
+  margin: 10px 0;
+}
+</style>

+ 298 - 0
src/views/common/process/config/TaskNodeConfig.vue

@@ -0,0 +1,298 @@
+<template>
+  <div>
+    <el-form label-position="top" label-width="90px">
+      <el-form-item label="⚙ 选择办理对象" prop="text" class="user-type">
+        <el-radio-group v-model="nodeProps.assignedType">
+          <el-radio v-for="t in approvalTypes" :label="t.type" :key="t.type">{{ t.name }}</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.assignedType === 'ASSIGN_USER'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectUser" round>选择人员</el-button>
+          <org-items v-model="nodeProps.assignedUser"/>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'SELF_SELECT'">
+          <el-radio-group size="mini" v-model="nodeProps.selfSelect.multiple">
+            <el-radio-button :label="false">自选一个人</el-radio-button>
+            <el-radio-button :label="true">自选多个人</el-radio-button>
+          </el-radio-group>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'LEADER_TOP'">
+          <el-divider/>
+          <el-form-item label="办理终点" prop="text" class="approve-end">
+            <el-radio-group v-model="nodeProps.leaderTop.endCondition">
+              <el-radio label="TOP">直到最上层主管</el-radio>
+              <el-radio label="LEAVE">不超过发起人的</el-radio>
+            </el-radio-group>
+            <div class="approve-end-leave" v-if="nodeProps.leaderTop.endCondition === 'LEAVE'">
+              <span>第 </span>
+              <el-input-number :min="1" :max="20" :step="1" size="mini" v-model="nodeProps.leaderTop.level"/>
+              <span> 级主管</span>
+            </div>
+          </el-form-item>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'LEADER'">
+          <el-divider/>
+          <el-form-item label="指定主管" prop="text">
+            <span>发起人的第 </span>
+            <el-input-number :min="1" :max="20" :step="1" size="mini"
+                             v-model="nodeProps.leader.level"></el-input-number>
+            <span> 级主管</span>
+            <div style="color: #409EFF; font-size: small;">👉 直接主管为 第 1 级主管</div>
+          </el-form-item>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'ROLE'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectRole" round>选择系统角色</el-button>
+          <org-items v-model="nodeProps.role"/>
+        </div>
+        <div v-else-if="nodeProps.assignedType === 'FORM_USER'">
+          <el-form-item label="选择表单联系人项" prop="text" class="approve-end">
+            <el-select style="width: 80%;" size="small" v-model="nodeProps.formUser" placeholder="请选择包含联系人的表单项">
+              <el-option v-for="op in forms" :label="op.title" :value="op.id" :key="op.id"></el-option>
+            </el-select>
+          </el-form-item>
+        </div>
+        <div v-else>
+          <span class="item-desc">发起人自己作为办理人进行办理</span>
+        </div>
+
+      </el-form-item>
+
+      <el-divider></el-divider>
+      <el-form-item label="👤 办理人为空时" prop="text" class="line-mode">
+        <el-radio-group v-model="nodeProps.nobody.handler">
+          <el-radio label="TO_PASS">自动通过</el-radio>
+          <el-radio label="TO_REFUSE">自动驳回</el-radio>
+          <el-radio label="TO_ADMIN">转交办理管理员</el-radio>
+          <el-radio label="TO_USER">转交到指定人员</el-radio>
+        </el-radio-group>
+
+        <div style="margin-top: 10px" v-if="nodeProps.nobody.handler === 'TO_USER'">
+          <el-button size="mini" icon="el-icon-plus" type="primary" @click="selectNoSetUser" round>选择人员</el-button>
+          <org-items v-model="nodeProps.nobody.assignedUser"/>
+        </div>
+
+      </el-form-item>
+
+      <div v-if="showMode">
+        <el-divider/>
+        <el-form-item label="👩‍👦‍👦 多人办理时办理方式" prop="text" class="approve-mode">
+          <el-radio-group v-model="nodeProps.mode">
+            <el-radio label="NEXT">会签 (按选择顺序办理,每个人必须同意)</el-radio>
+            <el-radio label="AND">会签(可同时办理,每个人必须同意)</el-radio>
+            <el-radio label="OR">或签(有一人同意即可)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </div>
+
+      <el-divider>高级设置</el-divider>
+      <el-form-item label="✍ 办理同意时是否需要签字" prop="text">
+        <el-switch inactive-text="不用" active-text="需要" v-model="nodeProps.sign"></el-switch>
+        <el-tooltip class="item" effect="dark" content="如果全局设置了需要签字,则此处不生效" placement="top-start">
+          <i class="el-icon-question" style="margin-left: 10px; font-size: medium; color: #b0b0b1"></i>
+        </el-tooltip>
+      </el-form-item>
+      <el-form-item label="⏱ 办理期限(为 0 则不生效)" prop="timeLimit">
+        <el-input style="width: 180px;" placeholder="时长" size="small" type="number"
+                  v-model="nodeProps.timeLimit.timeout.value">
+          <el-select style="width: 75px;" v-model="nodeProps.timeLimit.timeout.unit" slot="append" placeholder="请选择">
+            <el-option label="天" value="D"></el-option>
+            <el-option label="小时" value="H"></el-option>
+          </el-select>
+        </el-input>
+      </el-form-item>
+      <el-form-item label="办理期限超时后执行" prop="level" v-if="nodeProps.timeLimit.timeout.value > 0">
+        <el-radio-group v-model="nodeProps.timeLimit.handler.type">
+          <el-radio label="PASS">自动通过</el-radio>
+          <el-radio label="REFUSE">自动驳回</el-radio>
+          <el-radio label="NOTIFY">发送提醒</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.timeLimit.handler.type === 'NOTIFY'">
+          <div style="color:#409EEF; font-size: small">默认提醒当前办理人</div>
+          <el-switch inactive-text="循环" active-text="一次" v-model="nodeProps.timeLimit.handler.notify.once"></el-switch>
+          <span style="margin-left: 20px" v-if="!nodeProps.timeLimit.handler.notify.once">
+							每隔
+							<el-input-number :min="0" :max="10000" :step="1" size="mini"
+                               v-model="nodeProps.timeLimit.handler.notify.hour"/>
+							小时提醒一次
+						</span>
+        </div>
+      </el-form-item>
+      <el-form-item label="🙅‍ 如果办理被驳回 👇">
+        <el-radio-group v-model="nodeProps.refuse.type">
+          <el-radio label="TO_END">直接结束流程</el-radio>
+          <el-radio label="TO_BEFORE">驳回到上级办理节点</el-radio>
+          <el-radio label="TO_NODE">驳回到指定节点</el-radio>
+        </el-radio-group>
+        <div v-if="nodeProps.refuse.type === 'TO_NODE'">
+          <span>指定节点:</span>
+          <el-select style="margin-left: 10px; width: 150px;" placeholder="选择跳转步骤" size="small" v-model="nodeProps.refuse.target">
+            <el-option v-for="(node, i) in nodeOptions" :key="i" :label="node.name" :value="node.id"></el-option>
+          </el-select>
+        </div>
+
+      </el-form-item>
+    </el-form>
+    <org-picker :title="pickerTitle" multiple :type="orgPickerType" ref="orgPicker" :selected="orgPickerSelected"
+                @ok="selected"/>
+  </div>
+</template>
+
+<script>
+import OrgPicker from "@/components/common/OrgPicker";
+import OrgItems from "../OrgItems";
+
+export default {
+  name: "ApprovalNodeConfig",
+  components: {OrgPicker, OrgItems},
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      showOrgSelect: false,
+      orgPickerSelected: [],
+      orgPickerType: 'user',
+      approvalTypes: [
+        {name: '指定人员', type: 'ASSIGN_USER'},
+        {name: '发起人自选', type: 'SELF_SELECT'},
+        {name: '连续多级主管', type: 'LEADER_TOP'},
+        {name: '主管', type: 'LEADER'},
+        {name: '角色', type: 'ROLE'},
+        {name: '发起人自己', type: 'SELF'},
+        {name: '表单内联系人', type: 'FORM_USER'}
+      ]
+    }
+  },
+  computed: {
+    nodeProps() {
+      return this.$store.state.selectedNode.props
+    },
+    select() {
+      return this.config.assignedUser || []
+    },
+    forms() {
+      return this.$store.state.design.formItems.filter(f => {
+        return f.name === 'UserPicker'
+      })
+    },
+    pickerTitle() {
+      switch (this.orgPickerType) {
+        case 'user':
+          return '请选择人员';
+        case 'role':
+          return '请选择系统角色ccc';
+        default:
+          return null;
+      }
+    },
+    nodeOptions() {
+      let values = []
+      const excType = ['ROOT', 'EMPTY', "CONDITION", "CONDITIONS", "CONCURRENT", "CONCURRENTS"]
+      this.$store.state.nodeMap.forEach((v) => {
+        if (excType.indexOf(v.type) === -1) {
+          values.push({id: v.id, name: v.name})
+        }
+      })
+      return values
+    },
+    showMode() {
+      switch (this.nodeProps.assignedType) {
+        case "ASSIGN_USER":
+          return this.nodeProps.assignedUser.length > 0;
+        case "SELF_SELECT":
+          return this.nodeProps.selfSelect.multiple;
+        case "LEADER_TOP":
+          return this.nodeProps.formUser !== '';
+        case "FORM_USER":
+          return true;
+        case "ROLE":
+          return true;
+        default:
+          return false;
+      }
+    }
+  },
+  methods: {
+    selectUser() {
+      this.orgPickerSelected = this.select
+      this.orgPickerType = 'user'
+      this.$refs.orgPicker.show()
+    },
+    selectNoSetUser() {
+      this.orgPickerSelected = this.config.nobody.assignedUser
+      this.orgPickerType = 'user'
+      this.$refs.orgPicker.show()
+    },
+    selectRole() {
+      this.orgPickerSelected = this.select
+      this.orgPickerType = 'role'
+      this.$refs.orgPicker.show()
+    },
+    selected(select) {
+      console.log(select)
+      this.orgPickerSelected.length = 0
+      select.forEach(val => this.orgPickerSelected.push(val))
+    },
+    removeOrgItem(index) {
+      this.select.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.user-type {
+  /deep/ .el-radio {
+    width: 110px;
+    margin-top: 10px;
+    margin-bottom: 20px;
+  }
+}
+
+/deep/ .line-mode {
+  .el-radio {
+    width: 150px;
+    margin: 5px;
+  }
+}
+
+/deep/ .el-form-item__label {
+  line-height: 25px;
+}
+
+/deep/ .approve-mode {
+  .el-radio {
+    float: left;
+    width: 100%;
+    display: block;
+    margin-top: 15px;
+  }
+}
+
+/deep/ .approve-end {
+  position: relative;
+
+  .el-radio-group {
+    width: 160px;
+  }
+
+  .el-radio {
+    margin-bottom: 5px;
+    width: 100%;
+  }
+
+  .approve-end-leave {
+    position: absolute;
+    bottom: -5px;
+    left: 150px;
+  }
+}
+
+/deep/ .el-divider--horizontal {
+  margin: 10px 0;
+}
+</style>

+ 193 - 0
src/views/common/process/config/TriggerNodeConfig.vue

@@ -0,0 +1,193 @@
+<template>
+  <div>
+    <el-button type="primary">触发器支持模板变量替换语法 ${变量名}, 变量名支持所有的<br/>
+      【表单字段ID】及【扩展变量】<br/>
+      扩展变量如下:[formName 审批表单名] [instanceId 审批实例ID]<br/>
+      [ownerId 发起人ID] [ownerName 发起人名]<br/>
+      [ownerDeptId 发起人部门ID] [ownerDeptName 发起人部门名]<br/></el-button>
+    <el-form label-position="top" label-width="90px">
+      <el-form-item label="选择触发的动作" prop="text" class="user-type">
+        <el-radio-group v-model="config.type">
+          <el-radio label="WEBHOOK">发送网络请求</el-radio>
+          <el-radio label="EMAIL">发送邮件</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <div v-if="config.type === 'WEBHOOK'">
+        <el-form-item label="请求地址" prop="text">
+          <el-input placeholder="请输入URL地址" size="medium" v-model="config.http.url" >
+            <el-select v-model="config.http.method" style="width: 85px;" slot="prepend" placeholder="URL">
+              <el-option label="GET" value="GET"></el-option>
+              <el-option label="POST" value="POST"></el-option>
+              <el-option label="PUT" value="PUT"></el-option>
+              <el-option label="DELETE" value="DELETE"></el-option>
+            </el-select>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="Header请求头" prop="text">
+          <div slot="label">
+            <span style="margin-right: 10px">Header请求头</span>
+            <el-button type="text" @click="addItem(config.http.headers)"> + 添加</el-button>
+          </div>
+          <div v-for="(header, index) in config.http.headers" :key="header.name">
+            - <el-input placeholder="参数名" size="small" style="width: 100px;" v-model="header.name" />
+            <el-radio-group size="small" style="margin: 0 5px;" v-model="header.isField">
+              <el-radio-button :label="true">表单</el-radio-button>
+              <el-radio-button :label="false">固定</el-radio-button>
+            </el-radio-group>
+            <el-select v-if="header.isField" style="width: 180px;" v-model="header.value" size="small" placeholder="请选择表单字段">
+              <el-option v-for="form in forms" :key="form.id" :label="form.title" :value="form.id"></el-option>
+            </el-select>
+            <el-input v-else placeholder="请设置字段值" size="small" v-model="header.value" style="width: 180px;"/>
+            <el-icon class="el-icon-delete" @click="delItem(config.http.headers, index)" style="margin-left: 5px; color: #c75450; cursor: pointer"/>
+          </div>
+        </el-form-item>
+        <el-form-item label="Header请求参数" prop="text">
+          <div slot="label">
+            <span style="margin-right: 10px">Header请求参数   </span>
+            <el-button style="margin-right: 20px" type="text" @click="addItem(config.http.params)">  + 添加</el-button>
+            <span>参数类型 - </span>
+            <el-radio-group size="mini" style="margin: 0 5px;" v-model="config.http.contentType">
+              <el-radio-button label="JSON">json</el-radio-button>
+              <el-radio-button label="FORM">form</el-radio-button>
+            </el-radio-group>
+          </div>
+          <div v-for="(param, index) in config.http.params" :key="param.name">
+            - <el-input placeholder="参数名" size="small" style="width: 100px;" v-model="param.name" />
+            <el-radio-group size="small" style="margin: 0 5px;" v-model="param.isField">
+              <el-radio-button :label="true">表单</el-radio-button>
+              <el-radio-button :label="false">固定</el-radio-button>
+            </el-radio-group>
+            <el-select v-if="param.isField" style="width: 180px;" v-model="param.value" size="small" placeholder="请选择表单字段">
+              <el-option v-for="form in forms" :key="form.id" :label="form.title" :value="form.id"></el-option>
+            </el-select>
+            <el-input v-else placeholder="请设置字段值" size="small" v-model="param.value" style="width: 180px;"/>
+            <el-icon class="el-icon-delete" @click="delItem(config.http.params, index)" style="margin-left: 5px; color: #c75450; cursor: pointer"/>
+          </div>
+          <div>
+
+          </div>
+        </el-form-item>
+        <el-form-item label="请求结果处理" prop="text">
+          <div slot="label">
+            <span>请求结果处理</span>
+            <span style="margin-left: 20px">自定义脚本: </span>
+            <el-switch v-model="config.http.handlerByScript"></el-switch>
+          </div>
+          <span class="item-desc" v-if="config.http.handlerByScript">
+          👉 返回值为 ture 则流程通过,为 false 则流程将被驳回
+          <div>支持函数
+            <span style="color: dodgerblue">setFormByName(
+              <span style="color: #939494">'表单字段名', '表单字段值'</span>
+              )</span>
+            可改表单数据
+          </div>
+        </span>
+          <span class="item-desc" v-else>👉 无论请求结果如何,均通过</span>
+          <div v-if="config.http.handlerByScript">
+            <div>
+              <span>请求成功😀:</span>
+              <el-input type="textarea" v-model="config.http.success" :rows="3"></el-input>
+            </div>
+            <div>
+              <span>请求失败😥:</span>
+              <el-input type="textarea" v-model="config.http.fail" :rows="3"></el-input>
+            </div>
+          </div>
+        </el-form-item>
+      </div>
+      <div v-else-if="config.type === 'EMAIL'">
+        <el-form-item label="邮件主题" prop="text">
+          <el-input placeholder="请输入邮件主题" size="medium" v-model="config.email.subject" />
+        </el-form-item>
+        <el-form-item label="收件方" prop="text">
+          <el-input size="small" style="width: 100%;" v-model="config.email.to" filterable multiple allow-create default-first-option placeholder="请输入收件人">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="邮件正文" prop="text">
+          <el-input type="textarea" v-model="config.email.content" :rows="4" placeholder="邮件内容,支持变量提取表单数据 ${表单字段名} "></el-input>
+        </el-form-item>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+//import { codemirror } from 'vue-codemirror'
+// 引入主题 可以从 codemirror/theme/ 下引入多个
+//import 'codemirror/theme/idea.css'
+// 引入语言模式 可以从 codemirror/mode/ 下引入多个
+//import "codemirror/mode/javascript/javascript.js"
+
+export default {
+  name: "TriggerNodeConfig",
+  components: {/*codemirror*/},
+  props:{
+    config:{
+      type: Object,
+      default: ()=>{
+        return {}
+      }
+    }
+  },
+  computed:{
+    forms(){
+      return this.$store.state.design.formItems || []
+    }
+  },
+  data() {
+    return {
+      cmOptions:{
+        tabSize: 4, // tab
+        indentUnit: 4,
+        styleActiveLine: true, // 高亮选中行
+        lineNumbers: true, // 显示行号
+        styleSelectedText: true,
+        line: true,
+        foldGutter: true, // 块槽
+        gutters: ['CodeMirror-linenumbers', "lock", "warn"],
+        highlightSelectionMatches: { showToken: /w/, annotateScrollbar: true }, // 可以启用该选项来突出显示当前选中的内容的所有实例
+        mode:'javascript',
+        // hint.js options
+        hintOptions: {
+          // 当匹配只有一项的时候是否自动补全
+          completeSingle: false
+        },
+        // 快捷键 可提供三种模式 sublime、emacs、vim
+        keyMap: 'sublime',
+        matchBrackets: true,
+        showCursorWhenSelecting: false,
+        // scrollbarStyle:null,
+        // readOnly:true,  //是否只读
+        theme: 'material', // 主题 material
+        extraKeys: { 'Ctrl': 'autocomplete' }, // 可以用于为编辑器指定额外的键绑定,以及keyMap定义的键绑定
+        lastLineBefore:0
+      }
+    }
+  },
+  methods: {
+    addItem(items){
+      if (items.length > 0 && (items[items.length - 1].name.trim() === ''
+          || items[items.length - 1].value.trim() === '')){
+        this.$message.warning("请完善之前项后在添加")
+        return;
+      }
+      items.push({name: '', value: '', isField: true})
+    },
+    delItem(items, index){
+      items.splice(index, 1)
+    },
+    onCmCodeChange(){
+
+    },
+    onCmReady(){
+
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.item-desc{
+  color: #939494;
+}
+</style>

+ 141 - 0
src/views/common/process/nodes/ApprovalNode.vue

@@ -0,0 +1,141 @@
+<template>
+  <node :title="config.name" :show-error="showError" :content="content" :error-info="errorInfo"
+        @selected="$emit('selected')" @delNode="$emit('delNode')" @insertNode="type => $emit('insertNode', type)"
+        placeholder="请设置审批人" :header-bgc="headerBgc" header-icon="el-icon-s-check"/>
+</template>
+
+<script>
+import Node from './Node'
+
+export default {
+  name: "ApprovalNode",
+  props:{
+    config:{
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  components: {Node},
+  data() {
+    return {
+      showError: false,
+      errorInfo: '',
+    }
+  },
+  computed:{
+    headerBgc() {
+      if (this.$store.state.diagramMode === 'viewer') {
+        return this.config.props.headerBgc
+      } else {
+        return '#ff943e'
+      }
+    },
+    content(){
+      const config = this.config.props
+      switch (config.assignedType){
+        case "ASSIGN_USER":
+          if (config.assignedUser.length > 0){
+            let texts = []
+            config.assignedUser.forEach(org => texts.push(org.name))
+            return String(texts).replaceAll(',', '、')
+          }else {
+            return '请指定审批人'
+          }
+        case "SELF":
+          return '发起人自己'
+        case "SELF_SELECT":
+          return config.selfSelect.multiple ? '发起人自选多人':'发起人自选一人'
+        case "LEADER_TOP":
+          return '多级主管依次审批'
+        case "LEADER":
+          return config.leader.level > 1 ? '发起人的第 ' + config.leader.level + ' 级主管' : '发起人的直接主管'
+        case "FORM_USER":
+          if (!config.formUser || config.formUser === ''){
+            return '表单内联系人(未选择)'
+          }else {
+            let text = this.getFormItemById(config.formUser)
+            if (text && text.title){
+              return `表单(${text.title})内的人员`
+            }else {
+              return '该表单已被移除😥'
+            }
+          }
+        case "ROLE":
+          if (config.role.length > 0){
+            return String(config.role).replaceAll(',', '、')
+          }else {
+            return '指定角色(未设置)'
+          }
+          default: return '未知设置项😥'
+      }
+    }
+  },
+  methods: {
+    getFormItemById(id){
+      return this.$store.state.design.formItems.find(item => item.id === id)
+    },
+    //校验数据配置的合法性
+    validate(err){
+      try {
+        this.showError = !this[`validate_${this.config.props.assignedType}`](err)
+
+        if (this.config.props.nobody.handler === 'TO_USER' && this.config.props.nobody.assignedUser.length === 0) {
+          this.errorInfo = '审批人为空时, 转交给指定人员:【请指定一个具体的人】'
+          err.push('审批人为空时, 转交给指定人员:【请指定一个具体的人】')
+          this.showError = true
+        }
+
+        return this.showError
+      } catch (e) {
+        return true;
+      }
+    },
+    validate_ASSIGN_USER(err){
+      if(this.config.props.assignedUser.length > 0){
+        return true;
+      }else {
+        this.errorInfo = '请指定审批人员'
+        err.push(`${this.config.name} 未指定审批人员`)
+        return false
+      }
+    },
+    validate_SELF_SELECT(err){
+      return true;
+    },
+    validate_LEADER_TOP(err){
+      return true;
+    },
+    validate_LEADER(err){
+      return true;
+    },
+    validate_ROLE(err){
+      if (this.config.props.role.length <= 0){
+        this.errorInfo = '请指定负责审批的系统角色'
+        err.push(`${this.config.name} 未指定审批角色`)
+        return false
+      }
+      return true;
+    },
+    validate_SELF(err){
+      return true;
+    },
+    validate_FORM_USER(err){
+     if (this.config.props.formUser === ''){
+       this.errorInfo = '请指定表单中的人员组件'
+       err.push(`${this.config.name} 审批人为表单中人员,但未指定`)
+       return false
+     }
+      return true;
+    },
+    validate_REFUSE(err){
+      return true;
+    },
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

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