Commit dffd175ccf4e7782a13e12b56cfaf096f14fc763

Authored by YunaiV
1 parent 430f1369

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

yudao-admin-ui/src/utils/generator/README.md 0 → 100644
  1 +【add by 芋道源码】来自 https://github.com/JakHuang/form-generator/tree/dev/src/components/generator 目录
0 2 \ No newline at end of file
... ...
yudao-admin-ui/src/utils/index.js
... ... @@ -5,12 +5,12 @@ import { parseTime } from './ruoyi'
5 5 */
6 6 export function formatDate(cellValue) {
7 7 if (cellValue == null || cellValue == "") return "";
8   - var date = new Date(cellValue)
  8 + var date = new Date(cellValue)
9 9 var year = date.getFullYear()
10 10 var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
11   - var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
12   - var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
13   - var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
  11 + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
  12 + var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
  13 + var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
14 14 var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
15 15 return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
16 16 }
... ... @@ -250,26 +250,65 @@ export function debounce(func, wait, immediate) {
250 250 }
251 251 }
252 252  
253   -/**
254   - * This is just a simple version of deep copy
255   - * Has a lot of edge cases bug
256   - * If you want to use a perfect deep copy, use lodash's _.cloneDeep
257   - * @param {Object} source
258   - * @returns {Object}
259   - */
260   -export function deepClone(source) {
261   - if (!source && typeof source !== 'object') {
262   - throw new Error('error arguments', 'deepClone')
  253 +// /**
  254 +// * This is just a simple version of deep copy
  255 +// * Has a lot of edge cases bug
  256 +// * If you want to use a perfect deep copy, use lodash's _.cloneDeep
  257 +// * @param {Object} source
  258 +// * @returns {Object}
  259 +// */
  260 +// export function deepClone(source) {
  261 +// if (!source && typeof source !== 'object') {
  262 +// throw new Error('error arguments', 'deepClone')
  263 +// }
  264 +// const targetObj = source.constructor === Array ? [] : {}
  265 +// Object.keys(source).forEach(keys => {
  266 +// if (source[keys] && typeof source[keys] === 'object') {
  267 +// targetObj[keys] = deepClone(source[keys])
  268 +// } else {
  269 +// targetObj[keys] = source[keys]
  270 +// }
  271 +// })
  272 +// return targetObj
  273 +// }
  274 +
  275 +// 深拷贝对象
  276 +// 【add by 芋道源码】https://github.com/JakHuang/form-generator/blob/dev/src/utils/index.js#L107
  277 +export function deepClone(obj) {
  278 + const _toString = Object.prototype.toString
  279 +
  280 + // null, undefined, non-object, function
  281 + if (!obj || typeof obj !== 'object') {
  282 + return obj
263 283 }
264   - const targetObj = source.constructor === Array ? [] : {}
265   - Object.keys(source).forEach(keys => {
266   - if (source[keys] && typeof source[keys] === 'object') {
267   - targetObj[keys] = deepClone(source[keys])
268   - } else {
269   - targetObj[keys] = source[keys]
270   - }
271   - })
272   - return targetObj
  284 +
  285 + // DOM Node
  286 + if (obj.nodeType && 'cloneNode' in obj) {
  287 + return obj.cloneNode(true)
  288 + }
  289 +
  290 + // Date
  291 + if (_toString.call(obj) === '[object Date]') {
  292 + return new Date(obj.getTime())
  293 + }
  294 +
  295 + // RegExp
  296 + if (_toString.call(obj) === '[object RegExp]') {
  297 + const flags = []
  298 + if (obj.global) { flags.push('g') }
  299 + if (obj.multiline) { flags.push('m') }
  300 + if (obj.ignoreCase) { flags.push('i') }
  301 +
  302 + return new RegExp(obj.source, flags.join(''))
  303 + }
  304 +
  305 + const result = Array.isArray(obj) ? [] : obj.constructor ? new obj.constructor() : {}
  306 +
  307 + for (const key in obj) {
  308 + result[key] = deepClone(obj[key])
  309 + }
  310 +
  311 + return result
273 312 }
274 313  
275 314 /**
... ... @@ -330,7 +369,7 @@ export function makeMap(str, expectsLowerCase) {
330 369 ? val => map[val.toLowerCase()]
331 370 : val => map[val]
332 371 }
333   -
  372 +
334 373 export const exportDefault = 'export default '
335 374  
336 375 export const beautifierConf = {
... ... @@ -387,4 +426,4 @@ export function camelCase(str) {
387 426 export function isNumberStr(str) {
388 427 return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
389 428 }
390   -
  429 +
... ...
yudao-admin-ui/src/utils/parser/Parser.vue 0 → 100644
  1 +<script>
  2 +import { deepClone } from '@/utils/index'
  3 +// import render from '@/components/render/render.js'
  4 +import render from '../render/render.js' // edit by 芋道源码
  5 +
  6 +const ruleTrigger = {
  7 + 'el-input': 'blur',
  8 + 'el-input-number': 'blur',
  9 + 'el-select': 'change',
  10 + 'el-radio-group': 'change',
  11 + 'el-checkbox-group': 'change',
  12 + 'el-cascader': 'change',
  13 + 'el-time-picker': 'change',
  14 + 'el-date-picker': 'change',
  15 + 'el-rate': 'change'
  16 +}
  17 +
  18 +const layouts = {
  19 + colFormItem(h, scheme) {
  20 + const config = scheme.__config__
  21 + const listeners = buildListeners.call(this, scheme)
  22 +
  23 + let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
  24 + if (config.showLabel === false) labelWidth = '0'
  25 + return (
  26 + <el-col span={config.span}>
  27 + <el-form-item label-width={labelWidth} prop={scheme.__vModel__}
  28 + label={config.showLabel ? config.label : ''}>
  29 + <render conf={scheme} on={listeners} />
  30 + </el-form-item>
  31 + </el-col>
  32 + )
  33 + },
  34 + rowFormItem(h, scheme) {
  35 + let child = renderChildren.apply(this, arguments)
  36 + if (scheme.type === 'flex') {
  37 + child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}>
  38 + {child}
  39 + </el-row>
  40 + }
  41 + return (
  42 + <el-col span={scheme.span}>
  43 + <el-row gutter={scheme.gutter}>
  44 + {child}
  45 + </el-row>
  46 + </el-col>
  47 + )
  48 + }
  49 +}
  50 +
  51 +function renderFrom(h) {
  52 + const { formConfCopy } = this
  53 +
  54 + return (
  55 + <el-row gutter={formConfCopy.gutter}>
  56 + <el-form
  57 + size={formConfCopy.size}
  58 + label-position={formConfCopy.labelPosition}
  59 + disabled={formConfCopy.disabled}
  60 + label-width={`${formConfCopy.labelWidth}px`}
  61 + ref={formConfCopy.formRef}
  62 + // model不能直接赋值 https://github.com/vuejs/jsx/issues/49#issuecomment-472013664
  63 + props={{ model: this[formConfCopy.formModel] }}
  64 + rules={this[formConfCopy.formRules]}
  65 + >
  66 + {renderFormItem.call(this, h, formConfCopy.fields)}
  67 + {formConfCopy.formBtns && formBtns.call(this, h)}
  68 + </el-form>
  69 + </el-row>
  70 + )
  71 +}
  72 +
  73 +function formBtns(h) {
  74 + return <el-col>
  75 + <el-form-item size="large">
  76 + <el-button type="primary" onClick={this.submitForm}>提交</el-button>
  77 + <el-button onClick={this.resetForm}>重置</el-button>
  78 + </el-form-item>
  79 + </el-col>
  80 +}
  81 +
  82 +function renderFormItem(h, elementList) {
  83 + return elementList.map(scheme => {
  84 + const config = scheme.__config__
  85 + const layout = layouts[config.layout]
  86 +
  87 + if (layout) {
  88 + return layout.call(this, h, scheme)
  89 + }
  90 + throw new Error(`没有与${config.layout}匹配的layout`)
  91 + })
  92 +}
  93 +
  94 +function renderChildren(h, scheme) {
  95 + const config = scheme.__config__
  96 + if (!Array.isArray(config.children)) return null
  97 + return renderFormItem.call(this, h, config.children)
  98 +}
  99 +
  100 +function setValue(event, config, scheme) {
  101 + this.$set(config, 'defaultValue', event)
  102 + this.$set(this[this.formConf.formModel], scheme.__vModel__, event)
  103 +}
  104 +
  105 +function buildListeners(scheme) {
  106 + const config = scheme.__config__
  107 + const methods = this.formConf.__methods__ || {}
  108 + const listeners = {}
  109 +
  110 + // 给__methods__中的方法绑定this和event
  111 + Object.keys(methods).forEach(key => {
  112 + listeners[key] = event => methods[key].call(this, event)
  113 + })
  114 + // 响应 render.js 中的 vModel $emit('input', val)
  115 + listeners.input = event => setValue.call(this, event, config, scheme)
  116 +
  117 + return listeners
  118 +}
  119 +
  120 +export default {
  121 + components: {
  122 + render
  123 + },
  124 + props: {
  125 + formConf: {
  126 + type: Object,
  127 + required: true
  128 + }
  129 + },
  130 + data() {
  131 + const data = {
  132 + formConfCopy: deepClone(this.formConf),
  133 + [this.formConf.formModel]: {},
  134 + [this.formConf.formRules]: {}
  135 + }
  136 + this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel])
  137 + this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules])
  138 + return data
  139 + },
  140 + methods: {
  141 + initFormData(componentList, formData) {
  142 + componentList.forEach(cur => {
  143 + const config = cur.__config__
  144 + if (cur.__vModel__) formData[cur.__vModel__] = config.defaultValue
  145 + // debugger
  146 + if (config.children) this.initFormData(config.children, formData)
  147 + })
  148 + },
  149 + buildRules(componentList, rules) {
  150 + componentList.forEach(cur => {
  151 + const config = cur.__config__
  152 + if (Array.isArray(config.regList)) {
  153 + if (config.required) {
  154 + const required = { required: config.required, message: cur.placeholder }
  155 + if (Array.isArray(config.defaultValue)) {
  156 + required.type = 'array'
  157 + required.message = `请至少选择一个${config.label}`
  158 + }
  159 + required.message === undefined && (required.message = `${config.label}不能为空`)
  160 + config.regList.push(required)
  161 + }
  162 + rules[cur.__vModel__] = config.regList.map(item => {
  163 + item.pattern && (item.pattern = eval(item.pattern))
  164 + item.trigger = ruleTrigger && ruleTrigger[config.tag]
  165 + return item
  166 + })
  167 + }
  168 + if (config.children) this.buildRules(config.children, rules)
  169 + })
  170 + },
  171 + resetForm() {
  172 + this.formConfCopy = deepClone(this.formConf)
  173 + this.$refs[this.formConf.formRef].resetFields()
  174 + },
  175 + submitForm() {
  176 + this.$refs[this.formConf.formRef].validate(valid => {
  177 + if (!valid) return false
  178 + // 触发sumit事件
  179 + this.$emit('submit', this[this.formConf.formModel])
  180 + return true
  181 + })
  182 + }
  183 + },
  184 + render(h) {
  185 + return renderFrom.call(this, h)
  186 + }
  187 +}
  188 +</script>
... ...
yudao-admin-ui/src/utils/parser/README.md 0 → 100644
  1 +## form-generator JSON 解析器
  2 +>用于将form-generator导出的JSON解析成一个表单。
  3 +
  4 +### 安装组件
  5 +```
  6 +npm i form-gen-parser
  7 +```
  8 +或者
  9 +```
  10 +yarn add form-gen-parser
  11 +```
  12 +
  13 +### 使用示例
  14 +> [查看在线示例](https://mrhj.gitee.io/form-generator/#/parser)
  15 +
  16 +示例代码:
  17 +> [src\components\parser\example\Index.vue](https://github.com/JakHuang/form-generator/blob/dev/src/components/parser/example/Index.vue)
  18 +
  19 +【add by 芋道源码】https://github.com/JakHuang/form-generator/blob/dev/src/components/parser/
0 20 \ No newline at end of file
... ...
yudao-admin-ui/src/utils/parser/example/Index.vue 0 → 100644
  1 +<template>
  2 + <div class="test-form">
  3 + <parser :form-conf="formConf" @submit="sumbitForm1" />
  4 + <parser :key="key2" :form-conf="formConf" @submit="sumbitForm2" />
  5 + <el-button @click="change">
  6 + change
  7 + </el-button>
  8 + </div>
  9 +</template>
  10 +
  11 +<script>
  12 +import Parser from '../Parser'
  13 +
  14 +// 若parser是通过安装npm方式集成到项目中的,使用此行引入
  15 +// import Parser from 'form-gen-parser'
  16 +
  17 +export default {
  18 + components: {
  19 + Parser
  20 + },
  21 + props: {},
  22 + data() {
  23 + return {
  24 + key2: +new Date(),
  25 + formConf: {
  26 + fields: [
  27 + {
  28 + __config__: {
  29 + label: '单行文本',
  30 + labelWidth: null,
  31 + showLabel: true,
  32 + changeTag: true,
  33 + tag: 'el-input',
  34 + tagIcon: 'input',
  35 + required: true,
  36 + layout: 'colFormItem',
  37 + span: 24,
  38 + document: 'https://element.eleme.cn/#/zh-CN/component/input',
  39 + regList: [
  40 + {
  41 + pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
  42 + message: '手机号格式错误'
  43 + }
  44 + ]
  45 + },
  46 + __slot__: {
  47 + prepend: '',
  48 + append: ''
  49 + },
  50 + __vModel__: 'mobile',
  51 + placeholder: '请输入手机号',
  52 + style: {
  53 + width: '100%'
  54 + },
  55 + clearable: true,
  56 + 'prefix-icon': 'el-icon-mobile',
  57 + 'suffix-icon': '',
  58 + maxlength: 11,
  59 + 'show-word-limit': true,
  60 + readonly: false,
  61 + disabled: false
  62 + },
  63 + {
  64 + __config__: {
  65 + label: '日期范围',
  66 + tag: 'el-date-picker',
  67 + tagIcon: 'date-range',
  68 + defaultValue: null,
  69 + span: 24,
  70 + showLabel: true,
  71 + labelWidth: null,
  72 + required: true,
  73 + layout: 'colFormItem',
  74 + regList: [],
  75 + changeTag: true,
  76 + document:
  77 + 'https://element.eleme.cn/#/zh-CN/component/date-picker',
  78 + formId: 101,
  79 + renderKey: 1585980082729
  80 + },
  81 + style: {
  82 + width: '100%'
  83 + },
  84 + type: 'daterange',
  85 + 'range-separator': '至',
  86 + 'start-placeholder': '开始日期',
  87 + 'end-placeholder': '结束日期',
  88 + disabled: false,
  89 + clearable: true,
  90 + format: 'yyyy-MM-dd',
  91 + 'value-format': 'yyyy-MM-dd',
  92 + readonly: false,
  93 + __vModel__: 'field101'
  94 + },
  95 + {
  96 + __config__: {
  97 + layout: 'rowFormItem',
  98 + tagIcon: 'row',
  99 + label: '行容器',
  100 + layoutTree: true,
  101 + children: [
  102 + {
  103 + __config__: {
  104 + label: '评分',
  105 + tag: 'el-rate',
  106 + tagIcon: 'rate',
  107 + defaultValue: 0,
  108 + span: 24,
  109 + showLabel: true,
  110 + labelWidth: null,
  111 + layout: 'colFormItem',
  112 + required: true,
  113 + regList: [],
  114 + changeTag: true,
  115 + document: 'https://element.eleme.cn/#/zh-CN/component/rate',
  116 + formId: 102,
  117 + renderKey: 1586839671259
  118 + },
  119 + style: {},
  120 + max: 5,
  121 + 'allow-half': false,
  122 + 'show-text': false,
  123 + 'show-score': false,
  124 + disabled: false,
  125 + __vModel__: 'field102'
  126 + }
  127 + ],
  128 + document: 'https://element.eleme.cn/#/zh-CN/component/layout',
  129 + formId: 101,
  130 + span: 24,
  131 + renderKey: 1586839668999,
  132 + componentName: 'row101',
  133 + gutter: 15
  134 + },
  135 + type: 'default',
  136 + justify: 'start',
  137 + align: 'top'
  138 + },
  139 + {
  140 + __config__: {
  141 + label: '按钮',
  142 + showLabel: true,
  143 + changeTag: true,
  144 + labelWidth: null,
  145 + tag: 'el-button',
  146 + tagIcon: 'button',
  147 + span: 24,
  148 + layout: 'colFormItem',
  149 + document: 'https://element.eleme.cn/#/zh-CN/component/button',
  150 + renderKey: 1594288459289
  151 + },
  152 + __slot__: {
  153 + default: '测试按钮1'
  154 + },
  155 + type: 'primary',
  156 + icon: 'el-icon-search',
  157 + round: false,
  158 + size: 'medium',
  159 + plain: false,
  160 + circle: false,
  161 + disabled: false,
  162 + on: {
  163 + click: 'clickTestButton1'
  164 + }
  165 + }
  166 + ],
  167 + __methods__: {
  168 + clickTestButton1() {
  169 + console.log(
  170 + `%c【测试按钮1】点击事件里可以访问当前表单:
  171 + 1) formModel='formData', 所以this.formData可以拿到当前表单的model
  172 + 2) formRef='elForm', 所以this.$refs.elForm可以拿到当前表单的ref(vue组件)
  173 + `,
  174 + 'color:#409EFF;font-size: 15px'
  175 + )
  176 + console.log('表单的Model:', this.formData)
  177 + console.log('表单的ref:', this.$refs.elForm)
  178 + }
  179 + },
  180 + formRef: 'elForm',
  181 + formModel: 'formData',
  182 + size: 'small',
  183 + labelPosition: 'right',
  184 + labelWidth: 100,
  185 + formRules: 'rules',
  186 + gutter: 15,
  187 + disabled: false,
  188 + span: 24,
  189 + formBtns: true,
  190 + unFocusedComponentBorder: false
  191 + },
  192 + formConf2: {
  193 + fields: [
  194 + {
  195 + __config__: {
  196 + label: '单行文本',
  197 + labelWidth: null,
  198 + showLabel: true,
  199 + changeTag: true,
  200 + tag: 'el-input',
  201 + tagIcon: 'input',
  202 + required: true,
  203 + layout: 'colFormItem',
  204 + span: 24,
  205 + document: 'https://element.eleme.cn/#/zh-CN/component/input',
  206 + regList: [
  207 + {
  208 + pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
  209 + message: '手机号格式错误'
  210 + }
  211 + ]
  212 + },
  213 + __slot__: {
  214 + prepend: '',
  215 + append: ''
  216 + },
  217 + __vModel__: 'mobile',
  218 + placeholder: '请输入手机号',
  219 + style: {
  220 + width: '100%'
  221 + },
  222 + clearable: true,
  223 + 'prefix-icon': 'el-icon-mobile',
  224 + 'suffix-icon': '',
  225 + maxlength: 11,
  226 + 'show-word-limit': true,
  227 + readonly: false,
  228 + disabled: false
  229 + },
  230 + {
  231 + __config__: {
  232 + label: '日期范围',
  233 + tag: 'el-date-picker',
  234 + tagIcon: 'date-range',
  235 + defaultValue: null,
  236 + span: 24,
  237 + showLabel: true,
  238 + labelWidth: null,
  239 + required: true,
  240 + layout: 'colFormItem',
  241 + regList: [],
  242 + changeTag: true,
  243 + document:
  244 + 'https://element.eleme.cn/#/zh-CN/component/date-picker',
  245 + formId: 101,
  246 + renderKey: 1585980082729
  247 + },
  248 + style: {
  249 + width: '100%'
  250 + },
  251 + type: 'daterange',
  252 + 'range-separator': '至',
  253 + 'start-placeholder': '开始日期',
  254 + 'end-placeholder': '结束日期',
  255 + disabled: false,
  256 + clearable: true,
  257 + format: 'yyyy-MM-dd',
  258 + 'value-format': 'yyyy-MM-dd',
  259 + readonly: false,
  260 + __vModel__: 'field101'
  261 + }
  262 + ],
  263 + formRef: 'elForm',
  264 + formModel: 'formData',
  265 + size: 'small',
  266 + labelPosition: 'right',
  267 + labelWidth: 100,
  268 + formRules: 'rules',
  269 + gutter: 15,
  270 + disabled: false,
  271 + span: 24,
  272 + formBtns: true,
  273 + unFocusedComponentBorder: false
  274 + }
  275 + }
  276 + },
  277 + computed: {},
  278 + watch: {},
  279 + created() {},
  280 + mounted() {
  281 + // 表单数据回填,模拟异步请求场景
  282 + setTimeout(() => {
  283 + // 请求回来的表单数据
  284 + const data = {
  285 + mobile: '18836662555'
  286 + }
  287 + // 回填数据
  288 + this.fillFormData(this.formConf, data)
  289 + // 更新表单
  290 + this.key2 = +new Date()
  291 + }, 2000)
  292 + },
  293 + methods: {
  294 + fillFormData(form, data) {
  295 + form.fields.forEach(item => {
  296 + const val = data[item.__vModel__]
  297 + if (val) {
  298 + item.__config__.defaultValue = val
  299 + }
  300 + })
  301 + },
  302 + change() {
  303 + this.key2 = +new Date()
  304 + const t = this.formConf
  305 + this.formConf = this.formConf2
  306 + this.formConf2 = t
  307 + },
  308 + sumbitForm1(data) {
  309 + console.log('sumbitForm1提交数据:', data)
  310 + },
  311 + sumbitForm2(data) {
  312 + console.log('sumbitForm2提交数据:', data)
  313 + }
  314 + }
  315 +}
  316 +</script>
  317 +
  318 +<style lang="scss" scoped>
  319 +.test-form {
  320 + margin: 15px auto;
  321 + width: 800px;
  322 + padding: 15px;
  323 +}
  324 +</style>
... ...
yudao-admin-ui/src/utils/parser/index.js 0 → 100644
  1 +import Parser from './Parser'
  2 +
  3 +export default Parser
... ...
yudao-admin-ui/src/utils/parser/package.json 0 → 100644
  1 +{
  2 + "name": "form-gen-parser",
  3 + "version": "1.0.3",
  4 + "description": "表单json解析器",
  5 + "main": "lib/form-gen-parser.umd.js",
  6 + "directories": {
  7 + "example": "example"
  8 + },
  9 + "scripts": {
  10 + "test": "echo \"Error: no test specified\" && exit 1"
  11 + },
  12 + "repository": {
  13 + "type": "git",
  14 + "url": "git+https://github.com/JakHuang/form-generator.git"
  15 + },
  16 + "dependencies": {
  17 + "form-gen-render": "^1.0.0"
  18 + },
  19 + "author": "jakHuang",
  20 + "license": "MIT",
  21 + "bugs": {
  22 + "url": "https://github.com/JakHuang/form-generator/issues"
  23 + },
  24 + "homepage": "https://github.com/JakHuang/form-generator/blob/dev/src/components/parser"
  25 +}
... ...
yudao-admin-ui/src/utils/render/README.md 0 → 100644
  1 +【add by 芋道源码】https://github.com/JakHuang/form-generator/blob/dev/src/components/render/
0 2 \ No newline at end of file
... ...
yudao-admin-ui/src/utils/render/package.json 0 → 100644
  1 +{
  2 + "name": "form-gen-render",
  3 + "version": "1.0.4",
  4 + "description": "表单核心render",
  5 + "main": "lib/form-gen-render.umd.js",
  6 + "scripts": {
  7 + "test": "echo \"Error: no test specified\" && exit 1"
  8 + },
  9 + "repository": {
  10 + "type": "git",
  11 + "url": "git+https://github.com/JakHuang/form-generator.git"
  12 + },
  13 + "author": "jakhuang",
  14 + "license": "MIT",
  15 + "bugs": {
  16 + "url": "https://github.com/JakHuang/form-generator/issues"
  17 + },
  18 + "homepage": "https://github.com/JakHuang/form-generator#readme"
  19 +}
... ...
yudao-admin-ui/src/utils/render/render.js 0 → 100644
  1 +import { deepClone } from '@/utils/index'
  2 +
  3 +const componentChild = {}
  4 +/**
  5 + * 将./slots中的文件挂载到对象componentChild上
  6 + * 文件名为key,对应JSON配置中的__config__.tag
  7 + * 文件内容为value,解析JSON配置中的__slot__
  8 + */
  9 +const slotsFiles = require.context('./slots', false, /\.js$/)
  10 +const keys = slotsFiles.keys() || []
  11 +keys.forEach(key => {
  12 + const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
  13 + const value = slotsFiles(key).default
  14 + componentChild[tag] = value
  15 +})
  16 +
  17 +function vModel(dataObject, defaultValue) {
  18 + dataObject.props.value = defaultValue
  19 +
  20 + dataObject.on.input = val => {
  21 + this.$emit('input', val)
  22 + }
  23 +}
  24 +
  25 +function mountSlotFiles(h, confClone, children) {
  26 + const childObjs = componentChild[confClone.__config__.tag]
  27 + if (childObjs) {
  28 + Object.keys(childObjs).forEach(key => {
  29 + const childFunc = childObjs[key]
  30 + if (confClone.__slot__ && confClone.__slot__[key]) {
  31 + children.push(childFunc(h, confClone, key))
  32 + }
  33 + })
  34 + }
  35 +}
  36 +
  37 +function emitEvents(confClone) {
  38 + ['on', 'nativeOn'].forEach(attr => {
  39 + const eventKeyList = Object.keys(confClone[attr] || {})
  40 + eventKeyList.forEach(key => {
  41 + const val = confClone[attr][key]
  42 + if (typeof val === 'string') {
  43 + confClone[attr][key] = event => this.$emit(val, event)
  44 + }
  45 + })
  46 + })
  47 +}
  48 +
  49 +function buildDataObject(confClone, dataObject) {
  50 + Object.keys(confClone).forEach(key => {
  51 + const val = confClone[key]
  52 + if (key === '__vModel__') {
  53 + vModel.call(this, dataObject, confClone.__config__.defaultValue)
  54 + } else if (dataObject[key] !== undefined) {
  55 + if (dataObject[key] === null
  56 + || dataObject[key] instanceof RegExp
  57 + || ['boolean', 'string', 'number', 'function'].includes(typeof dataObject[key])) {
  58 + dataObject[key] = val
  59 + } else if (Array.isArray(dataObject[key])) {
  60 + dataObject[key] = [...dataObject[key], ...val]
  61 + } else {
  62 + dataObject[key] = { ...dataObject[key], ...val }
  63 + }
  64 + } else {
  65 + dataObject.attrs[key] = val
  66 + }
  67 + })
  68 +
  69 + // 清理属性
  70 + clearAttrs(dataObject)
  71 +}
  72 +
  73 +function clearAttrs(dataObject) {
  74 + delete dataObject.attrs.__config__
  75 + delete dataObject.attrs.__slot__
  76 + delete dataObject.attrs.__methods__
  77 +}
  78 +
  79 +function makeDataObject() {
  80 + // 深入数据对象:
  81 + // 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
  82 + return {
  83 + class: {},
  84 + attrs: {},
  85 + props: {},
  86 + domProps: {},
  87 + nativeOn: {},
  88 + on: {},
  89 + style: {},
  90 + directives: [],
  91 + scopedSlots: {},
  92 + slot: null,
  93 + key: null,
  94 + ref: null,
  95 + refInFor: true
  96 + }
  97 +}
  98 +
  99 +export default {
  100 + props: {
  101 + conf: {
  102 + type: Object,
  103 + required: true
  104 + }
  105 + },
  106 + render(h) {
  107 + const dataObject = makeDataObject()
  108 + const confClone = deepClone(this.conf)
  109 + const children = this.$slots.default || []
  110 +
  111 + // 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码
  112 + mountSlotFiles.call(this, h, confClone, children)
  113 +
  114 + // 将字符串类型的事件,发送为消息
  115 + emitEvents.call(this, confClone)
  116 +
  117 + // 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)”
  118 + buildDataObject.call(this, confClone, dataObject)
  119 +
  120 + return h(this.conf.__config__.tag, dataObject, children)
  121 + }
  122 +}
... ...
yudao-admin-ui/src/utils/render/slots/el-button.js 0 → 100644
  1 +export default {
  2 + default(h, conf, key) {
  3 + return conf.__slot__[key]
  4 + }
  5 +}
... ...
yudao-admin-ui/src/utils/render/slots/el-checkbox-group.js 0 → 100644
  1 +export default {
  2 + options(h, conf, key) {
  3 + const list = []
  4 + conf.__slot__.options.forEach(item => {
  5 + if (conf.__config__.optionType === 'button') {
  6 + list.push(<el-checkbox-button label={item.value}>{item.label}</el-checkbox-button>)
  7 + } else {
  8 + list.push(<el-checkbox label={item.value} border={conf.border}>{item.label}</el-checkbox>)
  9 + }
  10 + })
  11 + return list
  12 + }
  13 +}
... ...
yudao-admin-ui/src/utils/render/slots/el-input.js 0 → 100644
  1 +export default {
  2 + prepend(h, conf, key) {
  3 + return <template slot="prepend">{conf.__slot__[key]}</template>
  4 + },
  5 + append(h, conf, key) {
  6 + return <template slot="append">{conf.__slot__[key]}</template>
  7 + }
  8 +}
... ...
yudao-admin-ui/src/utils/render/slots/el-radio-group.js 0 → 100644
  1 +export default {
  2 + options(h, conf, key) {
  3 + const list = []
  4 + conf.__slot__.options.forEach(item => {
  5 + if (conf.__config__.optionType === 'button') {
  6 + list.push(<el-radio-button label={item.value}>{item.label}</el-radio-button>)
  7 + } else {
  8 + list.push(<el-radio label={item.value} border={conf.border}>{item.label}</el-radio>)
  9 + }
  10 + })
  11 + return list
  12 + }
  13 +}
... ...
yudao-admin-ui/src/utils/render/slots/el-select.js 0 → 100644
  1 +export default {
  2 + options(h, conf, key) {
  3 + const list = []
  4 + conf.__slot__.options.forEach(item => {
  5 + list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
  6 + })
  7 + return list
  8 + }
  9 +}
... ...
yudao-admin-ui/src/utils/render/slots/el-upload.js 0 → 100644
  1 +export default {
  2 + 'list-type': (h, conf, key) => {
  3 + const list = []
  4 + const config = conf.__config__
  5 + if (conf['list-type'] === 'picture-card') {
  6 + list.push(<i class="el-icon-plus"></i>)
  7 + } else {
  8 + list.push(<el-button size="small" type="primary" icon="el-icon-upload">{config.buttonText}</el-button>)
  9 + }
  10 + if (config.showTip) {
  11 + list.push(
  12 + <div slot="tip" class="el-upload__tip">只能上传不超过 {config.fileSize}{config.sizeUnit} 的{conf.accept}文件</div>
  13 + )
  14 + }
  15 + return list
  16 + }
  17 +}
... ...
yudao-admin-ui/src/views/bpm/form/index.vue
... ... @@ -38,6 +38,8 @@
38 38 </el-table-column>
39 39 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
40 40 <template slot-scope="scope">
  41 + <el-button size="mini" type="text" icon="el-icon-edit" @click="handleDetail(scope.row)"
  42 + v-hasPermi="['bpm:form:query']">详情</el-button>
41 43 <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
42 44 v-hasPermi="['bpm:form:update']">修改</el-button>
43 45 <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
... ... @@ -48,15 +50,24 @@
48 50 <!-- 分页组件 -->
49 51 <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
50 52 @pagination="getList"/>
  53 +
  54 + <!--表单配置详情-->
  55 + <el-dialog title="表单详情" :visible.sync="detailOpen" width="50%" append-to-body>
  56 + <div class="test-form">
  57 + <parser :key="new Date().getTime()" :form-conf="detailForm" />
  58 + </div>
  59 + </el-dialog>
51 60 </div>
52 61 </template>
53 62  
54 63 <script>
55   -import { deleteForm, getFormPage} from "@/api/bpm/form";
  64 +import {deleteForm, getForm, getFormPage} from "@/api/bpm/form";
  65 +import Parser from '@/utils/parser/Parser'
56 66  
57 67 export default {
58 68 name: "Form",
59 69 components: {
  70 + Parser
60 71 },
61 72 data() {
62 73 return {
... ... @@ -74,6 +85,9 @@ export default {
74 85 pageSize: 10,
75 86 name: null,
76 87 },
  88 + // 表单详情
  89 + detailOpen: false,
  90 + detailForm: {}
77 91 };
78 92 },
79 93 created() {
... ... @@ -102,6 +116,192 @@ export default {
102 116 this.resetForm("queryForm");
103 117 this.handleQuery();
104 118 },
  119 + /** 详情按钮操作 */
  120 + handleDetail(row) {
  121 + this.detailOpen = true
  122 + getForm(row.id).then(response => {
  123 + const data = response.data
  124 + // this.detailForm = {
  125 + // ...JSON.parse(data.conf),
  126 + // fields: this.decodeFields(data.fields)
  127 + // }
  128 + this.detailForm = {
  129 + fields: [
  130 + {
  131 + __config__: {
  132 + label: '单行文本',
  133 + labelWidth: null,
  134 + showLabel: true,
  135 + changeTag: true,
  136 + tag: 'el-input',
  137 + tagIcon: 'input',
  138 + required: true,
  139 + layout: 'colFormItem',
  140 + span: 24,
  141 + document: 'https://element.eleme.cn/#/zh-CN/component/input',
  142 + regList: [
  143 + {
  144 + pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
  145 + message: '手机号格式错误'
  146 + }
  147 + ]
  148 + },
  149 + __slot__: {
  150 + prepend: '',
  151 + append: ''
  152 + },
  153 + __vModel__: 'mobile',
  154 + placeholder: '请输入手机号',
  155 + style: {
  156 + width: '100%'
  157 + },
  158 + clearable: true,
  159 + 'prefix-icon': 'el-icon-mobile',
  160 + 'suffix-icon': '',
  161 + maxlength: 11,
  162 + 'show-word-limit': true,
  163 + readonly: false,
  164 + disabled: false
  165 + },
  166 + {
  167 + __config__: {
  168 + label: '日期范围',
  169 + tag: 'el-date-picker',
  170 + tagIcon: 'date-range',
  171 + defaultValue: null,
  172 + span: 24,
  173 + showLabel: true,
  174 + labelWidth: null,
  175 + required: true,
  176 + layout: 'colFormItem',
  177 + regList: [],
  178 + changeTag: true,
  179 + document:
  180 + 'https://element.eleme.cn/#/zh-CN/component/date-picker',
  181 + formId: 101,
  182 + renderKey: 1585980082729
  183 + },
  184 + style: {
  185 + width: '100%'
  186 + },
  187 + type: 'daterange',
  188 + 'range-separator': '至',
  189 + 'start-placeholder': '开始日期',
  190 + 'end-placeholder': '结束日期',
  191 + disabled: false,
  192 + clearable: true,
  193 + format: 'yyyy-MM-dd',
  194 + 'value-format': 'yyyy-MM-dd',
  195 + readonly: false,
  196 + __vModel__: 'field101'
  197 + },
  198 + {
  199 + __config__: {
  200 + layout: 'rowFormItem',
  201 + tagIcon: 'row',
  202 + label: '行容器',
  203 + layoutTree: true,
  204 + children: [
  205 + {
  206 + __config__: {
  207 + label: '评分',
  208 + tag: 'el-rate',
  209 + tagIcon: 'rate',
  210 + defaultValue: 0,
  211 + span: 24,
  212 + showLabel: true,
  213 + labelWidth: null,
  214 + layout: 'colFormItem',
  215 + required: true,
  216 + regList: [],
  217 + changeTag: true,
  218 + document: 'https://element.eleme.cn/#/zh-CN/component/rate',
  219 + formId: 102,
  220 + renderKey: 1586839671259
  221 + },
  222 + style: {},
  223 + max: 5,
  224 + 'allow-half': false,
  225 + 'show-text': false,
  226 + 'show-score': false,
  227 + disabled: false,
  228 + __vModel__: 'field102'
  229 + }
  230 + ],
  231 + document: 'https://element.eleme.cn/#/zh-CN/component/layout',
  232 + formId: 101,
  233 + span: 24,
  234 + renderKey: 1586839668999,
  235 + componentName: 'row101',
  236 + gutter: 15
  237 + },
  238 + type: 'default',
  239 + justify: 'start',
  240 + align: 'top'
  241 + },
  242 + {
  243 + __config__: {
  244 + label: '按钮',
  245 + showLabel: true,
  246 + changeTag: true,
  247 + labelWidth: null,
  248 + tag: 'el-button',
  249 + tagIcon: 'button',
  250 + span: 24,
  251 + layout: 'colFormItem',
  252 + document: 'https://element.eleme.cn/#/zh-CN/component/button',
  253 + renderKey: 1594288459289
  254 + },
  255 + __slot__: {
  256 + default: '测试按钮1'
  257 + },
  258 + type: 'primary',
  259 + icon: 'el-icon-search',
  260 + round: false,
  261 + size: 'medium',
  262 + plain: false,
  263 + circle: false,
  264 + disabled: false,
  265 + on: {
  266 + click: 'clickTestButton1'
  267 + }
  268 + }
  269 + ],
  270 + __methods__: {
  271 + clickTestButton1() {
  272 + console.log(
  273 + `%c【测试按钮1】点击事件里可以访问当前表单:
  274 + 1) formModel='formData', 所以this.formData可以拿到当前表单的model
  275 + 2) formRef='elForm', 所以this.$refs.elForm可以拿到当前表单的ref(vue组件)
  276 + `,
  277 + 'color:#409EFF;font-size: 15px'
  278 + )
  279 + console.log('表单的Model:', this.formData)
  280 + console.log('表单的ref:', this.$refs.elForm)
  281 + }
  282 + },
  283 + formRef: 'elForm',
  284 + formModel: 'formData',
  285 + size: 'small',
  286 + labelPosition: 'right',
  287 + labelWidth: 100,
  288 + formRules: 'rules',
  289 + gutter: 15,
  290 + disabled: false,
  291 + span: 24,
  292 + formBtns: true,
  293 + unFocusedComponentBorder: false
  294 + }
  295 + console.log(this.detailForm)
  296 + });
  297 + },
  298 + decodeFields(fields) {
  299 + const drawingList = []
  300 + fields.forEach(item => {
  301 + drawingList.push(JSON.parse(item))
  302 + })
  303 + return drawingList
  304 + },
105 305 /** 新增按钮操作 */
106 306 handleAdd() {
107 307 this.$router.push({
... ...