Просмотр исходного кода

初始化 form 动态表单的详情,暂未接入数据

YunaiV 3 лет назад
Родитель
Сommit
dffd175ccf

+ 1 - 0
yudao-admin-ui/src/utils/generator/README.md

@@ -0,0 +1 @@
+【add by 芋道源码】来自 https://github.com/JakHuang/form-generator/tree/dev/src/components/generator 目录

+ 64 - 25
yudao-admin-ui/src/utils/index.js

@@ -5,12 +5,12 @@ import { parseTime } from './ruoyi'
  */
 export function formatDate(cellValue) {
   if (cellValue == null || cellValue == "") return "";
-  var date = new Date(cellValue) 
+  var date = new Date(cellValue)
   var year = date.getFullYear()
   var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
-  var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() 
-  var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() 
-  var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() 
+  var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
+  var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
+  var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
   var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
   return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
 }
@@ -250,26 +250,65 @@ export function debounce(func, wait, immediate) {
   }
 }
 
-/**
- * This is just a simple version of deep copy
- * Has a lot of edge cases bug
- * If you want to use a perfect deep copy, use lodash's _.cloneDeep
- * @param {Object} source
- * @returns {Object}
- */
-export function deepClone(source) {
-  if (!source && typeof source !== 'object') {
-    throw new Error('error arguments', 'deepClone')
+// /**
+//  * This is just a simple version of deep copy
+//  * Has a lot of edge cases bug
+//  * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+//  * @param {Object} source
+//  * @returns {Object}
+//  */
+// export function deepClone(source) {
+//   if (!source && typeof source !== 'object') {
+//     throw new Error('error arguments', 'deepClone')
+//   }
+//   const targetObj = source.constructor === Array ? [] : {}
+//   Object.keys(source).forEach(keys => {
+//     if (source[keys] && typeof source[keys] === 'object') {
+//       targetObj[keys] = deepClone(source[keys])
+//     } else {
+//       targetObj[keys] = source[keys]
+//     }
+//   })
+//   return targetObj
+// }
+
+// 深拷贝对象
+// 【add by 芋道源码】https://github.com/JakHuang/form-generator/blob/dev/src/utils/index.js#L107
+export function deepClone(obj) {
+  const _toString = Object.prototype.toString
+
+  // null, undefined, non-object, function
+  if (!obj || typeof obj !== 'object') {
+    return obj
   }
-  const targetObj = source.constructor === Array ? [] : {}
-  Object.keys(source).forEach(keys => {
-    if (source[keys] && typeof source[keys] === 'object') {
-      targetObj[keys] = deepClone(source[keys])
-    } else {
-      targetObj[keys] = source[keys]
-    }
-  })
-  return targetObj
+
+  // DOM Node
+  if (obj.nodeType && 'cloneNode' in obj) {
+    return obj.cloneNode(true)
+  }
+
+  // Date
+  if (_toString.call(obj) === '[object Date]') {
+    return new Date(obj.getTime())
+  }
+
+  // RegExp
+  if (_toString.call(obj) === '[object RegExp]') {
+    const flags = []
+    if (obj.global) { flags.push('g') }
+    if (obj.multiline) { flags.push('m') }
+    if (obj.ignoreCase) { flags.push('i') }
+
+    return new RegExp(obj.source, flags.join(''))
+  }
+
+  const result = Array.isArray(obj) ? [] : obj.constructor ? new obj.constructor() : {}
+
+  for (const key in obj) {
+    result[key] = deepClone(obj[key])
+  }
+
+  return result
 }
 
 /**
@@ -330,7 +369,7 @@ export function makeMap(str, expectsLowerCase) {
     ? val => map[val.toLowerCase()]
     : val => map[val]
 }
- 
+
 export const exportDefault = 'export default '
 
 export const beautifierConf = {
@@ -387,4 +426,4 @@ export function camelCase(str) {
 export function isNumberStr(str) {
   return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
 }
- 
+

+ 188 - 0
yudao-admin-ui/src/utils/parser/Parser.vue

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

+ 19 - 0
yudao-admin-ui/src/utils/parser/README.md

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

+ 324 - 0
yudao-admin-ui/src/utils/parser/example/Index.vue

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

+ 3 - 0
yudao-admin-ui/src/utils/parser/index.js

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

+ 25 - 0
yudao-admin-ui/src/utils/parser/package.json

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

+ 1 - 0
yudao-admin-ui/src/utils/render/README.md

@@ -0,0 +1 @@
+【add by 芋道源码】https://github.com/JakHuang/form-generator/blob/dev/src/components/render/

+ 19 - 0
yudao-admin-ui/src/utils/render/package.json

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

+ 122 - 0
yudao-admin-ui/src/utils/render/render.js

@@ -0,0 +1,122 @@
+import { deepClone } from '@/utils/index'
+
+const componentChild = {}
+/**
+ * 将./slots中的文件挂载到对象componentChild上
+ * 文件名为key,对应JSON配置中的__config__.tag
+ * 文件内容为value,解析JSON配置中的__slot__
+ */
+const slotsFiles = require.context('./slots', false, /\.js$/)
+const keys = slotsFiles.keys() || []
+keys.forEach(key => {
+  const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
+  const value = slotsFiles(key).default
+  componentChild[tag] = value
+})
+
+function vModel(dataObject, defaultValue) {
+  dataObject.props.value = defaultValue
+
+  dataObject.on.input = val => {
+    this.$emit('input', val)
+  }
+}
+
+function mountSlotFiles(h, confClone, children) {
+  const childObjs = componentChild[confClone.__config__.tag]
+  if (childObjs) {
+    Object.keys(childObjs).forEach(key => {
+      const childFunc = childObjs[key]
+      if (confClone.__slot__ && confClone.__slot__[key]) {
+        children.push(childFunc(h, confClone, key))
+      }
+    })
+  }
+}
+
+function emitEvents(confClone) {
+  ['on', 'nativeOn'].forEach(attr => {
+    const eventKeyList = Object.keys(confClone[attr] || {})
+    eventKeyList.forEach(key => {
+      const val = confClone[attr][key]
+      if (typeof val === 'string') {
+        confClone[attr][key] = event => this.$emit(val, event)
+      }
+    })
+  })
+}
+
+function buildDataObject(confClone, dataObject) {
+  Object.keys(confClone).forEach(key => {
+    const val = confClone[key]
+    if (key === '__vModel__') {
+      vModel.call(this, dataObject, confClone.__config__.defaultValue)
+    } else if (dataObject[key] !== undefined) {
+      if (dataObject[key] === null
+        || dataObject[key] instanceof RegExp
+        || ['boolean', 'string', 'number', 'function'].includes(typeof dataObject[key])) {
+        dataObject[key] = val
+      } else if (Array.isArray(dataObject[key])) {
+        dataObject[key] = [...dataObject[key], ...val]
+      } else {
+        dataObject[key] = { ...dataObject[key], ...val }
+      }
+    } else {
+      dataObject.attrs[key] = val
+    }
+  })
+
+  // 清理属性
+  clearAttrs(dataObject)
+}
+
+function clearAttrs(dataObject) {
+  delete dataObject.attrs.__config__
+  delete dataObject.attrs.__slot__
+  delete dataObject.attrs.__methods__
+}
+
+function makeDataObject() {
+  // 深入数据对象:
+  // https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
+  return {
+    class: {},
+    attrs: {},
+    props: {},
+    domProps: {},
+    nativeOn: {},
+    on: {},
+    style: {},
+    directives: [],
+    scopedSlots: {},
+    slot: null,
+    key: null,
+    ref: null,
+    refInFor: true
+  }
+}
+
+export default {
+  props: {
+    conf: {
+      type: Object,
+      required: true
+    }
+  },
+  render(h) {
+    const dataObject = makeDataObject()
+    const confClone = deepClone(this.conf)
+    const children = this.$slots.default || []
+
+    // 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码
+    mountSlotFiles.call(this, h, confClone, children)
+
+    // 将字符串类型的事件,发送为消息
+    emitEvents.call(this, confClone)
+
+    // 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)”
+    buildDataObject.call(this, confClone, dataObject)
+
+    return h(this.conf.__config__.tag, dataObject, children)
+  }
+}

+ 5 - 0
yudao-admin-ui/src/utils/render/slots/el-button.js

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

+ 13 - 0
yudao-admin-ui/src/utils/render/slots/el-checkbox-group.js

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

+ 8 - 0
yudao-admin-ui/src/utils/render/slots/el-input.js

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

+ 13 - 0
yudao-admin-ui/src/utils/render/slots/el-radio-group.js

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

+ 9 - 0
yudao-admin-ui/src/utils/render/slots/el-select.js

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

+ 17 - 0
yudao-admin-ui/src/utils/render/slots/el-upload.js

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

+ 201 - 1
yudao-admin-ui/src/views/bpm/form/index.vue

@@ -38,6 +38,8 @@
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleDetail(scope.row)"
+                     v-hasPermi="['bpm:form:query']">详情</el-button>
           <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
                      v-hasPermi="['bpm:form:update']">修改</el-button>
           <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
@@ -48,15 +50,24 @@
     <!-- 分页组件 -->
     <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
                 @pagination="getList"/>
+
+    <!--表单配置详情-->
+    <el-dialog title="表单详情" :visible.sync="detailOpen" width="50%" append-to-body>
+      <div class="test-form">
+        <parser :key="new Date().getTime()" :form-conf="detailForm" />
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { deleteForm, getFormPage} from "@/api/bpm/form";
+import {deleteForm, getForm, getFormPage} from "@/api/bpm/form";
+import Parser from '@/utils/parser/Parser'
 
 export default {
   name: "Form",
   components: {
+    Parser
   },
   data() {
     return {
@@ -74,6 +85,9 @@ export default {
         pageSize: 10,
         name: null,
       },
+      // 表单详情
+      detailOpen: false,
+      detailForm: {}
     };
   },
   created() {
@@ -102,6 +116,192 @@ export default {
       this.resetForm("queryForm");
       this.handleQuery();
     },
+    /** 详情按钮操作 */
+    handleDetail(row) {
+      this.detailOpen = true
+      getForm(row.id).then(response => {
+        const data = response.data
+        // this.detailForm = {
+        //   ...JSON.parse(data.conf),
+        //   fields: this.decodeFields(data.fields)
+        // }
+        this.detailForm = {
+          fields: [
+            {
+              __config__: {
+                label: '单行文本',
+                labelWidth: null,
+                showLabel: true,
+                changeTag: true,
+                tag: 'el-input',
+                tagIcon: 'input',
+                required: true,
+                layout: 'colFormItem',
+                span: 24,
+                document: 'https://element.eleme.cn/#/zh-CN/component/input',
+                regList: [
+                  {
+                    pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
+                    message: '手机号格式错误'
+                  }
+                ]
+              },
+              __slot__: {
+                prepend: '',
+                append: ''
+              },
+              __vModel__: 'mobile',
+              placeholder: '请输入手机号',
+              style: {
+                width: '100%'
+              },
+              clearable: true,
+              'prefix-icon': 'el-icon-mobile',
+              'suffix-icon': '',
+              maxlength: 11,
+              'show-word-limit': true,
+              readonly: false,
+              disabled: false
+            },
+            {
+              __config__: {
+                label: '日期范围',
+                tag: 'el-date-picker',
+                tagIcon: 'date-range',
+                defaultValue: null,
+                span: 24,
+                showLabel: true,
+                labelWidth: null,
+                required: true,
+                layout: 'colFormItem',
+                regList: [],
+                changeTag: true,
+                document:
+                  'https://element.eleme.cn/#/zh-CN/component/date-picker',
+                formId: 101,
+                renderKey: 1585980082729
+              },
+              style: {
+                width: '100%'
+              },
+              type: 'daterange',
+              'range-separator': '至',
+              'start-placeholder': '开始日期',
+              'end-placeholder': '结束日期',
+              disabled: false,
+              clearable: true,
+              format: 'yyyy-MM-dd',
+              'value-format': 'yyyy-MM-dd',
+              readonly: false,
+              __vModel__: 'field101'
+            },
+            {
+              __config__: {
+                layout: 'rowFormItem',
+                tagIcon: 'row',
+                label: '行容器',
+                layoutTree: true,
+                children: [
+                  {
+                    __config__: {
+                      label: '评分',
+                      tag: 'el-rate',
+                      tagIcon: 'rate',
+                      defaultValue: 0,
+                      span: 24,
+                      showLabel: true,
+                      labelWidth: null,
+                      layout: 'colFormItem',
+                      required: true,
+                      regList: [],
+                      changeTag: true,
+                      document: 'https://element.eleme.cn/#/zh-CN/component/rate',
+                      formId: 102,
+                      renderKey: 1586839671259
+                    },
+                    style: {},
+                    max: 5,
+                    'allow-half': false,
+                    'show-text': false,
+                    'show-score': false,
+                    disabled: false,
+                    __vModel__: 'field102'
+                  }
+                ],
+                document: 'https://element.eleme.cn/#/zh-CN/component/layout',
+                formId: 101,
+                span: 24,
+                renderKey: 1586839668999,
+                componentName: 'row101',
+                gutter: 15
+              },
+              type: 'default',
+              justify: 'start',
+              align: 'top'
+            },
+            {
+              __config__: {
+                label: '按钮',
+                showLabel: true,
+                changeTag: true,
+                labelWidth: null,
+                tag: 'el-button',
+                tagIcon: 'button',
+                span: 24,
+                layout: 'colFormItem',
+                document: 'https://element.eleme.cn/#/zh-CN/component/button',
+                renderKey: 1594288459289
+              },
+              __slot__: {
+                default: '测试按钮1'
+              },
+              type: 'primary',
+              icon: 'el-icon-search',
+              round: false,
+              size: 'medium',
+              plain: false,
+              circle: false,
+              disabled: false,
+              on: {
+                click: 'clickTestButton1'
+              }
+            }
+          ],
+          __methods__: {
+            clickTestButton1() {
+              console.log(
+                `%c【测试按钮1】点击事件里可以访问当前表单:
+                1) formModel='formData', 所以this.formData可以拿到当前表单的model
+                2) formRef='elForm', 所以this.$refs.elForm可以拿到当前表单的ref(vue组件)
+              `,
+                'color:#409EFF;font-size: 15px'
+              )
+              console.log('表单的Model:', this.formData)
+              console.log('表单的ref:', this.$refs.elForm)
+            }
+          },
+          formRef: 'elForm',
+          formModel: 'formData',
+          size: 'small',
+          labelPosition: 'right',
+          labelWidth: 100,
+          formRules: 'rules',
+          gutter: 15,
+          disabled: false,
+          span: 24,
+          formBtns: true,
+          unFocusedComponentBorder: false
+        }
+        console.log(this.detailForm)
+      });
+    },
+    decodeFields(fields) {
+      const drawingList = []
+      fields.forEach(item => {
+        drawingList.push(JSON.parse(item))
+      })
+      return drawingList
+    },
     /** 新增按钮操作 */
     handleAdd() {
       this.$router.push({