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

将form-create/designer的源码挪到src中以规避白屏错误

kagg886 3 месяцев назад
Родитель
Сommit
4e20aff51e
100 измененных файлов с 13972 добавлено и 1 удалено
  1. 1 1
      package.json
  2. 49 0
      src/components/form-create-designer/components/DragBox.vue
  3. 201 0
      src/components/form-create-designer/components/DragTool.vue
  4. 546 0
      src/components/form-create-designer/components/EventConfig.vue
  5. 206 0
      src/components/form-create-designer/components/FcDesigner.vue
  6. 263 0
      src/components/form-create-designer/components/FetchConfig.vue
  7. 153 0
      src/components/form-create-designer/components/FieldInput.vue
  8. 304 0
      src/components/form-create-designer/components/FnConfig.vue
  9. 251 0
      src/components/form-create-designer/components/FnEditor.vue
  10. 103 0
      src/components/form-create-designer/components/FnInput.vue
  11. 125 0
      src/components/form-create-designer/components/HtmlEditor.vue
  12. 93 0
      src/components/form-create-designer/components/JsonPreview.vue
  13. 72 0
      src/components/form-create-designer/components/PropsInput.vue
  14. 75 0
      src/components/form-create-designer/components/Required.vue
  15. 25 0
      src/components/form-create-designer/components/Row.vue
  16. 142 0
      src/components/form-create-designer/components/Struct.vue
  17. 121 0
      src/components/form-create-designer/components/StructEditor.vue
  18. 162 0
      src/components/form-create-designer/components/TableOptions.vue
  19. 166 0
      src/components/form-create-designer/components/TreeOptions.vue
  20. 140 0
      src/components/form-create-designer/components/TypeSelect.vue
  21. 244 0
      src/components/form-create-designer/components/Validate.vue
  22. 88 0
      src/components/form-create-designer/components/ValueInput.vue
  23. 45 0
      src/components/form-create-designer/components/Warning.vue
  24. 165 0
      src/components/form-create-designer/components/language/LanguageConfig.vue
  25. 188 0
      src/components/form-create-designer/components/language/LanguageInput.vue
  26. 242 0
      src/components/form-create-designer/components/style/BorderInput.vue
  27. 166 0
      src/components/form-create-designer/components/style/BoxSizeInput.vue
  28. 269 0
      src/components/form-create-designer/components/style/BoxSpaceInput.vue
  29. 60 0
      src/components/form-create-designer/components/style/ColorInput.vue
  30. 118 0
      src/components/form-create-designer/components/style/ConfigItem.vue
  31. 174 0
      src/components/form-create-designer/components/style/FontInput.vue
  32. 164 0
      src/components/form-create-designer/components/style/RadiusInput.vue
  33. 329 0
      src/components/form-create-designer/components/style/ShadowContent.vue
  34. 93 0
      src/components/form-create-designer/components/style/ShadowInput.vue
  35. 118 0
      src/components/form-create-designer/components/style/SizeInput.vue
  36. 263 0
      src/components/form-create-designer/components/style/StyleConfig.vue
  37. 210 0
      src/components/form-create-designer/components/table/Table.vue
  38. 658 0
      src/components/form-create-designer/components/table/TableView.vue
  39. 390 0
      src/components/form-create-designer/components/tableForm/TableForm.vue
  40. 101 0
      src/components/form-create-designer/components/tableForm/TableFormColumnView.vue
  41. 45 0
      src/components/form-create-designer/components/tableForm/TableFormView.vue
  42. 43 0
      src/components/form-create-designer/config/base/field.js
  43. 116 0
      src/components/form-create-designer/config/base/form.js
  44. 26 0
      src/components/form-create-designer/config/base/style.js
  45. 15 0
      src/components/form-create-designer/config/base/validate.js
  46. 68 0
      src/components/form-create-designer/config/index.js
  47. 24 0
      src/components/form-create-designer/config/menu.js
  48. 45 0
      src/components/form-create-designer/config/rule/alert.js
  49. 49 0
      src/components/form-create-designer/config/rule/button.js
  50. 40 0
      src/components/form-create-designer/config/rule/card.js
  51. 121 0
      src/components/form-create-designer/config/rule/cascader.js
  52. 68 0
      src/components/form-create-designer/config/rule/checkbox.js
  53. 86 0
      src/components/form-create-designer/config/rule/col.js
  54. 30 0
      src/components/form-create-designer/config/rule/collapse.js
  55. 36 0
      src/components/form-create-designer/config/rule/collapseItem.js
  56. 53 0
      src/components/form-create-designer/config/rule/color.js
  57. 70 0
      src/components/form-create-designer/config/rule/date.js
  58. 64 0
      src/components/form-create-designer/config/rule/dateRange.js
  59. 31 0
      src/components/form-create-designer/config/rule/divider.js
  60. 31 0
      src/components/form-create-designer/config/rule/editor.js
  61. 53 0
      src/components/form-create-designer/config/rule/group.js
  62. 52 0
      src/components/form-create-designer/config/rule/html.js
  63. 32 0
      src/components/form-create-designer/config/rule/image.js
  64. 62 0
      src/components/form-create-designer/config/rule/input.js
  65. 49 0
      src/components/form-create-designer/config/rule/number.js
  66. 52 0
      src/components/form-create-designer/config/rule/password.js
  67. 43 0
      src/components/form-create-designer/config/rule/radio.js
  68. 44 0
      src/components/form-create-designer/config/rule/rate.js
  69. 46 0
      src/components/form-create-designer/config/rule/row.js
  70. 70 0
      src/components/form-create-designer/config/rule/select.js
  71. 53 0
      src/components/form-create-designer/config/rule/slider.js
  72. 44 0
      src/components/form-create-designer/config/rule/space.js
  73. 47 0
      src/components/form-create-designer/config/rule/subForm.js
  74. 46 0
      src/components/form-create-designer/config/rule/switch.js
  75. 29 0
      src/components/form-create-designer/config/rule/tabPane.js
  76. 35 0
      src/components/form-create-designer/config/rule/table.js
  77. 79 0
      src/components/form-create-designer/config/rule/tableForm.js
  78. 43 0
      src/components/form-create-designer/config/rule/tableFormColumn.js
  79. 38 0
      src/components/form-create-designer/config/rule/tabs.js
  80. 79 0
      src/components/form-create-designer/config/rule/tag.js
  81. 50 0
      src/components/form-create-designer/config/rule/text.js
  82. 63 0
      src/components/form-create-designer/config/rule/textarea.js
  83. 62 0
      src/components/form-create-designer/config/rule/time.js
  84. 53 0
      src/components/form-create-designer/config/rule/timeRange.js
  85. 59 0
      src/components/form-create-designer/config/rule/transfer.js
  86. 70 0
      src/components/form-create-designer/config/rule/tree.js
  87. 77 0
      src/components/form-create-designer/config/rule/treeSelect.js
  88. 98 0
      src/components/form-create-designer/config/rule/upload.js
  89. 136 0
      src/components/form-create-designer/index.js
  90. 867 0
      src/components/form-create-designer/locale/en.js
  91. 868 0
      src/components/form-create-designer/locale/zh-cn.js
  92. BIN
      src/components/form-create-designer/style/fonts/fc-icons.woff
  93. 632 0
      src/components/form-create-designer/style/icon.css
  94. 759 0
      src/components/form-create-designer/style/index.css
  95. 9 0
      src/components/form-create-designer/utils/form.js
  96. 307 0
      src/components/form-create-designer/utils/highlight/highlight.min.js
  97. 80 0
      src/components/form-create-designer/utils/highlight/javascript.min.js
  98. 1 0
      src/components/form-create-designer/utils/highlight/style.css
  99. 29 0
      src/components/form-create-designer/utils/highlight/xml.min.js
  100. 412 0
      src/components/form-create-designer/utils/index.js

+ 1 - 1
package.json

@@ -26,7 +26,7 @@
     "@antv/g2plot": "2.4.20",
     "@element-plus/icons-vue": "2.0.9",
     "@form-create/component-wangeditor": "^2.6.2",
-    "@form-create/designer": "^1.1.9",
+    "@form-create/designer": "3.2.11",
     "@form-create/element-ui": "^3",
     "@guolao/vue-monaco-editor": "^1.5.5",
     "@logicflow/core": "^2.0.16",

+ 49 - 0
src/components/form-create-designer/components/DragBox.vue

@@ -0,0 +1,49 @@
+<script>
+import {defineComponent, h} from 'vue';
+import draggable from 'vuedraggable/src/vuedraggable';
+
+export default defineComponent({
+    name: 'DragBox',
+    props: ['rule', 'tag', 'formCreateInject', 'list'],
+    render(ctx) {
+        const attrs = {...ctx.$props.rule.props, ...ctx.$attrs};
+        let _class = '_fd-' + ctx.$props.tag + '-drag _fd-drag-box';
+        if (!Object.keys(ctx.$slots).length) {
+            _class += ' drag-holder';
+        }
+        attrs.class = _class;
+        attrs.modelValue = ctx.$props.list || [...ctx.$props.formCreateInject.children];
+
+        const keys = {};
+        if (ctx.$slots.default) {
+            const children = ctx.$slots.default();
+            children.forEach(v => {
+                if (v.key) {
+                    keys[v.key] = v;
+                }
+            })
+        }
+        return h(draggable, attrs, {
+            item: ({element, index}) => {
+                const key = element?.__fc__?.key;
+                if (key) {
+                    let vnode = keys['_' + element.slot];
+                    if (vnode) {
+                        vnode.children.forEach(v => {
+                            if (v.key === key + 'fc') {
+                                vnode = v
+                            }
+                        });
+                    } else {
+                        vnode = keys[key + 'fc'];
+                    }
+                    if (vnode) {
+                        return h('div', {class: '_fc-' + ctx.$props.tag + '-item _fd-drag-item', key}, vnode);
+                    }
+                }
+                return h('div', {class: '_fc-' + ctx.$props.tag + '-item _fd-drag-item', key: index}, null);
+            }
+        });
+    }
+});
+</script>

+ 201 - 0
src/components/form-create-designer/components/DragTool.vue

@@ -0,0 +1,201 @@
+<template>
+    <div class="_fd-drag-tool" @click.stop="active" :class="{active: fcx.active === id}">
+        <div class="_fd-drag-mask" v-if="mask"></div>
+        <div class="_fd-drag-l" v-if="!hiddenBtn" @click.stop>
+            <div class="_fd-drag-btn" v-if="dragBtn !== false" v-show="fcx.active === id" style="cursor: move;">
+                <i class="fc-icon icon-move"></i>
+            </div>
+        </div>
+        <div class="_fd-drag-r" v-if="btns !== false && !hiddenMenu">
+            <slot name="handle">
+                <div class="_fd-drag-btn" v-if="isCreate && (btns === true || btns.indexOf('create') > -1)"
+                     @click.stop="$emit('create')">
+                    <i class="fc-icon icon-add"></i>
+                </div>
+                <div class="_fd-drag-btn" v-if="!only && (btns === true || btns.indexOf('copy') > -1)"
+                     @click.stop="$emit('copy')">
+                    <i class="fc-icon icon-copy"></i>
+                </div>
+                <div class="_fd-drag-btn" v-if="children && (btns === true || btns.indexOf('addChild') > -1)"
+                     @click.stop="$emit('addChild')">
+                    <i class="fc-icon icon-add-child"></i>
+                </div>
+                <div class="_fd-drag-btn _fd-drag-danger" v-if="btns === true || btns.indexOf('delete') > -1"
+                     @click.stop="$emit('delete')">
+                    <i class="fc-icon icon-delete"></i>
+                </div>
+            </slot>
+        </div>
+        <slot name="default"></slot>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'DragTool',
+    emits: ['create', 'copy', 'addChild', 'delete', 'active', 'fc.el'],
+    props: {
+        dragBtn: Boolean,
+        children: String,
+        mask: Boolean,
+        handleBtn: [Boolean, Array],
+        formCreateInject: Object,
+        unique: String,
+        only: Boolean
+    },
+    inject: {
+        fcx: {
+            default: null
+        },
+        designer: {
+            default: null
+        },
+        dragTool: {
+            default: null
+        },
+    },
+    provide() {
+        return {
+            dragTool: this
+        }
+    },
+    computed: {
+        isCreate() {
+            return this.dragTool ? !!this.dragTool.children : false;
+        },
+        btns() {
+            if (Array.isArray(this.handleBtn)) {
+                return this.handleBtn.length ? this.handleBtn : false;
+            }
+            return this.handleBtn !== false;
+        },
+        id() {
+            return this.unique || this.formCreateInject.id;
+        },
+        hiddenMenu() {
+            return this.designer.ctx.hiddenDragMenu;
+        },
+        hiddenBtn() {
+            return this.designer.ctx.hiddenDragBtn;
+        },
+    },
+    methods: {
+        active() {
+            if (this.fcx.active === this.id) return;
+            this.fcx.active = this.id;
+            this.$emit('active');
+        }
+    },
+    mounted() {
+        this.$emit('fc.el', this);
+    },
+});
+</script>
+
+<style>
+._fd-drag-tool {
+    position: relative;
+    display: block;
+    min-height: 20px;
+    box-sizing: border-box;
+    padding: 2px;
+    outline: 1px dashed var(--fc-tool-border-color);
+    overflow: hidden;
+    word-wrap: break-word;
+    word-break: break-all;
+    transition: outline-color 0.3s ease;
+    z-index: 0;
+}
+
+._fd-drag-tool ._fd-drag-tool {
+    height: calc(100% - 6px);
+    margin: 3px;
+}
+
+._fd-drag-tool + ._fd-drag-tool {
+    margin-top: 5px;
+}
+
+._fd-drag-tool.active {
+    outline: 2px solid #2E73FF;
+}
+
+._fd-drag-tool.active > div > ._fd-drag-btn {
+    display: flex;
+}
+
+._fd-drag-tool:not(.active):hover > div > ._fd-drag-btn {
+    display: flex !important;
+    opacity: 0.7;
+}
+
+._fd-drag-tool._fd-drop-hover ._fd-drag-box {
+    padding-top: 15px !important;
+    padding-bottom: 15px !important;
+}
+
+._fd-drag-tool ._fd-drag-btn {
+    display: none;
+}
+
+._fd-drag-r {
+    position: absolute;
+    right: 2px;
+    top: calc(100% - 20px);
+    z-index: 1904;
+}
+
+._fd-drag-l {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 1904
+
+}
+
+._fd-drag-btn {
+    height: 18px;
+    width: 18px;
+    color: #fff;
+    background-color: #2E73FF;
+    text-align: center;
+    line-height: 20px;
+    padding-bottom: 1px;
+    float: left;
+    cursor: pointer;
+    justify-content: center;
+}
+
+._fd-drag-btn + ._fd-drag-btn {
+    margin-left: 2px;
+}
+
+._fd-drag-danger {
+    background-color: #FF2E2E;
+}
+
+._fd-drag-btn i {
+    font-size: 14px;
+}
+
+._fd-drag-mask {
+    z-index: 1900;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;;
+}
+
+._fd-drag-tool:hover {
+    outline-color: #2E73FF;
+    outline-style: solid;
+    z-index: 1;
+}
+
+._fd-drag-tool:has(._fd-drag-tool:not(.active):hover, ._fd-drag-tool.active:hover) > div > ._fd-drag-btn {
+    display: none !important;
+}
+</style>

+ 546 - 0
src/components/form-create-designer/components/EventConfig.vue

@@ -0,0 +1,546 @@
+<template>
+    <div class="_fd-event">
+        <el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
+            <el-button size="small" @click="visible=true">{{ t('event.title') }}</el-button>
+        </el-badge>
+        <el-dialog class="_fd-event-dialog" :title="t('event.title')" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body
+                   width="980px">
+            <el-container class="_fd-event-con" style="height: 600px">
+                <el-aside style="width:300px;">
+                    <el-container class="_fd-event-l">
+                        <el-header class="_fd-event-head" height="40px">
+                            <el-dropdown popper-class="_fd-event-dropdown" trigger="click" size="default"
+                                         :placement="'bottom-start'">
+                              <span class="el-dropdown-link">
+                                <el-button link type="primary" size="default">
+                                    {{ t('event.create') }}<i class="el-icon-arrow-down el-icon--right"></i>
+                                </el-button>
+                              </span>
+                                <template #dropdown>
+                                    <el-dropdown-menu>
+                                        <el-dropdown-item v-for="name in eventName" :key="name" @click="add(name)"
+                                                          :disabled="Object.keys(event).indexOf(name) > -1">
+                                            <div class="_fd-event-item">
+                                                <span>{{ name }}</span>
+                                                <span class="_fd-label" v-if="eventInfo[name]">
+                                                    {{ eventInfo[name] }}
+                                                </span>
+                                            </div>
+                                        </el-dropdown-item>
+                                        <template v-for="(hook, idx) in hookList">
+                                            <el-dropdown-item :divided="eventName.length > 0 && !idx"
+                                                              @click="add(hook)"
+                                                              :disabled="Object.keys(event).indexOf(hook) > -1">
+                                                <div class="_fd-event-item">
+                                                    <div> {{ hook }}</div>
+                                                    <span class="_fd-label">
+                                                    {{ eventInfo[hook] }}
+                                                </span>
+                                                </div>
+                                            </el-dropdown-item>
+                                        </template>
+                                        <el-dropdown-item :divided="eventName.length > 0" @click="cusEvent">
+                                            <div>{{ t('props.custom') }}</div>
+                                        </el-dropdown-item>
+                                    </el-dropdown-menu>
+                                </template>
+                            </el-dropdown>
+                        </el-header>
+                        <el-main>
+                            <el-menu
+                                :default-active="defActive"
+                                v-model="activeData">
+                                <template v-for="(item, name) in event">
+                                    <template v-if="Array.isArray(item)">
+                                        <template v-for="(event, index) in item" :key="name + index">
+                                            <el-menu-item :index="name + index">
+                                                <div class="_fd-event-title"
+                                                     @click.stop="edit({name, item, index})">
+                                                    <div class="_fd-event-method">
+                                                        <span>function<span>{{
+                                                                name
+                                                            }}</span></span>
+                                                        <span class="_fd-label"
+                                                              v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
+                                                    </div>
+                                                    <i class="fc-icon icon-delete"
+                                                       @click.stop="rm({name, item, index})"></i>
+                                                </div>
+                                            </el-menu-item>
+                                        </template>
+                                    </template>
+                                    <el-menu-item v-else :index="name + 0">
+                                        <div class="_fd-event-title" @click.stop="edit({name})">
+                                            <div class="_fd-event-method">
+                                                <span>function<span>{{
+                                                        name
+                                                    }}</span></span>
+                                                <span class="_fd-label"
+                                                      v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
+                                            </div>
+                                            <i class="fc-icon icon-delete" @click.stop="rm({name})"></i>
+                                        </div>
+                                    </el-menu-item>
+                                </template>
+                                <el-menu-item v-if="cus" style="padding-left: 10px;" index="custom">
+                                    <div class="_fd-event-title" @click.stop>
+                                        <el-input type="text" v-model="cusValue" size="default"
+                                                  @keydown.enter="addCus"
+                                                  :placeholder="t('event.placeholder')">
+                                        </el-input>
+                                        <div>
+                                            <i class="fc-icon icon-add" @click.stop="addCus"></i>
+                                            <i class="fc-icon icon-delete" @click.stop="closeCus"></i>
+                                        </div>
+                                    </div>
+                                </el-menu-item>
+                            </el-menu>
+                        </el-main>
+                    </el-container>
+                </el-aside>
+                <el-main>
+                    <el-container class="_fd-event-r">
+                        <el-header class="_fd-event-head" height="40px" v-if="activeData">
+                            <div><a target="_blank" href="https://form-create.com/v3/instance/">{{t('form.document')}}</a></div>
+                            <div>
+                                <el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
+                                <el-button size="small" type="primary" @click="save" color="#2f73ff">{{
+                                        t('props.save')
+                                    }}
+                                </el-button>
+                            </div>
+                        </el-header>
+                        <el-main v-if="activeData">
+                            <FnEditor ref="fn" v-model="eventStr" body :name="activeData.name"
+                                      :args="fnArgs"
+                                      style="height: 519px;"/>
+                        </el-main>
+                    </el-container>
+                </el-main>
+            </el-container>
+            <template #footer>
+                <div>
+                    <el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" size="default" @click="submit" color="#2f73ff">{{
+                            t('props.ok')
+                        }}
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import unique from '@form-create/utils/lib/unique';
+import deepExtend from '@form-create/utils/lib/deepextend';
+import is from '@form-create/utils/lib/type';
+import {defineComponent} from 'vue';
+import FnEditor from './FnEditor.vue';
+import errorMessage from '../utils/message';
+import {getInjectArg} from '../utils';
+
+const $T = '$FNX:';
+
+const isFNX = v => {
+    return is.String(v) && v.indexOf($T) === 0;
+};
+
+export default defineComponent({
+    name: 'EventConfig',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, undefined, null],
+        componentName: '',
+        eventName: {
+            type: Array,
+            default: () => []
+        }
+    },
+    inject: ['designer'],
+    components: {
+        FnEditor,
+    },
+    data() {
+        return {
+            visible: false,
+            activeData: null,
+            val: null,
+            defActive: 'no',
+            hookList: ['hook_load', 'hook_mounted', 'hook_deleted', 'hook_watch', 'hook_value', 'hook_hidden'],
+            event: {},
+            cus: false,
+            cusValue: '',
+            eventStr: '',
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        eventInfo() {
+            const info = {};
+            this.eventName.forEach(v => {
+                info[v] = this.t('com.' + this.componentName + '.event.' + v) || this.t('eventInfo.' + v) || '';
+            })
+            this.hookList.forEach(v => {
+                info[v] = this.t('eventInfo.' + v) || '';
+            })
+            return info;
+        },
+        eventNum() {
+            let num = 0;
+            Object.keys(this.modelValue || {}).forEach(k => {
+                num += Array.isArray(this.modelValue[k]) ? this.modelValue[k].length : 1;
+            });
+            const hooks = this.activeRule ? {...this.activeRule.hook || {}} : {};
+            Object.keys(hooks).forEach(k => {
+                num += Array.isArray(this.activeRule.hook[k]) ? this.activeRule.hook[k].length : 1;
+            });
+            return num;
+        },
+        fnArgs() {
+            return [getInjectArg(this.t)];
+        }
+    },
+    watch: {
+        visible(v) {
+            this.event = v ? this.loadFN() : {};
+            if (!v) {
+                this.destroy();
+                this.closeCus();
+            }
+        },
+    },
+    methods: {
+        addCus() {
+            const val = this.cusValue && this.cusValue.trim();
+            if (val) {
+                this.closeCus();
+                this.add(val);
+            }
+        },
+        closeCus() {
+            this.cus = false;
+            this.cusValue = '';
+        },
+        cusEvent() {
+            this.cus = true;
+        },
+        loadFN() {
+            const e = deepExtend({}, this.modelValue || {});
+            const hooks = this.activeRule ? {...this.activeRule.hook || {}} : {};
+            Object.keys(hooks).forEach(k => {
+                e['hook_' + k] = hooks[k];
+            })
+            const val = {};
+            Object.keys(e).forEach(k => {
+                if (Array.isArray(e[k])) {
+                    const data = [];
+                    e[k].forEach(v => {
+                        if (isFNX(v)) {
+                            data.push(v.replace($T, ''));
+                        } else if (is.Function(v) && isFNX(v.__json)) {
+                            data.push(v.__json.replace($T, ''));
+                        } else if (v && v.indexOf('$GLOBAL:') === 0) {
+                            data.push(v);
+                        }
+                    });
+                    val[k] = data;
+                } else if (isFNX(e[k])) {
+                    val[k] = [e[k].replace($T, '')];
+                } else if (is.Function(e[k]) && isFNX(e[k].__json)) {
+                    val[k] = [e[k].__json.replace($T, '')];
+                } else if (e[k] && e[k].indexOf('$GLOBAL:') === 0) {
+                    val[k] = [e[k]];
+                }
+            });
+            return val;
+        },
+        parseFN(e) {
+            const on = {};
+            const hooks = {};
+            Object.keys(e).forEach(k => {
+                const lst = [];
+                e[k].forEach((v, i) => {
+                    lst[i] = v.indexOf('$GLOBAL:') !== 0 ? ($T + v) : v;
+                });
+                if (lst.length > 0) {
+                    if (k.indexOf('hook_') > -1) {
+                        hooks[k.replace('hook_', '')] = lst.length === 1 ? lst[0] : lst;
+                    } else {
+                        on[k] = lst.length === 1 ? lst[0] : lst;
+                    }
+                }
+            });
+            return {hooks, on};
+        },
+        add(name) {
+            let data = {};
+            if (Array.isArray(this.event[name])) {
+                this.event[name].push('');
+                data = {
+                    name,
+                    item: this.event[name],
+                    index: this.event[name].length - 1,
+                };
+            } else if (this.event[name]) {
+                const arr = [this.event[name], ''];
+                this.event[name] = arr;
+                data = {
+                    name,
+                    item: arr,
+                    index: 1,
+                };
+            } else {
+                const arr = [''];
+                this.event[name] = arr;
+                data = {
+                    name,
+                    item: arr,
+                    index: 0,
+                };
+            }
+            if (!this.activeData) {
+                this.edit(data);
+            }
+        },
+        edit(data) {
+            data.key = unique();
+            if (data.item) {
+                this.val = data.item[data.index];
+            } else {
+                this.val = this.event[data.name];
+            }
+            this.activeData = data;
+            this.eventStr = this.val;
+            this.defActive = data.name + (data.index || 0);
+        },
+        save() {
+            if (!this.$refs.fn.save()) {
+                return;
+            }
+            const str = this.eventStr;
+
+            if (this.activeData.item) {
+                this.activeData.item[this.activeData.index] = str;
+            } else {
+                this.event[this.activeData.name] = str;
+            }
+            this.destroy();
+        },
+        rm(data) {
+            if (data.index !== undefined) {
+                data.item.splice(data.index, 1);
+            } else {
+                this.$delete(this.event, data.name);
+            }
+            if (this.defActive === (data.name + (data.index || 0))) {
+                this.destroy();
+            }
+        },
+        destroy() {
+            this.activeData = null;
+            this.val = null;
+            this.defActive = 'no';
+        },
+        close() {
+            this.destroy();
+        },
+        submit() {
+            if (this.activeData) {
+                return errorMessage(this.t('event.saveMsg'));
+            }
+            const {on, hooks} = this.parseFN(this.event);
+            this.$emit('update:modelValue', on);
+            this.activeRule.hook = hooks;
+            this.visible = false;
+            this.destroy();
+            this.closeCus();
+        },
+    },
+    beforeCreate() {
+        window.$inject = {
+            $f: {},
+            rule: [],
+            self: {},
+            option: {},
+            inject: {},
+            args: [],
+        };
+    }
+});
+</script>
+
+<style>
+
+._fd-event .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-event .el-badge {
+    width: 100%;
+}
+
+._fd-event-dialog .el-dialog__body {
+    padding: 10px 20px;
+}
+
+._fd-event-con .el-main {
+    padding: 0;
+}
+
+._fd-event-l, ._fd-event-r {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    height: 100%;
+    border: 1px solid #ececec;
+}
+
+._fd-event-dropdown .el-dropdown-menu {
+    max-height: 500px;
+    overflow: auto;
+}
+
+._fd-event-head {
+    display: flex;
+    padding: 5px 15px;
+    border-bottom: 1px solid #eee;
+    background: #f8f9ff;
+    align-items: center;
+}
+
+._fd-event-head .el-button.is-link {
+    color: #2f73ff;
+}
+
+._fd-event-r {
+    border-left: 0 none;
+}
+
+._fd-event-r ._fd-event-head {
+    justify-content: space-between;
+}
+
+._fd-event-l > .el-main, ._fd-event-r > .el-main {
+    display: flex;
+    flex-direction: row;
+    flex: 1;
+    flex-basis: auto;
+    box-sizing: border-box;
+    min-width: 0;
+    width: 100%;
+}
+
+._fd-event-r > .el-main {
+    flex-direction: column;
+}
+
+._fd-event-item {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    max-width: 250px;
+    font-size: 14px;
+    overflow: hidden;
+    white-space: pre-wrap;
+}
+
+._fd-event-item ._fd-label {
+    font-size: 12px;
+    color: #AAAAAA;
+}
+
+._fd-event-l .el-menu {
+    padding: 0 10px 5px;
+    border-right: 0 none;
+    width: 100%;
+    border-top: 0 none;
+    overflow: auto;
+}
+
+._fd-event-l .el-menu-item.is-active {
+    background: #e4e7ed;
+    color: #303133;
+}
+
+._fd-event-l .el-menu-item {
+    height: auto;
+    line-height: 1em;
+    border: 1px solid #ECECEC;
+    border-radius: 5px;
+    padding: 0;
+    margin-top: 5px;
+}
+
+._fd-event-method {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    width: 225px;
+    font-size: 14px;
+    font-family: monospace;
+    color: #9D238C;
+    overflow: hidden;
+    white-space: pre-wrap;
+}
+
+._fd-event-method ._fd-label {
+    margin-top: 4px;
+    color: #AAAAAA;
+    font-size: 12px;
+}
+
+._fd-event-method > span:first-child, ._fd-fn-list-method > span:first-child {
+    color: #9D238C;
+}
+
+._fd-event-method > span:first-child > span, ._fd-fn-list-method > span:first-child > span {
+    color: #000;
+    margin-left: 10px;
+}
+
+._fd-event-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    padding: 10px 0;
+}
+
+._fd-event-title .fc-icon {
+    margin-right: 6px;
+    font-size: 18px;
+    color: #282828;
+}
+
+._fd-event-title .el-input {
+    width: 200px;
+}
+
+._fd-event-title .el-input__wrapper {
+    box-shadow: none;
+}
+
+._fd-event-title .el-menu-item.is-active i {
+    color: #282828;
+}
+
+._fd-event-con .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-event-con .CodeMirror-wrap pre.CodeMirror-line {
+    padding-left: 20px;
+}
+</style>

Разница между файлами не показана из-за своего большого размера
+ 206 - 0
src/components/form-create-designer/components/FcDesigner.vue


+ 263 - 0
src/components/form-create-designer/components/FetchConfig.vue

@@ -0,0 +1,263 @@
+<template>
+    <div class="_fd-gfc">
+        <el-badge type="warning" is-dot :hidden="!configured">
+            <el-button @click="visible=true" size="small">{{ t('struct.title') }}</el-button>
+        </el-badge>
+        <el-dialog class="_fd-gfc-dialog" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body
+                   width="980px">
+            <template #header>
+                {{ t('fetch.optionsType.fetch') }}
+                <Warning :tooltip="t('warning.fetch')"></Warning>
+            </template>
+            <el-container class="_fd-gfc-con" style="height: 450px;">
+                <el-tabs model-value="first" class="_fc-tabs" style="width: 100%">
+                    <el-tab-pane :label="t('fetch.config')" name="first">
+                        <DragForm v-model:api="form.api" v-model="form.formData" :rule="form.rule"
+                                  :option="form.options">
+                            <template #title="scope">
+                                <template v-if="scope.rule.warning">
+                                    <Warning :tooltip="scope.rule.warning">
+                                        {{ scope.rule.title }}
+                                    </Warning>
+                                </template>
+                                <template v-else>
+                                    {{ scope.rule.title }}
+                                </template>
+                            </template>
+                        </DragForm>
+                    </el-tab-pane>
+                    <el-tab-pane lazy name="second">
+                        <template #label>
+                            {{ t('fetch.parse') }}
+                            <Warning :tooltip="t('warning.fetchParse')"></Warning>
+                        </template>
+                        <FnEditor style="height: 415px;" v-model="form.parse" name="parse"
+                                  :args="[{name:'res', info: t('fetch.response')}, 'rule', 'api']"
+                                  ref="parse"></FnEditor>
+                    </el-tab-pane>
+                    <el-tab-pane lazy :label="t('fetch.onError')" name="third">
+                        <FnEditor style="height: 415px;" v-model="form.onError" name="onError"
+                                  :args="['e']"
+                                  ref="error"></FnEditor>
+                    </el-tab-pane>
+                </el-tabs>
+            </el-container>
+            <template #footer>
+                <div>
+                    <el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" size="default" @click="save" color="#2f73ff">{{
+                            t('props.ok')
+                        }}
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import FnEditor from './FnEditor.vue';
+import StructEditor from './StructEditor.vue';
+import {defineComponent} from 'vue';
+import {designerForm} from '../utils/form';
+import errorMessage from '../utils/message';
+import is from '@form-create/utils/lib/type';
+import Warning from './Warning.vue';
+
+const makeRule = (t) => {
+    return [
+        {
+            type: 'input',
+            field: 'action',
+            title: t('fetch.action'),
+            value: '',
+            props: {size: 'default'},
+            validate: [{required: true, message: t('fetch.actionRequired'), trigger: 'blur'}]
+        },
+        {
+            type: 'radio',
+            field: 'method',
+            title: t('fetch.method'),
+            value: 'GET',
+            props: {
+                size: 'default'
+            },
+            options: [
+                {label: 'GET', value: 'GET'},
+                {label: 'POST', value: 'POST'},
+            ],
+            $required: true,
+        },
+        {
+            type: 'radio',
+            field: 'dataType',
+            title: t('fetch.dataType'),
+            warning: t('warning.fetchDataType'),
+            value: 'json',
+            props: {
+                size: 'default'
+            },
+            options: [
+                {label: 'JSON', value: 'json'},
+                {label: 'FormData', value: 'formData'},
+            ],
+            $required: true,
+        },
+        {
+            type: 'TableOptions',
+            field: 'headers',
+            title: t('fetch.headers'),
+            value: {},
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object',
+                size: 'default'
+            },
+        },
+        {
+            type: 'TableOptions',
+            field: 'query',
+            title: t('fetch.query'),
+            warning: t('warning.fetchQuery'),
+            value: {},
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object',
+                size: 'default'
+            },
+        },
+        {
+            type: 'TableOptions',
+            field: 'data',
+            title: t('fetch.data'),
+            warning: t('warning.fetchData'),
+            value: {},
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object',
+                size: 'default'
+            },
+        }];
+}
+
+export default defineComponent({
+    name: 'FetchConfig',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, String],
+        to: String,
+    },
+    components: {
+        Warning,
+        DragForm: designerForm.$form(),
+        FnEditor,
+        StructEditor
+    },
+    inject: ['designer'],
+    data() {
+        return {
+            visible: false,
+            value: deepCopy(this.modelValue || {}),
+            form: {
+                api: {},
+                formData: {},
+                rule: [],
+                options: {
+                    form: {
+                        labelWidth: '90px',
+                        size: 'default'
+                    },
+                    submitBtn: false,
+                    resetBtn: false,
+                }
+            }
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        configured() {
+            return !is.empty(this.modelValue);
+        },
+    },
+    watch: {
+        visible(v) {
+            if (v) {
+                this.value = deepCopy(this.modelValue || {});
+                this.active();
+            }
+        },
+    },
+    methods: {
+        open() {
+            this.visible = true;
+        },
+        active() {
+            const formData = this.value;
+            this.form.rule = formData.type === 'static' ? [] : makeRule(this.t);
+            this.form.formData = {...formData};
+            this.form.label = formData.label;
+            this.form.type = formData.type;
+            this.form.data = formData.data;
+            this.form.dataType = formData.dataType;
+            this.form.parse = formData.parse || '';
+            this.form.onError = formData.onError || '';
+        },
+        save() {
+            this.form.api.validate().then(() => {
+                const formData = {...this.form.formData};
+                if ((this.$refs.parse && !this.$refs.parse.save()) || (this.$refs.error && !this.$refs.error.save())) {
+                    return;
+                }
+                formData.parse = this.form.parse;
+                formData.onError = this.form.onError;
+                formData.label = this.form.label;
+                formData.type = this.form.type;
+                formData.to = this.to || 'options';
+                this.$emit('update:modelValue', formData);
+                this.visible = false;
+            }).catch(err => {
+                console.error(err);
+                errorMessage(err[Object.keys(err)[0]][0].message);
+            });
+        },
+    },
+    created() {
+        this.active();
+    }
+});
+</script>
+
+<style>
+._fd-gfc, ._fd-gfc .el-badge {
+    width: 100%;
+}
+
+._fd-gfc .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-gfc-dialog .el-tabs__header {
+    margin-bottom: 0;
+}
+
+._fd-gfc-dialog .form-create {
+    margin-top: 15px;
+}
+
+._fd-gfc-con .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-gfc-con .CodeMirror-wrap pre.CodeMirror-line {
+    padding-left: 20px;
+}
+</style>

+ 153 - 0
src/components/form-create-designer/components/FieldInput.vue

@@ -0,0 +1,153 @@
+<template>
+    <div class="_fd-field-input">
+        <i class="fc-icon icon-group" @click.stop="copy"></i>
+        <el-input
+            v-model="value"
+            :readonly="fieldReadonly || disabled"
+            :disabled="fieldReadonly || disabled"
+            @focus="onFocus"
+            @blur="onInput"
+        >
+            <template #append v-if="!fieldReadonly">
+                <i class="fc-icon icon-auto" @click="makeField"></i>
+            </template>
+        </el-input>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import uniqueId from '@form-create/utils/lib/unique';
+import errorMessage from '../utils/message';
+import {copyTextToClipboard} from '../utils/index';
+import is from '@form-create/utils/lib/type';
+
+export default defineComponent({
+    name: 'FieldInput',
+    inject: ['designer'],
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: String,
+        disabled: Boolean,
+    },
+    computed: {
+        fieldReadonly() {
+            return this.designer.setupState.fieldReadonly;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    data() {
+        return {
+            value: this.modelValue || '',
+            oldValue: '',
+        }
+    },
+    watch: {
+        modelValue(n) {
+            this.value = n;
+        }
+    },
+    methods: {
+        copy() {
+            copyTextToClipboard(this.modelValue);
+        },
+        getSubChildren() {
+            let subChildren = this.designer.setupState.getSubFormChildren(this.activeRule) || [];
+            subChildren = is.trueArray(subChildren) ? subChildren : this.designer.setupState.children;
+            return subChildren;
+        },
+        getSubFieldChildren() {
+            const subChildren = this.getSubChildren();
+            const list = [];
+            const getRule = (children) => {
+                children && children.forEach(rule => {
+                    if (rule && rule._fc_drag_tag && rule.field) {
+                        list.push({...rule, children: []});
+                    } else if (rule && rule.children) {
+                        getRule(rule.children);
+                    }
+                });
+                return list;
+            }
+            return getRule(subChildren);
+        },
+        checkValue() {
+            const oldField = this.oldValue;
+            let field = (this.value || '').replace(/[\s\ ]/g, '');
+            if (!field) {
+                errorMessage(this.t('computed.fieldEmpty'));
+                return oldField;
+            } else if (!/^[a-zA-Z]/.test(field)) {
+                errorMessage(this.t('computed.fieldChar'));
+                return oldField;
+            } else if (oldField !== field) {
+                const flag = field.indexOf('.') > -1;
+                if (flag) {
+                    field = field.replaceAll('.', '_');
+                }
+                if (this.getSubFieldChildren().filter(v => v.field === field).length > 0) {
+                    errorMessage(this.t('computed.fieldExist', {label: field}));
+                    return oldField;
+                }
+                if (flag) {
+                    return field;
+                }
+            }
+            this.oldValue = '';
+            return field;
+        },
+        onFocus() {
+            this.oldValue = this.value;
+        },
+        makeField() {
+            this.oldValue = this.value;
+            this.value = uniqueId();
+            this.onInput();
+        },
+        onInput() {
+            if (this.value !== this.modelValue) {
+                this.value = this.checkValue();
+                this.oldValue = this.value;
+                if (this.value !== this.modelValue) {
+                    this.$emit('update:modelValue', this.value);
+                }
+            }
+        },
+    },
+});
+</script>
+
+<style>
+._fd-field-input {
+    width: 100%;
+}
+
+._fd-field-input > .fc-icon {
+    position: absolute;
+    right: 28px;
+    top: 1px;
+    z-index: 3;
+    color: #a8abb2;
+    cursor: pointer;
+    width: 24px;
+    height: 24px;
+    text-align: center;
+}
+
+._fd-field-input > .fc-icon:hover {
+    color: #2E73FF;
+}
+
+._fd-field-input .el-input-group__append {
+    width: 25px;
+    padding: 0;
+    margin: 0;
+    color: #606266;
+    cursor: pointer;
+}
+</style>

+ 304 - 0
src/components/form-create-designer/components/FnConfig.vue

@@ -0,0 +1,304 @@
+<template>
+    <div class="_fd-fn-list">
+        <el-badge :value="eventNum" type="warning" :hidden="eventNum < 1">
+            <el-button @click="visible=true" size="small">{{ t('event.title') }}</el-button>
+        </el-badge>
+        <el-dialog class="_fd-fn-list-dialog" :title="t('event.title')" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body
+                   width="980px">
+            <el-container class="_fd-fn-list-con" style="height: 600px">
+                <el-aside style="width:300px;">
+                    <el-container class="_fd-fn-list-l">
+                        <el-header class="_fd-fn-list-head" height="40px">
+                            <el-text type="primary" size="default">
+                                {{ t('event.list') }}
+                            </el-text>
+                        </el-header>
+                        <el-main>
+                            <el-menu
+                                :default-active="defActive"
+                                v-model="activeData">
+                                <template v-for="(item, name) in event">
+                                    <el-menu-item :index="name">
+                                        <div class="_fd-fn-list-method" @click.stop="edit(item)">
+                                            <span>function<span>{{ name }}</span></span>
+                                            <span class="_fd-label" v-if="eventInfo[name]">{{ eventInfo[name] }}</span>
+                                        </div>
+                                    </el-menu-item>
+                                </template>
+                            </el-menu>
+                        </el-main>
+                    </el-container>
+                </el-aside>
+                <el-main>
+                    <el-container class="_fd-fn-list-r">
+                        <el-header class="_fd-fn-list-head" height="40px" v-if="activeData">
+                            <el-button size="small" @click="close">{{ t('props.cancel') }}</el-button>
+                            <el-button size="small" type="primary" @click="save" color="#2f73ff">{{
+                                    t('props.save')
+                                }}
+                            </el-button>
+                        </el-header>
+                        <el-main v-if="activeData">
+                            <FnEditor ref="fn" v-model="eventStr" :name="activeData.item.name"
+                                      :args="activeData.item.args"/>
+                        </el-main>
+                    </el-container>
+                </el-main>
+            </el-container>
+            <template #footer>
+                <div>
+                    <el-button size="default" @click="visible=false">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" size="default" @click="submit" color="#2f73ff">{{
+                            t('props.ok')
+                        }}
+                    </el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import unique from '@form-create/utils/lib/unique';
+import deepExtend from '@form-create/utils/lib/deepextend';
+import {defineComponent} from 'vue';
+import FnEditor from './FnEditor.vue';
+import errorMessage from '../utils/message';
+
+const PREFIX = '[[FORM-CREATE-PREFIX-';
+const SUFFIX = '-FORM-CREATE-SUFFIX]]';
+
+export default defineComponent({
+    name: 'FnConfig',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, undefined, null],
+        eventConfig: {
+            type: Array,
+            default: () => []
+        },
+    },
+    inject: ['designer'],
+    components: {
+        FnEditor,
+    },
+    data() {
+        return {
+            visible: false,
+            activeData: null,
+            defActive: 'no',
+            event: {},
+            cus: false,
+            eventStr: '',
+        };
+    },
+    computed: {
+        eventInfo() {
+            const info = {};
+            this.eventConfig.forEach(v => {
+                info[v.name] = v.info;
+            });
+            return info;
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+        eventNum() {
+            let num = 0;
+            Object.keys(this.modelValue || {}).forEach(k => {
+                if (this.modelValue[k]) {
+                    num++;
+                }
+            });
+            return num;
+        },
+    },
+    watch: {
+        visible(v) {
+            this.event = v ? this.loadFN(deepExtend({}, this.modelValue || {})) : {};
+            if (!v) {
+                this.destroy();
+            }
+        },
+    },
+    methods: {
+        getArgs(item) {
+            return item.args.join(', ');
+        },
+        loadFN(e) {
+            const val = {};
+            this.eventConfig.forEach(item => {
+                const k = item.name;
+                const fn = e[k] || '';
+                val[k] = {
+                    item, fn
+                }
+            });
+            return val;
+        },
+        parseFN(e) {
+            const on = {};
+            Object.keys(e).forEach(k => {
+                if (e[k].fn) {
+                    on[k] = e[k].fn;
+                }
+            });
+            return on;
+        },
+        edit(data) {
+            data.key = unique();
+            this.activeData = data;
+            this.eventStr = data.fn || (PREFIX + `function ${data.item.name}(${this.getArgs(data.item)}){}` + SUFFIX);
+            this.defActive = data.item.name;
+        },
+        save() {
+            if (this.$refs.fn.save()) {
+                this.activeData.fn = this.eventStr;
+                this.destroy();
+                return true;
+            }
+            return false;
+        },
+        destroy() {
+            this.activeData = null;
+            this.defActive = 'no';
+        },
+        close() {
+            this.destroy();
+        },
+        submit() {
+            if (this.activeData && !this.save()) {
+                return;
+            }
+            this.$emit('update:modelValue', this.parseFN(this.event));
+            this.visible = false;
+            this.destroy();
+        },
+    }
+});
+</script>
+
+<style>
+._fd-fn-list, ._fd-fn-list .el-badge {
+    width: 100%;
+}
+
+._fd-fn-list .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-fn-list-dialog .el-dialog__body {
+    padding: 10px 20px;
+}
+
+._fd-fn-list-con .el-main {
+    padding: 0;
+}
+
+._fd-fn-list-l, ._fd-fn-list-r {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    height: 100%;
+    border: 1px solid #ececec;
+}
+
+._fd-fn-list-head {
+    display: flex;
+    padding: 5px 15px;
+    border-bottom: 1px solid #eee;
+    background: #f8f9ff;
+    align-items: center;
+}
+
+._fd-fn-list-head .el-button.is-link {
+    color: #2f73ff;
+}
+
+._fd-fn-list-r {
+    border-left: 0 none;
+}
+
+._fd-fn-list-r ._fd-fn-list-head {
+    justify-content: flex-end;
+}
+
+._fd-fn-list-l > .el-main, ._fd-fn-list-r > .el-main {
+    display: flex;
+    flex-direction: row;
+    flex: 1;
+    flex-basis: auto;
+    box-sizing: border-box;
+    min-width: 0;
+    width: 100%;
+}
+
+._fd-fn-list-r > .el-main {
+    flex-direction: column;
+}
+
+._fd-fn-list-l .el-menu {
+    padding: 0 10px 5px;
+    border-right: 0 none;
+    width: 100%;
+    border-top: 0 none;
+    overflow: auto;
+}
+
+._fd-fn-list-l .el-menu-item.is-active {
+    background: #e4e7ed;
+    color: #303133;
+}
+
+._fd-fn-list-l .el-menu-item {
+    height: auto;
+    line-height: 1em;
+    border: 1px solid #ECECEC;
+    border-radius: 5px;
+    padding: 0;
+    margin-top: 5px;
+}
+
+._fd-fn-list-method {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding: 10px 0;
+    font-size: 14px;
+    line-height: 1em;
+    font-family: monospace;
+    width: 100%;
+    overflow: hidden;
+    white-space: pre-wrap;
+}
+
+
+._fd-fn-list-method ._fd-label {
+    margin-top: 4px;
+    color: #AAAAAA;
+    font-size: 12px;
+}
+
+._fd-fn-list-method-info > span:first-child, ._fd-fn-list-method > span:first-child {
+    color: #9D238C;
+}
+
+._fd-fn-list-method-info > span:first-child > span, ._fd-fn-list-method > span:first-child > span {
+    color: #000;
+    margin-left: 10px;
+}
+
+._fd-fn-list-con .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-fn-list-con .CodeMirror-wrap pre.CodeMirror-line {
+    padding-left: 20px;
+}
+</style>

+ 251 - 0
src/components/form-create-designer/components/FnEditor.vue

@@ -0,0 +1,251 @@
+<template>
+    <div class="_fd-fn">
+        <div class="_fd-fn-tip">
+            <div class="_fd-fn-ind"></div>
+            <div class="cm-keyword"><span>function {{ name }}(<template
+                v-for="(item, idx) in argList">{{ idx > 0 ? ', ' : '' }}<template v-if="item.type === 'string'">
+<span>{{ item.name }}</span>
+</template><template v-else><el-popover placement="top-start" :width="400" :hide-after="0" trigger="click"
+                                        :title="item.name"
+                                        :content="item.info || ''"
+            ><template #reference><span class="_fd-fn-arg">{{ item.name }}<i
+                class="fc-icon icon-question"></i></span></template>
+                            <template v-if="item.columns">
+                                <el-table :data="item.columns" border>
+                            <el-table-column width="120" property="label" :label="t('event.label')"/>
+                            <el-table-column property="info" :label="t('event.info')"/>
+                            <el-table-column width="80" property="type" :label="t('event.type')"/>
+                          </el-table>
+                            </template>
+                        </el-popover>
+                    </template>
+                    </template>) {</span></div>
+        </div>
+        <div ref="editor" class="_fd-fn-editor"></div>
+        <div class="_fd-fn-tip">
+            <div class="_fd-fn-ind"></div>
+            <div class="cm-keyword">}</div>
+        </div>
+        <el-button v-if="visible && button" type="primary" size="small" @click="save">{{ t('props.save') }}</el-button>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/addon/hint/show-hint.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/addon/hint/show-hint';
+import 'codemirror/addon/hint/javascript-hint';
+import {defineComponent, markRaw} from 'vue';
+import {addAutoKeyMap, toJSON} from '../utils';
+import errorMessage from '../utils/message';
+
+const PREFIX = '[[FORM-CREATE-PREFIX-';
+const SUFFIX = '-FORM-CREATE-SUFFIX]]';
+
+export default defineComponent({
+    name: 'FnEditor',
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: [String, Function],
+        name: String,
+        args: Array,
+        body: Boolean,
+        button: Boolean,
+        fnx: Boolean,
+    },
+    inject: ['designer'],
+    data() {
+        return {
+            editor: null,
+            fn: '',
+            visible: false,
+            value: '',
+        };
+    },
+    watch: {
+        modelValue(n) {
+            if (n != this.value && (!n || !n.__json || (n.__json && n.__json != this.value))) {
+                this.editor && this.editor.setValue(this.tidyValue());
+            }
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        argStr() {
+            return (this.args || []).map(arg => {
+                if (typeof arg === 'string') {
+                    return arg;
+                }
+                return arg.name;
+            }).join(', ');
+        },
+        argList() {
+            return this.args.map(arg => {
+                if (typeof arg === 'string') {
+                    return {
+                        name: arg,
+                        type: 'string'
+                    }
+                }
+                return arg;
+            });
+        },
+    },
+    mounted() {
+        this.$nextTick(() => {
+            this.load();
+        });
+    },
+    methods: {
+        save() {
+            const str = this.editor.getValue() || '';
+            if (str.trim() === '') {
+                this.fn = '';
+            } else {
+                let fn;
+                try {
+                    fn = (new Function('return function ' + this.name + '(' + this.argStr + '){' + str + '}'))();
+                } catch (e) {
+                    console.error(e);
+                    errorMessage(this.t('struct.errorMsg'));
+                    return false;
+                }
+                if (this.body) {
+                    this.fn = (this.fnx ? '$FNX:' : '') + str;
+                } else {
+                    this.fn = PREFIX + fn + SUFFIX;
+                }
+            }
+            this.submit();
+            return true;
+        },
+        submit() {
+            this.$emit('update:modelValue', this.fn);
+            this.$emit('change', this.fn);
+            this.value = this.fn;
+            this.visible = false;
+        },
+        trimString(input) {
+            const firstIndex = input.indexOf('{');
+            const lastIndex = input.lastIndexOf('}');
+            if (firstIndex === -1 || lastIndex === -1 || firstIndex >= lastIndex) {
+                return input;
+            }
+            return input.slice(firstIndex + 1, lastIndex).replace(/^\n+|\n+$/g, '');
+        },
+        tidyValue() {
+            let value = this.modelValue || '';
+            if (value.__json) {
+                value = value.__json;
+            }
+            if (this.fnx && typeof value === 'string' && value.indexOf('$FNX:') === 0) {
+                value = value.slice(5);
+            }
+            if (typeof value === 'function') {
+                value = this.trimString(toJSON(value)).trim();
+            } else if (!this.body) {
+                value = this.trimString(value).trim();
+            }
+            this.value = value;
+            return value;
+        },
+        load() {
+            this.$nextTick(() => {
+                let value = this.tidyValue();
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: {name: 'javascript', globalVars: true},
+                    extraKeys: {'Ctrl-Space': 'autocomplete'},
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value,
+                }));
+                this.editor.on('inputRead', (cm, event) => {
+                    if (event.keyCode === 32 && event.ctrlKey) { // 检测 Ctrl + Space 快捷键
+                        CodeMirror.showHint(cm, CodeMirror.hint.javascript); // 触发代码提示
+                    }
+                });
+                this.editor.on('change', () => {
+                    this.visible = true;
+                });
+                addAutoKeyMap(this.editor);
+            });
+        },
+    }
+});
+</script>
+
+<style>
+
+._fd-fn {
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+._fd-fn .el-button {
+    position: absolute;
+    bottom: 3px;
+    right: 5px;
+    box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
+}
+
+._fd-fn-editor {
+    display: flex;
+    flex: 1;
+    width: 100%;
+    overflow: auto;
+}
+
+._fd-fn-editor .CodeMirror {
+    height: 100%;
+    width: 100%;
+}
+
+._fd-fn-tip {
+    color: #000;
+    font-family: monospace;
+    direction: ltr;
+}
+
+._fd-fn-tip .cm-keyword {
+    color: #708;
+    line-height: 24px;
+    white-space: nowrap;
+    overflow-x: auto;
+}
+
+._fd-fn-tip .cm-keyword::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+    background-color: transparent;
+}
+
+._fd-fn-ind {
+    background-color: #f7f7f7;
+    width: 29px;
+    height: 24px;
+    display: inline-block;
+    margin-right: 4px;
+    border-right: 1px solid #ddd;
+    float: left;
+}
+
+._fd-fn-arg {
+    text-decoration: underline;
+    cursor: pointer;
+}
+
+._fd-fn-arg i {
+    font-size: 12px;
+    color: #3073ff;
+}
+
+</style>

+ 103 - 0
src/components/form-create-designer/components/FnInput.vue

@@ -0,0 +1,103 @@
+<template>
+    <div class="_fd-fn-input">
+        <el-badge type="warning" is-dot :hidden="!configured">
+            <el-button @click="visible=true" size="small">
+                <slot>
+                    {{t('event.title')}}
+                </slot>
+            </el-button>
+        </el-badge>
+        <el-dialog class="_fd-fn-input-dialog _fd-config-dialog" :title="title || t('struct.title')" v-model="visible"
+                   destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body width="800px">
+            <FnEditor ref="editor" v-model="value" :name="name" :args="args" :body="body" :fnx="fnx"></FnEditor>
+            <template #footer>
+                <div>
+                    <el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/mode/javascript/javascript';
+import {defineComponent} from 'vue';
+import FnEditor from './FnEditor.vue';
+
+export default defineComponent({
+    name: 'FnInput',
+    components: {FnEditor},
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: [String, Function],
+        name: String,
+        args: Array,
+        title: String,
+        body: Boolean,
+        fnx: Boolean,
+        defaultValue: {
+            require: false
+        },
+        validate: Function,
+    },
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        configured() {
+            return !!this.modelValue;
+        },
+    },
+    data() {
+        return {
+            visible: false,
+            value: this.modelValue
+        };
+    },
+    watch: {
+        modelValue(n){
+            this.value = n;
+        }
+    },
+    methods: {
+        onOk() {
+            if(this.$refs.editor.save()) {
+                this.$emit('update:modelValue', this.value);
+                this.$emit('change', this.value);
+                this.visible = false;
+            }
+        },
+    }
+});
+</script>
+
+<style>
+._fd-fn-input {
+    width: 100%;
+}
+
+._fd-fn-input .el-badge {
+    width: 100%;
+}
+
+._fd-fn-input .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-fn-input-dialog .CodeMirror-lint-tooltip {
+    z-index: 2021 !important;
+}
+
+._fd-fn-input-dialog .el-dialog__body {
+    padding: 0px;
+    height: 500px;
+}
+</style>

+ 125 - 0
src/components/form-create-designer/components/HtmlEditor.vue

@@ -0,0 +1,125 @@
+<template>
+    <div class="_fd-html-editor">
+        <el-button @click="visible=true" style="width: 100%;">{{ title || t('struct.title') }}</el-button>
+        <el-dialog class="_fd-html-editor-con" :title="title || t('struct.title')" v-model="visible"
+                  :close-on-click-modal="false" append-to-body>
+            <div ref="editor" v-if="visible"></div>
+            <template #footer>
+                <div>
+                    <el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" @click="onOk" size="default">{{ t('props.ok') }}</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import {defineComponent, markRaw} from 'vue';
+import errorMessage from '../utils/message';
+
+export default defineComponent({
+    name: 'HtmlEditor',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: String,
+        title: String,
+        defaultValue: {
+            require: false
+        },
+    },
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            editor: null,
+            visible: false,
+            oldVal: null,
+        };
+    },
+    watch: {
+        modelValue() {
+            this.load();
+        },
+        visible(n) {
+            if (n) {
+                this.load();
+            }
+        }
+    },
+    methods: {
+        validateXML(xmlString) {
+            const parser = new DOMParser();
+            const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
+            const parseErrors = xmlDoc.getElementsByTagName('parsererror');
+            if (parseErrors.length > 0) {
+                return parseErrors[0].innerText.split('\n')[0] ?? '';
+            } else {
+                return '';
+            }
+        },
+        load() {
+            this.oldVal = this.modelValue;
+            this.$nextTick(() => {
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: 'xml',
+                    lint: true,
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value: this.modelValue || ''
+                }));
+            });
+        },
+        onOk() {
+            const str = this.editor.getValue();
+            if (this.validateXML(str)) {
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            this.visible = false;
+            if (str !== this.oldVal) {
+                this.$emit('update:modelValue', str);
+            }
+            return true;
+        },
+    }
+});
+</script>
+
+<style>
+._fd-html-editor {
+    width: 100%;
+}
+
+._fd-html-editor .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-html-editor-con .CodeMirror {
+    height: 450px;
+}
+
+._fd-html-editor-con .CodeMirror-line {
+    line-height: 16px !important;
+    font-size: 13px !important;
+}
+
+._fd-html-editor-con .CodeMirror-lint-tooltip {
+    z-index: 2021 !important;
+}
+
+._fd-html-editor-con .el-dialog__body {
+    padding: 0px 20px;
+}
+</style>

+ 93 - 0
src/components/form-create-designer/components/JsonPreview.vue

@@ -0,0 +1,93 @@
+<template>
+    <el-container class="_fc-json-preview">
+        <el-header height="40px" class="_fc-l-tabs">
+            <div class="_fc-l-tab"
+                 :class="{active: active==='rule'}"
+                 @click="active='rule'"> {{ t('designer.json') }}
+            </div>
+            <div class="_fc-l-tab"
+                 :class="{active: active==='options'}"
+                 @click="active='options'"> {{ t('designer.form') }}
+            </div>
+        </el-header>
+        <el-main style="padding: 8px;">
+            <StructEditor ref="editor" v-model="value" @blur="handleBlur" @focus="handleFocus" format
+                          style="height:100%;"></StructEditor>
+        </el-main>
+    </el-container>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import StructEditor from './StructEditor.vue';
+import {designerForm} from '../utils/form';
+
+export default defineComponent({
+    name: 'JsonPreview',
+    components: {StructEditor},
+    inject: ['designer'],
+    data() {
+        return {
+            active: 'rule',
+            value: this.designer.setupState.getRule(),
+            oldValue: '',
+        }
+    },
+    watch: {
+        active() {
+            this.updateValue();
+        }
+    },
+    computed: {
+        change() {
+            if (this.active === 'rule') {
+                return this.designer.setupState.children;
+            } else {
+                return this.designer.setupState.formOptions;
+            }
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    methods: {
+        updateValue() {
+            if (this.active === 'rule') {
+                this.value = this.designer.setupState.getRule();
+            } else {
+                this.value = this.designer.setupState.getOptions();
+            }
+        },
+        handleFocus() {
+            this.oldValue = designerForm.toJson(this.value);
+        },
+        handleBlur() {
+            if (this.$refs.editor.save() && designerForm.toJson(this.value) !== this.oldValue) {
+                if (this.active === 'rule') {
+                    this.designer.setupState.setRule(this.value || []);
+                } else {
+                    this.designer.setupState.setOptions(this.value || {});
+                }
+            }
+        }
+    },
+    mounted() {
+        this.$watch(() => this.change, () => {
+            this.updateValue();
+        }, {deep: true});
+    }
+});
+</script>
+
+<style>
+._fc-json-preview {
+    display: flex;
+    width: 100%;
+    color: #262626;
+}
+
+._fc-json-preview .CodeMirror {
+    height: 100%;
+    font-size: 12px;
+}
+</style>

+ 72 - 0
src/components/form-create-designer/components/PropsInput.vue

@@ -0,0 +1,72 @@
+<template>
+    <Struct class="_fd-props-input" :modelValue="props" @update:modelValue="onInput" :title="t('designer.customProps')">
+        <i class="fc-icon icon-edit"></i>
+    </Struct>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import Struct from './Struct.vue';
+import extend from '@form-create/utils/lib/extend';
+
+export default defineComponent({
+    name: 'PropsInput',
+    components: {Struct},
+    inject: ['designer'],
+    data() {
+        return {}
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        props() {
+            const propsKeys = this.activeRule._fc_store?.props_keys || [];
+            const props = {};
+            propsKeys.forEach(k => {
+                if (this.activeRule.props && this.activeRule.props[k]) {
+                    props[k] = this.activeRule.props[k];
+                }
+            });
+            return props;
+        },
+    },
+    methods: {
+        onInput(props) {
+            if (!this.activeRule.props) {
+                this.activeRule.props = {};
+            }
+            if (!this.activeRule._fc_store) {
+                this.activeRule._fc_store = {};
+            }
+            Object.keys(this.props).forEach(k => {
+                if ((props || {})[k] == null) {
+                    delete this.activeRule.props[k];
+                }
+            });
+            extend(this.activeRule.props, props || {});
+            const keys = Object.keys(props || {});
+            if (keys.length) {
+                this.activeRule._fc_store.props_keys = keys;
+            } else {
+                delete this.activeRule._fc_store.props_keys;
+            }
+        }
+    }
+
+});
+</script>
+
+<style>
+._fd-props-input {
+    display: inline-block;
+    width: 16px;
+}
+
+._fd-props-input .fc-icon {
+    cursor: pointer;
+}
+</style>

+ 75 - 0
src/components/form-create-designer/components/Required.vue

@@ -0,0 +1,75 @@
+<template>
+    <div class="_fd-required">
+        <el-switch v-model="required"></el-switch>
+        <LanguageInput v-model="requiredMsg" v-if="required"
+                       :placeholder="t('validate.requiredPlaceholder')"></LanguageInput>
+    </div>
+</template>
+
+<script>
+import is from '@form-create/utils/lib/type';
+import {defineComponent} from 'vue';
+import LanguageInput from './language/LanguageInput.vue';
+
+export default defineComponent({
+    name: 'Required',
+    components: {LanguageInput},
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: {}
+    },
+    inject: ['designer'],
+    watch: {
+        required() {
+            this.update();
+        },
+        requiredMsg() {
+            this.update();
+        },
+        modelValue(n) {
+            const flag = is.String(n);
+            this.required = n === undefined ? false : (flag ? true : !!n);
+            this.requiredMsg = flag ? n : '';
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        const flag = is.String(this.modelValue);
+        return {
+            required: this.modelValue === undefined ? false : (flag ? true : !!this.modelValue),
+            requiredMsg: flag ? this.modelValue : ''
+        };
+    },
+    methods: {
+        update() {
+            let val;
+            if (this.required === false) {
+                val = false;
+            } else {
+                val = this.requiredMsg || true;
+            }
+            this.$emit('update:modelValue', val);
+        },
+    }
+});
+</script>
+
+<style>
+._fd-required {
+    display: flex;
+    align-items: center;
+    width: 100%;
+}
+
+._fd-required .el-input {
+    margin-left: 15px;
+}
+
+._fd-required .el-switch {
+    height: 28px;
+}
+</style>

+ 25 - 0
src/components/form-create-designer/components/Row.vue

@@ -0,0 +1,25 @@
+<template>
+    <el-col :span="24">
+        <div class="_fd-row el-row" :class="{'_fc-child-empty' : !$slots.default}" v-bind="$attrs">
+            <slot name="default"></slot>
+        </div>
+    </el-col>
+
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'fcRow',
+    mounted() {
+    }
+
+});
+</script>
+
+<style>
+._fd-row {
+    width: 100%;
+}
+</style>

+ 142 - 0
src/components/form-create-designer/components/Struct.vue

@@ -0,0 +1,142 @@
+<template>
+    <div class="_fd-struct">
+        <el-badge type="warning" is-dot :hidden="!configured">
+            <div @click="visible=true">
+                <slot>
+                    <el-button class="_fd-plain-button" plain size="small">
+                        {{ title || t('struct.title') }}
+                    </el-button>
+                </slot>
+            </div>
+        </el-badge>
+        <el-dialog class="_fd-struct-con" :title="title || t('struct.title')" v-model="visible" destroy-on-close
+                   :close-on-click-modal="false"
+                   append-to-body width="800px">
+            <div ref="editor" v-if="visible"></div>
+            <template #footer>
+                <div>
+                    <el-button @click="visible = false" size="default">{{ t('props.cancel') }}</el-button>
+                    <el-button type="primary" @click="onOk" size="default" color="#2f73ff">{{ t('props.ok') }}</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import 'codemirror/mode/javascript/javascript';
+import {deepParseFn, toJSON} from '../utils/index';
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import {defineComponent, markRaw} from 'vue';
+import is from '@form-create/utils/lib/type';
+import errorMessage from '../utils/message';
+import beautify from 'js-beautify';
+
+export default defineComponent({
+    name: 'Struct',
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: [Object, Array, Function],
+        title: String,
+        defaultValue: {
+            require: false
+        },
+        validate: Function,
+    },
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        configured() {
+            return !is.empty(this.modelValue) && Object.keys(this.modelValue).length > 0;
+        },
+    },
+    data() {
+        return {
+            editor: null,
+            visible: false,
+            oldVal: null,
+        };
+    },
+    watch: {
+        modelValue() {
+            this.load();
+        },
+        visible(n) {
+            if (n) {
+                this.load();
+            }
+        },
+    },
+    methods: {
+        load() {
+            const val = toJSON(deepParseFn(this.modelValue ? deepCopy(this.modelValue) : this.defaultValue));
+            this.oldVal = val;
+            this.$nextTick(() => {
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: 'javascript',
+                    lint: true,
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value: val ? beautify.js(val, {
+                        indent_size: '2',
+                        indent_char: ' ',
+                        max_preserve_newlines: '5',
+                        indent_scripts: 'separate',
+                    }) : '',
+                }));
+            });
+        },
+        onOk() {
+            const str = (this.editor.getValue() || '').trim();
+            let val;
+            try {
+                val = (new Function('return ' + str))();
+            } catch (e) {
+                console.error(e);
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            if (this.validate && false === this.validate(val)) {
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            this.visible = false;
+            if (toJSON(val, null, 2) !== this.oldVal) {
+                this.$emit('update:modelValue', val);
+            }
+            return true;
+        },
+    }
+});
+</script>
+
+<style>
+._fd-struct {
+    width: 100%;
+}
+
+._fd-struct .el-badge {
+    width: 100%;
+}
+
+._fd-struct .el-button {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-struct-con .CodeMirror {
+    height: 500px;
+}
+
+._fd-struct-con .el-dialog__body {
+    padding: 0px;
+}
+</style>

+ 121 - 0
src/components/form-create-designer/components/StructEditor.vue

@@ -0,0 +1,121 @@
+<template>
+    <div class="_fd-struct-editor">
+        <div ref="editor"></div>
+    </div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css';
+import CodeMirror from 'codemirror/lib/codemirror';
+import 'codemirror/mode/javascript/javascript';
+import {toJSON} from '../utils/index';
+import {defineComponent, markRaw} from 'vue';
+import errorMessage from '../utils/message';
+import {designerForm} from '../utils/form';
+import beautify from 'js-beautify';
+
+export default defineComponent({
+    name: 'StructEditor',
+    props: {
+        modelValue: [Object, Array, Function],
+        format: Boolean,
+        defaultValue: {
+            require: false
+        }
+    },
+    emits: ['blur', 'focus', 'update:modelValue'],
+    inject: ['designer'],
+    data() {
+        return {
+            editor: null,
+            visible: false,
+            err: false,
+            oldVal: null,
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    watch: {
+        modelValue(n) {
+            if (this.editor) {
+                const val = n ? this.toJson(n) : '';
+                this.oldVal = val;
+                const scrollInfo = this.editor.getScrollInfo();
+                const scrollTop = scrollInfo.top;
+                this.editor.setValue(val);
+                this.editor.scrollTo(0, scrollTop);
+            }
+        }
+    },
+    mounted() {
+        this.$nextTick(() => {
+            this.load();
+        });
+    },
+    methods: {
+        toJson(val) {
+            return this.format ? designerForm.toJson(val, 2) : toJSON(val);
+        },
+        load() {
+            const val = this.modelValue ? this.toJson(this.modelValue) : '';
+            this.oldVal = val;
+            this.$nextTick(() => {
+                this.editor = markRaw(CodeMirror(this.$refs.editor, {
+                    lineNumbers: true,
+                    mode: 'javascript',
+                    lint: true,
+                    line: true,
+                    tabSize: 2,
+                    lineWrapping: true,
+                    value: val ? beautify.js(val, {
+                        indent_size: '2',
+                        indent_char: ' ',
+                        max_preserve_newlines: '5',
+                        indent_scripts: 'separate',
+                    }) : '',
+                }));
+                this.editor.on('blur', () => {
+                    this.$emit('blur');
+                });
+                this.editor.on('focus', () => {
+                    this.$emit('focus');
+                });
+            });
+        },
+        save() {
+            const str = (this.editor.getValue() || '').trim();
+            let val;
+            try {
+                val = (new Function('return ' + str))();
+            } catch (e) {
+                console.error(e);
+                errorMessage(this.t('struct.errorMsg'));
+                return false;
+            }
+            if (this.validate && false === this.validate(val)) {
+                this.err = true;
+                return false;
+            }
+            this.visible = false;
+            if (this.toJson(val) !== this.oldVal) {
+                this.$emit('update:modelValue', val);
+            }
+            return true;
+        },
+    }
+});
+</script>
+
+<style>
+._fd-struct-editor {
+    flex: 1;
+    width: 100%;
+}
+
+._fd-struct-editor > div {
+    height: 100%;
+}
+</style>

+ 162 - 0
src/components/form-create-designer/components/TableOptions.vue

@@ -0,0 +1,162 @@
+<template>
+    <div class="_td-table-opt">
+        <el-table
+            :data="value"
+            border
+            :size="size || 'small'"
+            style="width: 100%">
+            <template v-for="(col,idx) in column" :key="col.label + idx">
+                <el-table-column :label="col.label">
+                    <template #default="scope">
+                        <template v-if="col.value">
+                            <ValueInput :size="size || 'small'" :modelValue="scope.row[col.key]"
+                                        @update:modelValue="(n)=>(scope.row[col.key] = n)"
+                                        @blur="onInput(scope.row)" @change-type="onInput(scope.row)"></ValueInput>
+                        </template>
+                        <template v-else>
+                            <el-input :size="size || 'small'" :modelValue="scope.row[col.key] || ''"
+                                      @Update:modelValue="(n)=>(scope.row[col.key] = n)"
+                                      @blur="onInput(scope.row)"></el-input>
+                        </template>
+                    </template>
+                </el-table-column>
+            </template>
+            <el-table-column width="45" align="center" fixed="right">
+                <template #default="scope">
+                    <i class="fc-icon icon-delete" @click="del(scope.$index)"></i>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="_td-table-opt-handle">
+            <el-button link type="primary" @click="add" v-if="!max || max > value.length">
+                <i class="fc-icon icon-add"></i> {{ t('tableOptions.add') }}
+            </el-button>
+        </div>
+
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {copy} from '@form-create/utils/lib/extend';
+import ValueInput from './ValueInput.vue';
+
+export default defineComponent({
+    name: 'TableOptions',
+    emits: ['update:modelValue', 'change'],
+    components: {
+        ValueInput
+    },
+    props: {
+        modelValue: [Array, Object],
+        column: {
+            type: Array,
+            default: () => [{label: 'label', key: 'label'}, {label: 'value', key: 'value'}]
+        },
+        valueType: String,
+        max: Number,
+        size: String,
+    },
+    inject: ['designer'],
+    watch: {
+        modelValue() {
+            this.value = this.tidyModelValue();
+        }
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            value: this.tidyModelValue(),
+        };
+    },
+    methods: {
+        tidyModelValue() {
+            const modelValue = this.modelValue;
+            if (this.valueType === 'string') {
+                return (modelValue || []).map(value => {
+                    return {value: '' + value}
+                })
+            } else if (this.valueType === 'object') {
+                return Object.keys((modelValue || {})).map(label => {
+                    return {label, value: modelValue[label]}
+                })
+            } else {
+                return [...modelValue || []].map(v => {
+                    return copy(v);
+                });
+            }
+        },
+        tidyValue() {
+            if (this.valueType === 'object') {
+                const obj = {};
+                this.value.forEach(v => {
+                    if (v.label && v.value) {
+                        obj[v.label] = v.value;
+                    }
+                })
+                return obj;
+            } else {
+                return this.value.map(v => {
+                    if (this.valueType === 'string') {
+                        return v.value;
+                    }
+                    return {...v}
+                });
+            }
+        },
+        onInput(item) {
+            if (this.column.length === 1 && '' === item[this.column[0].key]) {
+                return;
+            }
+            const flag = this.column.every(v => {
+                if (v.required === false) {
+                    return true;
+                }
+                if (['object', 'string'].indexOf(this.valueType) > -1) {
+                    return item[v.key] !== undefined && item[v.key] !== '' && item[v.key] !== null;
+                }
+                return item[v.key] !== undefined;
+            })
+            if (flag) {
+                this.input();
+            }
+        },
+        input() {
+            const value = this.tidyValue();
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+        add() {
+            this.value.push(this.column.reduce((initial, v) => {
+                initial[v.key] = '';
+                return initial;
+            }, {}));
+        },
+        del(idx) {
+            this.value.splice(idx, 1);
+            this.input();
+        }
+    }
+});
+</script>
+
+<style scoped>
+._td-table-opt {
+    width: 100%;
+}
+
+._td-table-opt .el-table {
+    z-index: 1;
+}
+
+._td-table-opt-handle {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-right: 5px;
+}
+</style>

+ 166 - 0
src/components/form-create-designer/components/TreeOptions.vue

@@ -0,0 +1,166 @@
+<template>
+    <div class="_fd-tree-opt">
+        <el-tree
+            :data="value"
+            node-key="index"
+            :expand-on-click-node="false">
+            <template #default="{ node, data }">
+                <div class="_fd-tree-opt-node">
+                    <el-input class="_fd-tree-opt-first" v-model="data[overColumns.label]"
+                              @blur="change"/>
+                    <ValueInput class="_fd-tree-opt-last" v-model="data[overColumns.value]" @blur="change"
+                                @change-type="change">
+                        <template #append>
+                            <div class="_fd-tree-opt-btn" @click="add(node, data)">
+                                <i class="fc-icon icon-add"></i>
+                            </div>
+                            <div class="_fd-tree-opt-btn" @click="append(data)">
+                                <i class="fc-icon icon-add-child"></i>
+                            </div>
+                            <div class="_fd-tree-opt-btn _fd-tree-opt-danger" @click="remove(node, data)">
+                                <i class="fc-icon icon-delete"></i>
+                            </div>
+                        </template>
+                    </ValueInput>
+                </div>
+            </template>
+        </el-tree>
+    </div>
+
+</template>
+
+<script>
+
+import {defineComponent} from 'vue';
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import ValueInput from './ValueInput.vue';
+
+export default defineComponent({
+    name: 'TreeOptions',
+    emits: ['update:modelValue'],
+    components: {
+        ValueInput
+    },
+    props: {
+        modelValue: Array,
+        columns: Object,
+    },
+    inject: ['designer'],
+    data() {
+        return {
+            value: [...deepCopy(this.modelValue || [])],
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        overColumns() {
+            if (!this.columns) {
+                return {
+                    label: 'label',
+                    value: 'value',
+                };
+            }
+            return {
+                label: this.columns.label || 'label',
+                value: this.columns.value || 'value',
+            }
+        }
+    },
+    created() {
+        if (!this.value.length) {
+            this.value = [{}]
+        }
+    },
+    methods: {
+        tidyValue() {
+            return deepCopy(this.value);
+        },
+        change() {
+            this.$emit('update:modelValue', this.tidyValue());
+        },
+        add(node) {
+            const parent = node.parent;
+            const children = parent.data.children || parent.data;
+            children.push({});
+        },
+        append(data) {
+            if (!data.children) {
+                data.children = [];
+            }
+            data.children.push({});
+        },
+        remove(node, data) {
+            const parent = node.parent;
+            if (parent.data.children) {
+                parent.data.children.splice(parent.data.children.indexOf(data), 1);
+                if (!parent.data.children.length) {
+                    delete parent.data.children;
+                }
+            } else {
+                parent.data.splice(parent.data.indexOf(data), 1);
+            }
+            this.change();
+        },
+    }
+});
+</script>
+
+<style>
+._fd-tree-opt ._fd-tree-opt-btn {
+    height: 19px;
+    width: 18px;
+    color: #fff;
+    text-align: center;
+    line-height: 20px;
+    padding-bottom: 1px;
+    float: left;
+    cursor: pointer;
+    justify-content: center;
+    background-color: #2f73ff;
+}
+
+._fd-tree-opt-node {
+    display: flex;
+    align-items: center;
+}
+
+._fd-tree-opt-first {
+    width: 60px;
+    margin-right: 5px;
+}
+
+._fd-tree-opt-last {
+    width: 165px;
+}
+
+._fd-tree-opt-last._label {
+    width: 175px;
+}
+
+._fd-tree-opt-last._label .el-input-group__append {
+    width: 65px;
+}
+
+._fd-tree-opt ._fd-tree-opt-danger {
+    background-color: #ff2d2e;
+    border-radius: 0 2px 2px 0;
+}
+
+._fd-tree-opt .el-tree-node__content {
+    margin-bottom: 3px;
+    height: 28px;
+}
+
+._fd-tree-opt .el-input__inner {
+    border-right: 0 none;
+}
+
+._fd-tree-opt .el-input-group__append {
+    width: 90px;
+    padding-right: 2px;
+    padding-left: 1px;
+    background: #fff;
+}
+</style>

+ 140 - 0
src/components/form-create-designer/components/TypeSelect.vue

@@ -0,0 +1,140 @@
+<template>
+    <el-dropdown class="_fd-type-select" trigger="click" size="default" popper-class="_fd-type-select-pop"
+                 :disabled="!menus.length" @command="handleCommand">
+        <el-tag type="success" effect="plain" disable-transitions>
+            <template v-if="activeRule">
+                {{ t('com.' + (activeRule._menu.name) + '.name') || activeRule._menu.label }} <i
+                class="fc-icon icon-down" v-if="menus.length"></i>
+            </template>
+            <template v-else>
+                {{
+                    t('com.' + (customForm.config.name) + '.name') || customForm.config.label || customForm.config.name
+                }}
+            </template>
+        </el-tag>
+        <template #dropdown>
+            <el-dropdown-menu>
+                <el-dropdown-item :command="item" v-for="item in menus" :key="item.name">
+                    <div><i class="fc-icon" :class="item.icon || 'icon-input'"></i>{{ t('com.' + (item.name) + '.name') || item.label }}</div>
+                </el-dropdown-item>
+            </el-dropdown-menu>
+        </template>
+    </el-dropdown>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'TypeSelect',
+    inject: ['designer'],
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        activeRule() {
+            return this.designer.setupState.activeRule;
+        },
+        customForm() {
+            return this.designer.setupState.customForm;
+        },
+        menus() {
+            let menus = [];
+            const designer = this.designer.setupState;
+            if (this.activeRule) {
+                const name = this.activeRule._menu.name;
+                const switchConfig = designer.getConfig('switchType', []);
+                if (switchConfig === false) {
+                    return menus;
+                }
+                let switchs = [];
+                switchConfig.forEach(lst => {
+                    if (lst.indexOf(name) > -1) {
+                        switchs.push(...lst);
+                    }
+                });
+                switchs = switchs.filter((key, idx) => {
+                    return key !== name && switchs.indexOf(key) === idx;
+                });
+                if (switchs.length) {
+                    designer.menuList.forEach(item => {
+                        item.list.forEach(menu => {
+                            if (switchs.indexOf(menu.name) > -1) {
+                                menus.push(menu);
+                            }
+                        });
+                    });
+                } else {
+                    designer.menuList.forEach(item => {
+                        if (item.name === this.activeRule._menu.menu) {
+                            item.list.forEach(menu => {
+                                if (menu.name !== name) {
+                                    menus.push(menu);
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+            return menus.filter(menu => this.designer.setupState.hiddenItem.indexOf(menu.name) === -1);
+        }
+    },
+    methods: {
+        handleCommand(item) {
+            let activeRule = this.activeRule;
+            let rule = this.activeRule;
+            if (!rule._menu.inside) {
+                rule = rule.__fc__.parent.rule;
+            }
+            const children = rule.__fc__.parent.rule.children;
+            const replaceRule = this.designer.setupState.makeRule(item);
+            let newRule = replaceRule;
+            if (replaceRule.type === 'DragTool') {
+                newRule = replaceRule.children[0];
+            }
+            if (newRule.field && activeRule.field) {
+                ['title', 'info', 'field', 'validate', 'control', '$required'].forEach(k => {
+                    newRule[k] = activeRule[k];
+                });
+            } else if (activeRule?.computed?.hidden) {
+                newRule.computed = {hidden: activeRule.computed.hidden}
+            }
+            if (activeRule.name) {
+                newRule.name = activeRule.name;
+            }
+            ['name', 'id', 'on'].forEach(k => {
+                if (activeRule[k]) {
+                    newRule[k] = activeRule[k];
+                }
+            })
+            children.splice(children.indexOf(rule), 1, replaceRule);
+            this.$nextTick(() => {
+                this.designer.setupState.triggerActive(newRule);
+            });
+        }
+    }
+});
+</script>
+
+<style>
+._fd-type-select {
+    cursor: pointer;
+}
+
+._fd-type-select.is-disabled {
+    cursor: default;
+}
+
+._fd-type-select .fc-icon {
+    font-size: 14px;
+}
+
+._fd-type-select-pop {
+    max-height: 500px;
+    overflow: auto;
+}
+
+._fd-type-select-pop .fc-icon {
+    font-size: 14px;
+}
+</style>

+ 244 - 0
src/components/form-create-designer/components/Validate.vue

@@ -0,0 +1,244 @@
+<template>
+    <div class="_fd-validate">
+        <template v-for="(item, idx) in validate">
+            <div class="_fd-validate-item">
+                <div class="_fd-validate-title">
+                    <div>
+                        <span>{{ idx + 1 }}</span>
+                        {{ modes[item.mode] }}
+                    </div>
+                    <i class="fc-icon icon-delete2" @click="remove(idx)"></i>
+                </div>
+                <el-row>
+                    <el-col :span="getSpan(item)">
+                        <el-form-item :label="t('validate.mode')">
+                            <el-select v-model="item.trigger" @change="onInput">
+                                <el-option
+                                    v-for="item in triggers"
+                                    :key="item.value"
+                                    :label="item.label"
+                                    :value="item.value"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="getSpan(item)">
+                        <el-form-item :label="modes[item.mode]">
+                            <template v-if="item.mode === 'pattern'">
+                                <elInput v-model="item[item.mode]" @change="onInput"></elInput>
+                            </template>
+                            <template v-else-if="item.mode === 'validator'">
+                                <FnInput v-model="item[item.mode]" name="name" :args="['rule', 'value', 'callback']"
+                                         @change="onInput">{{ t('validate.modes.validator') }}
+                                </FnInput>
+                            </template>
+                            <template v-else>
+                                <el-input-number v-model="item[item.mode]" @change="onInput"></el-input-number>
+                            </template>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="24">
+                        <el-form-item :label="t('validate.message')">
+                            <LanguageInput v-model="item.message" :placeholder="t('validate.requiredPlaceholder')"
+                                           @change="onInput">
+                            </LanguageInput>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </div>
+        </template>
+
+        <el-dropdown trigger="click" size="default" popper-class="_fd-validate-pop" @command="handleCommand">
+            <el-button class="_fd-validate-btn" size="small">{{ t('validate.rule') }} +</el-button>
+            <template #dropdown>
+                <el-dropdown-menu>
+                    <el-dropdown-item :command="value" v-for="(label, value) in modes" :key="value">
+                        <div>{{ label }}</div>
+                    </el-dropdown-item>
+                </el-dropdown-menu>
+            </template>
+        </el-dropdown>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {localeOptions} from '../utils';
+import FnInput from './FnInput.vue';
+import {deepCopy} from '@form-create/utils/lib/deepextend';
+import LanguageInput from './language/LanguageInput.vue';
+
+export default defineComponent({
+    name: 'Validate',
+    inject: ['designer'],
+    emits: ['update:modelValue'],
+    props: {
+        modelValue: Array,
+    },
+    components: {
+        LanguageInput,
+        FnInput,
+    },
+    watch: {
+        modelValue(n) {
+            this.validate = this.parseValue(n || []);
+        }
+    },
+    data() {
+        return {
+            validate: this.parseValue(this.modelValue || []),
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        modes() {
+            const activeRule = this.designer.setupState.activeRule;
+            if (activeRule && activeRule._menu.subForm === 'object') {
+                return {
+                    validator: this.t('validate.modes.validator'),
+                }
+            } else {
+                return {
+                    min: this.t('validate.modes.min'),
+                    max: this.t('validate.modes.max'),
+                    len: this.t('validate.modes.len'),
+                    pattern: this.t('validate.modes.pattern'),
+                    validator: this.t('validate.modes.validator'),
+                }
+            }
+        },
+        triggers() {
+            return localeOptions(this.t, [
+                {label: 'blur', value: 'blur'},
+                {label: 'change', value: 'change'},
+                {label: 'submit', value: 'submit'},
+            ]);
+        }
+    },
+    methods: {
+        handleCommand(mode) {
+            this.validate.push({
+                transform: new Function('val', 'this.type = val == null ? \'string\' : (Array.isArray(val) ? \'array\' : (typeof val)); return val;'),
+                mode,
+                trigger: 'blur'
+            });
+        },
+        autoMessage(item) {
+            const title = this.designer.setupState.activeRule.title;
+            if (this.designer.setupState.activeRule) {
+                item.message = this.t('validate.autoRequired', {title})
+                this.onInput();
+            }
+        },
+        getSpan(item) {
+            return ['pattern', 'validator', 'required'].indexOf(item.mode) > -1 ? 24 : 12;
+        },
+        onInput: function () {
+            this.$emit('update:modelValue', this.validate.map(item => {
+                item = {...item};
+                if (!item.message) {
+                    delete item.message;
+                }
+                return item;
+            }));
+        },
+        remove(idx) {
+            this.validate.splice(idx, 1);
+            this.onInput();
+        },
+        parseValue(val) {
+            return deepCopy(val.map(v => {
+                if (v.validator) {
+                    v.mode = 'validator';
+                }
+                if (!v.mode) {
+                    Object.keys(v).forEach(k => {
+                        if (['message', 'type', 'trigger', 'mode'].indexOf(k) < 0) {
+                            v.mode = k;
+                        }
+                    });
+                }
+                return v;
+            }));
+        }
+    }
+});
+</script>
+
+<style>
+
+._fd-validate {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+}
+
+._fd-validate-btn {
+    font-weight: 400;
+    width: 100%;
+    border-color: #2E73FF;
+    color: #2E73FF;
+}
+
+._fd-validate-pop .el-dropdown-menu__item {
+    width: 248px;
+}
+
+._fd-validate-item {
+    border-bottom: 1px dashed #ECECEC;
+    margin-bottom: 10px;
+}
+
+._fd-validate-item .el-col-12:first-child {
+    padding-right: 5px;
+}
+
+._fd-validate-item .el-col-12 + .el-col-12 {
+    padding-left: 5px;
+}
+
+._fd-validate-item .el-input-number {
+    width: 100%;
+}
+
+._fd-validate-title {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    margin-bottom: 10px;
+}
+
+._fd-validate-title > div {
+    display: flex;
+    align-items: center;
+}
+
+._fd-validate-title > div > span {
+    width: 16px;
+    height: 16px;
+    background: #ECECEC;
+    text-align: center;
+    font-size: 12px;
+    line-height: 16px;
+    border-radius: 15px;
+    margin-right: 5px;
+}
+
+._fd-validate-title i {
+    cursor: pointer;
+}
+
+._fd-validate-title i:hover {
+    color: #FF2E2E;
+}
+
+._fd-validate .append-msg {
+    cursor: pointer;
+}
+
+._fd-validate .el-input-group__append {
+    padding: 0 10px;
+}
+</style>

+ 88 - 0
src/components/form-create-designer/components/ValueInput.vue

@@ -0,0 +1,88 @@
+<template>
+    <el-input class="_fd-value-input" v-model="value" @blur="onBlur" v-bind="$attrs">
+        <template #prepend>
+            <el-select v-model="type" style="width: 60px">
+                <el-option :label="t('validate.types.string')" value="1"/>
+                <el-option :label="t('validate.types.number')" value="2"/>
+                <el-option :label="t('validate.types.boolean')" value="3"/>
+            </el-select>
+        </template>
+        <template #append v-if="$slots.append">
+            <slot name="append"></slot>
+        </template>
+    </el-input>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'ValueInput',
+    emits: ['update:modelValue', 'change', 'change-type', 'blur'],
+    inject: ['designer'],
+    props: {
+        modelValue: [String, Number, Boolean],
+    },
+    data() {
+        return {
+            type: '1',
+            value: '',
+        }
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    watch: {
+        modelValue: {
+            handler: function (val) {
+                if (typeof val === 'number') {
+                    this.type = '2';
+                } else if (typeof val === 'boolean') {
+                    this.type = '3';
+                } else {
+                    this.type = '1';
+                }
+                this.value = null == val ? '' : ('' + val);
+            },
+            immediate: true,
+        },
+        type() {
+            this.updateValue(this.value);
+            this.$emit('change-type', this.type);
+        }
+    },
+    methods: {
+        onBlur(...args) {
+            if (this.value !== this.toValue(this.modelValue)) {
+                this.updateValue(this.value);
+            }
+            this.$emit('blur', ...args);
+        },
+        updateValue(val) {
+            const value = this.toValue(val);
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+        toValue(val) {
+            if (this.type === '1') {
+                return '' + val;
+            } else if (this.type === '2') {
+                return parseFloat(val) || 0;
+            }
+            return val === 'true';
+        }
+    }
+});
+</script>
+
+<style>
+._fd-value-input .el-input__validateIcon {
+    display: none;
+}
+
+._fd-value-input .el-select, ._fd-value-input .el-select__wrapper {
+    height: 100%;
+}
+</style>

+ 45 - 0
src/components/form-create-designer/components/Warning.vue

@@ -0,0 +1,45 @@
+<template>
+    <el-tooltip
+        effect="dark"
+        placement="top-start"
+        popper-class="_fd-warning-pop"
+    >
+        <template #content>
+            <span v-html="tooltip"></span>
+        </template>
+        <template v-if="$slots.default">
+            <span class="_fd-warning-text">
+                <slot></slot>
+            </span>
+        </template>
+        <template v-else>
+            <i class="fc-icon icon-question"></i>
+        </template>
+    </el-tooltip>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'Warning',
+    props: {
+        tooltip: String,
+    },
+    data() {
+        return {}
+    },
+});
+</script>
+
+<style>
+._fd-warning-pop {
+    max-width: 400px;
+}
+
+._fd-warning-text {
+    text-decoration: underline;
+    text-decoration-style: dashed;
+    cursor: help;
+}
+</style>

+ 165 - 0
src/components/form-create-designer/components/language/LanguageConfig.vue

@@ -0,0 +1,165 @@
+<template>
+    <div class="_fd-language-config">
+        <div class="_fc-l-label">{{ t('language.name') }}</div>
+        <div class="_fc-l-info">
+            {{ t('warning.language') }}
+        </div>
+        <div class="_fd-lc-header">
+            <el-button size="small" @click="addColumn">{{ t('language.add') }}</el-button>
+            <el-button size="small" type="danger" plain :disabled="!selected.length" @click="batchRmColumn">
+                {{ t('language.batchRemove') }}
+            </el-button>
+        </div>
+        <div class="_fd-lc-body">
+            <el-table :data="column" size="small" ref="table"
+                      @selection-change="selectionChange" row-key="key">
+                <el-table-column type="selection" width="30px"></el-table-column>
+                <el-table-column prop="key" label="Key" width="90px"></el-table-column>
+                <template v-for="item in localeOptions" :key="item.value">
+                    <el-table-column :prop="item.value" :label="item.label" min-width="100px">
+                        <template #default="scope">
+                            <template v-if="scope.row.input">
+                                <el-input size="small" v-model="scope.row[item.value]" @blur="saveColumn(scope.row, true)"></el-input>
+                            </template>
+                            <template v-else>
+                                {{ scope.row[item.value] || '-' }}
+                            </template>
+                        </template>
+                    </el-table-column>
+                </template>
+                <el-table-column width="75px" :label="t('tableOptions.handle')" fixed="right">
+                    <template #default="scope">
+                        <div class="_fd-lc-handle">
+                            <i class="fc-icon icon-edit" v-if="!scope.row.input" @click="scope.row.input = true"></i>
+                            <i class="fc-icon icon-check" v-else @click="saveColumn(scope.row)"></i>
+                            <i class="fc-icon icon-group" @click="copy(scope.row.key)"></i>
+                            <i class="fc-icon icon-delete-circle" @click="rmColumn(scope.$index)"></i>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+    </div>
+
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {copyTextToClipboard} from '../../utils';
+
+export default defineComponent({
+    name: 'LanguageConfig',
+    inject: ['designer'],
+    computed: {
+        localeOptions() {
+            return this.designer.setupState.getConfig('localeOptions', [
+                {value: 'zh-cn', label: '简体中文'},
+                {value: 'en', label: 'English'},
+            ]);
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            column: [],
+            uni: 0,
+            selected: [],
+        }
+    },
+    methods: {
+        copy(key) {
+            copyTextToClipboard(key);
+        },
+        addColumn() {
+            this.column.unshift({
+                key: this.randomString(),
+                input: true,
+            })
+        },
+        saveColumn(row, input) {
+            row.input = input || false;
+            const language = this.designer.setupState.formOptions.language;
+            this.localeOptions.forEach(item => {
+                if (!language[item.value]) {
+                    language[item.value] = {};
+                }
+                language[item.value][row.key] = row[item.value];
+            })
+        },
+        rmColumn(idx) {
+            const row = this.column[idx];
+            this.column.splice(idx, 1);
+            const language = this.designer.setupState.formOptions.language;
+            this.localeOptions.forEach(item => {
+                if (language[item.value]) {
+                    delete language[item.value][row.key]
+                }
+            })
+        },
+        batchRmColumn() {
+            this.selected.forEach(item => {
+                this.rmColumn(this.column.indexOf(item));
+            });
+            this.selected = [];
+        },
+        selectionChange(list) {
+            this.selected = list;
+        },
+        randomString() {
+            const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+            let result = '';
+            const charactersLength = characters.length;
+
+            for (let i = 0; i < 7; i++) {
+                result += characters.charAt(Math.floor(Math.random() * charactersLength));
+            }
+            return characters.charAt((this.uni++) % 26) + result;
+        }
+    },
+    mounted() {
+        const language = this.designer.setupState.formOptions.language || {};
+        const column = {};
+        Object.keys(language).forEach(lang => {
+            Object.keys(language[lang]).forEach(key => {
+                if (!column[key]) {
+                    column[key] = {
+                        key: key,
+                    }
+                }
+                column[key][lang] = language[lang][key];
+            })
+        });
+        this.column = Object.values(column);
+    }
+
+});
+</script>
+
+<style>
+._fd-lc-body, ._fd-lc-header {
+    padding: 0 12px;
+}
+
+._fd-lc-body {
+    overflow: auto;
+}
+
+._fd-lc-header {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 12px;
+}
+
+._fd-language-config .el-table__cell {
+    height: 34px;
+}
+
+._fd-lc-handle {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    cursor: pointer;
+}
+</style>

+ 188 - 0
src/components/form-create-designer/components/language/LanguageInput.vue

@@ -0,0 +1,188 @@
+<template>
+    <el-input class="_fd-language-input" :class="{'is-variable': isVar}" :placeholder="placeholder" :disabled="disabled"
+              :modelValue="modelValue"
+              @update:modelValue="onInput"
+              @blur="$emit('blur')"
+              :size="size || 'small'">
+        <template #append>
+            <el-popover placement="bottom-end" :width="300" :hide-after="0" trigger="click" ref="pop"
+                        popper-class="_fd-language-popover">
+                <template #reference>
+                    <i class="fc-icon icon-language"></i>
+                </template>
+                <div class="_fd-language-list">
+                    <div class="_fd-language-header">
+                        <div class="_fd-language-title">
+                            {{ t('language.select') }}<i class="fc-icon icon-setting" @click="openConfig"></i>
+                        </div>
+                        <div class="_fd-language-name">
+                            <template v-for="item in localeList" :key="item.value">
+                                <div>{{ item.label }}</div>
+                            </template>
+                        </div>
+                    </div>
+                    <template v-for="lang in language" :key="lang.key">
+                        <div class="_fd-language-item" @click="clickLang(lang.key)">
+                            <template v-for="item in localeList" :key="item.value">
+                                <div>{{ lang[item.value] || '-' }}</div>
+                            </template>
+                        </div>
+                    </template>
+                </div>
+            </el-popover>
+        </template>
+    </el-input>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'LanguageInput',
+    inject: ['designer'],
+    emits: ['update:modelValue', 'blur', 'change'],
+    props: {
+        size: String,
+        placeholder: String,
+        modelValue: String,
+        disabled: Boolean,
+    },
+    computed: {
+        isVar() {
+            return !!(this.modelValue || '').match(/^\{\{\s*\$t\.(.+)\s*\}\}$/);
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+        localeList() {
+            const localeOptions = this.designer.setupState.getConfig('localeOptions', [
+                {value: 'zh-cn', label: '简体中文'},
+                {value: 'en', label: 'English'},
+            ]);
+            const localeList = [];
+            const locale = this.designer.props?.locale?.name || 'zh-cn';
+            localeOptions.forEach((item) => {
+                if (item.value === locale) {
+                    localeList.unshift(item);
+                } else if (localeList.length < 2) {
+                    localeList.push(item);
+                }
+            });
+            if (localeList.length > 2) {
+                localeList.pop();
+            }
+            return localeList;
+        },
+        language() {
+            const language = this.designer.setupState.formOptions.language || {};
+            const column = {};
+            Object.keys(language).forEach(lang => {
+                Object.keys(language[lang]).forEach(key => {
+                    if (!column[key]) {
+                        column[key] = {
+                            key: key,
+                        }
+                    }
+                    column[key][lang] = language[lang][key];
+                })
+            });
+            return Object.values(column);
+        }
+    },
+    methods: {
+        openConfig() {
+            this.designer.setupState.activeModule = 'language';
+        },
+        clickLang(key) {
+            this.onInput(`{{$t.${key}}}`);
+            this.$refs.pop.hide();
+        },
+        onInput(val) {
+            this.$emit('update:modelValue', val);
+            this.$emit('change', val);
+        }
+    },
+    mounted() {
+    }
+
+});
+</script>
+
+<style>
+._fd-language-list {
+    max-height: 320px;
+    padding-top: 70px;
+    overflow: auto;
+}
+
+._fd-language-input .el-input-group__append {
+    width: 25px;
+    padding: 0;
+    margin: 0;
+    color: #AAAAAA;
+    cursor: pointer;
+}
+
+._fd-language-input.is-variable input {
+    color: #2E73FF;
+}
+
+._fd-language-header, ._fd-language-item {
+    display: flex;
+    border-bottom: 1px solid #ECECEC;
+    padding: 0 12px;
+}
+
+._fd-language-header {
+    font-weight: 500;
+    padding-top: 10px;
+    overflow: auto;
+    color: #262626;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    background-color: #FFFFFF;
+    flex-direction: column;
+}
+
+._fd-language-name > div, ._fd-language-item > div {
+    flex: 1;
+    font-size: 12px;
+    padding: 5px;
+    min-width: 70px;
+}
+
+._fd-language-title {
+    margin: 6px 0;
+}
+
+._fd-language-title .fc-icon {
+    color: #2E73FF;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+._fd-language-name {
+    display: flex;
+}
+
+._fd-language-name > div {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+._fd-language-item {
+    cursor: pointer;
+}
+
+._fd-language-item:hover {
+    color: #2E73FF;
+    background-color: #CCDFFF;
+}
+
+._fd-language-popover {
+    padding: 0 !important;
+}
+</style>

+ 242 - 0
src/components/form-create-designer/components/style/BorderInput.vue

@@ -0,0 +1,242 @@
+<template>
+    <ConfigItem :label="t('style.border')">
+        <div class="line-box" :style="borderStyleStr">
+            <div class="line-box-con"></div>
+        </div>
+        <template #append>
+            <div class="_fd-border-input">
+                <div class="_fd-bi-left">
+                    <div class="_fd-bil-row">
+                        <div class="_fd-bil-col" :class="active === 'Top' ? 'active' : ''" @click="active = 'Top'">┳
+                        </div>
+                    </div>
+                    <div class="_fd-bil-row">
+                        <div class="_fd-bil-col" :class="active === 'Left' ? 'active' : ''" @click="active = 'Left'">┣
+                        </div>
+                        <div class="_fd-bil-col" :class="active === '' ? 'active' : ''" @click="active = ''">╋</div>
+                        <div class="_fd-bil-col" :class="active === 'Right' ? 'active' : ''" @click="active = 'Right'">
+                            ┫
+                        </div>
+                    </div>
+                    <div class="_fd-bil-row">
+                        <div class="_fd-bil-col" :class="active === 'Bottom' ? 'active' : ''"
+                             @click="active = 'Bottom'">┻
+                        </div>
+                    </div>
+                </div>
+                <div class="_fd-bi-right">
+                    <el-select v-model="curStyle" clearable>
+                        <el-option
+                            v-for="item in lineType"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        >
+                            <div class="_fd-bi-opt">
+                                <div class="_line" :class="item.value"></div>
+                            </div>
+                        </el-option>
+                    </el-select>
+                    <SizeInput v-model="curWidth"/>
+                    <ColorInput v-model="curColor"/>
+                </div>
+            </div>
+        </template>
+    </ConfigItem>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import SizeInput from './SizeInput.vue';
+import ColorInput from './ColorInput.vue';
+import ConfigItem from './ConfigItem.vue';
+import {toLine} from '@form-create/utils';
+
+export default defineComponent({
+    name: 'BorderInput',
+    components: {ColorInput, SizeInput, ConfigItem},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+            this.initCur();
+        },
+        active() {
+            this.initCur();
+        },
+    },
+    computed: {
+        borderStyleStr() {
+            let str = '';
+            Object.keys(this.borderStyle).forEach((key) => {
+                if (this.borderStyle[key] !== '') {
+                    str += toLine(key) + ': ' + this.borderStyle[key] + ';';
+                }
+            }, {})
+            return str;
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            active: '',
+            borderStyle: {},
+            curStyle: '',
+            curColor: '',
+            curWidth: '',
+            lineType: ['solid', 'dashed', 'dotted', 'double'].map(k => {
+                return {value: k, label: t('style.' + k)}
+            }),
+            position: ['Top', 'Left', 'Bottom', 'Right'],
+            type: ['Style', 'Color', 'Width'],
+            unwatch: null,
+        }
+    },
+    methods: {
+        tidyValue() {
+            const key = [];
+            this.borderStyle = {};
+            ['', ...this.position].forEach(k => {
+                this.type.forEach(t => {
+                    key.push('border' + k + t);
+                });
+            });
+            key.forEach(k => {
+                this.borderStyle[k] = this.modelValue[k] || '';
+            });
+        },
+        onInput() {
+            const style = Object.keys(this.borderStyle).reduce((acc, key) => {
+                if (this.borderStyle[key] !== '') {
+                    acc[key] = this.borderStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+        pushCur() {
+            this.borderStyle['border' + this.active + 'Style'] = this.curStyle || '';
+            this.borderStyle['border' + this.active + 'Color'] = this.curColor || '';
+            this.borderStyle['border' + this.active + 'Width'] = this.curWidth || '';
+            this.onInput();
+        },
+        initCur() {
+            this.unwatch && this.unwatch();
+            this.curStyle = this.borderStyle['border' + this.active + 'Style'] || '';
+            this.curColor = this.borderStyle['border' + this.active + 'Color'] || '';
+            this.curWidth = this.borderStyle['border' + this.active + 'Width'] || '';
+            this.unwatch = this.$watch(() => [this.curStyle, this.curColor, this.curWidth], () => {
+                this.pushCur();
+            });
+        },
+    },
+    created() {
+        this.tidyValue();
+        this.initCur();
+    }
+
+});
+</script>
+
+<style>
+._fd-border-input {
+    width: 100%;
+    height: 110px;
+    display: flex;
+    justify-content: center;
+}
+
+._fd-border-input ._fd-bi-left {
+    width: 115px;
+    height: 115px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+._fd-border-input ._fd-bi-right {
+    width: 140px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    padding: 5px;
+}
+
+._fd-border-input ._fd-bi-right ._fd-color-input {
+    width: 140px;
+}
+
+._fd-bi-opt {
+    width: 100%;
+    display: flex;
+    height: 100%;
+    align-items: center;
+}
+
+._fd-bi-opt ._line {
+    width: 100%;
+}
+
+._fd-bi-opt .solid {
+    border: 1px solid #000;
+}
+
+._fd-bi-opt .dashed {
+    border: 1px dashed #000;
+}
+
+._fd-bi-opt .dotted {
+    border: 1px dotted #000;
+}
+
+._fd-bi-opt .double {
+    border: 1px double #000;
+}
+
+._fd-border-input ._fd-bil-row {
+    height: 38px;
+    display: flex;
+    justify-content: center;
+}
+
+._fd-border-input ._fd-bil-col {
+    width: 22px;
+    height: 22px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    margin: 8px;
+    font-size: 16px;
+}
+
+._fd-border-input ._fd-bil-col.active {
+    outline: 1px dashed #2E73FF;
+    color: #2E73FF;
+}
+
+.line-box {
+    width: 150px;
+    height: 20px;
+    padding: 1px;
+    box-sizing: border-box;
+}
+
+.line-box-con {
+    width: 100%;
+    height: 100%;
+    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAD5JREFUOE9jZGBg+M+AChjR+HjlQYqHgQFoXibNS+gBBjKMpDAZHAaQ5GQGBgYUV4+mA7QAgaYokgJ14NMBAK1TIAlUJpxYAAAAAElFTkSuQmCC");
+    opacity: 0.3;
+}
+
+</style>

+ 166 - 0
src/components/form-create-designer/components/style/BoxSizeInput.vue

@@ -0,0 +1,166 @@
+<template>
+    <div class="_fd-box-size-input">
+        <ConfigItem :label="t('props.size')" :info="Object.keys(modelValue).length > 0 ? t('struct.configured') : ''">
+            <template #append>
+                <el-form label-position="top" size="small">
+                    <el-form-item :label="t('style.' + key)" v-for="key in keys" :key="key">
+                        <SizeInput v-model="boxStyle[key]" @change="onInput"></SizeInput>
+                    </el-form-item>
+                    <el-form-item :label="t('style.overflow.name')" style="grid-column: span 2;">
+                        <el-radio-group :modelValue="boxStyle.overflow">
+                            <el-tooltip
+                                effect="dark"
+                                :content="t('style.overflow.' + item.value)"
+                                placement="top"
+                                persistent
+                                :hide-after="0"
+                                v-for="item in overflow"
+                                :key="item.value"
+                            >
+                                <el-radio-button :label="item.value" :value="item.value"
+                                                 @click="changeOverflow(item.value)">
+                                    <template v-if="item.text">
+                                        <span style="font-size: 12px;line-height: 16px;">Auto
+                                        </span>
+                                    </template>
+                                    <template v-else>
+                                        <i class="fc-icon" :class="item.icon"></i>
+                                    </template>
+                                </el-radio-button>
+                            </el-tooltip>
+                        </el-radio-group>
+                    </el-form-item>
+                </el-form>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import ConfigItem from './ConfigItem.vue';
+import SizeInput from './SizeInput.vue';
+
+export default defineComponent({
+    name: 'BoxSizeInput',
+    components: {SizeInput, ConfigItem},
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    data() {
+        return {
+            overflow: [
+                {
+                    value: 'visible',
+                    icon: 'icon-eye',
+                },
+                {
+                    value: 'hidden',
+                    icon: 'icon-eye-close',
+                },
+                {
+                    value: 'scroll',
+                    icon: 'icon-scroll',
+                },
+                {
+                    value: 'auto',
+                    text: 'Auto',
+                },
+            ],
+            keys: ['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
+            boxStyle: {
+                width: '',
+                minWidth: '',
+                maxWidth: '',
+                height: '',
+                minHeight: '',
+                maxHeight: '',
+                overflow: '',
+            },
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    methods: {
+        tidyValue() {
+            this.boxStyle = {
+                width: '',
+                minWidth: '',
+                maxWidth: '',
+                height: '',
+                minHeight: '',
+                maxHeight: '',
+                overflow: '',
+            };
+            if (!this.modelValue) {
+                return;
+            }
+            Object.keys(this.boxStyle).forEach(k => {
+                if (this.modelValue[k]) {
+                    this.boxStyle[k] = this.modelValue[k];
+                }
+            });
+        },
+        onInput() {
+            const style = Object.keys(this.boxStyle).reduce((acc, key) => {
+                if (this.boxStyle[key] !== '') {
+                    acc[key] = this.boxStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+        changeOverflow(val) {
+            this.boxStyle.overflow = this.boxStyle.overflow === val ? '' : val;
+            this.onInput();
+        },
+        change(type, e) {
+            this.boxStyle[type] = e.target.value;
+        },
+    },
+    created() {
+        this.tidyValue();
+    }
+});
+
+</script>
+
+<style>
+._fd-box-size-input .el-form {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    width: 100%;
+    grid-column-gap: 10px;
+}
+
+._fd-box-size-input .el-radio-group {
+    width: 100%;
+}
+
+._fd-box-size-input .el-radio-button__inner {
+    width: 100%;
+    padding: 4px;
+}
+
+._fd-box-size-input .el-radio-button {
+    flex: 1;
+}
+
+._fd-box-size-input ._fd-size-input .el-input-number--small {
+    width: 100%;
+}
+</style>

+ 269 - 0
src/components/form-create-designer/components/style/BoxSpaceInput.vue

@@ -0,0 +1,269 @@
+<template>
+    <div class="_fd-box-space-input">
+        <div class="_padding">
+            <span class="_padding-title">
+                {{ t('style.margin') }}
+            </span>
+            <input class="_fd-input _fd-top" placeholder="        " :value="boxStyle.marginTop" type="text"
+                   @blur="(e)=>setValue('margin','Top', e)" @input="(e)=>change('marginTop', e)">
+            <input class="_fd-input _fd-right" placeholder="        " :value="boxStyle.marginRight" type="text"
+                   @blur="(e)=>setValue('margin','Right', e)" @input="(e)=>change('marginRight', e)">
+            <input class="_fd-input _fd-bottom" placeholder="        " :value="boxStyle.marginBottom" type="text"
+                   @blur="(e)=>setValue('margin','Bottom', e)" @input="(e)=>change('marginBottom', e)">
+            <input class="_fd-input _fd-left" placeholder="        " :value="boxStyle.marginLeft" type="text"
+                   @blur="(e)=>setValue('margin','Left', e)" @input="(e)=>change('marginLeft', e)">
+            <div class="_fd-help">
+                <i class="fc-icon icon-link2" title="lock" :class="marginLock ? 'active' : ''"
+                   @click="lock('margin')"></i>
+                <i class="fc-icon icon-delete-circle" title="clear" @click="clear('margin')"></i>
+            </div>
+            <div class="_margin">
+                <span class="_margin-title">
+                    {{ t('style.padding') }}
+                </span>
+                <div class="_fd-help">
+                    <i class="fc-icon icon-link2" title="lock" :class="paddingLock ? 'active' : ''"
+                       @click="lock('padding')"></i>
+                    <i class="fc-icon icon-delete-circle" title="clear" @click="clear('padding')"></i>
+                </div>
+                <input class="_fd-input _fd-top" placeholder="        " :value="boxStyle.paddingTop" type="text"
+                       @blur="(e)=>setValue('padding','Top', e)" @input="(e)=>change('paddingTop', e)">
+                <input class="_fd-input _fd-right" placeholder="        " :value="boxStyle.paddingRight" type="text"
+                       @blur="(e)=>setValue('padding','Right', e)" @input="(e)=>change('paddingRight', e)">
+                <input class="_fd-input _fd-bottom" placeholder="        " :value="boxStyle.paddingBottom" type="text"
+                       @blur="(e)=>setValue('padding','Bottom', e)" @input="(e)=>change('paddingBottom', e)">
+                <input class="_fd-input _fd-left" placeholder="        " :value="boxStyle.paddingLeft" type="text"
+                       @blur="(e)=>setValue('padding','Left', e)" @input="(e)=>change('paddingLeft', e)">
+                <div class="_box">
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import ConfigItem from './ConfigItem.vue';
+
+export default defineComponent({
+    name: 'BoxSpaceInput',
+    components: {ConfigItem},
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    data() {
+        return {
+            position: ['Top', 'Right', 'Bottom', 'Left'],
+            boxStyle: {
+                margin: '',
+                padding: '',
+                marginLeft: '',
+                marginRight: '',
+                marginTop: '',
+                marginBottom: '',
+                paddingLeft: '',
+                paddingRight: '',
+                paddingTop: '',
+                paddingBottom: '',
+            },
+            marginLock: false,
+            paddingLock: false,
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        }
+    },
+    methods: {
+        tidyValue() {
+            this.boxStyle = {};
+            ['margin', 'padding'].forEach(k => {
+                this.boxStyle[k] = this.modelValue[k] || '';
+                this.position.forEach(p => {
+                    this.boxStyle[k + p] = this.tidySize(this.modelValue[k + p] || this.modelValue[k] || '');
+                });
+            })
+        },
+        onInput() {
+            const style = Object.keys(this.boxStyle).reduce((acc, key) => {
+                if (this.boxStyle[key] !== '') {
+                    acc[key] = this.boxStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+        tidySize(val) {
+            const regex = /^(\d*\.?\d+)(px|rem|%|vh|vw|em)$/
+            if (!regex.test(val)) {
+                if (val === 'auto') {
+                    return val;
+                }
+                const number = parseInt(val);
+                if (isNaN(number)) {
+                    return '';
+                } else {
+                    return number + 'px';
+                }
+            }
+            return val;
+        },
+        setValue(type, key, e) {
+            const value = this.tidySize(e.target.value);
+            if (!type) {
+                this.boxStyle[key] = value;
+            } else if (this[type + 'Lock']) {
+                this.position.forEach(v => {
+                    this.boxStyle[type + v] = value;
+                })
+            } else {
+                this.boxStyle[type + key] = value;
+            }
+            this.onInput();
+        },
+        change(type, e) {
+            this.boxStyle[type] = e.target.value;
+        },
+        clear(type) {
+            this.position.forEach(v => {
+                this.boxStyle[type + v] = '';
+            })
+            this.onInput();
+        },
+        lock(type) {
+            const key = type + 'Lock';
+            this[key] = !this[key];
+        },
+
+    },
+    created() {
+        this.tidyValue();
+    }
+});
+
+</script>
+
+<style>
+
+._fd-box-space-input {
+    color: #000000;
+}
+
+._fd-box-space-input ._padding, ._fd-box-space-input ._margin {
+    width: 100%;
+    height: 180px;
+    background-color: #F2CEA5;
+    padding: 40px 55px;
+    box-sizing: border-box;
+    position: relative;
+}
+
+html.dark ._fd-box-space-input ._padding, ._fd-box-space-input ._margin {
+    background-color: #A9855C;
+}
+
+._fd-box-space-input ._margin {
+    width: 100%;
+    height: 100px;
+    background-color: #C6CF92;
+}
+
+._fd-box-space-input ._fd-input {
+    display: inline-block;
+    width: 30%;
+    max-width: 40px;
+    height: 20px;
+    border: 0 none;
+    padding: 0;
+    margin: 0;
+    outline: 0 none;
+    text-align: center;
+    font-size: 12px;
+    background-color: unset;
+    text-decoration: underline;
+}
+
+._fd-box-space-input ._fd-input:hover, ._fd-box-space-input ._fd-input:focus {
+    background-color: #ECECEC;
+    opacity: 0.9;
+    color: #262626;
+}
+
+._fd-box-space-input ._fd-left, ._fd-box-space-input ._fd-right {
+    position: absolute;
+    left: 7px;
+    top: 50%;
+    margin-top: -10px;
+}
+
+._fd-box-space-input ._fd-top, ._fd-box-space-input ._fd-bottom {
+    position: absolute;
+    left: 50%;
+    top: 5px;
+    margin-left: -20px;
+}
+
+._fd-box-space-input ._fd-bottom {
+    top: unset;
+    bottom: 15px;
+}
+
+._fd-box-space-input ._fd-right {
+    left: unset;
+    right: 2px;
+}
+
+._fd-box-space-input ._box {
+    width: 100%;
+    height: 100%;
+    background-color: #94B5C0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+._fd-box-space-input ._padding-title, ._fd-box-space-input ._margin-title {
+    position: absolute;
+    top: 2px;
+    left: 4px;
+}
+
+._fd-box-space-input ._fd-help {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    color: #AAAAAA;
+}
+
+._fd-box-space-input ._padding .fc-icon {
+    cursor: pointer;
+    color: #262626;
+    font-size: 12px;
+}
+
+._fd-box-space-input ._padding .fc-icon + .fc-icon {
+    margin-left: 2px;
+}
+
+._fd-box-space-input .fc-icon.active {
+    color: #2E73FF;
+}
+
+._fd-box-space-input ._fd-x {
+    margin: 0 5px;
+}
+</style>

+ 60 - 0
src/components/form-create-designer/components/style/ColorInput.vue

@@ -0,0 +1,60 @@
+<template>
+    <div class="_fd-color-input">
+        <el-input clearable v-model="value">
+            <template #append>
+                <el-color-picker show-alpha color-format="hex" v-model="value"/>
+            </template>
+        </el-input>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'ColorInput',
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: String,
+    },
+    watch: {
+        modelValue() {
+            this.value = this.modelValue || '';
+        },
+        value(n) {
+            this.$emit('update:modelValue', n);
+            this.$emit('change', n);
+        },
+    },
+    data() {
+        return {
+            value: this.modelValue || ''
+        }
+    },
+    methods: {},
+    created() {
+    }
+
+});
+</script>
+
+<style>
+._fd-color-input {
+    width: 150px;
+}
+
+._fd-color-input .el-input .el-color-picker {
+    margin: 0;
+}
+
+._fd-color-input .el-input .el-input-group__append {
+    padding: 0;
+    width: 24px;
+}
+
+._fd-color-input .el-input .el-color-picker__trigger {
+    border-left: 0 none;
+    border-radius: 0px 3px 3px 0px;
+}
+</style>

+ 118 - 0
src/components/form-create-designer/components/style/ConfigItem.vue

@@ -0,0 +1,118 @@
+<template>
+    <div class="_fd-config-item">
+        <div class="_fd-ci-head">
+            <div class="_fd-ci-label" :class="$slots.append && arrow !== false ? 'is-arrow' : ''"
+                 @click="visit = $slots.append && arrow !== false && !visit">
+                <template v-if="warning">
+                    <Warning :tooltip="warning">
+                        <slot name="label">
+                            <span>{{ label }}</span>
+                        </slot>
+                    </Warning>
+                </template>
+                <template v-else>
+                    <slot name="label">
+                        <span>{{ label }}</span>
+                    </slot>
+                </template>
+                <i class="fc-icon icon-down" v-if="$slots.append && arrow !== false"
+                   :class="(showAppend || visit) ? 'down' : ''"></i>
+            </div>
+            <div class="_fd-ci-con" v-if="$slots.default || info">
+                <template v-if="$slots.default">
+                    <slot></slot>
+                </template>
+                <span class="_fd-ci-info" v-else>{{ info }}</span>
+            </div>
+        </div>
+        <div class="_fd-ci-append" v-if="showAppend || visit" :style="'background:' + appendBackground">
+            <slot name="append"></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import Warning from '../Warning.vue';
+
+export default defineComponent({
+    name: 'ConfigItem',
+    components: {Warning},
+    props: {
+        label: String,
+        info: String,
+        warning: String,
+        appendBackground: String,
+        arrow: {
+            type: Boolean,
+            default: true
+        },
+        showAppend: Boolean,
+    },
+    data() {
+        return {
+            visit: false,
+        }
+    }
+
+
+});
+</script>
+
+<style>
+._fd-config-item {
+    display: flex;
+    width: 100%;
+    flex-direction: column;
+    font-size: 12px;
+    color: #666666;
+    margin-bottom: 10px;
+}
+
+._fd-ci-head {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+._fd-ci-label {
+    display: flex;
+    align-items: center;
+    font-size: 12px;
+    color: #262626;
+}
+
+._fd-ci-con {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    min-width: 150px;
+}
+
+._fd-ci-label.is-arrow {
+    cursor: pointer;
+}
+
+._fd-ci-append {
+    display: flex;
+    flex-direction: column;
+    background: #F5F5F5;
+    margin: 5px 3px 3px;
+    padding: 4px;
+}
+
+
+._fd-ci-label i {
+    font-size: 12px;
+    font-weight: 600;
+}
+
+._fd-ci-label i.down {
+    transform: rotate(-180deg);
+}
+
+._fd-ci-info {
+    font-size: 12px;
+    padding-right: 5px;
+}
+</style>

+ 174 - 0
src/components/form-create-designer/components/style/FontInput.vue

@@ -0,0 +1,174 @@
+<template>
+    <ConfigItem :label="t('style.font.name')">
+        <div class="_fd-fi-box" :style="fontStyle">
+            {{ t('style.font.preview') }}
+        </div>
+        <template #append>
+            <div class="_fd-font-input">
+                <el-form label-width="50px" label-position="top" inline size="small">
+                    <el-form-item :label="t('style.font.size')">
+                        <SizeInput v-model="fontStyle.fontSize" @change="onInput"/>
+                    </el-form-item>
+                    <el-form-item :label="t('style.weight.name')">
+                        <el-select v-model="fontStyle.fontWeight" clearable @change="onInput">
+                            <el-option
+                                v-for="item in weightType"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                                <span :style="{fontWeight: item.value}">{{ item.label }}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item :label="t('style.decoration.name')">
+                        <el-select v-model="fontStyle.textDecoration" clearable @change="onInput">
+                            <el-option
+                                v-for="item in decorationType"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                                <span :style="{textDecoration: item.value}">{{ item.label }}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item :label="t('style.font.align')">
+                        <el-select v-model="fontStyle.textAlign" clearable @change="onInput">
+                            <el-option
+                                v-for="item in alignType"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            />
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item :label="t('style.font.height')">
+                        <SizeInput v-model="fontStyle.lineHeight" @change="onInput"/>
+                    </el-form-item>
+                    <el-form-item :label="t('style.font.spacing')">
+                        <SizeInput v-model="fontStyle.letterSpacing" @change="onInput"/>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </template>
+    </ConfigItem>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import SizeInput from './SizeInput.vue';
+import ColorInput from './ColorInput.vue';
+import ConfigItem from './ConfigItem.vue';
+import {toLine} from '@form-create/utils';
+
+export default defineComponent({
+    name: 'BorderInput',
+    components: {ColorInput, SizeInput, ConfigItem},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({}),
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyValue();
+        }
+    },
+    computed: {
+        borderStyleStr() {
+            let str = '';
+            Object.keys(this.borderStyle).forEach((key) => {
+                if (this.borderStyle[key] !== '') {
+                    str += toLine(key) + ': ' + this.borderStyle[key] + ';';
+                }
+            }, {})
+            return str;
+        },
+        alignType() {
+            return ['left', 'center', 'right'].map(v => {
+                return {label: this.t('props.' + v), value: v};
+            })
+        },
+        decorationType() {
+            return ['underline', 'line-through', 'overline'].map(v => {
+                return {label: this.t('style.decoration.' + v), value: v};
+            });
+        },
+        weightType() {
+            return [300, 400, 500, 700].map(v => {
+                return {label: this.t('style.weight.' + v), value: v};
+            });
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            fontStyle: {
+                fontSize: '',
+                fontWeight: '',
+                fontStyle: '',
+                textDecoration: '',
+                textAlign: '',
+                lineHeight: '',
+                letterSpacing: '',
+            },
+        }
+    },
+    methods: {
+        tidyValue() {
+            Object.keys(this.fontStyle).forEach(k => {
+                this.fontStyle[k] = this.modelValue[k] || '';
+            });
+        },
+        onInput() {
+            const style = Object.keys(this.fontStyle).reduce((acc, key) => {
+                if (this.fontStyle[key] !== '') {
+                    acc[key] = this.fontStyle[key]
+                }
+                return acc
+            }, {})
+            this.$emit('update:modelValue', style)
+            this.$emit('change', style)
+        },
+    },
+    created() {
+        this.tidyValue();
+    }
+
+});
+</script>
+
+<style>
+._fd-font-input {
+    display: flex;
+    justify-content: center;
+    padding: 0 5px;
+}
+
+._fd-fi-box {
+    width: 150px;
+    overflow: hidden;
+}
+
+._fd-font-input .el-form {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    width: 100%;
+    grid-column-gap: 10px;
+}
+
+._fd-font-input .el-form--inline .el-form-item {
+    margin: 0;
+    padding: 0;
+}
+
+._fd-font-input ._fd-size-input .el-input-number--small {
+    width: 100%;
+}
+
+</style>

+ 164 - 0
src/components/form-create-designer/components/style/RadiusInput.vue

@@ -0,0 +1,164 @@
+<template>
+    <div class="_fd-radius-input">
+        <ConfigItem :label="t('style.borderRadius')">
+            <SizeInput :unit="unit" v-model="style.com" @change="batch"/>
+            <template #append>
+                <div class="_fd-radius-con">
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon" style="transform: rotate(180deg);">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.left" @change="onInput"/>
+                    </div>
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon" style="transform: rotate(-90deg);">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.top" @change="onInput"/>
+                    </div>
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon" style="transform: rotate(90deg);">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.bottom" @change="onInput"/>
+                    </div>
+                    <div class="_fd-radius-item">
+                        <div class="_fd-radius-icon">
+                            <i class="fc-icon icon-radius"></i>
+                        </div>
+                        <SizeInput :unit="unit" v-model="style.right" @change="onInput"/>
+                    </div>
+                </div>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import SizeInput from './SizeInput.vue';
+import ColorInput from './ColorInput.vue';
+import ConfigItem from './ConfigItem.vue';
+
+export default defineComponent({
+    name: 'RadiusInput',
+    components: {ConfigItem, ColorInput, SizeInput},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: String
+    },
+    watch: {
+        modelValue(n) {
+            n !== this.oldValue && this.tidyValue();
+        },
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    data() {
+        return {
+            visit: false,
+            active: '',
+            style: {
+                com: '',
+                left: '',
+                right: '',
+                top: '',
+                bottom: '',
+            },
+            unit: ['px', '%'],
+            oldValue: '',
+        }
+    },
+    methods: {
+        batch() {
+            this.style.left = this.style.com;
+            this.style.right = this.style.com;
+            this.style.top = this.style.com;
+            this.style.bottom = this.style.com;
+            this.onInput();
+        },
+        tidyValue() {
+            this.style = {
+                com: '',
+                left: '',
+                right: '',
+                top: '',
+                bottom: '',
+            };
+            if (!this.modelValue) {
+                return;
+            }
+            let split = (this.modelValue || '').split(' ').filter(v => v !== '');
+            if (split.length === 1) {
+                split = [split[0], split[0], split[0], split[0]];
+            } else if (split.length === 2) {
+                split = [split[0], split[1], split[0], split[1]];
+            } else if (split.length === 3) {
+                split = [split[0], split[1], split[2], split[1]];
+            }
+            this.style.left = split[0];
+            this.style.top = split[1];
+            this.style.right = split[2];
+            this.style.bottom = split[3];
+            this.updateCom();
+        },
+        updateCom() {
+            let value = `${this.style.left || '0px'} ${this.style.top || '0px'} ${this.style.right || '0px'} ${this.style.bottom || '0px'}`;
+            this.style.com = value.replaceAll(this.style.left, '').trim() === '' ? this.style.left : '';
+        },
+        onInput() {
+            let value = `${this.style.left || '0px'} ${this.style.top || '0px'} ${this.style.right || '0px'} ${this.style.bottom || '0px'}`;
+            if ('' === `${this.style.left}${this.style.top}${this.style.right}${this.style.bottom}`.trim()) {
+                value = '';
+            } else {
+                this.updateCom();
+            }
+            this.oldValue = value;
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+    },
+    created() {
+        this.tidyValue();
+    }
+
+});
+</script>
+
+<style>
+._fd-radius-input {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+._fd-radius-con {
+    display: flex;
+    flex-wrap: wrap;
+}
+
+._fd-radius-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 50%;
+    padding: 5px 0;
+    box-sizing: border-box;
+}
+
+._fd-radius-item ._fd-radius-icon {
+    width: 24px;
+    height: 24px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+._fd-radius-item ._fd-size-input .el-input-number--small {
+    width: 70px;
+}
+</style>

+ 329 - 0
src/components/form-create-designer/components/style/ShadowContent.vue

@@ -0,0 +1,329 @@
+<template>
+    <div class="_fd-shadow-content">
+        <el-form label-width="50px" label-position="top" inline class="_fd-sc-form" size="small">
+            <el-form-item :label="t('style.shadow.mode')">
+                <el-radio-group v-model="form.type" @change="onInput" size="small" class="_fd-sc-radio">
+                    <template v-for="item in options" :key="item.key">
+                        <el-tooltip
+                            effect="dark"
+                            :content="t('style.shadow.' + item.key)"
+                            placement="top"
+                            :hide-after="0"
+                            persistent
+                        >
+                            <el-radio-button :label="item.key" :value="item.key">
+                                <i class="fc-icon" :class="'icon-' + item.icon"></i>
+                            </el-radio-button>
+                        </el-tooltip>
+                    </template>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item :label="t('style.color')">
+                <ColorInput v-model="form.color" @change="onInput"></ColorInput>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.x')">
+                <el-input v-model="form.x" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.x_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.y')">
+                <el-input v-model="form.y" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.y_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.vague')">
+                <el-input v-model="form.vague" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.vague_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+            <el-form-item :label="t('style.shadow.extend')">
+                <el-input v-model="form.extend" type="number" @change="onInput">
+                    <template #append>
+                        <el-select v-model="form.extend_unit" @change="onInput">
+                            <el-option v-for="item in units" :key="item" :label="item" :value="item"/>
+                        </el-select>
+                    </template>
+                </el-input>
+            </el-form-item>
+        </el-form>
+        <div class="_fd-sc-right">
+            <div
+                ref="box"
+                class="_fd-sc-box"
+                :class="down ? 'down' : ''"
+                @click="getMouseXY($event, 1)"
+                @mousedown="onMousedown"
+                @mouseup="onMouseup"
+                @mousemove="getMouseXY($event, 0)"
+            >
+                <span class="spot" :style="spotStyle">
+                  <i class="spot-id"/>
+                </span>
+                <span class="center-spot"/>
+                <div class="x-hr"/>
+                <div class="y-hr"/>
+            </div>
+        </div>
+    </div>
+</template>
+
+
+<script>
+import {defineComponent} from 'vue';
+import ColorInput from './ColorInput.vue';
+
+export default defineComponent({
+    name: 'ShadowContent',
+    components: {ColorInput},
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: {
+            default: '0px 0px 0px rgba(0, 0, 0, 0)',
+            type: String,
+        },
+    },
+    data() {
+        return {
+            max: 24,
+            boxSize: 250,
+            options: [
+                {key: 'external', icon: 'shadow'},
+                {key: 'inset', icon: 'shadow-inset'},
+            ],
+            form: {
+                color: '',
+                type: 'external',
+                x: 0,
+                y: 0,
+                vague: 0,
+                extend: 0,
+                x_unit: 'px',
+                y_unit: 'px',
+                vague_unit: 'px',
+                extend_unit: 'px',
+            },
+            units: ['px', '%', 'rem', 'em', 'vw', 'vh'],
+            down: false,
+            position: {
+                left: 0,
+                top: 0,
+            },
+        }
+    },
+    computed: {
+        spotStyle() {
+            return {
+                left: this.position.left + 'px',
+                top: this.position.top + 'px',
+            }
+        },
+        t() {
+            return this.designer.setupState.t;
+        },
+    },
+    watch: {
+        position(n) {
+            this.form.x = parseInt(String(((n.left - this.boxSize / 2) / this.boxSize) * this.max));
+            this.form.y = parseInt(String(((n.top - this.boxSize / 2) / this.boxSize) * this.max));
+            const i = this.max / 2;
+            this.form.x = this.form.x < 0 ? Math.max(this.form.x, i * -1) : Math.min(this.form.x, i);
+            this.form.y = this.form.y < 0 ? Math.max(this.form.y, i * -1) : Math.min(this.form.y, i);
+        },
+        modelValue(n) {
+            this.initStyle(n);
+        },
+    },
+    methods: {
+        getMouseXY(e, force) {
+            if (this.down || force) {
+                const _box = this.$refs.box.getBoundingClientRect()
+                this.position = {
+                    left: parseInt(String(e.clientX - _box.x)),
+                    top: parseInt(String(e.clientY - _box.y)),
+                }
+            }
+        },
+        onMouseup() {
+            this.down = false;
+            this.onInput();
+        },
+        onMousedown(e) {
+            this.getMouseXY(e, true);
+            this.down = true;
+        },
+        onInput() {
+            const n = this.form;
+            let value = `${n.x}${n.x_unit} ${n.y}${n.y_unit} ${n.vague}${n.vague_unit} ${n.extend}${n.extend_unit} ${n.color}`
+            if (`${n.x}${n.y}${n.vague}${n.extend}`.replaceAll('0', '') === '') {
+                value = '';
+            } else if (n.type === 'inset') {
+                value += ' inset'
+            }
+            this.$emit('update:modelValue', value);
+            this.$emit('change', value);
+        },
+        initStyle(modelValue) {
+            if ((this.modelValue || '').indexOf(' inset') > -1) {
+                this.form.type = 'inset'
+                modelValue = modelValue.replace(' inset', '')
+            }
+            const shadowData = modelValue.split('rgba')
+            let color, shadowValues;
+
+            if (shadowData.length > 1) {
+                // 将颜色值和其他阴影值进行分离
+                color = 'rgba' + shadowData[1].trim()
+                shadowValues = shadowData[0].trim().split(' ')
+            } else {
+                shadowValues = shadowData[0].trim().split(' ')
+                color = shadowValues.pop()
+            }
+            this.form.color = color || '#000'
+            this.form.x = parseInt(shadowValues[0]) || 0
+            this.form.y = parseInt(shadowValues[1]) || 0
+            this.form.vague = parseInt(shadowValues[2]) || 0
+            this.form.extend = parseInt(shadowValues[3]) || 0
+
+            const getUnit = (value) => {
+                return value?.replace(/[-\d.]/g, '') || 'px'
+            }
+            this.form.x_unit = getUnit(shadowValues[0])
+            this.form.y_unit = getUnit(shadowValues[1])
+            this.form.vague_unit = getUnit(shadowValues[2])
+            this.form.extend_unit = getUnit(shadowValues[3])
+            this.position.left = this.boxSize / 2 + (this.form.x / this.max) * this.boxSize || 0
+            this.position.top = this.boxSize / 2 + (this.form.y / this.max) * this.boxSize || 0
+        }
+    },
+    mounted() {
+        this.initStyle(this.modelValue);
+    }
+
+});
+</script>
+
+<style>
+._fd-shadow-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-direction: column;
+}
+
+._fd-sc-form .fc-icon {
+    font-size: 12px;
+}
+
+._fd-shadow-content .el-form-item {
+    width: 50%;
+    padding: 0 0 0 10px;
+    margin: 0 0 5px !important;
+    box-sizing: border-box;
+}
+
+._fd-shadow-content .el-input__wrapper {
+    flex: 1;
+}
+
+._fd-shadow-content ._fd-sc-box {
+    width: 250px;
+    height: 250px;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    position: relative;
+    cursor: pointer;
+    overflow: hidden
+}
+
+._fd-shadow-content ._fd-sc-box .spot {
+    position: absolute;
+    width: 0;
+    height: 0;
+    border-radius: 100%
+}
+
+._fd-shadow-content ._fd-sc-box .spot-id {
+    position: absolute;
+    top: -5px;
+    left: -5px;
+    width: 10px;
+    height: 10px;
+    background: #1989fa;
+    border-radius: 100%;
+    z-index: 9
+}
+
+._fd-shadow-content ._fd-sc-box.down .spot-id {
+    box-shadow: 1px 1px 10px 2px #1989fa
+}
+
+._fd-shadow-content ._fd-sc-box .center-spot {
+    position: absolute;
+    width: 0;
+    height: 0;
+    top: 125px;
+    left: 125px;
+    border-radius: 100%;
+    background: #1989fa
+}
+
+._fd-shadow-content ._fd-sc-box .x-hr {
+    width: 100%;
+    position: absolute;
+    height: 1px;
+    top: 125px;
+    background: #ccc
+}
+
+._fd-shadow-content ._fd-sc-box .y-hr {
+    height: 100%;
+    position: absolute;
+    width: 1px;
+    left: 125px;
+    background: #ccc
+}
+
+._fd-shadow-content .el-select__placeholder {
+    text-align: center;
+}
+
+._fd-shadow-content .el-input-group__append {
+    width: 55px;
+    padding: 0;
+}
+
+._fd-shadow-content .el-input {
+    width: 105px;
+}
+
+._fd-shadow-content ._fd-sc-right {
+    margin-top: 10px;
+}
+
+._fd-shadow-content ._fd-sc-radio {
+    width: 105px;
+}
+
+._fd-shadow-content ._fd-sc-radio .el-radio-button {
+    display: flex;
+    flex: 1;
+}
+
+._fd-shadow-content ._fd-sc-radio .el-radio-button__inner {
+    width: 100%;
+}
+
+</style>

+ 93 - 0
src/components/form-create-designer/components/style/ShadowInput.vue

@@ -0,0 +1,93 @@
+<template>
+    <div class="_fd-shadow-input">
+        <ConfigItem :label="t('style.shadow.name')">
+            <el-input clearable v-model="value" class="_fd-si-input">
+                <template #append>
+                    <el-dropdown>
+                        <i class="fc-icon icon-setting"></i>
+                        <template #dropdown>
+                            <el-dropdown-menu>
+                                <el-dropdown-item v-for="item in options" @click="changeValue(item.value)">
+                                    {{ item.label }}
+                                </el-dropdown-item>
+                            </el-dropdown-menu>
+                        </template>
+                    </el-dropdown>
+                </template>
+            </el-input>
+            <template #append>
+                <ShadowContent v-model="value"></ShadowContent>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import ShadowContent from './ShadowContent.vue';
+import ConfigItem from './ConfigItem.vue';
+
+export default defineComponent({
+    name: 'ShadowInput',
+    emits: ['update:modelValue', 'change'],
+    components: {ConfigItem, ShadowContent},
+    inject: ['designer'],
+    props: {
+        modelValue: String,
+    },
+    watch: {
+        modelValue() {
+            this.value = this.modelValue || '';
+        },
+        value(n) {
+            this.$emit('update:modelValue', n);
+            this.$emit('change', n);
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            options: [
+                {label: t('style.shadow.classic'), value: '3px 5px 7px 2px #CBCBCBFF'},
+                {label: t('style.shadow.flat'), value: '4px 4px 3px -2px #E7E5E5FF'},
+                {label: t('style.shadow.solid'), value: '1px 2px 4px 2px #979797FF'}
+            ],
+            value: this.modelValue || ''
+        }
+    },
+    methods: {
+        changeValue(val) {
+            this.value = val;
+        },
+    },
+    created() {
+    }
+
+});
+</script>
+
+<style>
+._fd-shadow-input ._fd-ci-con {
+    width: 150px;
+}
+
+._fd-shadow-input :focus-visible {
+    outline: 0 none;
+}
+
+._fd-si-input .el-input-group__append {
+    display: inline-flex;
+    width: 24px;
+    padding: 0;
+}
+
+._fd-si-input .el-input__wrapper {
+    flex: 1;
+}
+
+._fd-shadow-input ._fd-ci-con .fc-icon {
+    cursor: pointer;
+}
+
+</style>

+ 118 - 0
src/components/form-create-designer/components/style/SizeInput.vue

@@ -0,0 +1,118 @@
+<template>
+    <div class="_fd-size-input">
+        <template v-if="unit[idx] === 'auto'">
+            <el-button :size="size" style="width: 150px;" @click="changeType()">{{ unit[idx] }}</el-button>
+        </template>
+        <template v-else>
+            <el-inputNumber :size="size" v-model="num" @change="submit" controls-position="right"/>
+            <el-dropdown trigger="click" size="small">
+                <el-button :size="size">{{ unit[idx] }}</el-button>
+                <template #dropdown>
+                    <el-dropdown-menu>
+                        <el-dropdown-item v-for="(name, idx) in unit" :key="name" @click="changeType(idx)">
+                            <div>{{ name }}</div>
+                        </el-dropdown-item>
+                    </el-dropdown-menu>
+                </template>
+            </el-dropdown>
+        </template>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import {isNull} from '../../utils/index';
+
+export default defineComponent({
+    name: 'SizeInput',
+    inject: ['designer'],
+    emits: ['update:modelValue', 'change'],
+    props: {
+        modelValue: String,
+        size: String,
+        unit: {
+            type: Array,
+            default: () => ['auto', 'px', '%', 'vh', 'vw', 'em', 'rem']
+        },
+        defaultUnit: {
+            type: String,
+            default: 'px'
+        }
+    },
+    watch: {
+        modelValue() {
+            this.parseValue();
+        }
+    },
+    data() {
+        return {
+            idx: 1,
+            num: 0,
+            oldValue: this.modelValue || '',
+        }
+    },
+    methods: {
+        parseValue() {
+            if (this.modelValue !== 'auto') {
+                this.idx = Math.max(this.unit.indexOf(this.defaultUnit), 0);
+                this.unit.forEach((v, i) => {
+                    if ((this.modelValue || '').indexOf(v) > -1) {
+                        this.idx = i;
+                    }
+                });
+                this.num = isNull(this.modelValue) ? null : parseFloat(this.modelValue || 0);
+            } else {
+                this.idx = 0;
+                this.num = 0;
+            }
+        },
+        submit() {
+            this.oldValue = !isNull(this.num) ? '' + this.num + this.unit[this.idx] : '';
+            this.$emit('update:modelValue', this.oldValue);
+            this.$emit('change', this.oldValue);
+        },
+        changeType(idx) {
+            if (idx !== undefined) {
+                if (this.idx === idx) {
+                    return;
+                }
+                this.idx = idx;
+            } else {
+                this.idx++;
+                if (this.idx > 4) {
+                    this.idx = 0;
+                }
+            }
+            if (this.unit[this.idx] === 'auto') {
+                this.oldValue = 'auto';
+                this.$emit('update:modelValue', 'auto');
+                this.$emit('change', 'auto');
+            } else {
+                this.submit();
+            }
+        },
+    },
+    created() {
+        this.parseValue();
+    }
+
+});
+</script>
+
+<style>
+._fd-size-input {
+    display: flex;
+    align-items: center;
+}
+
+._fd-size-input .el-input-number--small {
+    width: 122px;
+}
+
+._fd-size-input .el-button {
+    font-size: 14px;
+    padding: 5px;
+    margin-left: 3px;
+    width: 25px;
+}
+</style>

+ 263 - 0
src/components/form-create-designer/components/style/StyleConfig.vue

@@ -0,0 +1,263 @@
+<template>
+    <div class="_fd-style-config">
+        <BoxSpaceInput v-model="space" @change="onInput" style="margin-bottom: 10px;"></BoxSpaceInput>
+        <BoxSizeInput v-model="size" @change="onInput"></BoxSizeInput>
+        <ConfigItem :label="t('style.color')">
+            <ColorInput v-model="color" @change="onInput"></ColorInput>
+        </ConfigItem>
+        <ConfigItem :label="t('style.backgroundColor')">
+            <ColorInput v-model="backgroundColor" @change="onInput"></ColorInput>
+        </ConfigItem>
+        <BorderInput v-model="border" @change="onInput"></BorderInput>
+        <RadiusInput v-model="radius" @change="onInput"/>
+        <FontInput v-model="font" @change="onInput"/>
+        <ShadowInput v-model="boxShadow" @change="onInput"></ShadowInput>
+        <ConfigItem :label="t('style.opacity')" class="_fd-opacity-input">
+            <el-slider :show-tooltip="false" v-model="opacity"
+                       @change="onInput"></el-slider>
+            <span>{{ opacity }}%</span>
+        </ConfigItem>
+        <ConfigItem :label="t('style.scale')" class="_fd-opacity-input">
+            <el-slider :min="80" :max="120" :show-tooltip="false" v-model="scale"
+                       @change="onInput"></el-slider>
+            <span>{{ scale }}%</span>
+        </ConfigItem>
+        <ConfigItem :label="t('props.custom')" :info="Object.keys(formData).length > 0 ? t('struct.configured') : ''">
+            <template #append>
+                <TableOptions v-model="formData" @change="onInput" v-bind="{
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object'
+            }"></TableOptions>
+            </template>
+        </ConfigItem>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+import BoxSizeInput from './BoxSizeInput.vue';
+import BoxSpaceInput from './BoxSpaceInput.vue';
+import BorderInput from './BorderInput.vue';
+import RadiusInput from './RadiusInput.vue';
+import FontInput from './FontInput.vue';
+import ConfigItem from './ConfigItem.vue';
+import ColorInput from './ColorInput.vue';
+import ShadowInput from './ShadowInput.vue';
+import {isNull} from '../../utils/index';
+import TableOptions from '../TableOptions.vue';
+
+const fontKey = [
+    'fontSize',
+    'fontWeight',
+    'fontStyle',
+    'textDecoration',
+    'textAlign',
+    'lineHeight',
+    'letterSpacing',
+];
+
+const sizeKey = [
+    'height',
+    'width',
+    'minWidth',
+    'minHeight',
+    'maxWidth',
+    'maxHeight',
+    'overflow'
+];
+
+const styleKey = [
+    'color',
+    'backgroundColor',
+    'scale',
+    'borderRadius',
+    'boxShadow',
+    'marginTop',
+    'marginRight',
+    'marginBottom',
+    'marginLeft',
+    'paddingTop',
+    'paddingRight',
+    'paddingBottom',
+    'paddingLeft',
+    'margin',
+    'padding',
+    'opacity',
+    'borderStyle',
+    'borderColor',
+    'borderWidth',
+    'borderTopStyle',
+    'borderTopColor',
+    'borderTopWidth',
+    'borderLeftStyle',
+    'borderLeftColor',
+    'borderLeftWidth',
+    'borderBottomStyle',
+    'borderBottomColor',
+    'borderBottomWidth',
+    'borderRightStyle',
+    'borderRightColor',
+    'borderRightWidth',
+    ...fontKey,
+    ...sizeKey
+];
+
+export default defineComponent({
+    name: 'StyleConfig',
+    inject: ['designer'],
+    emits: ['update:modelValue'],
+    components: {
+        TableOptions,
+        ColorInput,
+        ConfigItem,
+        RadiusInput,
+        BoxSizeInput,
+        BoxSpaceInput,
+        BorderInput,
+        ShadowInput,
+        FontInput,
+    },
+    props: {
+        modelValue: {
+            type: Object,
+            default: () => ({})
+        }
+    },
+    watch: {
+        modelValue() {
+            this.tidyStyle();
+        },
+    },
+    data() {
+        const t = this.designer.setupState.t;
+        return {
+            t,
+            formData: {},
+            size: {},
+            space: {},
+            border: {},
+            font: {},
+            radius: '',
+            backgroundColor: '',
+            color: '',
+            boxShadow: '',
+            opacity: 100,
+            scale: 100,
+        }
+    },
+    methods: {
+        tidyStyle() {
+            const style = {...this.modelValue || {}};
+            const space = {};
+            Object.keys(style).forEach(k => {
+                if (['margin', 'padding'].indexOf(k) > -1) {
+                    space[k] = style[k];
+                } else if (k.indexOf('margin') > -1 || k.indexOf('padding') > -1) {
+                    space[k] = style[k];
+                }
+            });
+
+            const size = {};
+            sizeKey.forEach(k => {
+                if (style[k]) {
+                    size[k] = style[k];
+                }
+            });
+
+            this.radius = style.borderRadius || '';
+            delete style.borderRadius;
+
+            const border = {};
+            Object.keys(style).forEach(k => {
+                if (k.indexOf('border') === 0) {
+                    border[k] = style[k];
+                }
+            });
+
+            let opacity = isNull(style.opacity) ? 100 : (parseFloat(style.opacity) || 0);
+            if (opacity && opacity < 1) {
+                opacity = opacity * 100;
+            }
+
+            let scale = style.scale;
+            if (isNull(style.scale)) {
+                scale = 100
+            } else if (isNaN(Number(scale))) {
+                scale = parseFloat(scale) || 100;
+            } else {
+                scale = scale > 0 ? scale * 100 : 0;
+            }
+
+            const font = {};
+            fontKey.forEach(k => {
+                if (style[k]) {
+                    font[k] = style[k];
+                }
+            });
+            this.opacity = opacity;
+            this.scale = scale;
+            this.size = size;
+            this.space = space;
+            this.border = border;
+            this.font = font;
+            this.boxShadow = style.boxShadow || '';
+            this.color = style.color || '';
+            this.backgroundColor = style.backgroundColor || '';
+            styleKey.forEach(k => {
+                delete style[k];
+            })
+            this.formData = style;
+        },
+        onInput() {
+            let temp = {...this.formData};
+            styleKey.forEach(k => {
+                delete temp[k];
+            })
+            const style = {
+                ...temp,
+                color: this.color || '',
+                backgroundColor: this.backgroundColor || '',
+                opacity: (this.opacity >= 0 && this.opacity < 100) ? (this.opacity + '%') : '',
+                borderRadius: this.radius || '',
+                boxShadow: this.boxShadow || '',
+                scale: (this.scale >= 0 && this.scale !== 100) ? (this.scale + '%') : '',
+                ...this.space, ...this.size, ...this.border, ...this.font
+            }
+            Object.keys(style).forEach(k => {
+                if (isNull(style[k])) {
+                    delete style[k];
+                }
+            })
+            this.$emit('update:modelValue', style);
+        },
+    },
+    created() {
+        this.tidyStyle();
+    }
+
+});
+</script>
+
+<style>
+._fd-style-config {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+._fd-opacity-input ._fd-ci-con {
+    display: flex;
+    justify-content: space-between;
+    width: 150px;
+    align-items: center;
+}
+
+._fd-opacity-input ._fd-ci-con > span {
+    width: 32px;
+}
+
+._fd-opacity-input .el-slider {
+    flex: 1;
+    margin-right: 15px;
+}
+</style>

+ 210 - 0
src/components/form-create-designer/components/table/Table.vue

@@ -0,0 +1,210 @@
+<template>
+    <el-col :span="24">
+        <div class="_fc-table">
+            <table border="1" cellspacing="0" cellpadding="0" :style="tableColor">
+                <template v-for="(_,pid) in rule.row" :key="pid">
+                    <tr>
+                        <template v-for="(_, idx) in rule.col" :key="`${pid}${idx}`">
+                            <td v-if="lattice[pid][idx].show"
+                                v-bind="lattice[pid][idx] ? {colspan:lattice[pid][idx].colspan, rowspan:lattice[pid][idx].rowspan} : {}"
+                                valign="top"
+                                :class="(tdClass && tdClass[`${pid}:${idx}`]) || ''"
+                                :style="[tableColor, (tdStyle && tdStyle[`${pid}:${idx}`]) || {}]">
+                                <slot :name="`${pid}:${idx}`"></slot>
+                                <template v-for="slot in lattice[pid][idx].slot">
+                                    <slot :name="`${slot}`"></slot>
+                                </template>
+                            </td>
+                        </template>
+                    </tr>
+                </template>
+            </table>
+        </div>
+    </el-col>
+</template>
+
+<script>
+
+export default {
+    name: 'FcTable',
+    props: {
+        label: String,
+        width: [Number, String],
+        border: {
+            type: Boolean,
+            default: true
+        },
+        borderWidth: String,
+        borderColor: String,
+        rule: {
+            type: Object,
+            default: () => ({row: 1, col: 1})
+        },
+    },
+    watch: {
+        rule: {
+            handler() {
+                this.initRule();
+                this.loadRule();
+                this.tdStyle = this.rule.style || {};
+                this.tdClass = this.rule.class || {};
+            },
+            immediate: true,
+            deep: true,
+        }
+    },
+    data() {
+        return {
+            tdStyle: {},
+            tdClass: {},
+            lattice: {},
+        };
+    },
+    computed: {
+        tableColor() {
+            const border = {};
+            if (this.border === false) {
+                border['border'] = '0 none';
+            } else {
+                if (this.borderColor) {
+                    border['borderColor'] = this.borderColor;
+                }
+                if (this.borderWidth) {
+                    border['borderWidth'] = this.borderWidth;
+                }
+            }
+            return border;
+        },
+    },
+    methods: {
+        initRule() {
+            const rule = this.rule;
+            if (!rule.style) {
+                rule.style = {};
+            }
+            if (!rule.layout) {
+                rule.layout = [];
+            }
+            if (!rule.row) {
+                rule.row = 1;
+            }
+            if (!rule.col) {
+                rule.col = 1;
+            }
+        },
+        loadRule() {
+            const lattice = [];
+            const rule = this.rule || {row: 1, col: 1};
+            for (let index = 0; index < rule.row; index++) {
+                const sub = [];
+                lattice.push(sub);
+                for (let idx = 0; idx < rule.col; idx++) {
+                    sub.push({rowspan: 1, colspan: 1, slot: [], show: true});
+                }
+            }
+            [...(rule.layout || [])].forEach((v, i) => {
+                if (((!v.row || v.row <= 0) && (!v.col || v.col <= 0)) || !lattice[v.top] || !lattice[v.top][v.left] || !lattice[v.top][v.left].show) {
+                    rule.layout.splice(i, 1);
+                    return;
+                }
+                const data = lattice[v.top][v.left];
+                data.layout = v;
+                let col = 1;
+                let row = 1;
+                if (v.col) {
+                    col = (v.col + v.left) > rule.col ? rule.col - v.left : v.col;
+                    data.colspan = col;
+                }
+                if (v.row) {
+                    row = (v.row + v.top) > rule.row ? rule.row - v.top : v.row;
+                    data.rowspan = row;
+                }
+                if (row && col) {
+                    for (let index = 0; index < row; index++) {
+                        const row = lattice[v.top + index];
+                        if (row) {
+                            for (let idx = 0; idx < col; idx++) {
+                                if (!idx && !index)
+                                    continue;
+
+                                if (row[v.left + idx]) {
+                                    row[v.left + idx].show = false;
+                                }
+                                data.slot.push(`${v.top + index}:${v.left + idx}`);
+                            }
+                        }
+                    }
+                }
+            });
+
+            const checkCol = (col) => {
+                return !!(!col || col.layout || !col.show);
+            };
+
+            lattice.forEach((v, index) => {
+                v.forEach((item, idx) => {
+                    let right = false;
+                    let bottom = false;
+                    if (item.layout) {
+                        const col = item.layout.col || 1;
+                        const row = item.layout.row || 1;
+                        for (let i = 0; i < col; i++) {
+                            if (!lattice[index + row] || checkCol(lattice[index + row][idx + i])) {
+                                bottom = true;
+                                continue;
+                            }
+                        }
+                        for (let i = 0; i < row; i++) {
+                            if (!lattice[index + i] || checkCol(lattice[index + i][idx + col])) {
+                                right = true;
+                                continue;
+                            }
+                        }
+                    } else {
+                        right = checkCol(v[idx + 1]);
+                        bottom = lattice[index + 1] ? checkCol(lattice[index + 1][idx]) : true;
+                    }
+                    item.right = right;
+                    item.bottom = bottom;
+                });
+            });
+            this.lattice = lattice;
+        },
+    }
+};
+</script>
+
+<style>
+
+._fc-table {
+    overflow: auto;
+}
+
+._fc-table > table {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+    border: 1px solid #EBEEF5;
+    border-bottom: 0 none;
+    border-right: 0 none;
+}
+
+._fc-table tr {
+    min-height: 50px;
+}
+
+._fc-table td {
+    padding: 5px;
+    min-height: 50px;
+    min-width: 80px;
+    position: relative;
+    box-sizing: border-box;
+    overflow-wrap: break-word;
+    /*white-space: nowrap;*/
+    overflow: hidden;
+    border: 0 none;
+    border-right: 1px solid #EBEEF5;
+    border-bottom: 1px solid #EBEEF5;
+}
+</style>

+ 658 - 0
src/components/form-create-designer/components/table/TableView.vue

@@ -0,0 +1,658 @@
+<template>
+    <div class="_fd-table-view">
+        <table border="1" cellspacing="0" cellpadding="0" :style="tableColor">
+            <template v-for="(_,pid) in rule.row" :key="pid">
+                <tr>
+                    <template v-for="(_, idx) in rule.col">
+                        <td v-if="lattice[pid][idx].show" :key="`${pid}${idx}`"
+                            v-bind="lattice[pid][idx] ? {colspan:lattice[pid][idx].colspan, rowspan:lattice[pid][idx].rowspan} : {}"
+                            :style="[tableColor, (style && style[`${pid}:${idx}`]) || {}]"
+                            :class="(rule.class && rule.class[`${pid}:${idx}`]) || ''">
+                            <div class="_fd-table-view-cell">
+                                <DragTool :drag-btn="false" :handle-btn="true" @active="active({pid, idx})"
+                                          :unique="lattice[pid][idx].id">
+                                    <DragBox v-bind="dragProp" @add="e=>dragAdd(e, {pid, idx})"
+                                             :ref="'drag' + pid + idx"
+                                             @end="e=>dragEnd(e, {pid, idx})" @start="e=>dragStart(e)"
+                                             @unchoose="e=>dragUnchoose(e)"
+                                             :list="getSlotChildren([`${pid}:${idx}`, ...lattice[pid][idx].slot])">
+                                        <slot :name="`${pid}:${idx}`"></slot>
+                                    </DragBox>
+                                    <template #handle>
+                                        <div class="_fd-drag-btn _fd-table-view-btn"
+                                             @click.stop="addRow({pid,idx,data: lattice[pid][idx]}, 0)">
+                                            <i class="fc-icon icon-add-col"></i>
+                                        </div>
+                                        <div class="_fd-drag-btn _fd-table-view-btn"
+                                             @click.stop="addCol({pid,idx,data: lattice[pid][idx]}, 0)">
+                                            <i class="fc-icon icon-add-col"
+                                               style="transform: rotate(90deg);"></i>
+                                        </div>
+                                        <div class="_fd-drag-btn _fd-table-view-btn" @click.stop>
+                                            <el-dropdown trigger="click" @command="command">
+                                                <i class="fc-icon icon-setting"></i>
+                                                <template #dropdown>
+                                                    <el-dropdown-menu>
+                                                        <el-dropdown-item
+                                                            :command="['addCol', [{pid,idx,data: lattice[pid][idx]}, 1]]">
+                                                            {{ t('tableOptions.addLeft') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :command="['addCol', [{pid,idx,data: lattice[pid][idx]}, 0]]">
+                                                            {{ t('tableOptions.addRight') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :command="['addRow', [{pid,idx,data: lattice[pid][idx]}, 1]]">
+                                                            {{ t('tableOptions.addTop') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :command="['addRow', [{pid,idx,data: lattice[pid][idx]}, 0]]">
+                                                            {{ t('tableOptions.addBottom') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item divided :disabled="lattice[pid][idx].right"
+                                                                          :command="['mergeRight', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.mergeRight') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item :disabled="lattice[pid][idx].bottom"
+                                                                          :command="['mergeBottom', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.mergeBottom') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item divided
+                                                                          :disabled="!(lattice[pid][idx].layout && lattice[pid][idx].layout.col > 1)"
+                                                                          :command="['splitCol', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.splitCol') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item
+                                                            :disabled="!(lattice[pid][idx].layout && lattice[pid][idx].layout.row > 1)"
+                                                            :command="['splitRow', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.splitRow') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item divided :disabled="rule.col < 2"
+                                                                          :command="['rmCol', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.rmCol') }}
+                                                        </el-dropdown-item>
+                                                        <el-dropdown-item :disabled="rule.row < 2"
+                                                                          :command="['rmRow', [{pid,idx,data: lattice[pid][idx]}]]">
+                                                            {{ t('tableOptions.rmRow') }}
+                                                        </el-dropdown-item>
+                                                    </el-dropdown-menu>
+                                                </template>
+                                            </el-dropdown>
+                                        </div>
+
+                                    </template>
+                                </DragTool>
+                            </div>
+                        </td>
+                    </template>
+                </tr>
+            </template>
+        </table>
+    </div>
+</template>
+
+<script>
+
+import DragTool from '../DragTool.vue';
+import DragBox from '../DragBox.vue';
+import {defineComponent} from 'vue';
+import uniqueId from '@form-create/utils/lib/unique';
+
+
+export default defineComponent({
+    name: 'FcTableView',
+    props: {
+        label: String,
+        width: [Number, String],
+        formCreateInject: Object,
+        border: {
+            type: Boolean,
+            default: true
+        },
+        borderWidth: String,
+        borderColor: String,
+        rule: {
+            type: Object,
+            default: () => ({row: 1, col: 1})
+        },
+    },
+    inject: ['designer'],
+    components: {
+        DragTool,
+        DragBox,
+    },
+    watch: {
+        rule: {
+            handler() {
+                this.initRule();
+                this.style = this.rule.style;
+            },
+            immediate: true,
+        }
+    },
+    data() {
+        return {
+            unique: {},
+            style: {},
+            dragProp: {
+                rule: {
+                    props: {
+                        tag: 'el-col',
+                        group: 'default',
+                        ghostClass: 'ghost',
+                        animation: 150,
+                        handle: '._fd-drag-btn',
+                        emptyInsertThreshold: 0,
+                        direction: 'vertical',
+                        itemKey: 'type',
+                    }
+                },
+                tag: 'tableCell',
+            },
+            lattice: {},
+            uni: {},
+
+        };
+    },
+    computed: {
+        t() {
+            return this.designer.setupState.t;
+        },
+        tableColor() {
+            const border = {};
+            if (this.border === false) {
+                border['border'] = '0 none';
+            } else {
+                if (this.borderColor) {
+                    border['borderColor'] = this.borderColor;
+                }
+                if (this.borderWidth) {
+                    border['borderWidth'] = this.borderWidth;
+                }
+            }
+            return border;
+        },
+    },
+    methods: {
+        getUnique(key) {
+            if (!this.unique[key]) {
+                this.unique[key] = uniqueId();
+            }
+            return this.unique[key];
+        },
+        getSlotChildren(slots) {
+            const children = [];
+            this.formCreateInject.children.forEach(child => {
+                if (slots.indexOf(child.slot) > -1) {
+                    children.push(child);
+                }
+            });
+            return children;
+        },
+        dragAdd(e, item) {
+            // console.log('dragAdd');
+            const designer = this.designer.setupState;
+            const children = this.formCreateInject.children;
+            const slot = `${item.pid}:${item.idx}`;
+            const rule = e.item._underlying_vm_;
+            const flag = designer.addRule && designer.addRule.children === designer.moveRule;
+            if (flag) {
+                designer.moveRule.splice(designer.moveRule.indexOf(rule), 1);
+            }
+            let idx = 0;
+            const refKey = 'drag' + item.pid + item.idx;
+            if (this.$refs[refKey][0].list.length) {
+                let beforeRule = this.$refs[refKey][0].list[!e.newIndex ? 0 : e.newIndex - 1];
+                idx = children.indexOf(beforeRule) + (e.newIndex ? 1 : 0);
+            } else if (children.length) {
+                const dragSlotKeys = Object.keys(this.$refs);
+                for (let i = dragSlotKeys.indexOf(refKey) - 1; i >= 0; i--) {
+                    if (!this.$refs[dragSlotKeys[i]] || !this.$refs[dragSlotKeys[i]].length) {
+                        continue;
+                    }
+                    const list = this.$refs[dragSlotKeys[i]][0].list || [];
+                    if (list.length) {
+                        idx = children.indexOf(list[list.length - 1]) + 1;
+                        break;
+                    }
+                }
+            }
+            e.newIndex = idx;
+            if (flag) {
+                rule.slot = slot;
+                children.splice(e.newIndex, 0, rule);
+                designer.added = true;
+                designer.handleSortAfter({rule});
+            } else {
+                designer.dragAdd(children, e, `${item.pid}:${item.idx}`);
+            }
+        },
+        dragEnd(e, item) {
+            // console.log('dragEnd');
+            const designer = this.designer.setupState;
+            const children = this.formCreateInject.children;
+            const rule = e.item._underlying_vm_;
+            const oldIdx = children.indexOf(rule);
+            e.newIndex = oldIdx + (e.newIndex - e.oldIndex);
+            e.oldIndex = oldIdx;
+            designer.dragEnd(this.formCreateInject.children, e, `${item.pid}:${item.idx}`);
+        },
+        dragStart() {
+            // console.log('dragStart');
+            this.designer.setupState.dragStart(this.formCreateInject.children);
+        },
+        dragUnchoose(e) {
+            // console.log('dragUnchoose');
+            this.designer.setupState.dragUnchoose(this.formCreateInject.children, e);
+        },
+        initRule() {
+            const rule = this.rule;
+            if (!rule.style) {
+                rule.style = {};
+            }
+            if (!rule.class) {
+                rule.class = {};
+            }
+            if (!rule.layout) {
+                rule.layout = [];
+            }
+            if (!rule.row) {
+                rule.row = 1;
+            }
+            if (!rule.col) {
+                rule.col = 1;
+            }
+        },
+        active(item) {
+            const key = `${item.pid}:${item.idx}`;
+            this.designer.setupState.customActive({
+                name: 'fcTableGrid',
+                onPaste: (rule) => {
+                    rule.slot = key;
+                    this.formCreateInject.children.push(rule);
+                },
+                style: {
+                    formData: {
+                        style: this.rule.style[key] || {},
+                        class: this.rule.class[key] || '',
+                    },
+                    change: (field, value) => {
+                        this.rule[field][key] = value || {};
+                    },
+                }
+            });
+        },
+        command(type) {
+            this[type[0]](...type[1]);
+        },
+        rmSlot(slot, rmSlot) {
+            const slotKey = Object.keys(slot);
+            const children = this.formCreateInject.children;
+            let del = 0;
+            [...children].forEach((child, index) => {
+                if (!child.slot) {
+                    return;
+                }
+                let idx;
+                if (rmSlot.indexOf(child.slot) > -1) {
+                    children.splice(index - del, 1);
+                    del++;
+                } else if (((idx = slotKey.indexOf(child.slot)) > -1)) {
+                    child.slot = slot[slotKey[idx]];
+                }
+            });
+            rmSlot.forEach(v => {
+                delete this.style[v];
+            });
+            this.loadRule();
+        },
+        rmRow(row) {
+            this.rule.row--;
+            const slot = {};
+            const rmSlot = [];
+            for (let index = row.pid; index < this.rule.row + 1; index++) {
+                for (let idx = 0; idx < this.rule.col; idx++) {
+                    if (index === row.pid) {
+                        rmSlot.push(`${row.pid}:${idx}`);
+                    } else {
+                        slot[`${index}:${idx}`] = `${index - 1}:${idx}`;
+                    }
+                }
+            }
+            let del = 0;
+            const layout = this.rule.layout;
+            [...layout].forEach((v, i) => {
+                if (v.top === row.pid) {
+                    layout.splice(i - del, 1);
+                    del++;
+                }
+            });
+            layout.forEach(v => {
+                if (v.top > row.pid) {
+                    v.top--;
+                }
+            });
+            this.rmSlot(slot, rmSlot);
+        },
+        rmCol(row) {
+            this.rule.col--;
+            const slot = {};
+            const rmSlot = [];
+            for (let index = 0; index < this.rule.row; index++) {
+                for (let idx = row.idx + 1; idx < this.rule.col + 1; idx++) {
+                    slot[`${index}:${idx}`] = `${index}:${idx - 1}`;
+                }
+                rmSlot.push(`${index}:${row.idx}`);
+            }
+            let del = 0;
+            const layout = this.rule.layout;
+            [...layout].forEach((v, i) => {
+                if (v.left === row.idx) {
+                    layout.splice(i - del, 1);
+                    del++;
+                }
+            });
+            layout.forEach(v => {
+                if (v.left > row.idx) {
+                    v.left--;
+                }
+            });
+            this.rmSlot(slot, rmSlot);
+        },
+        splitRow(item) {
+            const layout = item.data.layout;
+            const row = layout.row;
+            layout.row = 0;
+            if (row > 1) {
+                for (let i = 1; i < row; i++) {
+                    this.rule.layout.push({
+                        ...layout, top: layout.top + i
+                    });
+                }
+            }
+            this.loadRule();
+        },
+        splitCol(item) {
+            const layout = item.data.layout;
+            const col = layout.col;
+            layout.col = 0;
+            if (col > 1) {
+                for (let i = 1; i < col; i++) {
+                    this.rule.layout.push({
+                        ...layout, left: layout.left + i
+                    });
+                }
+            }
+            this.loadRule();
+        },
+        makeMap(layout) {
+            let map = [];
+            for (let x = layout.top; x < (layout.row || layout.top + 1); x++) {
+                for (let y = layout.left; y < (layout.col || layout.left + 1); y++) {
+                    map.push(`${x}:${y}`);
+                }
+            }
+            return map;
+        },
+        mergeRight(item) {
+            let layout;
+            if (item.data.layout) {
+                const col = (item.data.layout.col || 1) + 1;
+                item.data.layout.col = (col + item.idx) > this.rule.col ? this.rule.col - item.idx : col;
+                layout = item.data.layout;
+            } else {
+                layout = {
+                    top: item.pid,
+                    left: item.idx,
+                    col: 2,
+                };
+                this.rule.layout.push(layout);
+            }
+            const map = this.makeMap(layout);
+            this.formCreateInject.children.forEach(child => {
+                if (!child.slot) return;
+                if (map.indexOf(child.slot) > -1) {
+                    child.slot = `${item.pid}:${item.idx}`;
+                }
+            });
+            this.loadRule();
+        },
+        mergeBottom(item) {
+            let layout;
+            if (item.data.layout) {
+                const row = (item.data.layout.row || 1) + 1;
+                item.data.layout.row = (row + row.pid) > this.rule.col ? this.rule.col - item.pid : row;
+                layout = item.data.layout;
+            } else {
+                layout = {
+                    top: item.pid,
+                    left: item.idx,
+                    row: 2,
+                };
+                this.rule.layout.push(layout);
+            }
+            const map = this.makeMap(layout);
+            this.formCreateInject.children.forEach(child => {
+                if (!child.slot) return;
+                if (map.indexOf(child.slot) > -1) {
+                    child.slot = `${item.pid}:${item.idx}`;
+                }
+            });
+            this.loadRule();
+        },
+        addCol(row, type) {
+            this.rule.col++;
+            this.rule.layout.forEach(v => {
+                if (v.left > (type ? row.idx - 1 : row.idx)) {
+                    v.left++;
+                }
+            });
+            if (type || row.idx < this.rule.col - 2) {
+                const slot = {};
+                for (let index = 0; index < this.rule.row; index++) {
+                    for (let idx = type ? row.idx - 1 : row.idx + 1; idx < this.rule.col - 1; idx++) {
+                        slot[`${index}:${idx}`] = `${index}:${idx + 1}`;
+                    }
+                }
+                const slotKey = Object.keys(slot);
+                this.formCreateInject.children.forEach(child => {
+                    let idx;
+                    if (child.slot && ((idx = slotKey.indexOf(child.slot)) > -1)) {
+                        child.slot = slot[slotKey[idx]];
+                    }
+                });
+                slotKey.forEach(v => {
+                    if (this.style[v]) {
+                        this.style[slot[v]] = this.style[v];
+                        delete this.style[v];
+                    }
+                });
+            }
+            this.loadRule();
+        },
+        addRow(row, type) {
+            this.rule.row++;
+            this.rule.layout.forEach(v => {
+                if (v.top > (type ? row.pid - 1 : row.pid)) {
+                    v.top++;
+                }
+            });
+            if (type || row.pid < this.rule.row - 2) {
+                const slot = {};
+                for (let index = type ? row.pid - 1 : row.pid + 1; index < this.rule.row; index++) {
+                    for (let idx = 0; idx < this.rule.col; idx++) {
+                        slot[`${index}:${idx}`] = `${index + 1}:${idx}`;
+                    }
+                }
+                const slotKey = Object.keys(slot);
+                this.formCreateInject.children.forEach(child => {
+                    let idx;
+                    if (child.slot && ((idx = slotKey.indexOf(child.slot)) > -1)) {
+                        child.slot = slot[slotKey[idx]];
+                    }
+                });
+                slotKey.reverse().forEach(v => {
+                    if (this.style[v]) {
+                        this.style[slot[v]] = this.style[v];
+                        delete this.style[v];
+                    }
+                });
+            }
+            this.loadRule();
+        },
+        loadRule() {
+            const lattice = [];
+            const rule = this.rule || {row: 1, col: 1};
+            for (let index = 0; index < rule.row; index++) {
+                const sub = [];
+                lattice.push(sub);
+                for (let idx = 0; idx < rule.col; idx++) {
+                    sub.push({rowspan: 1, colspan: 1, slot: [], show: true, id: this.getUnique(`${index}${idx}`)});
+                }
+            }
+            [...(rule.layout || [])].forEach((v, i) => {
+                if (((!v.row || v.row <= 0) && (!v.col || v.col <= 0)) || !lattice[v.top] || !lattice[v.top][v.left] || !lattice[v.top][v.left].show) {
+                    rule.layout.splice(i, 1);
+                    return;
+                }
+                const data = lattice[v.top][v.left];
+                data.layout = v;
+                let col = 1;
+                let row = 1;
+                if (v.col) {
+                    col = (v.col + v.left) > rule.col ? rule.col - v.left : v.col;
+                    data.colspan = col;
+                }
+                if (v.row) {
+                    row = (v.row + v.top) > rule.row ? rule.row - v.top : v.row;
+                    data.rowspan = row;
+                }
+                if (row && col) {
+                    for (let index = 0; index < row; index++) {
+                        const row = lattice[v.top + index];
+                        if (row) {
+                            for (let idx = 0; idx < col; idx++) {
+                                if (!idx && !index)
+                                    continue;
+
+                                if (row[v.left + idx]) {
+                                    row[v.left + idx].show = false;
+                                }
+                                data.slot.push(`${v.top + index}:${v.left + idx}`);
+                            }
+                        }
+                    }
+                }
+            });
+
+            const checkCol = (col) => {
+                return !!(!col || col.layout || !col.show);
+            };
+
+            lattice.forEach((v, index) => {
+                v.forEach((item, idx) => {
+                    let right = false;
+                    let bottom = false;
+                    if (item.layout) {
+                        const col = item.layout.col || 1;
+                        const row = item.layout.row || 1;
+                        for (let i = 0; i < col; i++) {
+                            if (!lattice[index + row] || checkCol(lattice[index + row][idx + i])) {
+                                bottom = true;
+                                continue;
+                            }
+                        }
+                        for (let i = 0; i < row; i++) {
+                            if (!lattice[index + i] || checkCol(lattice[index + i][idx + col])) {
+                                right = true;
+                                continue;
+                            }
+                        }
+                    } else {
+                        right = checkCol(v[idx + 1]);
+                        bottom = lattice[index + 1] ? checkCol(lattice[index + 1][idx]) : true;
+                    }
+                    item.right = right;
+                    item.bottom = bottom;
+                });
+            });
+            this.lattice = lattice;
+            this.formCreateInject.rule.props.rule = rule;
+        },
+    },
+    beforeMount() {
+        this.loadRule();
+    }
+});
+</script>
+
+<style>
+
+._fd-table-view {
+    overflow: auto;
+}
+
+._fd-table-view-cell {
+    min-height: 50px;
+    height: 100%;
+    border: 1px inset rgba(0, 0, 0, .1);
+    background: #fff;
+}
+
+._fd-table-view-cell > ._fd-drag-tool {
+    height: 100%;
+    border: 0px;
+    margin: 0px;
+}
+
+._fd-table-view-btn {
+    flex-direction: column;
+    padding: 0;
+}
+
+._fd-table-view-btn .fc-icon {
+    width: 18px;
+    color: #fff;
+    font-size: 16px;
+}
+
+._fd-table-view-icon {
+    color: #FFFFFF;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    margin-top: 1px;
+}
+
+._fd-table-view > table {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+    border: 1px solid #EBEEF5;
+    border-bottom: 0 none;
+    border-right: 0 none;
+}
+
+._fd-table-view tr {
+    min-height: 50px;
+}
+
+._fd-table-view td {
+    padding: 0;
+    min-height: 50px;
+    min-width: 80px;
+    position: relative;
+    box-sizing: border-box;
+    overflow-wrap: break-word;
+    white-space: nowrap;
+    border: 0 none;
+    border-right: 1px solid #EBEEF5;
+    border-bottom: 1px solid #EBEEF5;
+}
+
+._fd-tableCell-drag {
+    height: 100%;
+}
+</style>

+ 390 - 0
src/components/form-create-designer/components/tableForm/TableForm.vue

@@ -0,0 +1,390 @@
+<template>
+    <div class="_fc-table-form" :class="{'_fc-disabled': disabled}">
+        <component :is="Form" :option="options" :rule="rule" :extendOption="true"
+                   :disabled="disabled"
+                   @change="formChange"
+                   v-model:api="fapi"
+                   @emit-event="$emit"></component>
+        <el-button link type="primary" class="fc-clock" v-if="!max || max > this.trs.length"
+                   @click="addRaw(true)"><i class="fc-icon icon-add-circle" style="font-weight: 700;"></i>
+            {{ formCreateInject.t('add') || '添加' }}
+        </el-button>
+    </div>
+</template>
+
+<script>
+import {markRaw, reactive} from 'vue';
+
+export default {
+    name: 'TableForm',
+    emits: ['change', 'add', 'delete', 'update:modelValue'],
+    props: {
+        formCreateInject: Object,
+        modelValue: {
+            type: Array,
+            default: () => [],
+        },
+        columns: {
+            type: Array,
+            required: true,
+            default: () => []
+        },
+        filterEmptyColumn: {
+            type: Boolean,
+            default: true,
+        },
+        options: {
+            type: Object,
+            default: () => reactive(({
+                submitBtn: false,
+                resetBtn: false,
+            }))
+        },
+        max: Number,
+        disabled: Boolean,
+    },
+    watch: {
+        modelValue: {
+            handler() {
+                this.updateTable()
+            },
+            deep: true
+        },
+        'formCreateInject.preview': function (n) {
+            this.emptyRule.children[0].props.colspan = this.columns.length + (n ? 1 : 2);
+        },
+    },
+    data() {
+        return {
+            rule: [],
+            trs: [],
+            fapi: {},
+            Form: markRaw(this.formCreateInject.form.$form()),
+            copyTrs: '',
+            oldValue: '',
+            emptyRule: {
+                type: 'tr',
+                _isEmpty: true,
+                native: true,
+                subRule: true,
+                children: [
+                    {
+                        type: 'td',
+                        style: {
+                            textAlign: 'center',
+                        },
+                        native: true,
+                        subRule: true,
+                        props: {
+                            colspan: this.columns.length + (this.formCreateInject.preview ? 1 : 2),
+                        },
+                        children: [this.formCreateInject.t('dataEmpty') || '暂无数据']
+                    }
+                ]
+            },
+        };
+    },
+    methods: {
+        formChange() {
+            this.updateValue();
+        },
+        updateValue() {
+            const value = this.trs.map((tr, idx) => {
+                return {
+                    ...(this.modelValue[idx] || {}),
+                    ...this.fapi.getChildrenFormData(tr)
+                }
+            }).filter(v => {
+                if (!this.filterEmptyColumn) {
+                    return true;
+                }
+                if (v === undefined || v === null) {
+                    return false;
+                }
+                let flag = false;
+                Object.keys(v).forEach(k => {
+                    flag = flag || (v[k] !== undefined && v[k] !== '' && v[k] !== null)
+                })
+                return flag;
+            });
+            const str = JSON.stringify(value);
+            if (str !== this.oldValue) {
+                this.oldValue = str;
+                this.$emit('update:modelValue', value);
+                this.$emit('change', value);
+            }
+        },
+        setRawData(idx, formData) {
+            const raw = this.trs[idx];
+            this.fapi.setChildrenFormData(raw, formData, true);
+        },
+        updateTable() {
+            const str = JSON.stringify(this.modelValue);
+            if (this.oldValue === str) {
+                return;
+            }
+            this.oldValue = str;
+            this.trs = this.trs.splice(0, this.modelValue.length);
+            if (!this.modelValue.length) {
+                this.addEmpty();
+            } else {
+                this.clearEmpty();
+            }
+            this.modelValue.forEach((data, idx) => {
+                if (!this.trs[idx]) {
+                    this.addRaw();
+                }
+                this.setRawData(idx, data || {});
+            });
+            this.rule[0].children[1].children = this.trs;
+        },
+        addEmpty() {
+            if (this.trs.length) {
+                this.trs.splice(0, this.trs.length);
+            }
+            this.trs.push(this.emptyRule);
+        },
+        clearEmpty() {
+            if (this.trs[0] && this.trs[0]._isEmpty) {
+                this.trs.splice(0, 1);
+            }
+        },
+        delRaw(idx) {
+            if (this.disabled) {
+                return;
+            }
+            this.trs.splice(idx, 1);
+            this.updateValue();
+            if (this.trs.length) {
+                this.trs.forEach(tr => this.updateRaw(tr));
+            } else {
+                this.addEmpty();
+            }
+            this.$emit('delete', idx);
+        },
+        addRaw(flag) {
+            if (flag && this.disabled) {
+                return;
+            }
+            const tr = this.formCreateInject.form.parseJson(this.copyTrs)[0];
+            if (this.trs.length === 1 && this.trs[0]._isEmpty) {
+                this.trs.splice(0, 1);
+            }
+            this.trs.push(tr);
+            this.updateRaw(tr);
+            if (flag) {
+                this.$emit('add', this.trs.length);
+                this.updateValue();
+            }
+        },
+        updateRaw(tr) {
+            const idx = this.trs.indexOf(tr);
+            tr.children[0].props.innerText = idx + 1;
+            tr.children[tr.children.length - 1].children[0].props.onClick = () => {
+                this.delRaw(idx);
+            };
+        },
+        loadRule() {
+            const header = [{
+                type: 'th',
+                native: true,
+                class: '_fc-tf-head-idx',
+                props: {
+                    innerText: '#'
+                }
+            }];
+            let body = [{
+                type: 'td',
+                class: '_fc-tf-idx',
+                native: true,
+                props: {
+                    innerText: '0'
+                }
+            }];
+            this.columns.forEach((column) => {
+                header.push({
+                    type: 'th',
+                    native: true,
+                    style: column.style,
+                    class: column.required ? '_fc-tf-head-required' : '',
+                    props: {
+                        innerText: column.label || ''
+                    }
+                });
+                body.push({
+                    type: 'td',
+                    native: true,
+                    children: [...(column.rule || [])]
+                });
+            });
+            header.push({
+                type: 'th',
+                native: true,
+                class: '_fc-tf-edit fc-clock',
+                props: {
+                    innerText: this.formCreateInject.t('operation') || '操作'
+                }
+            });
+            body.push({
+                type: 'td',
+                native: true,
+                class: '_fc-tf-btn fc-clock',
+                children: [
+                    {
+                        type: 'i',
+                        native: true,
+                        class: 'fc-icon icon-delete',
+                        props: {},
+                    }
+                ],
+            });
+            this.copyTrs = this.formCreateInject.form.toJson([
+                {
+                    type: 'tr',
+                    native: true,
+                    subRule: true,
+                    children: body
+                }
+            ]);
+            this.rule = [
+                {
+                    type: 'table',
+                    native: true,
+                    class: '_fc-tf-table',
+                    props: {
+                        border: '1',
+                        cellspacing: '0',
+                        cellpadding: '0',
+                    },
+                    children: [
+                        {
+                            type: 'thead',
+                            native: true,
+                            children: [
+                                {
+                                    type: 'tr',
+                                    native: true,
+                                    children: header
+                                }
+                            ]
+                        },
+                        {
+                            type: 'tbody',
+                            native: true,
+                            children: this.trs
+                        }
+                    ]
+                }
+            ]
+        },
+    },
+    created() {
+        this.loadRule();
+    },
+    mounted() {
+        this.updateTable();
+    }
+};
+</script>
+
+<style>
+._fc-table-form {
+    overflow: auto;
+    color: #666666;
+}
+
+._fc-table-form .form-create .el-form-item {
+    margin-bottom: 1px;
+}
+
+._fc-table-form .form-create .el-form-item.is-error {
+    margin-bottom: 22px;
+}
+
+._fc-table-form .el-form-item__label, ._fc-table-form .van-field__label {
+    display: none !important;
+}
+
+._fc-table-form .el-form-item__content {
+    display: flex;
+    margin-left: 0px !important;
+    width: 100% !important;
+}
+
+._fc-tf-head-idx, ._fc-tf-idx {
+    width: 40px;
+    min-width: 40px;
+    font-weight: 500;
+    text-align: center;
+}
+
+._fc-tf-edit, ._fc-tf-btn {
+    width: 70px;
+    min-width: 70px;
+    text-align: center;
+}
+
+._fc-tf-btn .fc-icon {
+    cursor: pointer;
+}
+
+._fc-table-form._fc-disabled ._fc-tf-btn .fc-icon, ._fc-table-form._fc-disabled > .el-button {
+    cursor: not-allowed;
+}
+
+._fc-tf-table {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    table-layout: fixed;
+    border: 1px solid #EBEEF5;
+    border-bottom: 0 none;
+}
+
+._fc-table-form ._fc-tf-table > thead > tr > th {
+    border: 0 none;
+    border-bottom: 1px solid #EBEEF5;
+    height: 40px;
+    font-weight: 500;
+}
+
+._fc-table-form ._fc-tf-table > thead > tr > th + th {
+    border-left: 1px solid #EBEEF5;
+}
+
+._fc-table-form tr {
+    min-height: 50px;
+}
+
+._fc-table-form ._fc-read-view {
+    text-align: center;
+    width: 100%;
+}
+
+._fc-table-form td {
+    padding: 5px;
+    min-height: 50px;
+    min-width: 80px;
+    position: relative;
+    box-sizing: border-box;
+    overflow-wrap: break-word;
+    /*white-space: nowrap;*/
+    overflow: hidden;
+    border: 0 none;
+    border-bottom: 1px solid #EBEEF5;
+}
+
+._fc-table-form td + td {
+    border-left: 1px solid #EBEEF5;
+}
+
+._fc-tf-table .el-input-number, ._fc-tf-table .el-select, ._fc-tf-table .el-slider, ._fc-tf-table .el-cascader, ._fc-tf-table .el-date-editor {
+    width: 100%;
+}
+
+._fc-tf-head-required:before {
+    content: '*';
+    color: #f56c6c;
+    margin-right: 4px;
+}
+</style>

+ 101 - 0
src/components/form-create-designer/components/tableForm/TableFormColumnView.vue

@@ -0,0 +1,101 @@
+<template>
+    <div class="_fd-tf-col" :style="colStyle">
+        <div class="_fd-tf-title">
+            <span v-if="required" class="_fd-tf-required">*</span>{{ label || '' }}
+        </div>
+        <div class="_fd-tf-con">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+import is from '@form-create/utils/lib/type';
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'TableFormColumnView',
+    props: {
+        label: String,
+        width: [Number, String],
+        color: String,
+        required: Boolean,
+    },
+    computed: {
+        colStyle() {
+            const w = this.width;
+            const style = {width: is.Number(w) ? `${w}px` : ((!w || w === 'auto') ? '180px' : w)};
+            if (this.color) {
+                style.color = this.color;
+            }
+            return style;
+        }
+    },
+    data() {
+        return {};
+    }
+});
+</script>
+
+<style>
+
+._fd-tf-col ._fd-tf-con .el-form-item {
+    margin-bottom: 1px !important;
+}
+
+._fd-tf-col {
+    display: flex;
+    flex-wrap: wrap;
+    flex-direction: column;
+    width: 180px;
+    flex-shrink: 0;
+}
+
+._fd-tf-con .el-form-item__label {
+    display: none !important;
+}
+
+._fd-tf-con {
+    display: flex;
+    flex: 1;
+    width: 100%;
+}
+
+._fd-tf-con .el-form-item__content {
+    display: flex;
+    margin-left: 0px !important;
+    width: 100% !important;
+}
+
+
+._fd-tf-title {
+    display: flex;
+    height: 40px;
+    width: 100% !important;
+    border-bottom: 1px solid #ebeef5;
+    align-items: center;
+    margin-bottom: 0px;
+    padding-left: 5px;
+}
+
+._fd-tf-required {
+    color: #f56c6c;
+    margin-right: 4px;
+}
+
+._fd-tf-con ._fc-l-item {
+    display: flex;
+    width: 100%;
+    margin-top: 4px;
+    flex-shrink: 0;
+}
+
+._fd-tf-con ._fc-l-item > * {
+    display: none !important;
+}
+
+._fd-tf-con .el-input-number, ._fd-tf-con .el-select, ._fd-tf-con .el-slider, ._fd-tf-con .el-cascader, ._fd-tf-con .el-date-editor {
+    width: 100%;
+}
+
+</style>

+ 45 - 0
src/components/form-create-designer/components/tableForm/TableFormView.vue

@@ -0,0 +1,45 @@
+<template>
+    <div class="_fd-table-form">
+        <div class="_fd-tf-wrap" v-if="$slots.default">
+            <slot></slot>
+        </div>
+        <div class="_fc-child-empty" v-else></div>
+    </div>
+</template>
+
+<script>
+import {defineComponent} from 'vue';
+
+export default defineComponent({
+    name: 'TableFormView',
+    data() {
+        return {};
+    },
+});
+</script>
+
+<style>
+._fd-table-form {
+    min-height: 130px;
+    width: 100%;
+    border: 1px solid #ECECEC;
+    background: #fff;
+}
+
+._fc-child-empty {
+    min-height: 130px;
+}
+
+._fd-tf-wrap {
+    display: flex;
+    overflow: auto;
+}
+
+._fd-tf-wrap > ._fd-drag-tool {
+    flex-shrink: 0;
+    display: flex;
+    margin: 2px;
+    height: auto;
+}
+
+</style>

+ 43 - 0
src/components/form-create-designer/config/base/field.js

@@ -0,0 +1,43 @@
+export default function field({t}) {
+    return [
+        {
+            type: 'FieldInput',
+            field: 'field',
+            value: '',
+            title: t('form.field'),
+            warning: t('warning.field'),
+        }, {
+            type: 'LanguageInput',
+            field: 'title',
+            value: '',
+            title: t('form.title'),
+        }, {
+            type: 'LanguageInput',
+            field: 'info',
+            value: '',
+            title: t('form.info'),
+        }, {
+            type: 'SizeInput',
+            field: 'formCreateWrap>labelWidth',
+            value: '',
+            title: t('form.labelWidth'),
+        }, {
+            type: 'Struct',
+            field: '_control',
+            name: 'control',
+            value: [],
+            title: t('form.control'),
+            warning: t('form.controlDocument', {doc: '<a target="_blank" href="https://form-create.com/v3/guide/control" style="color: inherit;text-decoration: underline;">' + t('form.document') + '</a>'}),
+            props: {
+                defaultValue: [],
+                validate(val) {
+                    if (!Array.isArray(val)) return false;
+                    if (!val.length) return true;
+                    return !val.some(({rule}) => {
+                        return !Array.isArray(rule);
+                    });
+                }
+            }
+        },
+    ];
+}

+ 116 - 0
src/components/form-create-designer/config/base/form.js

@@ -0,0 +1,116 @@
+import {localeOptions} from '../../utils';
+
+export default function form({t}) {
+    return [
+        {
+            type: 'input',
+            field: '>formName',
+            value: '',
+            title: t('form.formName'),
+        }, {
+            type: 'radio',
+            field: 'labelPosition',
+            value: 'left',
+            title: t('form.labelPosition'),
+            options: localeOptions(t, [
+                {value: 'left', label: 'left'},
+                {value: 'right', label: 'right'},
+                {value: 'top', label: 'top'},
+            ])
+        }, {
+            type: 'radio',
+            field: 'size',
+            value: 'small',
+            title: t('form.size'),
+            options: localeOptions(t, [
+                {value: 'large', label: 'large'},
+                {value: 'default', label: 'default'},
+                {value: 'small', label: 'small'},
+            ])
+        }, {
+            type: 'input',
+            field: 'labelSuffix',
+            value: '',
+            title: t('form.labelSuffix'),
+            style: {
+                width: '150px'
+            }
+        }, {
+            type: 'SizeInput',
+            field: 'labelWidth',
+            value: '125px',
+            title: t('form.labelWidth'),
+        }, {
+            type: 'switch',
+            field: 'hideRequiredAsterisk',
+            value: false,
+            title: t('form.hideRequiredAsterisk'),
+        }, {
+            type: 'switch',
+            field: 'showMessage',
+            value: true,
+            title: t('form.showMessage'),
+        }, {
+            type: 'switch',
+            field: 'inlineMessage',
+            value: false,
+            title: t('form.inlineMessage'),
+        }, {
+            type: 'switch',
+            field: '_submitBtn>show',
+            value: true,
+            title: t('form.submitBtn'),
+        }, {
+            type: 'switch',
+            field: '_resetBtn>show',
+            value: false,
+            title: t('form.resetBtn'),
+        }, {
+            type: 'FnConfig',
+            field: '>_event',
+            warning: t('form.controlDocument', {doc: '<a target="_blank" href="https://form-create.com/v3/guide/global-event" style="color: inherit;text-decoration: underline;">' + t('form.document') + '</a>'}),
+            value: {},
+            col: {show: true},
+            props: {
+                eventConfig: [
+                    {
+                        name: 'onSubmit',
+                        info: t('form.onSubmit'),
+                        args: ['formData', 'api'],
+                    },
+                    {
+                        name: 'onReset',
+                        info: t('form.onReset'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onCreated',
+                        info: t('form.onCreated'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onMounted',
+                        info: t('form.onMounted'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onReload',
+                        info: t('form.onReload'),
+                        args: ['api'],
+                    },
+                    {
+                        name: 'onChange',
+                        info: t('form.onChange'),
+                        args: ['field', 'value', 'options'],
+                    },
+                    {
+                        name: 'beforeFetch',
+                        info: t('form.beforeFetch'),
+                        args: ['config', 'data'],
+                    },
+                ]
+            },
+            title: t('form.event'),
+        },
+    ];
+}

+ 26 - 0
src/components/form-create-designer/config/base/style.js

@@ -0,0 +1,26 @@
+export default function field({t}) {
+    return [
+        {
+            type: 'input',
+            title: 'ID',
+            field: 'id',
+            wrap: {
+                labelWidth: '45px'
+            }
+        },
+        {
+            type: 'input',
+            title: 'Class',
+            field: 'class',
+            wrap: {
+                labelWidth: '45px'
+            }
+        },
+        {
+            type: 'StyleConfig',
+            field: 'style',
+            title: '',
+            value: {},
+        }
+    ];
+}

+ 15 - 0
src/components/form-create-designer/config/base/validate.js

@@ -0,0 +1,15 @@
+export default function validate({t}) {
+    return [
+        {
+            type: 'Required',
+            field: '$required',
+            title: t('validate.required')
+        },
+        {
+            type: 'validate',
+            field: 'validate',
+            title: t('validate.rule'),
+            value: []
+        },
+    ];
+}

+ 68 - 0
src/components/form-create-designer/config/index.js

@@ -0,0 +1,68 @@
+import radio from './rule/radio';
+import checkbox from './rule/checkbox';
+import input from './rule/input';
+import textarea from './rule/textarea';
+import password from './rule/password';
+import number from './rule/number';
+import select from './rule/select';
+import _switch from './rule/switch';
+import slider from './rule/slider';
+import time from './rule/time';
+import timeRange from './rule/timeRange';
+import date from './rule/date';
+import dateRange from './rule/dateRange';
+import rate from './rule/rate';
+import color from './rule/color';
+import row from './rule/row';
+import col from './rule/col';
+import tabPane from './rule/tabPane';
+import divider from './rule/divider';
+import cascader from './rule/cascader';
+import upload from './rule/upload';
+import transfer from './rule/transfer';
+import tree from './rule/tree';
+import alert from './rule/alert';
+import text from './rule/text';
+import space from './rule/space';
+import tabs from './rule/tabs';
+import button from './rule/button';
+import editor from './rule/editor';
+import group from './rule/group';
+import subForm from './rule/subForm';
+import card from './rule/card';
+import collapse from './rule/collapse';
+import collapseItem from './rule/collapseItem';
+import treeSelect from './rule/treeSelect';
+import tag from './rule/tag';
+import html from './rule/html';
+import table from './rule/table';
+import tableForm from './rule/tableForm';
+import tableFormColumn from './rule/tableFormColumn';
+import image from './rule/image';
+
+
+const ruleList = [
+    input, textarea, password, number, radio, checkbox, select, _switch, rate, time, timeRange, slider, date, dateRange, color, cascader, upload, transfer, tree, treeSelect, editor,
+    group, subForm, tableForm, tableFormColumn,
+    alert, button, text, html, divider, tag, image,
+    row, table, tabs, space, card, collapse,
+    col, tabPane, collapseItem,
+];
+
+export default ruleList;
+
+export function defaultDrag(rule) {
+    return {
+        icon: rule.field ? 'icon-input' : 'icon-cell',
+        label: rule.field || rule.type,
+        name: '_',
+        mask: true,
+        handleBtn: ['delete'],
+        rule() {
+            return rule;
+        },
+        props() {
+            return [];
+        }
+    }
+}

+ 24 - 0
src/components/form-create-designer/config/menu.js

@@ -0,0 +1,24 @@
+export default function createMenu() {
+    return [
+        {
+            name: 'main',
+            title: '基础组件',
+            list: []
+        },
+        {
+            name: 'subform',
+            title: '子表单组件',
+            list: []
+        },
+        {
+            name: 'aide',
+            title: '辅助组件',
+            list: []
+        },
+        {
+            name: 'layout',
+            title: '布局组件',
+            list: []
+        },
+    ];
+}

+ 45 - 0
src/components/form-create-designer/config/rule/alert.js

@@ -0,0 +1,45 @@
+import {localeProps} from '../../utils';
+
+const label = '提示';
+const name = 'elAlert';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-alert',
+    label,
+    name,
+    event: ['close'],
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                title: t('com.elAlert.name'),
+                description: t('com.elAlert.description'),
+                type: 'success',
+                effect: 'dark',
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'input', field: 'title'}, {
+            type: 'select',
+            field: 'type',
+            options: [{label: 'success', value: 'success'}, {label: 'warning', value: 'warning'}, {
+                label: 'info',
+                value: 'info'
+            }, {label: 'error', value: 'error'}]
+        }, {type: 'input', field: 'description'}, {
+            type: 'switch',
+            field: 'closable',
+            value: true
+        }, {type: 'switch', field: 'center', value: true}, {
+            type: 'input',
+            field: 'closeText'
+        }, {type: 'switch', field: 'showIcon'}, {
+            type: 'select',
+            field: 'effect',
+            options: [{label: 'light', value: 'light'}, {label: 'dark', value: 'dark'}]
+        }]);
+    }
+};

+ 49 - 0
src/components/form-create-designer/config/rule/button.js

@@ -0,0 +1,49 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '按钮';
+const name = 'elButton';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-button',
+    label,
+    name,
+    mask: true,
+    event: ['click'],
+    rule({t}) {
+        return {
+            type: name,
+            props: {},
+            children: [t('com.elButton.name')],
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'formCreateChild',
+        }, {
+            type: 'select',
+            field: 'size',
+            options: localeOptions(t, [{label: 'large', value: 'large'}, {label: 'default', value: 'default'}, {
+                label: 'small',
+                value: 'small'
+            }])
+        }, {
+            type: 'select',
+            field: 'type',
+            options: [{label: 'primary', value: 'primary'}, {
+                label: 'success',
+                value: 'success'
+            }, {label: 'warning', value: 'warning'}, {label: 'danger', value: 'danger'}, {
+                label: 'info',
+                value: 'info'
+            }]
+        }, {type: 'switch', field: 'plain'}, {
+            type: 'switch',
+            field: 'round'
+        }, {type: 'switch', field: 'circle'}, {
+            type: 'switch',
+            field: 'loading'
+        }, {type: 'switch', field: 'disabled'}]);
+    }
+};

+ 40 - 0
src/components/form-create-designer/config/rule/card.js

@@ -0,0 +1,40 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '卡片';
+const name = 'elCard';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-card',
+    label,
+    name,
+    drag: true,
+    inside: false,
+    mask: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                header: t('com.elCard.props.header')
+            },
+            style: {
+                width: '100%'
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'header',
+        }, {
+            type: 'select',
+            field: 'shadow',
+            value: 'always',
+            options: localeOptions(t, [{label: 'always', value: 'always'}, {label: 'never', value: 'never'}, {
+                label: 'hover',
+                value: 'hover'
+            }])
+        }]);
+    }
+};

+ 121 - 0
src/components/form-create-designer/config/rule/cascader.js

@@ -0,0 +1,121 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils/index';
+
+const label = '级联选择器';
+const name = 'cascader';
+
+export default {
+    menu: 'main',
+    icon: 'icon-cascader',
+    label,
+    name,
+    input: true,
+    event: ['change', 'expandChange', 'blur', 'focus', 'visibleChange', 'removeTag'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.cascader.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {
+                options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 3)
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.options'),
+            ...[
+
+                {
+                    type: 'switch',
+                    field: 'disabled'
+                },
+                {
+                    type: 'switch',
+                    field: 'clearable'
+                },
+                {
+                    type: 'input',
+                    field: 'placeholder'
+                },
+                {
+                    type: 'Object',
+                    field: 'props',
+                    props: {
+                        rule: localeProps(t, name + '.propsOpt', [{
+                            type: 'switch',
+                            field: 'multiple'
+                        }, {
+                            type: 'select',
+                            field: 'expandTrigger',
+                            options: localeOptions(t, [{label: 'click', value: 'click'}, {
+                                label: 'hover',
+                                value: 'hover'
+                            }])
+                        }, {
+                            type: 'switch',
+                            field: 'checkStrictly'
+                        }, {
+                            type: 'switch',
+                            field: 'emitPath',
+                            value: true
+                        }, {
+                            type: 'input',
+                            field: 'value',
+                            value: 'value'
+                        }, {
+                            type: 'input',
+                            field: 'label',
+                            value: 'label'
+                        }, {
+                            type: 'input',
+                            field: 'children',
+                            value: 'children'
+                        }, {
+                            type: 'input',
+                            field: 'disabled',
+                            value: 'disabled'
+                        }, {type: 'input', field: 'leaf'}])
+                    }
+                },
+                {
+                    type: 'switch',
+                    field: 'showAllLevels',
+                    value: true
+                },
+                {
+                    type: 'switch',
+                    field: 'collapseTags'
+                },
+                {
+                    type: 'switch',
+                    field: 'collapseTagsTooltip'
+                },
+                {
+                    type: 'input',
+                    field: 'separator'
+                },
+                {
+                    type: 'switch',
+                    field: 'filterable'
+                },
+                {
+                    type: 'select',
+                    field: 'tagType',
+                    options: [
+                        {label: 'success', value: 'success'},
+                        {label: 'info', value: 'info'},
+                        {label: 'warning', value: 'warning'},
+                        {label: 'danger', value: 'danger'},
+                    ]
+                },
+            ]
+        ]);
+    }
+};

+ 68 - 0
src/components/form-create-designer/config/rule/checkbox.js

@@ -0,0 +1,68 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeOptionsRule, makeTreeOptions} from '../../utils/index';
+
+const label = '多选框';
+const name = 'checkbox';
+
+export default {
+    menu: 'main',
+    icon: 'icon-checkbox',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.checkbox.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {},
+            options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 1)
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeOptionsRule(t, 'options'),
+            ...[
+                {
+                    type: 'switch',
+                    field: 'disabled'
+                },
+                {type: 'switch', field: 'input'},
+                {
+                    type: 'switch',
+                    field: 'type',
+                    props: {activeValue: 'button', inactiveValue: 'default'}
+                },
+                {
+                    field: 'min',
+                    type: 'inputNumber',
+                    props: {
+                        min: 0
+                    }
+                },
+                {
+                    field: 'max',
+                    type: 'inputNumber',
+                    props: {
+                        min: 0
+                    }
+                },
+                {
+                    type: 'ColorInput',
+                    field: 'textColor'
+                },
+                {
+                    type: 'ColorInput',
+                    field: 'fill'
+                }
+            ]
+        ]);
+    }
+};

+ 86 - 0
src/components/form-create-designer/config/rule/col.js

@@ -0,0 +1,86 @@
+import {localeProps} from '../../utils';
+
+const name = 'col';
+
+const devices = {
+    xs: '<768px',
+    sm: '≥768px',
+    md: '≥992px',
+    lg: '≥1200px',
+    xl: '≥1920px',
+};
+
+export default {
+    name,
+    label: '格子',
+    drag: true,
+    dragBtn: false,
+    inside: true,
+    mask: false,
+    rule() {
+        return {
+            type: name,
+            props: {span: 12},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {type: 'slider', field: 'span', value: 12, props: {min: 0, max: 24}},
+            {type: 'slider', field: 'offset', props: {min: 0, max: 24}},
+            {type: 'slider', field: 'push', props: {min: 0, max: 24}},
+            {type: 'slider', field: 'pull', props: {min: 0, max: 24}},
+            {
+                type: 'ConfigItem',
+                props: {
+                    label: t('props.reactive')
+                },
+                children: [
+                    {
+                        type: 'elTabs',
+                        style: {
+                            width: '100%'
+                        },
+                        slot: 'append',
+                        children: Object.keys(devices).map(k => {
+                            return {
+                                type: 'elTabPane',
+                                props: {
+                                    label: devices[k]
+                                },
+                                style: 'padding:0 10px;',
+                                children: [
+                                    {
+                                        type: 'slider',
+                                        field: k + '>span',
+                                        title: t('com.col.props.span'),
+                                        value: 12,
+                                        props: {min: 0, max: 24},
+                                    },
+                                    {
+                                        type: 'slider',
+                                        field: k + '>offset',
+                                        title: t('com.col.props.offset'),
+                                        props: {min: 0, max: 24},
+                                    },
+                                    {
+                                        type: 'slider',
+                                        field: k + '>push',
+                                        title: t('com.col.props.push'),
+                                        props: {min: 0, max: 24},
+                                    },
+                                    {
+                                        type: 'slider',
+                                        field: k + '>pull',
+                                        title: t('com.col.props.pull'),
+                                        props: {min: 0, max: 24},
+                                    }
+                                ]
+                            };
+                        })
+                    }
+                ]
+            },
+        ]);
+    }
+};

+ 30 - 0
src/components/form-create-designer/config/rule/collapse.js

@@ -0,0 +1,30 @@
+import {localeProps} from '../../utils';
+
+const label = '折叠面板';
+const name = 'elCollapse';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-collapse',
+    label,
+    name,
+    mask: false,
+    children: 'elCollapseItem',
+    event: ['change'],
+    rule() {
+        return {
+            type: name,
+            props: {},
+            style: {
+                width: '100%',
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'accordion'
+        }]);
+    }
+};

+ 36 - 0
src/components/form-create-designer/config/rule/collapseItem.js

@@ -0,0 +1,36 @@
+import {localeProps} from '../../utils';
+
+const label = '面板';
+const name = 'elCollapseItem';
+
+export default {
+    icon: 'icon-cell',
+    label,
+    name,
+    drag: true,
+    dragBtn: false,
+    inside: true,
+    mask: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                title: t('com.elCollapseItem.name')
+            },
+            style: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'title',
+        }, {
+            type: 'input',
+            field: 'name',
+        }, {
+            type: 'switch',
+            field: 'disabled'
+        }]);
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/color.js

@@ -0,0 +1,53 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '颜色选择器';
+const name = 'colorPicker';
+
+export default {
+    menu: 'main',
+    icon: 'icon-color',
+    label,
+    name,
+    input: true,
+    event: ['change', 'activeChange', 'focus', 'blur'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.colorPicker.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'showAlpha'
+            },
+            {
+                type: 'select',
+                field: 'colorFormat',
+                options: [{label: 'hsl', value: 'hsl'}, {label: 'hsv', value: 'hsv'}, {
+                    label: 'hex',
+                    value: 'hex'
+                }, {label: 'rgb', value: 'rgb'}]
+            },
+            {
+                type: 'tableOptions',
+                field: 'predefine',
+                props: {
+                    column: [{label: t('props.value'), key: 'value'}],
+                    valueType: 'string'
+                }
+            },
+        ]);
+    }
+};

+ 70 - 0
src/components/form-create-designer/config/rule/date.js

@@ -0,0 +1,70 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '日期';
+const name = 'datePicker';
+
+export default {
+    menu: 'main',
+    icon: 'icon-date',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'calendarChange', 'panelChange', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.datePicker.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'select',
+            field: 'type',
+            options: localeOptions(t, [{label: 'year', value: 'year'}, {label: 'month', value: 'month'}, {
+                label: 'date',
+                value: 'date'
+            }, {label: 'dates', value: 'dates'}, {label: 'week', value: 'week'}, {
+                label: 'datetime',
+                value: 'datetime'
+            }, {label: 'datetimerange', value: 'datetimerange'}, {
+                label: 'daterange',
+                value: 'daterange'
+            }, {label: 'monthrange', value: 'monthrange'}])
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'placeholder'
+        }, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'input',
+            field: 'format'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }, {type: 'input', field: 'rangeSeparator'}, {
+            type: 'switch',
+            field: 'unlinkPanels'
+        }]);
+    }
+};

+ 64 - 0
src/components/form-create-designer/config/rule/dateRange.js

@@ -0,0 +1,64 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '日期区间';
+const name = 'dateRange';
+
+export default {
+    menu: 'main',
+    icon: 'icon-date-range',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'calendarChange', 'panelChange', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: 'datePicker',
+            field: uniqueId(),
+            title: t('com.dateRange.name'),
+            info: '',
+            $required: false,
+            props: {
+                type: 'datetimerange',
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, 'datePicker.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'select',
+            field: 'type',
+            options: localeOptions(t, [
+                {label: 'datetimerange', value: 'datetimerange'},
+                {label: 'daterange', value: 'daterange'},
+                {label: 'monthrange', value: 'monthrange'}
+            ])
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'input',
+            field: 'format'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }, {type: 'input', field: 'rangeSeparator'}, {
+            type: 'switch',
+            field: 'unlinkPanels'
+        }]);
+    }
+};

+ 31 - 0
src/components/form-create-designer/config/rule/divider.js

@@ -0,0 +1,31 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '分割线';
+const name = 'elDivider';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-divider',
+    label,
+    name,
+    rule({t}) {
+        return {
+            type: name,
+            props: {},
+            children: [t('com.elDivider.name')],
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'input',
+            field: 'formCreateChild',
+        }, {
+            type: 'select',
+            field: 'contentPosition',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'right', value: 'right'}, {
+                label: 'center',
+                value: 'center'
+            }])
+        }]);
+    }
+};

+ 31 - 0
src/components/form-create-designer/config/rule/editor.js

@@ -0,0 +1,31 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '富文本框';
+const name = 'fcEditor';
+
+export default {
+    menu: 'main',
+    icon: 'icon-editor',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.fcEditor.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }]);
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/group.js

@@ -0,0 +1,53 @@
+import {localeProps} from '../../utils';
+import uniqueId from '@form-create/utils/lib/unique';
+
+const label = '子表单';
+const name = 'group';
+
+export default {
+    menu: 'subform',
+    icon: 'icon-subform',
+    label,
+    name,
+    inside: false,
+    drag: true,
+    dragBtn: true,
+    mask: false,
+    input: true,
+    event: ['change'],
+    subForm: 'array',
+    loadRule(rule) {
+        rule.children = rule.props.rule || [];
+        rule.type = 'FcRow';
+        delete rule.props.rule;
+    },
+    parseRule(rule) {
+        rule.props.rule = rule.children;
+        rule.type = 'group';
+        delete rule.children;
+        delete rule.props.mode;
+    },
+    rule({t}) {
+        return {
+            type: 'fcRow',
+            field: uniqueId(),
+            title: t('com.group.name'),
+            info: '',
+            $required: false,
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {type: 'switch', field: 'syncDisabled', value: true},
+        {type: 'switch', field: 'button', value: true},
+        {type: 'switch', field: 'sortBtn', value: true},
+        {type: 'inputNumber', field: 'expand'},
+        {type: 'inputNumber', field: 'min'},
+        {type: 'inputNumber', field: 'max'},
+        ]);
+    }
+};

+ 52 - 0
src/components/form-create-designer/config/rule/html.js

@@ -0,0 +1,52 @@
+import {localeProps} from '../../utils';
+
+const label = 'HTML';
+const name = 'html';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-html',
+    label,
+    name,
+    rule() {
+        return {
+            type: name,
+            title: '',
+            native: true,
+            attrs: {
+                innerHTML: ''
+            },
+            style: {
+                display: 'block',
+                width: '100%',
+            },
+            children: ['<div style="color:blue;">\n' +
+            ' html html html html html html html html html\n' +
+            '  </div>'],
+        };
+    },
+    watch: {
+        formCreateNative({value, rule}) {
+            if (value) {
+                rule.title = '';
+            }
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch', field: 'formCreateNative', props: {
+                    activeValue: false,
+                    inactiveValue: true,
+                },
+                control: [{value: false, rule: ['formCreateTitle']}]
+            }, {
+                type: 'input',
+                field: 'formCreateTitle',
+            }, {
+                type: 'HtmlEditor',
+                field: 'formCreateChild',
+            }
+        ]);
+    }
+};

+ 32 - 0
src/components/form-create-designer/config/rule/image.js

@@ -0,0 +1,32 @@
+import {localeProps} from '../../utils';
+
+const label = '图片';
+const name = 'elImage';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-image',
+    label,
+    name,
+    rule() {
+        return {
+            type: name,
+            title: '',
+            style: {
+                width: '100px',
+                height: '100px',
+            },
+            props: {
+                src: 'https://static.form-create.com/example.png',
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'input',
+                field: 'src',
+            }
+        ]);
+    }
+};

+ 62 - 0
src/components/form-create-designer/config/rule/input.js

@@ -0,0 +1,62 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '输入框';
+const name = 'input';
+
+export default {
+    menu: 'main',
+    icon: 'icon-input',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change', 'input', 'clear'],
+    validate: ['string', 'url', 'email'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.input.name'),
+            info: '',
+            $required: false,
+            props: {}
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'readonly'
+            },
+            {
+                type: 'select',
+                field: 'type',
+                options: localeOptions(t, [
+                    {label: 'text', value: 'text'},
+                    {label: 'number', value: 'number'},
+                    {label: 'time', value: 'time'},
+                    {label: 'date', value: 'date'},
+                    {label: 'month', value: 'month'},
+                    {label: 'datetime-local', value: 'datetime-local'},
+                ])
+            },
+            {
+                type: 'inputNumber',
+                field: 'maxlength',
+                props: {min: 0}
+            },
+            {
+                type: 'input',
+                field: 'placeholder'
+            },
+            {
+                type: 'switch',
+                field: 'clearable'
+            },
+        ]);
+    }
+};

+ 49 - 0
src/components/form-create-designer/config/rule/number.js

@@ -0,0 +1,49 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '计数器';
+const name = 'inputNumber';
+
+export default {
+    menu: 'main',
+    icon: 'icon-number',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change'],
+    validate: ['number', 'integer', 'float'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.inputNumber.name'),
+            info: '',
+            $required: false,
+            props: {}
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'disabled'}, {
+            type: 'inputNumber',
+            field: 'min'
+        }, {
+            type: 'inputNumber',
+            field: 'max',
+        },  {
+            type: 'inputNumber',
+            title: 'precision',
+            field: 'precision',
+        }, {type: 'inputNumber', field: 'step', props: {min: 0}}, {
+            type: 'switch',
+            field: 'stepStrictly'
+        }, {
+            type: 'switch',
+            field: 'controls',
+            value: true
+        }, {
+            type: 'select',
+            field: 'controlsPosition',
+            options: localeOptions(t, [{label: 'default', value: ''}, {label: 'right', value: 'right'}])
+        }, {type: 'input', field: 'placeholder'}]);
+    }
+};

+ 52 - 0
src/components/form-create-designer/config/rule/password.js

@@ -0,0 +1,52 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '密码输入框';
+const name = 'password';
+
+export default {
+    menu: 'main',
+    icon: 'icon-password',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change', 'input', 'clear'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: 'input',
+            field: uniqueId(),
+            title: t('com.password.name'),
+            info: '',
+            $required: false,
+            props: {
+                type: 'password'
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'readonly'
+            },
+            {
+                type: 'inputNumber',
+                field: 'maxlength',
+                props: {min: 0}
+            },
+            {
+                type: 'input',
+                field: 'placeholder'
+            },
+            {
+                type: 'switch',
+                field: 'clearable'
+            },
+        ]);
+    }
+};

+ 43 - 0
src/components/form-create-designer/config/rule/radio.js

@@ -0,0 +1,43 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeOptionsRule, makeTreeOptions} from '../../utils/index';
+
+const label = '单选框';
+const name = 'radio';
+
+export default {
+    menu: 'main',
+    icon: 'icon-radio',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['string', 'number'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.radio.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {},
+            options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 1)
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeOptionsRule(t, 'options'),
+            {type: 'switch', field: 'disabled'},
+            {type: 'switch', field: 'input'},
+            {
+                type: 'switch',
+                field: 'type',
+                props: {activeValue: 'button', inactiveValue: 'default'}
+            }, {type: 'ColorInput', field: 'textColor'}, {
+                type: 'ColorInput',
+                field: 'fill'
+            }]);
+    }
+};

+ 44 - 0
src/components/form-create-designer/config/rule/rate.js

@@ -0,0 +1,44 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '评分';
+const name = 'rate';
+
+export default {
+    menu: 'main',
+    icon: 'icon-rate',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    validate: ['number'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.rate.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {type: 'inputNumber', field: 'max', props: {min: 0}}, {
+                type: 'switch',
+                field: 'disabled'
+            }, {type: 'switch', field: 'allowHalf'}, {
+                type: 'ColorInput',
+                field: 'voidColor'
+            }, {type: 'ColorInput', field: 'disabledVoidColor'}, {
+                type: 'input',
+                field: 'voidIconClass'
+            }, {type: 'input', field: 'disabledVoidIconClass'}, {
+                type: 'switch',
+                field: 'showScore'
+            }, {type: 'ColorInput', field: 'textColor'}, {
+                type: 'input',
+                field: 'scoreTemplate'
+            }]);
+    }
+};

+ 46 - 0
src/components/form-create-designer/config/rule/row.js

@@ -0,0 +1,46 @@
+import {localeProps} from '../../utils';
+
+const label = '栅格布局';
+const name = 'fcRow';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-row',
+    label,
+    name,
+    mask: false,
+    children: 'col',
+    childrenLen: 2,
+    rule() {
+        return {
+            type: name,
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'inputNumber',
+            field: 'gutter',
+            props: {min: 0}
+        }, {
+            type: 'switch',
+            field: 'type',
+            props: {activeValue: 'flex', inactiveValue: 'default'}
+        }, {
+            type: 'select',
+            field: 'justify',
+            options: [{label: 'start', value: 'start'}, {label: 'end', value: 'end'}, {
+                label: 'center',
+                value: 'center'
+            }, {label: 'space-around', value: 'space-around'}, {label: 'space-between', value: 'space-between'}]
+        }, {
+            type: 'select',
+            field: 'align',
+            options: [{label: 'top', value: 'top'}, {label: 'middle', value: 'middle'}, {
+                label: 'bottom',
+                value: 'bottom'
+            }]
+        }]);
+    }
+};

+ 70 - 0
src/components/form-create-designer/config/rule/select.js

@@ -0,0 +1,70 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {getInjectArg, localeProps, makeOptionsRule, makeTreeOptions} from '../../utils/index';
+
+const label = '选择器';
+const name = 'select';
+
+export default {
+    menu: 'main',
+    icon: 'icon-select',
+    label,
+    name,
+    input: true,
+    event: ['change', 'visibleChange', 'removeTag', 'clear', 'blur', 'focus'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.select.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {},
+            options: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 1)
+        };
+    },
+    watch: {
+        multiple({rule}) {
+            rule.key = uniqueId();
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeOptionsRule(t, 'options'),
+            {type: 'switch', field: 'multiple'}, {
+                type: 'switch',
+                field: 'disabled'
+            }, {type: 'switch', field: 'clearable'}, {
+                type: 'switch',
+                field: 'collapseTags'
+            }, {
+                type: 'inputNumber',
+                field: 'multipleLimit',
+                props: {min: 0}
+            }, {type: 'input', field: 'placeholder'}, {
+                type: 'switch',
+                field: 'filterable'
+            }, {
+                type: 'switch',
+                field: 'remote',
+            }, {
+                type: 'FnInput',
+                field: 'remoteMethod',
+                props: {
+                    body: true,
+                    fnx: true,
+                    name: 'remoteMethod',
+                    args: [getInjectArg(t)],
+                },
+            }, {type: 'switch', field: 'allowCreate'}, {
+                type: 'input',
+                field: 'noMatchText'
+            }, {type: 'input', field: 'noDataText'}, {
+                type: 'switch',
+                field: 'reserveKeyword'
+            }, {type: 'switch', field: 'defaultFirstOption'}])
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/slider.js

@@ -0,0 +1,53 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '滑块';
+const name = 'slider';
+
+export default {
+    menu: 'main',
+    icon: 'icon-slider',
+    label,
+    name,
+    input: true,
+    event: ['change', 'input'],
+    validate: ['number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.slider.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'disabled'}, {
+            type: 'switch',
+            field: 'range'
+        }, {
+            type: 'inputNumber',
+            field: 'min',
+            props: {min: 0}
+        }, {
+            type: 'inputNumber',
+            field: 'max',
+            props: {min: 0},
+        }, {
+            type: 'inputNumber',
+            field: 'step',
+            props: {min: 0},
+        }, {type: 'switch', field: 'showInput'}, {
+            type: 'switch',
+            field: 'showInputControls',
+            value: true
+        }, {type: 'switch', field: 'showStops'}, {
+            type: 'switch',
+            field: 'vertical'
+        }, {
+            type: 'input',
+            field: 'height'
+        }]);
+    }
+};

+ 44 - 0
src/components/form-create-designer/config/rule/space.js

@@ -0,0 +1,44 @@
+import {localeProps} from '../../utils';
+
+const label = '间距';
+const name = 'space';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-space',
+    label,
+    name,
+    rule() {
+        return {
+            type: 'div',
+            wrap: {
+                show: false
+            },
+            native: true,
+            style: {
+                width: '100%',
+                height: '20px',
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return [
+            {
+                type: 'object',
+                field: 'formCreateStyle',
+                native: true,
+                props: {
+                    rule: localeProps(t, name + '.props', [
+                        {
+                            type: 'input',
+                            field: 'height',
+                            title: 'height',
+                        },
+                    ])
+                }
+            }
+
+        ];
+    }
+};

+ 47 - 0
src/components/form-create-designer/config/rule/subForm.js

@@ -0,0 +1,47 @@
+import {localeProps} from '../../utils';
+import uniqueId from '@form-create/utils/lib/unique';
+
+const label = '分组';
+const name = 'subForm';
+
+export default {
+    menu: 'subform',
+    icon: 'icon-group',
+    label,
+    name,
+    inside: false,
+    drag: true,
+    dragBtn: true,
+    mask: false,
+    input: true,
+    subForm: 'object',
+    event: ['change'],
+    loadRule(rule) {
+        rule.children = rule.props.rule || [];
+        rule.type = 'FcRow';
+        delete rule.props.rule;
+    },
+    parseRule(rule) {
+        rule.props.rule = rule.children;
+        rule.type = 'subForm';
+        delete rule.children;
+    },
+    rule({t}) {
+        return {
+            type: 'fcRow',
+            field: uniqueId(),
+            title: t('com.subForm.name'),
+            info: '',
+            $required: false,
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {type: 'switch', field: 'syncDisabled', value: true},
+        ]);
+    }
+};

+ 46 - 0
src/components/form-create-designer/config/rule/switch.js

@@ -0,0 +1,46 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '开关';
+const name = 'switch';
+
+export default {
+    menu: 'main',
+    icon: 'icon-switch',
+    label,
+    name,
+    input: true,
+    event: ['change'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.switch.name'),
+            info: '',
+            $required: false,
+            props: {
+                activeValue: true,
+                inactiveValue: false,
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'inputNumber',
+            field: 'width',
+            props: {min: 0},
+        }, {type: 'input', field: 'activeText'}, {
+            type: 'input',
+            field: 'inactiveText'
+        }, {type: 'ValueInput', field: 'activeValue'}, {
+            type: 'ValueInput',
+            field: 'inactiveValue'
+        }, {type: 'ColorInput', field: 'activeColor'}, {
+            type: 'ColorInput',
+            field: 'inactiveColor'
+        }]);
+    }
+};

+ 29 - 0
src/components/form-create-designer/config/rule/tabPane.js

@@ -0,0 +1,29 @@
+import {localeProps} from '../../utils';
+
+const label = '选项卡';
+const name = 'elTabPane';
+
+export default {
+    label,
+    name,
+    inside: true,
+    drag: true,
+    dragBtn: false,
+    mask: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {label: t('com.elTabPane.name')},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'input', field: 'label'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {type: 'input', field: 'name'}, {
+            type: 'switch',
+            field: 'lazy'
+        }]);
+    }
+};

+ 35 - 0
src/components/form-create-designer/config/rule/table.js

@@ -0,0 +1,35 @@
+import {localeProps} from '../../utils';
+
+const label = '表格布局';
+const name = 'fcTable';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-table',
+    label,
+    name,
+    inside: false,
+    mask: false,
+    rule() {
+        return {
+            type: name,
+            props: {
+                rule: {
+                    row: 3,
+                    col: 4,
+                    style: {},
+                    class: {},
+                    layout: []
+                }
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {type: 'switch', field: 'border', value: true},
+            {type: 'ColorInput', field: 'borderColor'},
+            {type: 'input', field: 'borderWidth'},
+        ]);
+    }
+};

+ 79 - 0
src/components/form-create-designer/config/rule/tableForm.js

@@ -0,0 +1,79 @@
+import unique from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '表格表单';
+const name = 'tableForm';
+
+export default {
+    menu: 'subform',
+    icon: 'icon-table-form',
+    label,
+    name,
+    mask: false,
+    input: true,
+    subForm: 'array',
+    event: ['change', 'add', 'delete'],
+    languageKey: ['add', 'operation', 'dataEmpty'],
+    children: 'tableFormColumn',
+    loadRule(rule) {
+        if (!rule.props) rule.props = {};
+        const columns = rule.props.columns || [];
+        rule.children = columns.map(column => {
+            return {
+                type: 'tableFormColumn',
+                _fc_drag_tag: 'tableFormColumn',
+                props: {
+                    label: column.label,
+                    required: column.required || false,
+                    width: column.style.width || '',
+                    color: column.style.color || '',
+                },
+                children: column.rule || []
+            }
+        });
+        delete rule.props.columns;
+    },
+    parseRule(rule) {
+        const children = rule.children || [];
+        rule.props.columns = children.map(column => {
+            return {
+                label: column.props.label,
+                required: column.props.required,
+                style: {
+                    width: column.props.width,
+                    color: column.props.color,
+                },
+                rule: column.children || []
+            };
+        })
+        rule.children = [];
+    },
+    rule({t}) {
+        return {
+            type: name,
+            field: unique(),
+            title: t('com.tableForm.name'),
+            info: '',
+            props: {},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'filterEmptyColumn',
+                value: true,
+            },
+            {
+                type: 'inputNumber',
+                field: 'max',
+                props: {min: 0}
+            },
+        ]);
+    }
+};

+ 43 - 0
src/components/form-create-designer/config/rule/tableFormColumn.js

@@ -0,0 +1,43 @@
+import {localeProps} from '../../utils';
+
+const name = 'tableFormColumn';
+
+export default {
+    icon: 'icon-cell',
+    name,
+    aide: true,
+    drag: true,
+    dragBtn: false,
+    mask: false,
+    style: false,
+    rule({t}) {
+        return {
+            type: name,
+            props: {
+                label: t('com.tableFormColumn.label'),
+                width: 'auto'
+            },
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'input',
+                field: 'label',
+            },
+            {
+                type: 'switch',
+                field: 'required',
+            },
+            {
+                type: 'input',
+                field: 'width',
+            },
+            {
+                type: 'ColorInput',
+                field: 'color',
+            }
+        ]);
+    }
+};

+ 38 - 0
src/components/form-create-designer/config/rule/tabs.js

@@ -0,0 +1,38 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '标签页';
+const name = 'elTabs';
+
+export default {
+    menu: 'layout',
+    icon: 'icon-tab',
+    label,
+    name,
+    mask: false,
+    event: ['tabClick', 'tabChange', 'tabRemove', 'tabAdd', 'edit'],
+    children: 'elTabPane',
+    rule() {
+        return {
+            type: name,
+            style: {width: '100%'},
+            children: []
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'select',
+            field: 'type',
+            options: [{
+                label: 'card',
+                value: 'card'
+            }, {label: 'border-card', value: 'border-card'}]
+        }, {type: 'switch', field: 'closable'}, {
+            type: 'select',
+            field: 'tabPosition',
+            options: localeOptions(t, [{label: 'top', value: 'top'}, {label: 'right', value: 'right'}, {
+                label: 'left',
+                value: 'left'
+            }])
+        }, {type: 'switch', field: 'stretch'}]);
+    }
+};

+ 79 - 0
src/components/form-create-designer/config/rule/tag.js

@@ -0,0 +1,79 @@
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '标签';
+const name = 'elTag';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-tag',
+    label,
+    name,
+    mask: true,
+    event: ['click', 'close'],
+    rule({t}) {
+        return {
+            type: name,
+            title: '',
+            native: true,
+            children: [t('com.elTag.name')]
+        };
+    },
+    watch: {
+        formCreateNative({value, rule}) {
+            if (value) {
+                rule.title = '';
+            }
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch', field: 'formCreateNative', props: {
+                    activeValue: false,
+                    inactiveValue: true,
+                },
+                control: [{value: false, rule: ['formCreateTitle']}]
+            },
+            {
+                type: 'input',
+                field: 'formCreateTitle',
+            }, {
+                type: 'input',
+                field: 'formCreateChild'
+            }, {
+                type: 'select',
+                field: 'type',
+                options: [{label: 'primary', value: 'primary'}, {
+                    label: 'success',
+                    value: 'success'
+                }, {label: 'warning', value: 'warning'}, {label: 'danger', value: 'danger'}, {
+                    label: 'info',
+                    value: 'info'
+                }]
+            }, {
+                type: 'select',
+                field: 'size',
+                options: localeOptions(t, [{label: 'large', value: 'large'}, {
+                    label: 'default',
+                    value: 'default'
+                }, {label: 'small', value: 'small'}])
+            }, {
+                type: 'select',
+                field: 'effect',
+                options: [{label: 'dark', value: 'dark'}, {
+                    label: 'light',
+                    value: 'light'
+                }, {label: 'plain', value: 'plain'}]
+            }, {
+                type: 'switch', field: 'closable'
+            }, {
+                type: 'switch', field: 'disableTransitions'
+            }, {
+                type: 'switch', field: 'hit'
+            }, {
+                type: 'switch', field: 'round'
+            }, {
+                type: 'ColorInput', field: 'color'
+            }]);
+    }
+};

+ 50 - 0
src/components/form-create-designer/config/rule/text.js

@@ -0,0 +1,50 @@
+import {localeProps} from '../../utils';
+
+const label = '文字';
+const name = 'text';
+
+export default {
+    menu: 'aide',
+    icon: 'icon-span',
+    label,
+    name,
+    rule({t}) {
+        return {
+            type: 'div',
+            title: '',
+            native: true,
+            style: {
+                whiteSpace: 'pre-line',
+                width: '100%',
+            },
+            children: [t('com.text.name')],
+        };
+    },
+    watch: {
+        formCreateNative({value, rule}) {
+            if (value) {
+                rule.title = '';
+            }
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch', field: 'formCreateNative', props: {
+                    activeValue: false,
+                    inactiveValue: true,
+                },
+                control: [{value: false, rule: ['formCreateTitle']}]
+            }, {
+                type: 'input',
+                field: 'formCreateTitle',
+            }, {
+                type: 'input',
+                field: 'formCreateChild',
+                props: {
+                    type: 'textarea'
+                }
+            }
+        ]);
+    }
+};

+ 63 - 0
src/components/form-create-designer/config/rule/textarea.js

@@ -0,0 +1,63 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps} from '../../utils';
+
+const label = '多行输入框';
+const name = 'textarea';
+
+export default {
+    menu: 'main',
+    icon: 'icon-textarea',
+    label,
+    name,
+    input: true,
+    event: ['blur', 'focus', 'change', 'input'],
+    validate: ['string'],
+    rule({t}) {
+        return {
+            type: 'input',
+            field: uniqueId(),
+            title: t('com.textarea.name'),
+            info: '',
+            $required: false,
+            props: {
+                type: 'textarea'
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            {
+                type: 'switch',
+                field: 'disabled'
+            },
+            {
+                type: 'switch',
+                field: 'readonly'
+            },
+            {
+                type: 'inputNumber',
+                field: 'maxlength',
+                props: {min: 0}
+            },
+            {
+                type: 'switch',
+                field: 'showWordLimit'
+            },
+            {
+                type: 'input',
+                field: 'placeholder'
+            },
+            {
+                type: 'inputNumber',
+                field: 'rows',
+                props: {
+                    min: 0
+                }
+            },
+            {
+                type: 'switch',
+                field: 'autosize'
+            },
+        ]);
+    }
+};

+ 62 - 0
src/components/form-create-designer/config/rule/time.js

@@ -0,0 +1,62 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '时间';
+const name = 'timePicker';
+
+export default {
+    menu: 'main',
+    icon: 'icon-time',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.timePicker.name'),
+            info: '',
+            $required: false,
+            props: {},
+        };
+    },
+    watch: {
+        isRange({rule}) {
+            rule.key = uniqueId();
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'switch',
+            field: 'isRange'
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'placeholder'
+        }, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'switch',
+            field: 'arrowControl'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }]);
+    }
+};

+ 53 - 0
src/components/form-create-designer/config/rule/timeRange.js

@@ -0,0 +1,53 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeOptions, localeProps} from '../../utils';
+
+const label = '时间区间';
+const name = 'timeRange';
+
+export default {
+    menu: 'main',
+    icon: 'icon-time-range',
+    label,
+    name,
+    input: true,
+    event: ['change', 'blur', 'focus', 'visibleChange'],
+    rule({t}) {
+        return {
+            type: 'timePicker',
+            field: uniqueId(),
+            title: t('com.timeRange.name'),
+            info: '',
+            $required: false,
+            props: {
+                isRange: true,
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, 'timePicker.props', [{type: 'switch', field: 'readonly'}, {
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'switch',
+            field: 'clearable',
+            value: true
+        }, {
+            type: 'Struct',
+            field: 'pickerOptions',
+            props: {defaultValue: {}}
+        }, {type: 'switch', field: 'editable', value: true}, {
+            type: 'input',
+            field: 'startPlaceholder'
+        }, {type: 'input', field: 'endPlaceholder'}, {
+            type: 'switch',
+            field: 'arrowControl'
+        }, {
+            type: 'select',
+            field: 'align',
+            options: localeOptions(t, [{label: 'left', value: 'left'}, {label: 'center', value: 'center'}, {
+                label: 'right',
+                value: 'right'
+            }])
+        }]);
+    }
+};

+ 59 - 0
src/components/form-create-designer/config/rule/transfer.js

@@ -0,0 +1,59 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils';
+
+const label = '穿梭框';
+const name = 'elTransfer';
+
+export default {
+    menu: 'main',
+    icon: 'icon-transfer',
+    label,
+    name,
+    input: true,
+    event: ['change', 'leftCheckChange', 'rightCheckChange'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.elTransfer.name'),
+            info: '',
+            $required: false,
+            props: {
+                data: makeTreeOptions(t('props.option'), {label: 'label', value: 'key'}, 1)
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.data', 'label', 'key'),
+            {type: 'switch', field: 'filterable'}, {
+                type: 'input',
+                field: 'filterPlaceholder'
+            }, {
+                type: 'select',
+                field: 'targetOrder',
+                warning: t('com.elTransfer.props.targetOrderInfo'),
+                options: [{label: 'original', value: 'original'}, {
+                    label: 'push',
+                    value: 'push'
+                }, {label: 'unshift', value: 'unshift'}]
+            }, {
+                type: 'TableOptions',
+                field: 'titles',
+                props: {
+                    column: [{label: t('props.value'), key: 'value'}],
+                    valueType: 'string',
+                    max: 2,
+                }
+            }, {
+                type: 'TableOptions',
+                field: 'buttonTexts',
+                props: {
+                    column: [{label: t('props.value'), key: 'value'}],
+                    valueType: 'string',
+                    max: 2,
+                }
+            }]);
+    }
+};

+ 70 - 0
src/components/form-create-designer/config/rule/tree.js

@@ -0,0 +1,70 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils/index';
+
+const label = '树形控件';
+const name = 'tree';
+
+export default {
+    menu: 'main',
+    icon: 'icon-tree',
+    label,
+    name,
+    input: true,
+    event: ['nodeClick', 'nodeContextmenu', 'checkChange', 'check', 'currentChange', 'nodeExpand', 'nodeCollapse', 'nodeDragStart', 'nodeDragEnter', 'nodeDragLeave', 'nodeDragOver', 'nodeDragEnd', 'nodeDrop'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.tree.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {
+                props: {
+                    label: 'label',
+                },
+                showCheckbox: true,
+                nodeKey: 'id',
+                data: makeTreeOptions(t('props.option'), {label: 'label', value: 'id'}, 3),
+            },
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.data', 'label', 'id'),
+            {type: 'input', field: 'emptyText'}, {
+                type: 'TableOptions',
+                field: 'props',
+                props: {
+                    column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                    valueType: 'object'
+                }
+            }, {
+                type: 'switch',
+                field: 'renderAfterExpand',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'defaultExpandAll',
+            }, {
+                type: 'switch',
+                field: 'expandOnClickNode',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'checkOnClickNode'
+            }, {type: 'switch', field: 'autoExpandParent', value: true}, {
+                type: 'switch',
+                field: 'checkStrictly'
+            }, {type: 'switch', field: 'accordion'}, {
+                type: 'inputNumber',
+                field: 'indent'
+            }, {
+                type: 'input',
+                field: 'nodeKey'
+            }]);
+    }
+};

+ 77 - 0
src/components/form-create-designer/config/rule/treeSelect.js

@@ -0,0 +1,77 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {localeProps, makeTreeOptions, makeTreeOptionsRule} from '../../utils/index';
+
+const label = '树形选择';
+const name = 'elTreeSelect';
+
+export default {
+    menu: 'main',
+    icon: 'icon-tree-select',
+    label,
+    name,
+    input: true,
+    event: ['change', 'visibleChange', 'removeTag', 'clear', 'blur', 'focus'],
+    validate: ['string', 'number', 'array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.elTreeSelect.name'),
+            info: '',
+            effect: {
+                fetch: ''
+            },
+            $required: false,
+            props: {
+                nodeKey: 'value',
+                showCheckbox: true,
+                data: makeTreeOptions(t('props.option'), {label: 'label', value: 'value'}, 3),
+            },
+        };
+    },
+    watch: {
+        multiple({rule}) {
+            rule.key = uniqueId();
+        }
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [
+            makeTreeOptionsRule(t, 'props.data', 'label', 'value'),
+            {type: 'switch', field: 'multiple'}, {
+                type: 'switch',
+                field: 'disabled'
+            }, {type: 'switch', field: 'clearable'}, {
+                type: 'switch',
+                field: 'collapseTags'
+            }, {
+                type: 'inputNumber',
+                field: 'multipleLimit',
+                props: {min: 0}
+            }, {type: 'input', field: 'placeholder'},
+            {
+                type: 'TableOptions',
+                field: 'props',
+                props: {
+                    column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                    valueType: 'object'
+                }
+            }, {
+                type: 'switch',
+                field: 'renderAfterExpand',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'defaultExpandAll',
+            }, {
+                type: 'switch',
+                field: 'expandOnClickNode',
+                value: true
+            }, {
+                type: 'switch',
+                field: 'checkOnClickNode'
+            }, {
+                type: 'input',
+                field: 'nodeKey'
+            }]);
+    }
+};

+ 98 - 0
src/components/form-create-designer/config/rule/upload.js

@@ -0,0 +1,98 @@
+import uniqueId from '@form-create/utils/lib/unique';
+import {getInjectArg, localeOptions, localeProps} from '../../utils';
+
+const label = '上传';
+const name = 'upload';
+
+export default {
+    menu: 'main',
+    icon: 'icon-upload',
+    label,
+    name,
+    input: true,
+    event: ['change', 'remove', 'preview', 'error', 'progress', 'exceed'],
+    languageKey: ['clickToUpload'],
+    validate: ['array'],
+    rule({t}) {
+        return {
+            type: name,
+            field: uniqueId(),
+            title: t('com.upload.name'),
+            info: '',
+            $required: false,
+            props: {
+                action: '/',
+                onSuccess: new Function('res', 'file', 'file.url = res.data.url;')
+            }
+        };
+    },
+    props(_, {t}) {
+        return localeProps(t, name + '.props', [{
+            type: 'switch',
+            field: 'disabled'
+        }, {
+            type: 'select',
+            field: 'listType',
+            options: localeOptions(t, [{label: 'text', value: 'text'}, {
+                label: 'picture',
+                value: 'picture'
+            }, {
+                label: 'picture-card',
+                value: 'picture-card'
+            }]),
+        }, {type: 'switch', field: 'multiple'}, {
+            type: 'input',
+            field: 'action'
+        }, {
+            type: 'FnInput',
+            field: 'beforeUpload',
+            props: {
+                args: ['file'],
+                name: 'beforeUpload',
+            }
+        }, {
+            type: 'FnInput',
+            field: 'beforeRemove',
+            props: {
+                body: true,
+                button: true,
+                fnx: true,
+                args: [getInjectArg(t)],
+                name: 'beforeRemove',
+            }
+        }, {
+            type: 'FnInput',
+            field: 'onSuccess',
+            warning: t('com.upload.info'),
+            props: {
+                args: ['res', 'file'],
+                name: 'onSuccess',
+            }
+        }, {
+            type: 'TableOptions',
+            field: 'headers',
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object'
+            }
+        }, {
+            type: 'TableOptions',
+            field: 'data',
+            props: {
+                column: [{label: t('props.key'), key: 'label'}, {label: t('props.value'), key: 'value'}],
+                valueType: 'object'
+            }
+        }, {type: 'input', field: 'name'}, {
+            type: 'switch',
+            field: 'withCredentials'
+        }, {type: 'input', field: 'accept'}, {
+            type: 'switch',
+            field: 'autoUpload',
+            value: true
+        }, {
+            type: 'inputNumber',
+            field: 'limit',
+            props: {min: 0},
+        }]);
+    }
+};

+ 136 - 0
src/components/form-create-designer/index.js

@@ -0,0 +1,136 @@
+import FcDesigner from './components/FcDesigner.vue';
+import DragTool from './components/DragTool.vue';
+import Struct from './components/Struct.vue';
+import Row from './components/Row.vue';
+import HtmlEditor from './components/HtmlEditor.vue';
+import FnEditor from './components/FnEditor.vue';
+import FnInput from './components/FnInput.vue';
+import FetchConfig from './components/FetchConfig.vue';
+import ConfigItem from './components/style/ConfigItem.vue';
+import FieldInput from './components/FieldInput.vue';
+import EventConfig from './components/EventConfig.vue';
+import FnConfig from './components/FnConfig.vue';
+import TableView from './components/table/TableView.vue';
+import Table from './components/table/Table.vue';
+import Validate from './components/Validate.vue';
+import DragBox from './components/DragBox.vue';
+import Required from './components/Required.vue';
+import TableOptions from './components/TableOptions.vue';
+import TreeOptions from './components/TreeOptions.vue';
+import TableFormView from './components/tableForm/TableFormView.vue';
+import TableForm from './components/tableForm/TableForm.vue';
+import TableFormColumnView from './components/tableForm/TableFormColumnView.vue';
+import SizeInput from './components/style/SizeInput.vue';
+import ColorInput from './components/style/ColorInput.vue';
+import StyleConfig from './components/style/StyleConfig.vue';
+import LanguageInput from './components/language/LanguageInput.vue';
+import ValueInput from './components/ValueInput.vue';
+import formCreate, {designerForm} from './utils/form';
+import FcEditor from '@form-create/component-wangeditor';
+import draggable from 'vuedraggable/src/vuedraggable';
+import {
+    compareVersion,
+    copyTextToClipboard,
+    getInjectArg,
+    localeOptions,
+    localeProps,
+    makeOptionsRule,
+    makeRequiredRule,
+    makeTreeOptions,
+    makeTreeOptionsRule,
+    toJSON
+} from './utils/index';
+import globalUseLocale, {t} from './utils/locale';
+import './style/index.css';
+import './style/icon.css';
+import './utils/highlight/style.css';
+
+/**FIXME:在node_modules引用此组件会导致无法加载,暂时排查不出来原因。
+ * 需要复制粘贴组件
+ * */
+const addComponent = (id, component, previewComponent) => {
+    designerForm.component(id, previewComponent || component);
+    formCreate.component(id, component);
+}
+
+designerForm.component('draggable', draggable);
+designerForm.component('DragTool', DragTool);
+designerForm.component('DragBox', DragBox);
+designerForm.component('Validate', Validate);
+designerForm.component('Struct', Struct);
+designerForm.component('HtmlEditor', HtmlEditor);
+designerForm.component('FetchConfig', FetchConfig);
+designerForm.component('FnEditor', FnEditor);
+designerForm.component('FnInput', FnInput);
+designerForm.component('Required', Required);
+designerForm.component('TableOptions', TableOptions);
+designerForm.component('TreeOptions', TreeOptions);
+designerForm.component('TableFormColumn', TableFormColumnView);
+designerForm.component('EventConfig', EventConfig);
+designerForm.component('ColorInput', ColorInput);
+designerForm.component('SizeInput', SizeInput);
+designerForm.component('StyleConfig', StyleConfig);
+designerForm.component('LanguageInput', LanguageInput);
+designerForm.component('ConfigItem', ConfigItem);
+designerForm.component('FieldInput', FieldInput);
+designerForm.component('FnConfig', FnConfig);
+designerForm.component('FcRow', Row);
+designerForm.component('ValueInput', ValueInput);
+addComponent('FcEditor', FcEditor);
+addComponent('TableForm', TableForm, TableFormView);
+addComponent('FcTable', Table, TableView);
+
+const install = function (Vue) {
+    Vue.component('FcDesigner', FcDesigner);
+};
+
+FcDesigner.install = install;
+FcDesigner.makeOptionsRule = makeOptionsRule;
+FcDesigner.copyTextToClipboard = copyTextToClipboard;
+FcDesigner.getInjectArg = getInjectArg;
+FcDesigner.localeOptions = localeOptions;
+FcDesigner.localeProps = localeProps;
+FcDesigner.makeRequiredRule = makeRequiredRule;
+FcDesigner.makeTreeOptions = makeTreeOptions;
+FcDesigner.makeTreeOptionsRule = makeTreeOptionsRule;
+FcDesigner.toJSON = toJSON;
+FcDesigner.formCreate = formCreate;
+FcDesigner.designerForm = designerForm;
+FcDesigner.component = addComponent;
+FcDesigner.useLocale = globalUseLocale;
+FcDesigner.t = t;
+
+FcDesigner.utils = {
+    copyTextToClipboard,
+    getInjectArg,
+    localeOptions,
+    localeProps,
+    makeOptionsRule,
+    makeRequiredRule,
+    makeTreeOptions,
+    makeTreeOptionsRule,
+    toJSON
+}
+
+const minVersion = '3.2.18';
+
+if (compareVersion(minVersion, formCreate.version) === 1) {
+    console.warn('Please use FormCreate version ' + minVersion + ' or greater, see https://github.com/xaboy/form-create.');
+}
+
+export default FcDesigner;
+
+export {
+    formCreate,
+    designerForm,
+    install,
+    copyTextToClipboard,
+    getInjectArg,
+    localeOptions,
+    localeProps,
+    makeOptionsRule,
+    makeRequiredRule,
+    makeTreeOptions,
+    makeTreeOptionsRule,
+    toJSON
+};

+ 867 - 0
src/components/form-create-designer/locale/en.js

@@ -0,0 +1,867 @@
+const En = {
+    name: 'en',
+    form: {
+        field: 'Field',
+        title: 'Title',
+        info: 'Info',
+        control: 'Control',
+        labelPosition: 'Label position',
+        labelStyle: 'Label style',
+        labelSuffix: 'Label suffix',
+        size: 'Form size',
+        event: 'Form event',
+        labelWidth: 'Label width',
+        hideRequiredAsterisk: 'Hide the red asterisk next to the label of a required field',
+        showMessage: 'Display verification error message',
+        inlineMessage: 'Display validation information inline',
+        submitBtn: 'Whether to display the form submit button',
+        resetBtn: 'Whether to display the form reset button',
+        appendChild: 'Insert child',
+        formMode: 'Form mode',
+        formName: 'Form name',
+        componentMode: 'Component',
+        htmlMode: 'HTML',
+        document: 'Document',
+        controlDocument: 'Need more detailed configuration methods? Please view {doc}',
+        onSubmit: 'Triggered when form is submitted',
+        onReset: 'Triggered after form is reset',
+        onCreated: 'Triggered after the form component is initialized',
+        onMounted: 'Triggered after the form component is mounted',
+        onReload: 'Triggered after the form rendering rule is reloaded',
+        onChange: 'Triggered when the component value changes',
+        beforeFetch: 'Triggered before remote data request is sent',
+    },
+    warning: {
+        name: 'Unique identifier for the component, used to access and modify its configuration rules.',
+        field: 'Field name for binding data to the component. Must start with a letter for proper recognition.',
+        fetch: 'Loads remote data through requests, updating the component based on the returned result.',
+        fetchQuery: 'Defines GET parameters for requests, passed via the URL.',
+        fetchData: 'Defines POST parameters for requests, passed in the request body.',
+        fetchDataType: 'Selects the data type for the request body to ensure correct format.',
+        fetchParse: 'Processes the response data after the request and converts it into the required structure.',
+        language: 'Manages multilingual data, allowing easy language switching for content display.',
+    },
+    computed: {
+        fieldUsed: '[{label}] Is used in the calculation formula, please modify the corresponding formula first',
+        fieldExist: '[{label}] Field already exists',
+        fieldEmpty: 'Field is required',
+        fieldChar: 'Field must begin with a letter',
+    },
+    validate: {
+        type: 'Type',
+        typePlaceholder: 'Please select',
+        trigger: 'Trigger',
+        mode: 'Verification method',
+        modes: {
+            required: 'required',
+            pattern: 'pattern',
+            validator: 'validator',
+            min: 'min',
+            max: 'max',
+            len: 'length',
+        },
+        types: {
+            string: 'String',
+            boolean: 'Boolean',
+            array: 'Multiple',
+            number: 'Number',
+            integer: 'Integer',
+            float: 'Float',
+            object: 'Collection',
+            date: 'Date',
+            url: 'Url',
+            email: 'Email',
+        },
+        message: 'Error',
+        auto: 'Automatic',
+        autoRequired: 'Please enter {title}',
+        autoMode: 'Please enter the correct {title}',
+        requiredPlaceholder: 'Please enter',
+        required: 'Is it required',
+        rule: 'Validation',
+    },
+    tableOptions: {
+        handle: 'Operation',
+        add: 'Add',
+        empty1: 'Click the lower right corner',
+        empty2: 'Button to add a column',
+        rmCol: 'Delete current column',
+        rmRow: 'Delete current row',
+        splitRow: 'Split into rows',
+        splitCol: 'Split into columns',
+        mergeBottom: 'Merge downward',
+        mergeRight: 'Merge right',
+        addTop: 'Add top column',
+        addBottom: 'Add the following',
+        addLeft: 'Add left column',
+        addRight: 'Add right column',
+        keyValue: 'key-value',
+    },
+    struct: {
+        title: 'Edit',
+        only: '[{label}] Only one allowed to be added',
+        errorMsg: 'The input content is syntactically incorrect',
+        configured: 'Configured',
+    },
+    event: {
+        title: 'Edit',
+        create: 'Create',
+        list: 'List',
+        placeholder: 'Please enter the name of the event',
+        saveMsg: 'Please save the event currently being edited',
+        type: 'Type',
+        info: 'Info',
+        label: 'Field',
+        inject: {
+            api: 'API of current form',
+            rule: 'Generate rules for the current form',
+            self: 'Component generation rule',
+            option: 'Form configuration',
+            args: 'Original parameters of event',
+        }
+    },
+    eventInfo: {
+        blur: 'Triggered when focus is lost',
+        focus: 'Triggered when focus is obtained',
+        change: 'Triggered when the binding value changes',
+        input: 'Trigger when value changes',
+        clear: 'Triggered when the clear button is clicked',
+        close: 'Triggered when the component is closed',
+        click: 'Fires when the component is clicked',
+        add: 'Trigger when added',
+        delete: 'Triggered when deleted',
+        visibleChange: 'Triggered when the drop-down box appears/hides',
+        calendarChange: 'Triggered when the selected date in the calendar changes',
+        panelChange: 'Fires when the date panel changes',
+        open: 'Triggered when opening',
+        opened: 'Triggered when opening animation ends',
+        closed: 'Triggered when closing animation ends',
+        openAutoFocus: 'Triggered when entering focus on content',
+        closeAutoFocus: 'Triggered when entering focus from content',
+        submit: 'Triggered when submitting table',
+        confirm: 'Triggered when clicking confirm',
+        validateFail: 'Triggered when table verification fails',
+        hook_load: 'Triggered after component rules are loaded',
+        hook_mounted: 'Triggered after component is mounted',
+        hook_deleted: 'Triggered after component rules are removed',
+        hook_watch: 'Triggered after component rules change',
+        hook_value: 'Triggered after component value changes',
+        hook_hidden: 'Triggered after component display status changes',
+    },
+    fetch: {
+        title: 'Set data',
+        create: 'Create data',
+        config: 'Request',
+        action: 'Action',
+        actionRequired: 'Please enter the correct link',
+        placeholder: 'Please enter the name of the data source',
+        method: 'Method',
+        data: 'Attached',
+        dataType: 'DataType',
+        headers: 'Headers',
+        query: 'Query',
+        parse: 'Processing',
+        response: 'Data returned by the interface',
+        onError: 'onError',
+        remote: 'Remote',
+        static: 'Static',
+        optionsType: {
+            fetch: 'Fetch',
+            struct: 'Static',
+        }
+    },
+    style: {
+        width: 'Width',
+        height: 'Height',
+        color: 'Color',
+        backgroundColor: 'Background color',
+        margin: 'Margin',
+        padding: 'Padding',
+        borderRadius: 'Border radius',
+        border: 'Border',
+        solid: 'Solid',
+        dashed: 'Dashed',
+        dotted: 'Dotted',
+        double: 'Double',
+        opacity: 'Opacity',
+        scale: 'Scale',
+        minWidth: 'Min Width',
+        minHeight: 'Min Height',
+        maxWidth: 'Max Width',
+        maxHeight: 'Max Height',
+        overflow: {
+            name: 'Overflow',
+            visible: 'Visible',
+            hidden: 'Hidden',
+            scroll: 'Scroll',
+            auto: 'Auto scroll after overflow',
+        },
+        shadow: {
+            name: 'Shadow',
+            x: 'x-axis offset',
+            y: 'y-axis offset',
+            vague: 'blurred radius',
+            extend: 'diffusion radius',
+            inset: 'inward',
+            external: 'outward',
+            mode: 'Mode',
+            classic: 'Classic',
+            flat: 'Flat',
+            solid: 'Stereoscopic',
+        },
+        font: {
+            name: 'Font',
+            size: 'Size',
+            align: 'Align',
+            height: 'line-height',
+            spacing: 'letter-spacing',
+            preview: 'Preview',
+        },
+        decoration: {
+            name: 'Decoration',
+            underline: 'underline',
+            'line-through': 'line-through',
+            overline: 'overline',
+        },
+        weight: {
+            name: 'font-weight',
+            300: 'Fine',
+            400: 'Default',
+            500: 'Medium',
+            700: 'Bold',
+        }
+    },
+    designer: {
+        component: 'Component',
+        id: 'Unique id',
+        name: 'Serial number',
+        type: 'Type',
+        form: 'Form',
+        json: 'Rule',
+        style: 'Style',
+        rule: 'Basis',
+        advanced: 'Advanced',
+        props: 'Props',
+        customProps: 'Custom props',
+        validate: 'Validate',
+        event: 'Event',
+        clearWarn: 'It cannot be restored after clearing it. Are you sure you want to clear it? ',
+        childEmpty: 'Click the \\e789  button in the lower right corner to add a column',
+        dragEmpty: 'Drag the components from the list on the left here',
+        unload: 'Are you sure you want to leave the current page?',
+        comList: 'Component',
+    },
+    language: {
+        name: 'Language',
+        add: 'Add',
+        batchRemove: 'Batch Deletion',
+        select: 'Select language',
+    },
+    menu: {
+        main: 'Basic',
+        aide: 'Auxiliary',
+        layout: 'Layout',
+        component: 'Component',
+        subform: 'Subform',
+        tree: 'Structure'
+    },
+    props: {
+        disabled: 'disabled',
+        time: 'time',
+        size: 'Size',
+        email: 'email',
+        number: 'number',
+        globalData: 'Global data',
+        mobile: 'Mobile',
+        pc: 'Pc',
+        reactive: 'Reactive',
+        title: 'Title',
+        content: 'Content',
+        collection: 'Collection',
+        group: 'Group',
+        custom: 'Custom',
+        change: 'Change',
+        blur: 'Blur',
+        preview: 'Preview',
+        clear: 'Clear',
+        cancel: 'Cancel',
+        close: 'Close',
+        ok: 'Ok',
+        save: 'Save',
+        refresh: 'Refresh',
+        submit: 'Submit',
+        reset: 'Reset',
+        copy: 'Copy',
+        delete: 'Delete',
+        hide: 'Hidden',
+        show: 'Show',
+        position: 'Position',
+        render: 'Render',
+        large: 'large',
+        default: 'default',
+        small: 'small',
+        always: 'always',
+        never: 'never',
+        hover: 'hover',
+        click: 'click',
+        button: 'button',
+        year: 'year',
+        month: 'month',
+        date: 'date',
+        dates: 'dates',
+        week: 'week',
+        datetime: 'datetime',
+        'datetime-local': 'datetime',
+        datetimerange: 'datetimerange',
+        daterange: 'daterange',
+        monthrange: 'monthrange',
+        left: 'left',
+        right: 'right',
+        top: 'top',
+        text: 'text',
+        picture: 'picture',
+        'picture-card': 'picture-card',
+        center: 'center',
+        vertical: 'vertical',
+        horizontal: 'horizontal',
+        manage: 'Manage',
+        key: 'key',
+        name: 'Name',
+        value: 'value',
+        inputData: 'Default value',
+        append: 'Append',
+        options: 'Options',
+        option: 'Option',
+        callback: 'Callback',
+        _self: 'Current Window',
+        _blank: 'New Window',
+        _parent: 'Parent Window',
+        _top: 'Top Window',
+    },
+    com: {
+        cascader: {
+            name: 'Cascader',
+            event: {
+                expandChange: 'Triggered when the expanded node changes',
+                removeTag: 'In multi-select mode, triggered when Tag is removed'
+            },
+            props: {
+                props: 'Options',
+                placeholder: 'Placeholder',
+                disabled: 'Disabled',
+                clearable: 'Whether clearing options are supported',
+                showAllLevels: 'Whether the full path of the selected value is displayed in the input box',
+                collapseTags: 'Whether to collapse Tags in multi-select mode',
+                collapseTagsTooltip: 'Whether to display all selected tags when the mouse hovers over the text of a collapsed tag',
+                separator: 'Separator',
+                filterable: 'Whether this option can be searched',
+                tagType: 'Type',
+            },
+            propsOpt: {
+                multiple: 'Whether there are multiple selections',
+                expandTrigger: 'How to expand the secondary menu',
+                checkStrictly: 'Whether it is strictly observed that parent and child nodes are not related to each other',
+                emitPath: 'When the selected node changes, whether to return an array consisting of the values of the menus at each level where the node is located',
+                value: 'The value of the specified option is an attribute value of the option object',
+                label: 'Specify the option label as a certain attribute value of the option object',
+                children: 'The child option of the specified option is a certain attribute value of the option object',
+                disabled: 'The specified option is disabled as a certain attribute value of the option object',
+                leaf: 'The flag bit of the leaf node of the specified option is an attribute value of the option object',
+            }
+        },
+        checkbox: {
+            name: 'Checkbox',
+            props: {
+                input: 'Whether to fill in',
+                type: 'Type',
+                disabled: 'Disabled',
+                min: 'Minimum number that can be checked',
+                max: 'The maximum number that can be checked',
+                textColor: 'Font color when the button is active',
+                fill: 'Border and background color when the button is active'
+            }
+        },
+        col: {
+            name: 'Col',
+            props: {
+                span: 'Number of columns occupied by grid',
+                offset: 'Number of spaces on the left side of the grid',
+                push: 'Move the grid to the right by the number of cells',
+                pull: 'Move the grid to the left by the number of cells'
+            }
+        },
+        colorPicker: {
+            name: 'ColorPicker',
+            event: {
+                activeChange: 'Triggered when the color currently displayed in the panel changes'
+            },
+            props: {
+                disabled: 'Disabled',
+                showAlpha: 'Whether transparency selection is supported',
+                colorFormat: 'Color format',
+                predefine: 'Predefined color',
+            }
+        },
+        datePicker: {
+            name: 'Date',
+            props: {
+                pickerOptions: 'Options specific to the current time and date picker',
+                readonly: 'Readonly',
+                disabled: 'Disabled',
+                type: 'Type',
+                editable: 'Text box can be input',
+                clearable: 'Whether to display the clear button',
+                placeholder: 'Placeholder content for non-range selection',
+                startPlaceholder: 'Placeholder content for the start date when selecting the range',
+                endPlaceholder: 'Placeholder content for the end date when selecting a range',
+                format: 'Format displayed in the input box',
+                align: 'Alignment',
+                rangeSeparator: 'Separator when selecting range',
+                unlinkPanels: 'Unlink the two date panels in the range selector',
+            }
+        },
+        dateRange: {
+            name: 'DateRange',
+        },
+        timeRange: {
+            name: 'TimeRange',
+        },
+        elAlert: {
+            name: 'Alert',
+            description: 'Description',
+            props: {
+                title: 'Title',
+                type: 'Type',
+                description: 'Supporting text',
+                closable: 'Whether it can be closed',
+                center: 'Whether the text is centered',
+                closeText: 'Close button custom text',
+                showIcon: 'Whether to display the icon',
+                effect: 'Select a provided theme'
+            }
+        },
+        elButton: {
+            name: 'Button',
+            props: {
+                formCreateChild: 'Content',
+                size: 'Size',
+                type: 'Type',
+                plain: 'Whether the button is plain',
+                round: 'Whether the button has rounded corners',
+                circle: 'Whether the button is round',
+                loading: 'Whether it is loading status',
+                disabled: 'Disabled',
+            }
+        },
+        elCard: {
+            name: 'Card',
+            props: {
+                header: 'Title',
+                shadow: 'Shadow display timing',
+            }
+        },
+        elCollapse: {
+            name: 'Collapse',
+            event: {
+                change: 'Switch the currently active panel, its type is string in accordion mode and array in other modes',
+            },
+            props: {
+                accordion: 'Whether it is in accordion mode'
+            }
+        },
+        elCollapseItem: {
+            name: 'CollapseItem',
+            props: {
+                title: 'Panel title',
+                name: 'Identifier',
+                disabled: 'Disabled',
+            }
+        },
+        elDivider: {
+            name: 'Divider',
+            props: {
+                formCreateChild: 'Set Content',
+                contentPosition: 'Set content position'
+            }
+        },
+        elTabPane: {
+            name: 'TabPane',
+            props: {
+                label: 'Title',
+                disabled: 'Disabled',
+                name: 'Identifier of the tab',
+                lazy: 'Whether the label is delayed in rendering'
+            }
+        },
+        elTabs: {
+            name: 'Tabs',
+            event: {
+                tabClick: 'Triggered when tab is selected',
+                tabChange: 'Triggered when activeName changes',
+                tabRemove: 'Triggered when the tab remove button is clicked',
+                tabAdd: 'Triggered when a new tab button is clicked',
+                edit: 'Triggered after clicking the add or remove button of the tab',
+            },
+            props: {
+                type: 'Type',
+                closable: 'Whether the label can be closed',
+                tabPosition: 'Tab position',
+                stretch: 'Whether the width of the label is self-stretching'
+            }
+        },
+        elTag: {
+            name: 'Tag',
+            props: {
+                formCreateNative: 'Whether to display title',
+                formCreateTitle: 'Title',
+                formCreateChild: 'Content',
+                type: 'Type',
+                size: 'Label size',
+                effect: 'Label theme',
+                closable: 'Whether it can be closed',
+                disableTransitions: 'Whether to disable gradient animation',
+                hit: 'Whether there is a border stroke',
+                round: 'Whether it is round',
+                color: 'Background color'
+            }
+        },
+        elTransfer: {
+            name: 'Transfer',
+            event: {
+                leftCheckChange: 'Triggered when the left list element is selected/unselected by the user',
+                rightCheckChange: 'Triggered when the right list element is selected/unselected by the user'
+            },
+            props: {
+                filterable: 'Is it searchable',
+                filterPlaceholder: 'Search box placeholder',
+                targetOrder: 'Sort strategy of list elements on the right',
+                targetOrderInfo: 'If it is original, keep the same order as the data; if it is push, the newly added elements will be ranked last; if it is unshift, the newly added elements will be ranked first',
+                titles: 'Title',
+                buttonTexts: 'Set button content',
+                props: 'Field alias of data source'
+            }
+        },
+        elTreeSelect: {
+            name: 'TreeSelect',
+            event: {
+                removeTag: 'Triggered when tag is removed in multi-select mode'
+            },
+            props: {
+                multiple: 'Whether there are multiple selections',
+                disabled: 'Disabled',
+                clearable: 'Whether the option can be cleared',
+                collapseTags: 'Whether to display the selected value as text during multi-selection',
+                multipleLimit: 'The maximum number of items that the user can select during multiple selection, if it is 0, there is no limit',
+                placeholder: 'Placeholder',
+                props: 'Options',
+                renderAfterExpand: 'Whether to render its child nodes after expanding a tree node for the first time',
+                defaultExpandAll: 'Whether to expand all nodes by default',
+                expandOnClickNode: 'Whether to expand or shrink nodes when clicking on them',
+                checkOnClickNode: 'Whether to select the node when clicking the node',
+                nodeKey: 'Each tree node is used as an attribute for unique identification, and the entire tree should be unique'
+            }
+        },
+        elImage: {
+            name: 'Image',
+            props: {
+                src: 'Image path'
+            }
+        },
+        fcEditor: {
+            name: 'Editor',
+            props: {
+                disabled: 'Disabled'
+            }
+        },
+        fcRow: {
+            name: 'Row',
+            props: {
+                gutter: 'Grid interval',
+                type: 'Flex layout mode',
+                justify: 'Horizontal arrangement under flex layout',
+                align: 'Vertical arrangement under flex layout'
+            }
+        },
+        fcTable: {
+            name: 'Table',
+            props: {
+                border: 'Whether to display border',
+                borderColor: 'Border color',
+                borderWidth: 'Border width'
+            }
+        },
+        fcTableGrid: {
+            name: 'Grid',
+        },
+        group: {
+            name: 'Subform',
+            props: {
+                disabled: 'Disabled',
+                syncDisabled: 'Whether to force synchronization of the disabled state with the subform',
+                expand: 'Set the default expansion items',
+                button: 'Whether to display the operation button',
+                sortBtn: 'Whether to display the sort button',
+                min: 'Set the minimum number of items to add',
+                max: 'Set the maximum number of items to add',
+            }
+        },
+        html: {
+            name: 'HTML',
+            props: {
+                formCreateNative: 'Whether to display title',
+                formCreateTitle: 'Title',
+                formCreateChild: 'Content',
+            }
+        },
+        input: {
+            name: 'Input',
+            event: {
+                change: 'Triggered when the value changes, when the component loses focus or the user presses Enter',
+            },
+            props: {
+                type: 'Type',
+                maxlength: 'Maximum input length',
+                minlength: 'Minimum input length',
+                placeholder: 'Placeholder',
+                clearable: 'Whether to display the clear button',
+                disabled: 'Disabled',
+                readonly: 'Readonly',
+            }
+        },
+        inputNumber: {
+            name: 'InputNumber',
+            props: {
+                precision: 'Precision of input value',
+                min: 'Set the minimum value allowed for the counter',
+                max: 'Set the maximum allowed value of the counter',
+                step: 'Step',
+                stepStrictly: 'Whether only multiples of step can be entered',
+                disabled: 'Disabled',
+                controls: 'Whether to use control buttons',
+                controlsPosition: 'Control button position',
+                placeholder: 'Placeholder'
+            }
+        },
+        password: {
+            name: 'Password',
+            event: {
+                change: 'Triggered when the value changes, when the component loses focus or the user presses Enter',
+            },
+            props: {
+                disabled: 'Disabled',
+                readonly: 'Readonly',
+                maxlength: 'Maximum input length',
+                minlength: 'Minimum input length',
+                placeholder: 'Placeholder',
+                clearable: 'Whether to display the clear button'
+            }
+        },
+        radio: {
+            name: 'Radio',
+            props: {
+                input: 'Whether to fill in',
+                disabled: 'Disabled',
+                type: 'Type',
+                textColor: 'Text color when button form is activated',
+                fill: 'Fill color and border color when the button form is activated'
+            }
+        },
+        rate: {
+            name: 'Rate',
+            props: {
+                max: 'Maximum score',
+                disabled: 'Disabled',
+                allowHalf: 'Whether to allow half selection',
+                voidColor: 'Color of the icon when not selected',
+                disabledVoidColor: 'The color of the icon when it is not selected when read-only',
+                voidIconClass: 'Class name of the icon when not selected',
+                disabledVoidIconClass: 'The class name of the icon when it is not selected when read-only',
+                showScore: 'Whether to display the current score',
+                textColor: 'Color of auxiliary text',
+                scoreTemplate: 'Score display template'
+            }
+        },
+        select: {
+            name: 'Select',
+            event: {
+                removeTag: 'Triggered when tag is removed in multi-select mode'
+            },
+            props: {
+                multiple: 'Whether there are multiple selections',
+                disabled: 'Disabled',
+                clearable: 'Whether the option can be cleared',
+                collapseTags: 'Whether to display the selected value as text during multi-selection',
+                multipleLimit: 'The maximum number of items that the user can select when multiple-selecting, if it is 0, there is no limit',
+                placeholder: 'Placeholder',
+                filterable: 'Is it searchable',
+                allowCreate: 'Whether users are allowed to create new entries',
+                noMatchText: 'Text displayed when no search conditions match',
+                noDataText: 'Text displayed when option is empty',
+                reserveKeyword: 'When multiple selections are searchable, whether to retain the current search keyword after selecting an option',
+                defaultFirstOption: 'Press Enter in the input box and select the first matching item',
+                remote: 'Whether the options are loaded remotely from the server',
+                remoteMethod: 'Custom remote search methods',
+            }
+        },
+        slider: {
+            name: 'Slider',
+            props: {
+                min: 'Minimum value',
+                max: 'Maximum value',
+                disabled: 'Disabled',
+                step: 'Step',
+                showInput: 'Whether to display the input box, it is only valid during non-range selection',
+                showInputControls: 'Whether to display the control buttons of the input box when the input box is displayed',
+                showStops: 'Whether to display discontinuities',
+                range: 'Whether it is a range selection',
+                vertical: 'Whether portrait mode',
+                height: 'Slider height, required in portrait mode'
+            }
+        },
+        space: {
+            name: 'Space',
+            props: {
+                height: 'Height',
+            }
+        },
+        subForm: {
+            name: 'Group',
+            props: {
+                disabled: 'Disabled',
+                syncDisabled: 'Whether to force synchronization of the disabled state with the subform'
+            }
+        },
+        switch: {
+            name: 'Switch',
+            props: {
+                disabled: 'Disabled',
+                width: 'Width (px)',
+                activeText: 'Text description when opening',
+                inactiveText: 'Text description when closing',
+                activeValue: 'Value when opening',
+                inactiveValue: 'Value when closed',
+                activeColor: 'Background color when opening',
+                inactiveColor: 'Background color when closed'
+            }
+        },
+        tableForm: {
+            name: 'TableForm',
+            props: {
+                disabled: 'Disabled',
+                filterEmptyColumn: 'Whether to filter empty rows',
+                max: 'Maximum number of rows to add, if 0, there is no limit',
+            }
+        },
+        tableFormColumn: {
+            name: 'TableFormColumn',
+            label: 'TableFormColumn',
+            props: {
+                label: 'Title',
+                width: 'Width',
+                color: 'Color',
+                required: 'Whether to display required asterisks',
+            }
+        },
+        text: {
+            name: 'Text',
+            props: {
+                formCreateNative: 'Whether to display title',
+                formCreateTitle: 'Title',
+                formCreateChild: 'Content'
+            }
+        },
+        textarea: {
+            name: 'Textarea',
+            event: {
+                change: 'Triggered when the value changes, when the component loses focus or the user presses Enter',
+            },
+            props: {
+                disabled: 'Disabled',
+                readonly: 'Readonly',
+                maxlength: 'Maximum input length',
+                minlength: 'Minimum input length',
+                showWordLimit: 'Whether to display word count statistics',
+                placeholder: 'Placeholder',
+                rows: 'Number of input box rows',
+                autosize: 'Whether the height is adaptive'
+            }
+        },
+        timePicker: {
+            name: 'Time',
+            props: {
+                pickerOptions: 'Options specific to the current time and date picker',
+                readonly: 'Readonly',
+                disabled: 'Disabled',
+                editable: 'Text box can be input',
+                clearable: 'Whether to display the clear button',
+                placeholder: 'Placeholder content for non-range selection',
+                startPlaceholder: 'Placeholder content for the start date when selecting the range',
+                endPlaceholder: 'Placeholder content for the start date when selecting the range',
+                isRange: 'Whether to select a time range',
+                arrowControl: 'Whether to use arrows for time selection',
+                align: 'Align'
+            }
+        },
+        tree: {
+            name: 'Tree',
+            event: {
+                nodeClick: 'Triggered when the node is clicked',
+                nodeContextmenu: 'This event will be triggered when a node is right-clicked',
+                checkChange: 'Triggered when the check box is clicked',
+                check: 'Triggered after clicking the node checkbox',
+                currentChange: 'Event triggered when the currently selected node changes',
+                nodeExpand: 'Event triggered when a node is expanded',
+                nodeCollapse: 'Event triggered when a node is closed',
+                nodeDragStart: 'Event triggered when a node starts dragging',
+                nodeDragEnter: 'Event triggered when dragging into other nodes',
+                nodeDragLeave: 'Event triggered when dragging leaves a node',
+                nodeDragOver: 'Event triggered when dragging a node',
+                nodeDragEnd: 'Event triggered when drag ends',
+                nodeDrop: 'Event triggered when drag and drop is successfully completed'
+            },
+            props: {
+                emptyText: 'Text displayed when the content is empty',
+                props: 'Options',
+                renderAfterExpand: 'Whether to render its child nodes after expanding a tree node for the first time',
+                defaultExpandAll: 'Whether to expand all nodes by default',
+                expandOnClickNode: 'Whether to expand or contract the node when clicking the node, if it is false, the node will only be expanded or contracted when the arrow icon is clicked. ',
+                checkOnClickNode: 'Whether to select the node when clicking the node',
+                autoExpandParent: 'Whether to automatically expand the parent node when expanding the child node',
+                checkStrictly: 'When the check box is displayed, whether the parent and child are strictly not related to each other should be strictly followed',
+                accordion: 'Whether to open only one sibling tree node for expansion at a time',
+                indent: 'Horizontal indent (px) between adjacent level nodes',
+                nodeKey: 'Each tree node is used as an attribute for unique identification, and the entire tree should be unique'
+            }
+        },
+        upload: {
+            name: 'Upload',
+            info: 'After a successful upload, assign the returned URL to file.url or the result to file.value for use in subsequent form submissions.',
+            event: {
+                remove: 'Triggered when a file is removed from the file list',
+                preview: 'Triggered when clicking an uploaded file in the file list',
+                error: 'Triggered when file upload fails',
+                progress: 'Triggered when file is uploaded',
+                exceed: 'Triggered when the limit is exceeded'
+            },
+            props: {
+                listType: 'Upload type',
+                multiple: 'Whether multiple selection of files is supported',
+                action: 'Upload address (required)',
+                beforeUpload: 'Triggered before uploading a file',
+                onSuccess: 'Triggered when the upload is successful',
+                beforeRemove: 'Triggered before deleting a file',
+                headers: 'Set upload request headers',
+                data: 'Extra parameters attached when uploading',
+                name: 'Uploaded file field name',
+                withCredentials: 'Support sending cookie credential information',
+                accept: 'Accept uploaded file types',
+                autoUpload: 'Whether to upload the file immediately after selecting it',
+                disabled: 'Disabled',
+                limit: 'Maximum number of uploads allowed'
+            }
+        }
+    },
+};
+
+export default En;
+

+ 868 - 0
src/components/form-create-designer/locale/zh-cn.js

@@ -0,0 +1,868 @@
+const ZhCn = {
+    name: 'zh-cn',
+    form: {
+        field: '字段 ID',
+        title: '字段名称',
+        info: '提示信息',
+        control: '联动数据',
+        labelPosition: '标签的位置',
+        labelStyle: '标签的样式',
+        labelSuffix: '标签的后缀',
+        size: '表单的尺寸',
+        event: '表单事件',
+        labelWidth: '标签的宽度',
+        hideRequiredAsterisk: '隐藏必填字段的标签旁边的红色星号',
+        showMessage: '显示校验错误信息',
+        inlineMessage: '以行内形式展示校验信息',
+        submitBtn: '是否显示表单提交按钮',
+        resetBtn: '是否显示表单重置按钮',
+        appendChild: '添加子级',
+        formMode: '表单模式',
+        formName: '表单名称',
+        componentMode: '生成组件',
+        htmlMode: '生成HTML',
+        document: '帮助文档',
+        controlDocument: '需要更详细的配置方法?请查看{doc}',
+        onSubmit: '表单提交时触发',
+        onReset: '表单重置后触发',
+        onCreated: '表单组件初始化完毕后触发',
+        onMounted: '表单组件渲染完毕后触发',
+        onReload: '表单渲染规则重载后触发',
+        onChange: '表单组件的值发生变化时触发',
+        beforeFetch: '远程数据请求发送前触发',
+    },
+    warning: {
+        name: '组件的唯一标识,用于获取和修改该组件的配置规则。通过该标识可以精确定位组件,实现对组件属性和行为的控制。',
+        field: '组件对应的字段名用于与组件的数据进行绑定。字段名需以字母开头,以确保能够正确识别。',
+        fetch: '远程数据通过远程请求加载组件的配置项。配置请求参数后,组件会自动发起请求,获取远程数据并根据返回的结果更新组件。',
+        fetchQuery: '定义请求的 GET 参数,通过 URL 传递数据。',
+        fetchData: '定义请求的 POST 参数,通过请求体传递数据。',
+        fetchDataType: '选择请求体的数据类型,确保数据格式正确。',
+        fetchParse: '请求返回后,可以通过处理函数对返回的结果进行处理,将结果转换为组件所需的数据和结构。',
+        language: '管理页面的多语言数据,在组件中配置不同语言的文本,支持一键切换语言体系,便于在多语言环境下使用和展示内容。',
+    },
+    computed: {
+        fieldUsed: '【{label}】在计算公式中被使用,请先修改对应公式',
+        fieldExist: '【{label}】字段已存在',
+        fieldEmpty: '字段名称不能为空',
+        fieldChar: '字段名称必须以字母开头',
+    },
+    validate: {
+        type: '字段类型',
+        typePlaceholder: '请选择',
+        trigger: '触发方式',
+        mode: '验证方式',
+        modes: {
+            required: '必填',
+            pattern: '正则表达式',
+            validator: '自定义验证',
+            min: '最小值',
+            max: '最大值',
+            len: '长度',
+        },
+        types: {
+            string: '文本',
+            boolean: '布尔',
+            array: '多选',
+            number: '数字',
+            integer: '整数',
+            float: '小数',
+            object: '合集',
+            date: '日期',
+            url: 'URL链接',
+            email: '邮箱地址',
+        },
+        message: '错误信息',
+        auto: '自动获取',
+        autoRequired: '请输入{title}',
+        autoMode: '请输入正确的{title}',
+        requiredPlaceholder: '请输入提示语',
+        required: '是否必填',
+        rule: '验证规则',
+    },
+    tableOptions: {
+        handle: '操作',
+        add: '添加',
+        empty1: '点击右下角',
+        empty2: '按钮添加一列',
+        rmCol: '删除当前列',
+        rmRow: '删除当前行',
+        splitRow: '拆分成行',
+        splitCol: '拆分成列',
+        mergeBottom: '向下合并',
+        mergeRight: '向右合并',
+        addTop: '添加上列',
+        addBottom: '添加下列',
+        addLeft: '添加左列',
+        addRight: '添加右列',
+        keyValue: '键值对',
+    },
+    struct: {
+        title: '编辑数据',
+        only: '【{label}】只允许添加一个',
+        errorMsg: '输入的内容语法错误',
+        configured: '已配置',
+    },
+    event: {
+        title: '设置事件',
+        create: '创建事件',
+        list: '事件列表',
+        placeholder: '请输入事件的名称',
+        saveMsg: '请先保存当前正在编辑的事件',
+        type: '类型',
+        info: '说明',
+        label: '字段',
+        inject: {
+            api: '当前表单的api',
+            rule: '当前表单的生成规则',
+            self: '组件的生成规则',
+            option: '表单的配置',
+            args: '事件的原始参数',
+        }
+    },
+    eventInfo: {
+        blur: '失去焦点时触发',
+        focus: '获得焦点时触发',
+        change: '当绑定值变化时触发',
+        input: '在值改变时触发',
+        clear: '在点击清空按钮时触发',
+        close: '关闭组件时触发',
+        click: '点击组件时触发',
+        add: '增加时触发',
+        delete: '删除时触发',
+        visibleChange: '下拉框出现/隐藏时触发',
+        calendarChange: '在日历所选日期更改时触发',
+        panelChange: '当日期面板改变时触发',
+        open: '打开的回调',
+        opened: '打开动画结束时的回调',
+        closed: '关闭动画结束时的回调',
+        openAutoFocus: '输入焦点聚焦在内容时的回调',
+        closeAutoFocus: '输入焦点从内容失焦时的回调',
+        submit: '表单提交时触发',
+        confirm: '点击确认按钮时触发',
+        validateFail: '表单验证失败时触发',
+        hook_load: '组件规则加载后触发',
+        hook_mounted: '组件挂载后触发',
+        hook_deleted: '组件规则被移除后触发',
+        hook_watch: '组件规则发生变化后触发',
+        hook_value: '组件的值发生变化后触发',
+        hook_hidden: '组件显示状态发生变化后触发',
+    },
+    fetch: {
+        title: '设置数据源',
+        create: '创建数据源',
+        config: '请求配置',
+        action: '请求链接',
+        actionRequired: '请输入正确的链接',
+        placeholder: '请输入数据源的名称',
+        method: '请求方式',
+        data: '附带数据',
+        dataType: '数据类型',
+        headers: '请求头部',
+        query: '请求参数',
+        parse: '数据处理',
+        response: '接口返回的数据',
+        onError: '错误处理',
+        remote: '远程数据',
+        static: '静态数据',
+        optionsType: {
+            fetch: '远程数据',
+            struct: '静态数据',
+        }
+    },
+    style: {
+        width: '宽度',
+        height: '高度',
+        color: '颜色',
+        backgroundColor: '背景色',
+        margin: '外边距',
+        padding: '内边距',
+        borderRadius: '圆角',
+        border: '边框',
+        solid: '实线',
+        dashed: '虚线',
+        dotted: '点状虚线',
+        double: '双实线',
+        opacity: '透明度',
+        scale: '缩放',
+        minWidth: '最小宽',
+        minHeight: '最小高',
+        maxWidth: '最大宽',
+        maxHeight: '最大高',
+        overflow: {
+            name: '溢出',
+            visible: '可见',
+            hidden: '隐藏',
+            scroll: '滚动',
+            auto: '溢出后自动滚动',
+        },
+        shadow: {
+            name: '阴影',
+            x: 'x轴偏移量',
+            y: 'y轴偏移量',
+            vague: '模糊半径',
+            extend: '扩散半径',
+            inset: '向内',
+            external: '向外',
+            mode: '模式',
+            classic: '经典',
+            flat: '扁平',
+            solid: '立体',
+        },
+        font: {
+            name: '字体',
+            size: '大小',
+            align: '对齐方式',
+            height: '行高',
+            spacing: '字间距',
+            preview: '样式预览',
+        },
+        decoration: {
+            name: '修饰',
+            underline: '下划线',
+            'line-through': '删除线',
+            overline: '上划线',
+        },
+        weight: {
+            name: '粗细',
+            300: '细体',
+            400: '常规体',
+            500: '中黑体',
+            700: '中粗体',
+        }
+    },
+    designer: {
+        component: '组件配置',
+        id: '唯一值',
+        name: '编号',
+        type: '组件类型',
+        form: '表单配置',
+        json: '渲染规则',
+        style: '组件样式配置',
+        rule: '基础配置',
+        advanced: '高级配置',
+        props: '属性配置',
+        customProps: '自定义属性配置',
+        validate: '验证配置',
+        event: '事件配置',
+        clearWarn: '清空后将不能恢复,确定要清空吗?',
+        childEmpty: '点击右下角 \\e789  按钮添加一列',
+        dragEmpty: '拖拽左侧列表中的组件到此处',
+        unload: '确定离开当前页面吗?',
+        comList: '组件列表',
+    },
+    language: {
+        name: '国际化配置',
+        add: '新增词条',
+        batchRemove: '批量删除',
+        select: '选择多语言',
+    },
+    menu: {
+        main: '基础组件',
+        aide: '辅助组件',
+        layout: '布局组件',
+        component: '组件',
+        subform: '子表单组件',
+        tree: '大纲'
+    },
+    props: {
+        disabled: '禁用',
+        time: '时间',
+        size: '尺寸',
+        email: '邮箱',
+        number: '数字',
+        globalData: '全局数据',
+        mobile: '移动端',
+        reactive: '响应式',
+        pc: '电脑端',
+        title: '标题',
+        content: '内容',
+        collection: '合集',
+        group: '分组',
+        custom: '自定义',
+        change: '改变',
+        blur: '失去焦点',
+        preview: '预览',
+        clear: '清空',
+        cancel: '取消',
+        close: '关闭',
+        ok: '确定',
+        save: '保存',
+        refresh: '刷新',
+        submit: '提交',
+        reset: '重置',
+        copy: '复制',
+        delete: '删除',
+        hide: '隐藏',
+        show: '显示',
+        position: '位置',
+        render: '渲染',
+        large: '大',
+        default: '默认',
+        small: '小',
+        always: '常显',
+        never: '不显示',
+        hover: '悬浮',
+        click: '点击',
+        button: '按钮',
+        year: '年份',
+        month: '月份',
+        date: '日期',
+        dates: '日期多选',
+        week: '一周',
+        datetime: '日期时间',
+        'datetime-local': '日期时间',
+        datetimerange: '日期时间区间',
+        daterange: '日期区间',
+        monthrange: '月份区间',
+        left: '左对齐',
+        right: '右对齐',
+        top: '顶部',
+        text: '文字',
+        picture: '图片',
+        'picture-card': '卡片',
+        center: '居中',
+        vertical: '竖向',
+        horizontal: '横向',
+        manage: '管理',
+        key: '键名',
+        name: '名称',
+        value: '值',
+        inputData: '默认值',
+        append: '插入',
+        options: '选项数据',
+        option: '选项',
+        callback: '回调',
+        _self: '当前窗口',
+        _blank: '新的窗口',
+        _parent: '父级窗口',
+        _top: '顶级窗口',
+    },
+    com: {
+        cascader: {
+            name: '级联选择器',
+            event: {
+                expandChange: '当展开节点发生变化时触发',
+                removeTag: '在多选模式下,移除Tag时触发'
+            },
+            props: {
+                props: '配置选项',
+                placeholder: '输入框占位文本',
+                disabled: '是否禁用',
+                clearable: '是否支持清空选项',
+                showAllLevels: '输入框中是否显示选中值的完整路径',
+                collapseTags: '多选模式下是否折叠Tag',
+                collapseTagsTooltip: '当鼠标悬停于折叠标签的文本时,是否显示所有选中的标签',
+                separator: '选项分隔符',
+                filterable: '该选项是否可以被搜索',
+                tagType: '标签类型',
+            },
+            propsOpt: {
+                multiple: '是否多选',
+                expandTrigger: '次级菜单的展开方式',
+                checkStrictly: '是否严格的遵守父子节点不互相关联',
+                emitPath: '在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组',
+                value: '指定选项的值为选项对象的某个属性值',
+                label: '指定选项标签为选项对象的某个属性值',
+                children: '指定选项的子选项为选项对象的某个属性值',
+                disabled: '指定选项的禁用为选项对象的某个属性值',
+                leaf: '指定选项的叶子节点的标志位为选项对象的某个属性值',
+            }
+        },
+        checkbox: {
+            name: '多选框',
+            props: {
+                input: '是否可以填写',
+                type: '按钮类型',
+                disabled: '是否禁用',
+                min: '可被勾选的最小数量',
+                max: '可被勾选的最大数量',
+                textColor: '当按钮为活跃状态时的字体颜色',
+                fill: '当按钮为活跃状态时的边框和背景颜色'
+            }
+        },
+        col: {
+            name: '布局格子',
+            props: {
+                span: '栅格占据的列数',
+                offset: '栅格左侧的间隔格数',
+                push: '栅格向右移动格数',
+                pull: '栅格向左移动格数'
+            }
+        },
+        colorPicker: {
+            name: '颜色选择器',
+            event: {
+                activeChange: '面板中当前显示的颜色发生改变时触发'
+            },
+            props: {
+                disabled: '是否禁用',
+                showAlpha: '是否支持透明度选择',
+                colorFormat: '颜色的格式',
+                predefine: '预定义颜色',
+            }
+        },
+        datePicker: {
+            name: '日期',
+            props: {
+                pickerOptions: '当前时间日期选择器特有的选项',
+                readonly: '完全只读',
+                disabled: '禁用',
+                type: '显示类型',
+                editable: '文本框可输入',
+                clearable: '是否显示清除按钮',
+                placeholder: '非范围选择时的占位内容',
+                startPlaceholder: '范围选择时开始日期的占位内容',
+                endPlaceholder: '范围选择时结束日期的占位内容',
+                format: '显示在输入框中的格式',
+                align: '对齐方式',
+                rangeSeparator: '选择范围时的分隔符',
+                unlinkPanels: '在范围选择器里取消两个日期面板之间的联动',
+            }
+        },
+        dateRange: {
+            name: '日期区间',
+        },
+        timeRange: {
+            name: '时间区间',
+        },
+        elAlert: {
+            name: '提示',
+            description: '说明文字',
+            props: {
+                title: '标题',
+                type: '主题',
+                description: '辅助性文字',
+                closable: '是否可关闭',
+                center: '文字是否居中',
+                closeText: '关闭按钮自定义文本',
+                showIcon: '是否显示图标',
+                effect: '选择提供的主题'
+            }
+        },
+        elButton: {
+            name: '按钮',
+            props: {
+                formCreateChild: '内容',
+                size: '尺寸',
+                type: '类型',
+                plain: '是否朴素按钮',
+                round: '是否圆角按钮',
+                circle: '是否圆形按钮',
+                loading: '是否加载中状态',
+                disabled: '是否禁用状态',
+            }
+        },
+        elCard: {
+            name: '卡片',
+            props: {
+                header: '标题',
+                shadow: '阴影显示时机',
+            }
+        },
+        elCollapse: {
+            name: '折叠面板',
+            event: {
+                change: '切换当前活动面板,在手风琴模式下其类型是string,在其他模式下是array',
+            },
+            props: {
+                accordion: '是否手风琴模式'
+            }
+        },
+        elCollapseItem: {
+            name: '面板',
+            props: {
+                title: '面板标题',
+                name: '唯一标志符',
+                disabled: '是否禁用',
+            }
+        },
+        elDivider: {
+            name: '分割线',
+            props: {
+                formCreateChild: '设置分割线文案',
+                contentPosition: '设置分割线文案的位置'
+            }
+        },
+        elTabPane: {
+            name: '选项卡',
+            props: {
+                label: '选项卡标题',
+                disabled: '是否禁用',
+                name: '选项卡的标识符',
+                lazy: '标签是否延迟渲染'
+            }
+        },
+        elTabs: {
+            name: '标签页',
+            event: {
+                tabClick: 'tab 被选中时触发',
+                tabChange: 'activeName 改变时触发',
+                tabRemove: '点击 tab 移除按钮时触发',
+                tabAdd: '点击 tab 新增按钮时触发',
+                edit: '点击 tab 的新增或移除按钮后触发',
+            },
+            props: {
+                type: '风格类型',
+                closable: '标签是否可关闭',
+                tabPosition: '选项卡所在位置',
+                stretch: '标签的宽度是否自撑开'
+            }
+        },
+        elTag: {
+            name: '标签',
+            props: {
+                formCreateNative: '是否显示标题',
+                formCreateTitle: '标题',
+                formCreateChild: '标签内容',
+                type: '标签的类型',
+                size: '标签的尺寸',
+                effect: '标签的主题',
+                closable: '是否可关闭',
+                disableTransitions: '是否禁用渐变动画',
+                hit: '是否有边框描边',
+                round: '是否为圆形',
+                color: '背景色'
+            }
+        },
+        elTransfer: {
+            name: '穿梭框',
+            event: {
+                leftCheckChange: '左侧列表元素被用户选中 / 取消选中时触发',
+                rightCheckChange: '右侧列表元素被用户选中 / 取消选中时触发'
+            },
+            props: {
+                filterable: '是否可搜索',
+                filterPlaceholder: '搜索框占位符',
+                targetOrder: '右侧列表元素的排序策略',
+                targetOrderInfo: '若为 original,则保持与数据相同的顺序;若为 push,则新加入的元素排在最后;若为 unshift,则新加入的元素排在最前',
+                titles: '自定义列表标题',
+                buttonTexts: '自定义按钮文案',
+                props: '数据源的字段别名'
+            }
+        },
+        elTreeSelect: {
+            name: '树形选择',
+            event: {
+                removeTag: '多选模式下移除tag时触发'
+            },
+            props: {
+                multiple: '是否多选',
+                disabled: '是否禁用',
+                clearable: '是否可以清空选项',
+                collapseTags: '多选时是否将选中值按文字的形式展示',
+                multipleLimit: '多选时用户最多可以选择的项目数,为 0 则不限制',
+                placeholder: '占位符',
+                props: '配置选项',
+                renderAfterExpand: '是否在第一次展开某个树节点后才渲染其子节点',
+                defaultExpandAll: '是否默认展开所有节点',
+                expandOnClickNode: '是否在点击节点的时候展开或者收缩节点',
+                checkOnClickNode: '是否在点击节点的时候选中节点',
+                nodeKey: '每个树节点用来作为唯一标识的属性,整棵树应该是唯一的'
+            }
+        },
+        elImage: {
+            name: '图片',
+            props: {
+                src: '图片链接'
+            }
+        },
+        fcEditor: {
+            name: '富文本框',
+            props: {
+                disabled: '是否禁用'
+            }
+        },
+        fcRow: {
+            name: '栅格布局',
+            props: {
+                gutter: '栅格间隔',
+                type: 'flex布局模式',
+                justify: 'flex布局下的水平排列方式',
+                align: 'flex布局下的垂直排列方式'
+            }
+        },
+        fcTable: {
+            name: '表格布局',
+            props: {
+                border: '是否显示边框',
+                borderColor: '边框颜色',
+                borderWidth: '边框宽度'
+            }
+        },
+        fcTableGrid: {
+            name: '格子',
+        },
+        group: {
+            name: '子表单',
+            props: {
+                disabled: '是否禁用',
+                syncDisabled: '是否与子表单强制同步禁用状态',
+                expand: '设置默认展开几项',
+                button: '是否显示操作按钮',
+                sortBtn: '是否显示排序按钮',
+                min: '设置最小添加几项',
+                max: '设置最多添加几项',
+            }
+        },
+        html: {
+            name: 'HTML',
+            props: {
+                formCreateNative: '是否显示标题',
+                formCreateTitle: '标题',
+                formCreateChild: '内容',
+            }
+        },
+        input: {
+            name: '输入框',
+            event: {
+                change: '当值改变时,当组件失去焦点或用户按Enter时触发',
+            },
+            props: {
+                type: '类型',
+                maxlength: '最大输入长度',
+                minlength: '最小输入长度',
+                placeholder: '输入框占位文本',
+                clearable: '是否显示清除按钮',
+                disabled: '是否禁用',
+                readonly: '是否只读',
+            }
+        },
+        inputNumber: {
+            name: '计数器',
+            props: {
+                precision: '数值精度',
+                min: '设置计数器允许的最小值',
+                max: '设置计数器允许的最大值',
+                step: '计数器步长',
+                stepStrictly: '是否只能输入 step 的倍数',
+                disabled: '是否禁用计数器',
+                controls: '是否使用控制按钮',
+                controlsPosition: '控制按钮位置',
+                placeholder: '输入框占位文本'
+            }
+        },
+        password: {
+            name: '密码输入框',
+            event: {
+                change: '当值改变时,当组件失去焦点或用户按Enter时触发',
+            },
+            props: {
+                disabled: '是否禁用',
+                readonly: '是否只读',
+                maxlength: '最大输入长度',
+                minlength: '最小输入长度',
+                placeholder: '输入框占位文本',
+                clearable: '是否显示清除按钮'
+            }
+        },
+        radio: {
+            name: '单选框',
+            props: {
+                input: '是否可以填写',
+                disabled: '是否禁用',
+                type: '按钮形式',
+                textColor: '按钮形式激活时的文本颜色',
+                fill: '按钮形式激活时的填充色和边框色'
+            }
+        },
+        rate: {
+            name: '评分',
+            props: {
+                max: '最大分值',
+                disabled: '是否禁用',
+                allowHalf: '是否允许半选',
+                voidColor: '未选中时图标的颜色',
+                disabledVoidColor: '只读时未选中时图标的颜色',
+                voidIconClass: '未选中时图标的类名',
+                disabledVoidIconClass: '只读时未选中时图标的类名',
+                showScore: '是否显示当前分数',
+                textColor: '辅助文字的颜色',
+                scoreTemplate: '分数显示模板'
+            }
+        },
+        select: {
+            name: '选择器',
+            event: {
+                removeTag: '多选模式下移除tag时触发'
+            },
+            props: {
+
+                multiple: '是否多选',
+                disabled: '是否禁用',
+                clearable: '是否可以清空选项',
+                collapseTags: '多选时是否将选中值按文字的形式展示',
+                multipleLimit: '多选时用户最多可以选择的项目数,为 0 则不限制',
+                placeholder: '占位符',
+                filterable: '是否可搜索',
+                allowCreate: '是否允许用户创建新条目',
+                noMatchText: '搜索条件无匹配时显示的文字',
+                noDataText: '选项为空时显示的文字',
+                reserveKeyword: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词',
+                defaultFirstOption: '在输入框按下回车,选择第一个匹配项',
+                remote: '其中的选项是否从服务器远程加载',
+                remoteMethod: '自定义远程搜索方法',
+            }
+        },
+        slider: {
+            name: '滑块',
+            props: {
+                min: '最小值',
+                max: '最大值',
+                disabled: '是否禁用',
+                step: '步长',
+                showInput: '是否显示输入框,仅在非范围选择时有效',
+                showInputControls: '在显示输入框的情况下,是否显示输入框的控制按钮',
+                showStops: '是否显示间断点',
+                range: '是否为范围选择',
+                vertical: '是否竖向模式',
+                height: 'Slider 高度,竖向模式时必填'
+            }
+        },
+        space: {
+            name: '间距',
+            props: {
+                height: '高度',
+            }
+        },
+        subForm: {
+            name: '分组',
+            props: {
+                disabled: '是否禁用',
+                syncDisabled: '是否与子表单强制同步禁用状态'
+            }
+        },
+        switch: {
+            name: '开关',
+            props: {
+                disabled: '是否禁用',
+                width: '宽度(px)',
+                activeText: '打开时的文字描述',
+                inactiveText: '关闭时的文字描述',
+                activeValue: '打开时的值',
+                inactiveValue: '关闭时的值',
+                activeColor: '打开时的背景色',
+                inactiveColor: '关闭时的背景色'
+            }
+        },
+        tableForm: {
+            name: '表格表单',
+            props: {
+                disabled: '是否禁用',
+                filterEmptyColumn: '是否过滤空行的数据',
+                max: '最多添加几行,为 0 则不限制',
+            }
+        },
+        tableFormColumn: {
+            name: '表格格子',
+            label: '自定义名称',
+            props: {
+                label: '标题',
+                width: '宽度',
+                color: '颜色',
+                required: '是否显示必填星号',
+            }
+        },
+        text: {
+            name: '文字',
+            props: {
+                formCreateNative: '是否显示标题',
+                formCreateTitle: '标题',
+                formCreateChild: '内容'
+            }
+        },
+        textarea: {
+            name: '多行输入框',
+            event: {
+                change: '当值改变时,当组件失去焦点或用户按Enter时触发',
+            },
+            props: {
+                disabled: '是否禁用',
+                readonly: '是否只读',
+                maxlength: '最大输入长度',
+                minlength: '最小输入长度',
+                showWordLimit: '是否显示统计字数',
+                placeholder: '输入框占位文本',
+                rows: '输入框行数',
+                autosize: '高度是否自适应'
+            }
+        },
+        timePicker: {
+            name: '时间',
+            props: {
+                pickerOptions: '当前时间日期选择器特有的选项',
+                readonly: '完全只读',
+                disabled: '禁用',
+                editable: '文本框可输入',
+                clearable: '是否显示清除按钮',
+                placeholder: '非范围选择时的占位内容',
+                startPlaceholder: '范围选择时开始日期的占位内容',
+                endPlaceholder: '范围选择时开始日期的占位内容',
+                isRange: '是否为时间范围选择',
+                arrowControl: '是否使用箭头进行时间选择',
+                align: '对齐方式'
+            }
+        },
+        tree: {
+            name: '树形控件',
+            event: {
+                nodeClick: '当节点被点击的时候触发',
+                nodeContextmenu: '当某一节点被鼠标右键点击时会触发该事件',
+                checkChange: '当复选框被点击的时候触发',
+                check: '点击节点复选框之后触发',
+                currentChange: '当前选中节点变化时触发的事件',
+                nodeExpand: '节点被展开时触发的事件',
+                nodeCollapse: '节点被关闭时触发的事件',
+                nodeDragStart: '节点开始拖拽时触发的事件',
+                nodeDragEnter: '拖拽进入其他节点时触发的事件',
+                nodeDragLeave: '拖拽离开某个节点时触发的事件',
+                nodeDragOver: '在拖拽节点时触发的事件(类似浏览器的 mouseover 事件)',
+                nodeDragEnd: '拖拽结束时(可能未成功)触发的事件',
+                nodeDrop: '拖拽成功完成时触发的事件'
+            },
+            props: {
+                emptyText: '内容为空的时候展示的文本',
+                props: '配置选项',
+                renderAfterExpand: '是否在第一次展开某个树节点后才渲染其子节点',
+                defaultExpandAll: '是否默认展开所有节点',
+                expandOnClickNode: '是否在点击节点的时候展开或者收缩节点,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。',
+                checkOnClickNode: '是否在点击节点的时候选中节点',
+                autoExpandParent: '展开子节点的时候是否自动展开父节点',
+                checkStrictly: '在显示复选框的情况下,是否严格的遵循父子不互相关联的做法',
+                accordion: '是否每次只打开一个同级树节点展开',
+                indent: '相邻级节点间的水平缩进(px)',
+                nodeKey: '每个树节点用来作为唯一标识的属性,整棵树应该是唯一的'
+            }
+        },
+        upload: {
+            name: '上传',
+            info: '上传成功后,将接口返回的 URL 赋值给 file.url,或将返回结果赋值给 file.value,以便在后续的表单提交时获取这些数据。',
+            event: {
+                remove: '文件列表移除文件时触发',
+                preview: '点击文件列表中已上传的文件时触发',
+                error: '文件上传失败时触发',
+                progress: '文件上传时触发',
+                exceed:'当超出限制时触发'
+            },
+            props: {
+                listType: '上传类型',
+                multiple: '是否支持多选文件',
+                action: '上传的地址(必填)',
+                beforeUpload: '上传文件之前触发',
+                onSuccess: '上传成功时触发',
+                beforeRemove: '删除文件之前触发',
+                headers: '设置上传的请求头部',
+                data: '上传时附带的额外参数',
+                name: '上传的文件字段名',
+                withCredentials: '支持发送 cookie 凭证信息',
+                accept: '接受上传的文件类型',
+                autoUpload: '是否在选取文件后立即进行上传',
+                disabled: '是否禁用',
+                limit: '最大允许上传个数'
+            }
+        }
+    },
+};
+
+export default ZhCn;
+

BIN
src/components/form-create-designer/style/fonts/fc-icons.woff


+ 632 - 0
src/components/form-create-designer/style/icon.css

@@ -0,0 +1,632 @@
+@font-face {
+    font-family: "fc-icon";
+    src: url(fonts/fc-icons.woff) format('woff');
+}
+
+.fc-icon {
+    font-family: "fc-icon" !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-up:before {
+    content: "\e697";
+    display: inline-block;
+    transform: rotate(180deg);
+}
+
+.icon-down:before {
+    content: "\e609";
+}
+
+.icon-alignitems-flexstart:before {
+    content: "\e67f";
+    display: inline-block;
+    transform: rotate(180deg);
+}
+
+.icon-align-center:before {
+    content: "\e6a5";
+    display: inline-block;
+    transform: rotate(90deg);
+}
+
+.icon-align-flexstart:before {
+    content: "\e6a4";
+    display: inline-block;
+    transform: rotate(90deg);
+}
+
+.icon-align-flexend:before {
+    content: "\e6a4";
+    display: inline-block;
+    transform: rotate(-90deg);
+}
+
+.icon-align-spacearound:before {
+    content: "\e670";
+    display: inline-block;
+    transform: rotate(-90deg);
+}
+
+.icon-align-spacebetween:before {
+    content: "\e695";
+    display: inline-block;
+    transform: rotate(-90deg);
+}
+
+.icon-align-stretch:before {
+    content: "\e6a7";
+    display: inline-block;
+    transform: rotate(-90deg);
+}
+
+.icon-align-flexend:before {
+    content: "\e6a4";
+    display: inline-block;
+    transform: rotate(-90deg);
+}
+
+.icon-justify-flexend:before {
+    content: "\e6a4";
+    display: inline-block;
+    transform: rotate(180deg);
+}
+
+.icon-direction-row:before {
+    content: "\e68b";
+    display: inline-block;
+    transform: rotate(180deg);
+}
+
+.icon-direction-column:before {
+    content: "\e68b";
+    display: inline-block;
+    transform: rotate(-90deg);
+}
+
+.icon-direction-columnreverse:before {
+    content: "\e68b";
+    display: inline-block;
+    transform: rotate(90deg);
+}
+
+.icon-arrow:before {
+    content: "\e697";
+    display: inline-block;
+    transform: rotate(180deg);
+}
+
+.icon-cell:before {
+    content: "\e654";
+}
+
+.icon-table:before {
+    content: "\eb0a";
+}
+
+.icon-next-step:before {
+    content: "\e6b4";
+    display: inline-block;
+    transform: rotateY(180deg);
+}
+
+.icon-grid:before {
+    content: "\e65c";
+    display: inline-block;
+    transform: rotate(90deg);
+}
+
+.icon-alignitems-stretch:before {
+    content: "\e67e";
+}
+
+.icon-alignitems-flexend:before {
+    content: "\e67f";
+}
+
+.icon-check:before {
+    content: "\e680";
+}
+
+.icon-auto:before {
+    content: "\e681";
+}
+
+.icon-config-event:before {
+    content: "\e66e";
+}
+
+.icon-calendar:before {
+    content: "\e683";
+}
+
+.icon-config-style:before {
+    content: "\e684";
+}
+
+.icon-copy:before {
+    content: "\e676";
+}
+
+.icon-config-advanced:before {
+    content: "\e686";
+}
+
+.icon-config-props:before {
+    content: "\e687";
+}
+
+.icon-delete-circle2:before {
+    content: "\e688";
+}
+
+.icon-delete-circle:before {
+    content: "\e689";
+}
+
+.icon-delete2:before {
+    content: "\e689";
+}
+
+.icon-delete:before {
+    content: "\e68a";
+}
+
+.icon-direction-rowreverse:before {
+    content: "\e68b";
+}
+
+.icon-display-flex:before {
+    content: "\e68c";
+}
+
+.icon-dialog:before {
+    content: "\e66f";
+}
+
+.icon-drag:before {
+    content: "\e68e";
+}
+
+.icon-display-block:before {
+    content: "\e68f";
+}
+
+.icon-data:before {
+    content: "\e690";
+}
+
+.icon-edit2:before {
+    content: "\e691";
+}
+
+.icon-edit:before {
+    content: "\e692";
+}
+
+.icon-add-col:before {
+    content: "\e693";
+}
+
+.icon-display-inlineblock:before {
+    content: "\e694";
+}
+
+.icon-config-base:before {
+    content: "\e6bf";
+}
+
+.icon-config-validate:before {
+    content: "\e696";
+}
+
+.icon-down:before {
+    content: "\e697";
+}
+
+.icon-display-inline:before {
+    content: "\e698";
+}
+
+.icon-eye:before {
+    content: "\e699";
+}
+
+.icon-eye-close:before {
+    content: "\e69a";
+}
+
+.icon-import:before {
+    content: "\e6a6";
+}
+
+.icon-preview:before {
+    content: "\e69b";
+}
+
+.icon-flex-nowrap:before {
+    content: "\e69c";
+}
+
+.icon-folder:before {
+    content: "\e69d";
+}
+
+.icon-form-circle:before {
+    content: "\e69e";
+}
+
+.icon-flex-wrap:before {
+    content: "\e69f";
+}
+
+.icon-form:before {
+    content: "\e6a0";
+}
+
+.icon-form-item:before {
+    content: "\e6a1";
+}
+
+.icon-icon:before {
+    content: "\e6a2";
+}
+
+.icon-image:before {
+    content: "\e6a3";
+}
+
+.icon-justify-flexstart:before {
+    content: "\e6a4";
+}
+
+.icon-justify-center:before {
+    content: "\e6a5";
+}
+
+.icon-justify-spacearound:before {
+    content: "\e670";
+}
+
+.icon-justify-stretch:before {
+    content: "\e6a7";
+}
+
+.icon-link2:before {
+    content: "\e6a8";
+}
+
+.icon-justify-spacebetween:before {
+    content: "\e695";
+}
+
+.icon-minus:before {
+    content: "\e6aa";
+}
+
+.icon-menu2:before {
+    content: "\e6ab";
+}
+
+.icon-more:before {
+    content: "\e6ac";
+}
+
+.icon-menu:before {
+    content: "\e6ad";
+}
+
+.icon-language:before {
+    content: "\e6ae";
+}
+
+.icon-pad:before {
+    content: "\e6af";
+}
+
+.icon-mobile:before {
+    content: "\e6b0";
+}
+
+.icon-page-max:before {
+    content: "\e6b1";
+}
+
+.icon-move:before {
+    content: "\e6b2";
+}
+
+.icon-page-min:before {
+    content: "\e6b3";
+}
+
+.icon-pre-step:before {
+    content: "\e6b4";
+}
+
+.icon-pc:before {
+    content: "\e6b5";
+}
+
+.icon-page:before {
+    content: "\e6b6";
+}
+
+.icon-refresh:before {
+    content: "\e6b7";
+}
+
+.icon-radius:before {
+    content: "\e6b8";
+}
+
+.icon-save-filled:before {
+    content: "\e6b9";
+}
+
+.icon-question:before {
+    content: "\e6ba";
+}
+
+.icon-scroll:before {
+    content: "\e6bb";
+}
+
+.icon-script:before {
+    content: "\e6bc";
+}
+
+.icon-setting:before {
+    content: "\e6bd";
+}
+
+.icon-save:before {
+    content: "\e6be";
+}
+
+.icon-save-online:before {
+    content: "\e6be";
+}
+
+.icon-task-add:before {
+    content: "\e68d";
+}
+
+.icon-shadow:before {
+    content: "\e6c0";
+}
+
+.icon-variable:before {
+    content: "\e6c1";
+}
+
+.icon-yes:before {
+    content: "\e6c2";
+}
+
+.icon-shadow-inset:before {
+    content: "\e6c3";
+}
+
+.icon-date:before {
+    content: "\e642";
+}
+
+.icon-date-range:before {
+    content: "\e643";
+}
+
+.icon-collapse:before {
+    content: "\e644";
+}
+
+.icon-slider:before {
+    content: "\e665";
+}
+
+.icon-switch:before {
+    content: "\e646";
+}
+
+.icon-subform:before {
+    content: "\e647";
+}
+
+.icon-time-range:before {
+    content: "\e685";
+}
+
+.icon-tree-select:before {
+    content: "\e649";
+}
+
+.icon-value:before {
+    content: "\e64a";
+}
+
+.icon-table-form3:before {
+    content: "\e6a9";
+}
+
+.icon-alert:before {
+    content: "\e64c";
+}
+
+.icon-card:before {
+    content: "\e64d";
+}
+
+.icon-checkbox:before {
+    content: "\e64e";
+}
+
+.icon-cascader:before {
+    content: "\e64f";
+}
+
+.icon-button:before {
+    content: "\e650";
+}
+
+.icon-data-table:before {
+    content: "\e651";
+}
+
+.icon-group:before {
+    content: "\e652";
+}
+
+.icon-divider:before {
+    content: "\e653";
+}
+
+.icon-flex:before {
+    content: "\e654";
+}
+
+.icon-descriptions:before {
+    content: "\e655";
+}
+
+.icon-html:before {
+    content: "\e656";
+}
+
+.icon-editor:before {
+    content: "\e657";
+}
+
+.icon-input:before {
+    content: "\e658";
+}
+
+.icon-link:before {
+    content: "\e659";
+}
+
+.icon-password:before {
+    content: "\e65a";
+}
+
+.icon-radio:before {
+    content: "\e65b";
+}
+
+.icon-row:before {
+    content: "\e65c";
+}
+
+.icon-inline:before {
+    content: "\e65d";
+}
+
+.icon-rate:before {
+    content: "\e65e";
+}
+
+.icon-color:before {
+    content: "\e65f";
+}
+
+.icon-select:before {
+    content: "\e660";
+}
+
+.icon-json:before {
+    content: "\e661";
+}
+
+.icon-number:before {
+    content: "\e662";
+}
+
+.icon-space:before {
+    content: "\e664";
+}
+
+.icon-step-form:before {
+    content: "\e663";
+}
+
+.icon-table-form:before {
+    content: "\e666";
+}
+
+.icon-table-form2:before {
+    content: "\e667";
+}
+
+.icon-time:before {
+    content: "\e668";
+}
+
+.icon-span:before {
+    content: "\e669";
+}
+
+.icon-textarea:before {
+    content: "\e66a";
+}
+
+.icon-tooltip:before {
+    content: "\e66b";
+}
+
+.icon-slot:before {
+    content: "\e66c";
+}
+
+.icon-transfer:before {
+    content: "\e66d";
+}
+
+.icon-upload:before {
+    content: "\e673";
+}
+
+.icon-tag:before {
+    content: "\e671";
+}
+
+.icon-watermark:before {
+    content: "\e672";
+}
+
+.icon-tab:before {
+    content: "\e674";
+}
+
+.icon-tree:before {
+    content: "\e675";
+}
+
+.icon-table:before {
+    content: "\e677";
+}
+
+.icon-add-child:before {
+    content: "\e678";
+}
+
+.icon-add2:before {
+    content: "\e679";
+}
+
+.icon-add:before {
+    content: "\e67a";
+}
+
+.icon-alignitems-baseline:before {
+    content: "\e67b";
+}
+
+.icon-add-circle:before {
+    content: "\e67c";
+}
+
+.icon-alignitems-center:before {
+    content: "\e67d";
+}

+ 759 - 0
src/components/form-create-designer/style/index.css

@@ -0,0 +1,759 @@
+._fc-designer {
+    height: 100%;
+    min-height: 500px;
+    overflow: hidden;
+    cursor: default;
+    position: relative;
+    background-color: #FFF;
+    --fc-drag-empty: "拖拽左侧列表中的组件到此处";
+    --fc-child-empty: "点击右下角 \e789  按钮添加一列";
+    --fc-tool-border-color: #2E73FF;
+}
+
+._fc-designer > .el-main {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    padding: 0;
+}
+
+._fc-l-menu {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    border-top: 1px solid #ECECEC;
+    border-right: 1px solid #ECECEC;
+}
+
+._fc-l-menu-item {
+    display: flex;
+    width: 100%;
+    align-items: center;
+    justify-content: center;
+    height: 40px;
+    cursor: pointer;
+    box-sizing: border-box;
+}
+
+._fc-l-menu-item.active {
+    color: #2E73FF;
+}
+
+._fc-l-menu-form {
+    border-bottom: 1px solid #ECECEC;
+}
+
+._fc-l-menu-item i {
+    font-size: 22px;
+}
+
+._fc-l-menu-item i:hover {
+    color: #2E73FF;
+}
+
+._fc-l-menu-item .el-badge__content {
+    --el-badge-size: 15px;
+    --el-badge-padding: 4px;
+    background-color: #2E73FF;
+}
+
+._fc-l-label {
+    font-weight: 500;
+    font-size: 14px;
+    color: #262626;
+    line-height: 17px;
+    padding: 12px;
+    margin-top: 5px;
+}
+
+._fc-l-info {
+    font-weight: 400;
+    font-size: 12px;
+    color: #AAAAAA;
+    line-height: 17px;
+    text-align: left;
+    font-style: normal;
+    padding: 0 12px;
+}
+
+._fc-l > .el-container {
+    height: 100%;
+}
+
+._fc-m .form-create ._fc-l-item {
+    display: flex !important;
+    align-items: center;
+    justify-content: center;
+    background: #f5f5f5;
+    color: #000;
+    width: 100%;
+    margin: 5px 0;
+    height: 30px;
+    overflow: hidden;
+    transition: all .3s ease;
+    border: 1px dashed #000;
+    border-radius: 4px;
+    padding-bottom: 0;
+}
+
+._fc-m .form-create ._fc-l-item ._fc-l-icon {
+    display: inline-block !important;
+    padding: 0 4px;
+}
+
+._fc-m .form-create ._fc-l-item ._fc-l-name {
+    display: inline-block !important;
+    font-size: 12px;
+}
+
+._fc-l, ._fc-m, ._fc-r {
+    border-top: 1px solid #ECECEC;
+    box-sizing: border-box;
+}
+
+._fc-r {
+    --el-color-primary: #2E73FF;
+}
+
+._fc-r-tab-props {
+    padding: 0 20px;
+    position: relative;
+}
+
+._fc-r-tools-close {
+    position: absolute;
+    right: 24px;
+    top: 12px;
+    transform: rotate(45deg);
+    color: #666666;
+    cursor: pointer;
+}
+
+._fc-r-title {
+    font-size: 12px;
+    color: #333333;
+    margin: 15px 0 5px 0;
+}
+
+._fc-r-sub ._fc-r-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+
+._fc-r-sub .fc-icon {
+    cursor: pointer;
+}
+
+._fc-r-sub ._fd-config-item + ._fd-config-item {
+    margin-top: 8px;
+}
+
+._fc-r-sub > ._fd-config-item > ._fd-ci-head {
+    position: relative;
+    padding-left: 8px;
+}
+
+._fc-r-sub > ._fd-config-item > ._fd-ci-head:before {
+    content: ' ';
+    position: absolute;
+    width: 5px;
+    height: 5px;
+    background-color: #333;
+    border-radius: 25px;
+    left: 0;
+}
+
+._fc-r-config {
+    display: grid;
+    grid-template-columns: repeat(1, 1fr);
+    grid-template-areas: "base" "props" "style" "event" "validate";
+}
+
+._fc-r-name-input .el-input-group__append {
+    width: 25px;
+    padding: 0;
+    margin: 0;
+    color: #606266;
+    cursor: pointer;
+}
+
+._fc-r-name-input .icon-group {
+    cursor: pointer
+}
+
+._fc-r-name-input .icon-group:hover {
+    color: #2E73FF;
+}
+
+._fc-r .el-main {
+    padding-bottom: 100px;
+}
+
+._fc-l .el-main {
+    padding: 0;
+}
+
+._fc-l .el-tree-node__label {
+    padding: 3px;
+    font-weight: 400;
+    color: #333;
+}
+
+._fc-l .el-tree-node__content {
+    height: 30px;
+    margin-top: 5px;
+}
+
+._fc-l .el-tree-node__content > .el-tree-node__expand-icon {
+    color: #333;
+}
+
+._fc-l .el-tree-node__expand-icon.is-leaf {
+    color: transparent;
+}
+
+@keyframes rotating {
+    0% {
+        transform: rotate(0)
+    }
+
+    to {
+        transform: rotate(360deg)
+    }
+}
+
+._fc-loading {
+    animation: rotating 2s linear infinite;
+}
+
+._fc-tree-node {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    height: 26px;
+    line-height: 26px;
+    padding-right: 5px
+}
+
+._fc-tree-node.active, ._fc-tree-node.active .icon-more {
+    color: #2E73FF;
+}
+
+._fc-tree-label {
+    display: flex;
+    align-items: center;
+}
+
+._fc-tree-label > i {
+    margin-right: 5px;
+    font-weight: 700;
+}
+
+._fc-tree-more {
+    display: flex;
+    align-items: center;
+    padding: 0 15px;
+    font-weight: 700;
+}
+
+._fc-l-tabs {
+    display: flex;
+    border-bottom: 1px solid #ECECEC;
+    padding: 0;
+}
+
+._fc-l-tab {
+    height: 40px;
+    box-sizing: border-box;
+    line-height: 40px;
+    display: inline-block;
+    list-style: none;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    position: relative;
+    flex: 1;
+    text-align: center;
+}
+
+._fc-l ._fc-l-tab.active {
+    color: #409EFF;
+    border-bottom: 2px solid #409EFF;
+}
+
+._fc-l-group {
+    border: 1px solid #EEEEEE;
+    padding: 0;
+    margin: 12px;
+    user-select: none;
+}
+
+._fc-l-group ._fc-l-list {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    align-items: stretch;
+}
+
+._fc-l-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    cursor: pointer;
+    font-weight: 600;
+    font-size: 14px;
+    padding: 12px;
+    margin: 0;
+}
+
+._fc-l-title i {
+    font-size: 14px;
+}
+
+._fc-l-title i.down {
+    transform: rotate(90deg);
+}
+
+._fc-l-item {
+    display: inline-block;
+    background: #FFF;
+    color: #000;
+    line-height: 1;
+    text-align: center;
+    transition: all .2s ease;
+    cursor: pointer;
+    padding-bottom: 10px;
+}
+
+._fc-l-item i {
+    font-size: 21px;
+    display: inline-block;
+}
+
+._fc-l-item ._fc-l-name {
+    font-size: 12px;
+}
+
+._fc-l-item ._fc-l-icon {
+    padding: 10px 5px 12px;
+}
+
+._fc-l-item:hover {
+    background: #2E73FF;
+    color: #fff;
+}
+
+._fc-m-tools {
+    height: 40px;
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    border: 1px solid #ECECEC;
+    border-top: 0 none;
+    white-space: nowrap;
+}
+
+._fc-m-tools-l, ._fc-m-tools-r {
+    display: flex;
+    align-items: center;
+}
+
+._fc-m-tools-r {
+    overflow: auto;
+}
+
+
+._fc-m-tools-l .devices .fc-icon {
+    width: 18px;
+    cursor: pointer;
+}
+
+._fc-m-tools-l .devices .fc-icon.active {
+    color: #2E73FF;
+}
+
+._fc-m-tools-l .devices .fc-icon + .fc-icon {
+    margin-left: 5px;
+}
+
+._fc-m-tools .line {
+    width: 1px;
+    height: 24px;
+    background: #D8D8D8;
+    margin: 0 10px;
+}
+
+._fc-m-tools .el-button {
+    padding: 5px 10px;
+    display: flex;
+    align-items: center;
+    border-radius: 5px;
+}
+
+._fc-m-tools .el-button > span {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+}
+
+._fc-m-tools .el-dropdown, ._fc-m-tools .el-button + .el-button {
+    margin-left: 10px;
+}
+
+._fc-m-tools ._fd-m-extend {
+    color: #666;
+    border-color: #ccc;
+    background-color: #f1f1f1;
+    border-radius: 5px;
+    padding: 5px;
+}
+
+._fc-m-tools ._fd-m-extend .fc-icon {
+    margin-right: 0;
+}
+
+._fc-m-tools ._fd-input-btn {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 12px;
+}
+
+._fc-m-tools ._fd-input-btn .el-switch__action {
+    width: 18px;
+    height: 18px;
+    left: 2px;
+}
+
+._fc-m-tools ._fd-input-btn .is-checked .el-switch__action {
+    --el-color-white: #2E73FF;
+    left: calc(100% - 20px);
+}
+
+._fc-m-tools ._fd-input-btn .el-switch__core {
+    --el-switch-on-color: var(--el-switch-off-color);
+    border-radius: 16px;
+    height: 24px;
+    width: 46px;
+}
+
+._fc-m-tools ._fd-input-btn .is-checked .el-switch__core {
+    --el-switch-on-color: var(--el-switch-off-color);
+    border-radius: 16px;
+    height: 24px;
+    width: 46px;
+}
+
+._fc-m-tools-r .fc-icon {
+    font-size: 14px;
+}
+
+._fc-m-tools-l .fc-icon {
+    font-size: 18px;
+    cursor: pointer;
+}
+
+._fc-m-tools-l .fc-icon + .fc-icon {
+    margin-left: 10px;
+}
+
+._fc-m-tools-l .fc-icon.disabled {
+    color: #999;
+    cursor: not-allowed;
+}
+
+._fc-r .el-tabs__nav-wrap::after {
+    height: 1px;
+    background-color: #ECECEC;
+}
+
+._fc-r ._fc-r-tabs {
+    display: flex;
+    padding: 0;
+    border-bottom: 1px solid #ECECEC;
+}
+
+._fc-r .el-table__cell .cell, ._fc-r .el-button, ._fc-r .el-radio-button__inner {
+    font-weight: 400;
+}
+
+._fc-r ._fc-r-tab {
+    height: 40px;
+    box-sizing: border-box;
+    line-height: 40px;
+    display: inline-block;
+    list-style: none;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    position: relative;
+    flex: 1;
+    text-align: center;
+}
+
+._fc-r ._fc-r-tab.active {
+    color: #409EFF;
+    border-bottom: 2px solid #409EFF;
+}
+
+._fc-m-con {
+    position: relative;
+    background: #F5F5F5;
+    padding: 20px 20px 36px;
+}
+
+._fc-m-drag {
+    margin: 0 auto;
+    overflow: auto;
+    padding: 2px;
+    box-sizing: border-box;
+}
+
+._fc-m-input {
+    padding: 5px 5px 80px;
+}
+
+._fc-m-input-handle {
+    position: absolute;
+    bottom: 17px;
+    left: 0;
+    right: 0;
+    padding: 12px;
+    background: #FFFFFF;
+    box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.05);
+    text-align: center;
+    margin: 20px;
+    z-index: 99;
+}
+
+._fc-m-drag.mobile {
+    width: 400px;
+}
+
+._fc-m-drag.pad {
+    width: 770px;
+}
+
+._fc-m-drag, .draggable-drag {
+    background: #fff;
+    height: 100%;
+    position: relative;
+}
+
+._fc-m-drag > form, ._fc-m-drag > form > .el-row {
+    height: 100%;
+}
+
+._fc-m-drag .el-tree {
+    width: 100%;
+}
+
+._fd-drag-box {
+    width: 100%;
+    height: 100%;
+    min-height: 80px;
+    transition: padding-bottom, padding-top .3s ease;
+}
+
+._fd-drag-box ._fd-drag-box {
+    outline: 1px dashed #ECECEC;
+}
+
+._fd-drag-tool > ._fd-drag-box {
+    outline: none;
+}
+
+._fd-drag-box > div[data-draggable] {
+    margin-bottom: 1px;
+}
+
+._fc-r ._fc-group-container + ._fc-group-container {
+    margin-top: 20px;
+}
+
+._fc-r ._fc-group-container {
+    margin: 0;
+    padding: 10px;
+}
+
+._fc-r ._fc-group-handle {
+    right: 15px;
+}
+
+._fc-r .el-form-item {
+    margin-bottom: 10px !important;
+}
+
+._fc-r .el-form-item__label {
+    color: #333333;
+}
+
+._fc-upload-preview {
+    display: inline-block;
+    width: 120px;
+    height: 120px;
+    border-radius: 5px;
+    overflow: hidden;
+}
+
+._fc-tabs .el-tabs__item {
+    font-weight: 400;
+}
+
+._fc-tabs .el-tabs__content {
+    overflow: auto;
+}
+
+._fc-tabs .el-tabs__nav-scroll {
+    padding: 0 15px;
+}
+
+._fc-tabs .el-tab-pane {
+    margin-right: 15px;
+}
+
+._fc-tabs .el-tabs__nav-wrap::after {
+    height: 1px;
+}
+
+.form-create .fc-none {
+    display: none;
+}
+
+._fd-draggable-drag.drag-holder, ._fd-tableFormColumn-drag.drag-holder, ._fd-elTabPane-drag.drag-holder, ._fd-group-drag.drag-holder, ._fd-subForm-drag.drag-holder, ._fd-elCard-drag.drag-holder, ._fd-elCollapseItem-drag.drag-holder {
+    position: relative;
+    background: #f5f5f5;
+    background-size: 0;
+    min-height: 90px;
+}
+
+._fc-child-empty:after, ._fd-draggable-drag.drag-holder:after, ._fd-tableFormColumn-drag.drag-holder:after, ._fd-elTabPane-drag.drag-holder:after, ._fd-group-drag.drag-holder:after, ._fd-subForm-drag.drag-holder:after, ._fd-elCard-drag.drag-holder:after, ._fd-elCollapseItem-drag.drag-holder:after {
+    content: var(--fc-drag-empty);
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: #aaa;
+    font-size: 12px;
+}
+
+._fc-designer ._fc-m-drag ._fd-draggable-drag {
+    overflow: auto;
+    padding: 2px 2px 100px;
+}
+
+._fc-m-drag._fd-drop-hover ._fd-draggable-drag {
+    padding-top: 20px;
+}
+
+._fd-draggable-drag.drag-holder {
+    background-color: #ffffff;
+}
+
+._fd-draggable-drag.drag-holder:after {
+    font-size: 16px;
+}
+
+._fc-child-empty:after {
+    font-family: "fc-icon" !important;
+    content: var(--fc-child-empty);
+}
+
+.fc-configured {
+    color: #999;
+    margin-left: 5px;
+}
+
+._fc-manage-text {
+    cursor: pointer;
+    color: #2f73ff;
+    margin-left: 4px;
+    font-size: 12px;
+}
+
+._fc-manage-text i {
+    font-size: 12px;
+}
+
+._fc-message-error {
+    top: 16px;
+    z-index: 2116;
+    border-radius: 8px;
+    padding: 9px 13px;
+    background-color: #fff;
+    border-color: #fff;
+    box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
+}
+
+._fc-message-error > .el-icon {
+    font-size: 18px;
+}
+
+._fc-message-error .el-message__content {
+    color: rgba(0, 0, 0, 0.88);
+    font-size: 14px;
+}
+
+._fd-preview-copy{
+    display: flex;
+    position: absolute;
+    right: 35px;
+    top: 65px;
+    align-items: center;
+    justify-content: center;
+    width: 28px;
+    height: 28px;
+    background: #2E73FF33;
+    border-radius: 10px;
+    color: #2E73FF;
+    cursor: pointer;
+}
+
+._fd-preview-dialog {
+    border-radius: 6px;
+    padding-top: 0;
+}
+
+._fd-preview-dialog .el-dialog__header {
+    position: absolute;
+    right: 0;
+    top: 0;
+    float: right;
+    z-index: 9;
+}
+
+._fd-preview-code {
+    margin-top: 0;
+    max-height: 510px;
+    overflow: auto;
+}
+
+._fd-preview-tabs .el-tabs__nav-wrap::after {
+    height: 1px;
+}
+
+._fd-preview-tabs .el-tabs__item {
+    height: 46px;
+}
+
+._fd-preview-code > code {
+    white-space: pre-wrap;
+}
+
+._fd-row-line {
+    width: 100%;
+    height: 1px;
+    margin: 10px 0;
+    background: #D8D8D8;
+}
+
+.CodeMirror-hints {
+    z-index: 999999;
+}

+ 9 - 0
src/components/form-create-designer/utils/form.js

@@ -0,0 +1,9 @@
+import formCreate from '@form-create/element-ui';
+
+const viewForm = formCreate;
+
+const designerForm = formCreate.factory();
+
+export default viewForm;
+
+export {designerForm};

+ 307 - 0
src/components/form-create-designer/utils/highlight/highlight.min.js

@@ -0,0 +1,307 @@
+/*!
+  Highlight.js v11.9.0 (git: b7ec4bfafc)
+  (c) 2006-2024 undefined and other contributors
+  License: BSD-3-Clause
+ */
+function e(t){ return t instanceof Map?t.clear=t.delete=t.set=()=>{
+    throw Error('map is read-only') }:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
+    throw Error('set is read-only')
+}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{
+    const i=t[n],s=typeof i;'object'!==s&&'function'!==s||Object.isFrozen(i)||e(i)
+})),t }class t{constructor(e){
+    void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1 }
+ignoreMatch(){ this.isMatchIgnored=!0 }}function n(e){
+    return e.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#x27;')
+}function i(e,...t){ const n=Object.create(null);for(const t in e)n[t]=e[t]
+;return t.forEach((e=>{ for(const t in e)n[t]=e[t] })),n }const s=e=>!!e.scope
+;class r{constructor(e,t){
+    this.buffer='',this.classPrefix=t.classPrefix,e.walk(this) }addText(e){
+    this.buffer+=n(e) }openNode(e){ if(!s(e))return;const t=((e,{prefix:t})=>{
+    if(e.startsWith('language:'))return e.replace('language:','language-')
+    ;if(e.includes('.')){ const n=e.split('.')
+;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${'_'.repeat(t+1)}`))].join(' ')
+    }return`${t}${e}` })(e.scope,{prefix:this.classPrefix});this.span(t) }
+closeNode(e){ s(e)&&(this.buffer+='</span>') }value(){ return this.buffer }span(e){
+    this.buffer+=`<span class="${e}">` }}const o=(e={})=>{ const t={children:[]}
+;return Object.assign(t,e),t };class a{constructor(){
+    this.rootNode=o(),this.stack=[this.rootNode] }get top(){
+    return this.stack[this.stack.length-1] }get root(){ return this.rootNode }add(e){
+    this.top.children.push(e) }openNode(e){ const t=o({scope:e})
+;this.add(t),this.stack.push(t) }closeNode(){
+    if(this.stack.length>1)return this.stack.pop() }closeAllNodes(){
+    for(;this.closeNode();); }toJSON(){ return JSON.stringify(this.rootNode,null,4) }
+walk(e){ return this.constructor._walk(e,this.rootNode) }static _walk(e,t){
+    return'string'==typeof t?e.addText(t):t.children&&(e.openNode(t),
+    t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e }static _collapse(e){
+    'string'!=typeof e&&e.children&&(e.children.every((e=>'string'==typeof e))?e.children=[e.children.join('')]:e.children.forEach((e=>{
+        a._collapse(e) }))) }}class c extends a{constructor(e){ super(),this.options=e }
+    addText(e){ ''!==e&&this.add(e) }startScope(e){ this.openNode(e) }endScope(){
+        this.closeNode() }__addSublanguage(e,t){ const n=e.root
+;t&&(n.scope='language:'+t),this.add(n) }toHTML(){
+        return new r(this,this.options).value() }finalize(){
+        return this.closeAllNodes(),!0 }}function l(e){
+    return e?'string'==typeof e?e:e.source:null }function g(e){ return h('(?=',e,')') }
+function u(e){ return h('(?:',e,')*') }function d(e){ return h('(?:',e,')?') }
+function h(...e){ return e.map((e=>l(e))).join('') }function f(...e){ const t=(e=>{
+    const t=e[e.length-1]
+;return'object'==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
+})(e);return'('+(t.capture?'':'?:')+e.map((e=>l(e))).join('|')+')' }
+function p(e){ return RegExp(e.toString()+'|').exec('').length-1 }
+const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
+;function m(e,{joinWith:t}){ let n=0;return e.map((e=>{ n+=1;const t=n
+;let i=l(e),s='';for(;i.length>0;){ const e=b.exec(i);if(!e){ s+=i;break }
+    s+=i.substring(0,e.index),
+    i=i.substring(e.index+e[0].length),'\\'===e[0][0]&&e[1]?s+='\\'+(Number(e[1])+t):(s+=e[0],
+    '('===e[0]&&n++) }return s })).map((e=>`(${e})`)).join(t) }
+const E='[a-zA-Z]\\w*',x='[a-zA-Z_]\\w*',w='\\b\\d+(\\.\\d+)?',y='(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)',_='\\b(0b[01]+)',O={
+        begin:'\\\\[\\s\\S]',relevance:0},k={scope:'string',begin:'\'',end:'\'',
+        illegal:'\\n',contains:[O]},v={scope:'string',begin:'"',end:'"',illegal:'\\n',
+        contains:[O]},N=(e,t,n={})=>{ const s=i({scope:'comment',begin:e,end:t,
+        contains:[]},n);s.contains.push({scope:'doctag',
+        begin:'[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)',
+        end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
+    ;const r=f('I','a','is','so','us','to','at','if','in','it','on',/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
+;return s.contains.push({begin:h(/[ ]+/,'(',r,/[.]?[:]?([.][ ]|[ ])/,'){3}')}),s
+    },S=N('//','$'),M=N('/\\*','\\*/'),R=N('#','$');var A=Object.freeze({
+    __proto__:null,APOS_STRING_MODE:k,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{
+        scope:'number',begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N,
+    C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:'number',
+        begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{
+        'on:begin':(e,t)=>{ t.data._beginMatch=e[1] },'on:end':(e,t)=>{
+            t.data._beginMatch!==e[1]&&t.ignoreMatch() }}),HASH_COMMENT_MODE:R,IDENT_RE:E,
+    MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:'\\.\\s*'+x,relevance:0},
+    NUMBER_MODE:{scope:'number',begin:w,relevance:0},NUMBER_RE:w,
+    PHRASAL_WORDS_MODE:{
+        begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
+    },QUOTE_STRING_MODE:v,REGEXP_MODE:{scope:'regexp',begin:/\/(?=[^/\n]*\/)/,
+        end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]},
+    RE_STARTERS_RE:'!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~',
+    SHEBANG:(e={})=>{ const t=/^#![ ]*\//
+;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:'meta',begin:t,
+        end:/$/,relevance:0,'on:begin':(e,t)=>{ 0!==e.index&&t.ignoreMatch() }},e) },
+    TITLE_MODE:{scope:'title',begin:E,relevance:0},UNDERSCORE_IDENT_RE:x,
+    UNDERSCORE_TITLE_MODE:{scope:'title',begin:x,relevance:0}});function j(e,t){
+    '.'===e.input[e.index-1]&&t.ignoreMatch() }function I(e,t){
+    void 0!==e.className&&(e.scope=e.className,delete e.className) }function T(e,t){
+    t&&e.beginKeywords&&(e.begin='\\b('+e.beginKeywords.split(' ').join('|')+')(?!\\.)(?=\\b|\\s)',
+    e.__beforeBegin=j,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
+    void 0===e.relevance&&(e.relevance=0)) }function L(e,t){
+    Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal)) }function B(e,t){
+    if(e.match){
+        if(e.begin||e.end)throw Error('begin & end are not supported with match')
+        ;e.begin=e.match,delete e.match } }function P(e,t){
+    void 0===e.relevance&&(e.relevance=1) }const D=(e,t)=>{ if(!e.beforeMatch)return
+    ;if(e.starts)throw Error('beforeMatch cannot be used with starts')
+    ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{ delete e[t]
+    })),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={
+        relevance:0,contains:[Object.assign(n,{endsParent:!0})]
+    },e.relevance=0,delete n.beforeMatch
+    },H=['of','and','for','in','not','or','if','then','parent','list','value'],C='keyword'
+;function $(e,t,n=C){ const i=Object.create(null)
+;return'string'==typeof e?s(n,e.split(' ')):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
+    Object.assign(i,$(e[n],t,n)) })),i;function s(e,n){
+    t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{ const n=t.split('|')
+;i[n[0]]=[e,U(n[0],n[1])] })) } }function U(e,t){
+    return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1 }const z={},W=e=>{
+        console.error(e) },X=(e,...t)=>{ console.log('WARN: '+e,...t) },G=(e,t)=>{
+        z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0)
+    },K=Error();function F(e,t,{key:n}){ let i=0;const s=e[n],r={},o={}
+;for(let e=1;e<=t.length;e++)o[e+i]=s[e],r[e+i]=!0,i+=p(t[e-1])
+;e[n]=o,e[n]._emit=r,e[n]._multi=!0 }function Z(e){ (e=>{
+    e.scope&&'object'==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
+    delete e.scope) })(e),'string'==typeof e.beginScope&&(e.beginScope={
+    _wrap:e.beginScope}),'string'==typeof e.endScope&&(e.endScope={_wrap:e.endScope
+}),(e=>{ if(Array.isArray(e.begin)){
+    if(e.skip||e.excludeBegin||e.returnBegin)throw W('skip, excludeBegin, returnBegin not compatible with beginScope: {}'),
+    K
+    ;if('object'!=typeof e.beginScope||null===e.beginScope)throw W('beginScope must be object'),
+    K;F(e,e.begin,{key:'beginScope'}),e.begin=m(e.begin,{joinWith:''}) } })(e),(e=>{
+    if(Array.isArray(e.end)){
+        if(e.skip||e.excludeEnd||e.returnEnd)throw W('skip, excludeEnd, returnEnd not compatible with endScope: {}'),
+        K
+        ;if('object'!=typeof e.endScope||null===e.endScope)throw W('endScope must be object'),
+        K;F(e,e.end,{key:'endScope'}),e.end=m(e.end,{joinWith:''}) } })(e) }function V(e){
+    function t(t,n){
+        return RegExp(l(t),'m'+(e.case_insensitive?'i':'')+(e.unicodeRegex?'u':'')+(n?'g':''))
+    }class n{constructor(){
+        this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0 }
+    addRule(e,t){
+        t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
+        this.matchAt+=p(e)+1 }compile(){ 0===this.regexes.length&&(this.exec=()=>null)
+    ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:'|'
+    }),!0),this.lastIndex=0 }exec(e){ this.matcherRe.lastIndex=this.lastIndex
+    ;const t=this.matcherRe.exec(e);if(!t)return null
+    ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
+;return t.splice(0,n),Object.assign(t,i) }}class s{constructor(){
+        this.rules=[],this.multiRegexes=[],
+        this.count=0,this.lastIndex=0,this.regexIndex=0 }getMatcher(e){
+        if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
+;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
+        t.compile(),this.multiRegexes[e]=t,t }resumingScanAtSamePosition(){
+        return 0!==this.regexIndex }considerAll(){ this.regexIndex=0 }addRule(e,t){
+        this.rules.push([e,t]),'begin'===t.type&&this.count++ }exec(e){
+        const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
+        ;let n=t.exec(e)
+;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
+            const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e) }
+        return n&&(this.regexIndex+=n.position+1,
+        this.regexIndex===this.count&&this.considerAll()),n }}
+    if(e.compilerExtensions||(e.compilerExtensions=[]),
+    e.contains&&e.contains.includes('self'))throw Error('ERR: contains `self` is not supported at the top-level of a language.  See documentation.')
+    ;return e.classNameAliases=i(e.classNameAliases||{}),function n(r,o){ const a=r
+;if(r.isCompiled)return a
+    ;[I,B,Z,D].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))),
+    r.__beforeBegin=null,[T,L,P].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null
+;return'object'==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords),
+    c=r.keywords.$pattern,
+    delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=$(r.keywords,e.case_insensitive)),
+    a.keywordPatternRe=t(c,!0),
+    o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/),
+    r.end&&(a.endRe=t(a.end)),
+    a.terminatorEnd=l(a.end)||'',r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?'|':'')+o.terminatorEnd)),
+    r.illegal&&(a.illegalRe=t(r.illegal)),
+    r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{
+        variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{
+        starts:e.starts?i(e.starts):null
+    }):Object.isFrozen(e)?i(e):e))('self'===e?r:e)))),r.contains.forEach((e=>{ n(e,a)
+    })),r.starts&&n(r.starts,o),a.matcher=(e=>{ const t=new s
+;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:'begin'
+    }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:'end'
+    }),e.illegal&&t.addRule(e.illegal,{type:'illegal'}),t })(a),a }(e) }function q(e){
+    return!!e&&(e.endsWithParent||q(e.starts)) }class J extends Error{
+    constructor(e,t){ super(e),this.name='HTMLInjectionError',this.html=t }}
+const Y=n,Q=i,ee=Symbol('nomatch'),te=n=>{
+        const i=Object.create(null),s=Object.create(null),r=[];let o=!0
+;const a='Could not find the language \'{}\', did you forget to load/include a language module?',l={
+            disableAutodetect:!0,name:'Plain text',contains:[]};let p={
+            ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
+            languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:'hljs-',
+            cssSelector:'pre code',languages:null,__emitter:c};function b(e){
+            return p.noHighlightRe.test(e) }function m(e,t,n){ let i='',s=''
+;'object'==typeof t?(i=e,
+        n=t.ignoreIllegals,s=t.language):(G('10.7.0','highlight(lang, code, ...args) has been deprecated.'),
+        G('10.7.0','Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277'),
+        s=e,i=t),void 0===n&&(n=!0);const r={code:i,language:s};N('before:highlight',r)
+        ;const o=r.result?r.result:E(r.language,r.code,n)
+;return o.code=r.code,N('after:highlight',o),o }function E(e,n,s,r){
+            const c=Object.create(null);function l(){ if(!N.keywords)return M.addText(R)
+            ;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n=''
+;for(;t;){ n+=R.substring(e,t.index)
+            ;const s=_.case_insensitive?t[0].toLowerCase():t[0],r=(i=s,N.keywords[i]);if(r){
+                const[e,i]=r
+;if(M.addText(n),n='',c[s]=(c[s]||0)+1,c[s]<=7&&(A+=i),e.startsWith('_'))n+=t[0];else{
+                    const n=_.classNameAliases[e]||e;u(t[0],n) } }else n+=t[0]
+            ;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R) }var i
+;n+=R.substring(e),M.addText(n) }function g(){ null!=N.subLanguage?(()=>{
+                if(''===R)return;let e=null;if('string'==typeof N.subLanguage){
+                    if(!i[N.subLanguage])return M.addText(R)
+                    ;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top
+                }else e=x(R,N.subLanguage.length?N.subLanguage:null)
+                ;N.relevance>0&&(A+=e.relevance),M.__addSublanguage(e._emitter,e.language)
+            })():l(),R='' }function u(e,t){
+                ''!==e&&(M.startScope(t),M.addText(e),M.endScope()) }function d(e,t){ let n=1
+;const i=t.length-1;for(;n<=i;){ if(!e._emit[n]){ n++;continue }
+                const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=''),n++ } }
+            function h(e,t){
+                return e.scope&&'string'==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope),
+                e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
+                R=''):e.beginScope._multi&&(d(e.beginScope,t),R='')),N=Object.create(e,{parent:{
+                    value:N}}),N }function f(e,n,i){ let s=((e,t)=>{ const n=e&&e.exec(t)
+;return n&&0===n.index })(e.endRe,i);if(s){ if(e['on:end']){ const i=new t(e)
+;e['on:end'](n,i),i.isMatchIgnored&&(s=!1) }if(s){
+                for(;e.endsParent&&e.parent;)e=e.parent;return e } }
+            if(e.endsWithParent)return f(e.parent,n,i) }function b(e){
+                return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0) }function m(e){
+                const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const r=N
+;N.endScope&&N.endScope._wrap?(g(),
+                u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(),
+                d(N.endScope,e)):r.skip?R+=t:(r.returnEnd||r.excludeEnd||(R+=t),
+                g(),r.excludeEnd&&(R=t));do{
+                    N.scope&&M.closeNode(),N.skip||N.subLanguage||(A+=N.relevance),N=N.parent
+                }while(N!==s.parent);return s.starts&&h(s.starts,e),r.returnEnd?0:t.length }
+            let w={};function y(i,r){ const a=r&&r[0];if(R+=i,null==a)return g(),0
+            ;if('begin'===w.type&&'end'===r.type&&w.index===r.index&&''===a){
+                if(R+=n.slice(r.index,r.index+1),!o){ const t=Error(`0 width match regex (${e})`)
+;throw t.languageName=e,t.badRule=w.rule,t }return 1 }
+            if(w=r,'begin'===r.type)return(e=>{
+                const n=e[0],i=e.rule,s=new t(i),r=[i.__beforeBegin,i['on:begin']]
+;for(const t of r)if(t&&(t(e,s),s.isMatchIgnored))return b(n)
+                ;return i.skip?R+=n:(i.excludeBegin&&(R+=n),
+                g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length })(r)
+            ;if('illegal'===r.type&&!s){
+                const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||'<unnamed>')+'"')
+;throw e.mode=N,e }if('end'===r.type){ const e=m(r);if(e!==ee)return e }
+            if('illegal'===r.type&&''===a)return 1
+            ;if(I>1e5&&I>3*r.index)throw Error('potential infinite loop, way more iterations than matches')
+            ;return R+=a,a.length }const _=O(e)
+;if(!_)throw W(a.replace('{}',e)),Error('Unknown language: "'+e+'"')
+            ;const k=V(_);let v='',N=r||k;const S={},M=new p.__emitter(p);(()=>{ const e=[]
+;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope)
+            ;e.forEach((e=>M.openNode(e))) })();let R='',A=0,j=0,I=0,T=!1;try{
+                if(_.__emitTokens)_.__emitTokens(n,M);else{ for(N.matcher.considerAll();;){
+                    I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=j
+                    ;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(j,e.index),e)
+;j=e.index+t }y(n.substring(j)) }return M.finalize(),v=M.toHTML(),{language:e,
+                    value:v,relevance:A,illegal:!1,_emitter:M,_top:N} }catch(t){
+                if(t.message&&t.message.includes('Illegal'))return{language:e,value:Y(n),
+                    illegal:!0,relevance:0,_illegalBy:{message:t.message,index:j,
+                        context:n.slice(j-100,j+100),mode:t.mode,resultSoFar:v},_emitter:M};if(o)return{
+                    language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N}
+                ;throw t } }function x(e,t){ t=t||p.languages||Object.keys(i);const n=(e=>{
+                const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)}
+;return t._emitter.addText(e),t })(e),s=t.filter(O).filter(v).map((t=>E(t,e,!1)))
+;s.unshift(n);const r=s.sort(((e,t)=>{
+                if(e.relevance!==t.relevance)return t.relevance-e.relevance
+                ;if(e.language&&t.language){ if(O(e.language).supersetOf===t.language)return 1
+                ;if(O(t.language).supersetOf===e.language)return-1 }return 0 })),[o,a]=r,c=o
+;return c.secondBest=a,c }function w(e){ let t=null;const n=(e=>{
+            let t=e.className+' ';t+=e.parentNode?e.parentNode.className:''
+            ;const n=p.languageDetectRe.exec(t);if(n){ const t=O(n[1])
+;return t||(X(a.replace('{}',n[1])),
+            X('Falling back to no-highlight mode for this block.',e)),t?n[1]:'no-highlight' }
+            return t.split(/\s+/).find((e=>b(e)||O(e))) })(e);if(b(n))return
+        ;if(N('before:highlightElement',{el:e,language:n
+        }),e.dataset.highlighted)return console.log('Element previously highlighted. To highlight again, first unset `dataset.highlighted`.',e)
+        ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn('One of your code blocks includes unescaped HTML. This is a potentially serious security risk.'),
+        console.warn('https://github.com/highlightjs/highlight.js/wiki/security'),
+        console.warn('The element with unescaped HTML:'),
+        console.warn(e)),p.throwUnescapedHTML))throw new J('One of your code blocks includes unescaped HTML.',e.innerHTML)
+        ;t=e;const i=t.textContent,r=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
+;e.innerHTML=r.value,e.dataset.highlighted='yes',((e,t,n)=>{ const i=t&&s[t]||n
+;e.classList.add('hljs'),e.classList.add('language-'+i)
+        })(e,n,r.language),e.result={language:r.language,re:r.relevance,
+            relevance:r.relevance},r.secondBest&&(e.secondBest={
+            language:r.secondBest.language,relevance:r.secondBest.relevance
+        }),N('after:highlightElement',{el:e,result:r,text:i}) }let y=!1;function _(){
+            'loading'!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0
+        }function O(e){ return e=(e||'').toLowerCase(),i[e]||i[s[e]] }
+        function k(e,{languageName:t}){ 'string'==typeof e&&(e=[e]),e.forEach((e=>{
+            s[e.toLowerCase()]=t })) }function v(e){ const t=O(e)
+;return t&&!t.disableAutodetect }function N(e,t){ const n=e;r.forEach((e=>{
+            e[n]&&e[n](t) })) }
+        'undefined'!=typeof window&&window.addEventListener&&window.addEventListener('DOMContentLoaded',(()=>{
+            y&&_() }),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_,
+            highlightElement:w,
+            highlightBlock:e=>(G('10.7.0','highlightBlock will be removed entirely in v12.0'),
+            G('10.7.0','Please use highlightElement now.'),w(e)),configure:e=>{ p=Q(p,e) },
+            initHighlighting:()=>{
+                _(),G('10.6.0','initHighlighting() deprecated.  Use highlightAll() now.') },
+            initHighlightingOnLoad:()=>{
+                _(),G('10.6.0','initHighlightingOnLoad() deprecated.  Use highlightAll() now.')
+            },registerLanguage:(e,t)=>{ let s=null;try{ s=t(n) }catch(t){
+                if(W('Language definition for \'{}\' could not be registered.'.replace('{}',e)),
+                !o)throw t;W(t),s=l }
+            s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&k(s.aliases,{
+                languageName:e}) },unregisterLanguage:e=>{ delete i[e]
+            ;for(const t of Object.keys(s))s[t]===e&&delete s[t] },
+            listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:k,
+            autoDetection:v,inherit:Q,addPlugin:e=>{ (e=>{
+                e['before:highlightBlock']&&!e['before:highlightElement']&&(e['before:highlightElement']=t=>{
+                    e['before:highlightBlock'](Object.assign({block:t.el},t))
+                }),e['after:highlightBlock']&&!e['after:highlightElement']&&(e['after:highlightElement']=t=>{
+                    e['after:highlightBlock'](Object.assign({block:t.el},t)) }) })(e),r.push(e) },
+            removePlugin:e=>{ const t=r.indexOf(e);-1!==t&&r.splice(t,1) }}),n.debugMode=()=>{
+            o=!1 },n.safeMode=()=>{ o=!0 },n.versionString='11.9.0',n.regex={concat:h,
+            lookahead:g,either:f,optional:d,anyNumberOfTimes:u}
+        ;for(const t in A)'object'==typeof A[t]&&e(A[t]);return Object.assign(n,A),n
+    },ne=te({});ne.newInstance=()=>te({});export{ne as default};

+ 80 - 0
src/components/form-create-designer/utils/highlight/javascript.min.js

@@ -0,0 +1,80 @@
+/*! `javascript` grammar compiled for Highlight.js 11.9.0 */
+var hljsGrammar=(()=>{ 'use strict'
+;const e='[A-Za-z$_][0-9A-Za-z$_]*',n=['as','in','of','if','for','while','finally','var','new','function','do','return','void','else','break','catch','instanceof','with','throw','case','default','try','switch','continue','typeof','delete','let','yield','const','class','debugger','async','await','static','import','from','export','extends'],a=['true','false','null','undefined','NaN','Infinity'],t=['Object','Function','Boolean','Symbol','Math','Date','Number','BigInt','String','RegExp','Array','Float32Array','Float64Array','Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Int32Array','Uint16Array','Uint32Array','BigInt64Array','BigUint64Array','Set','Map','WeakSet','WeakMap','ArrayBuffer','SharedArrayBuffer','Atomics','DataView','JSON','Promise','Generator','GeneratorFunction','AsyncFunction','Reflect','Proxy','Intl','WebAssembly'],s=['Error','EvalError','InternalError','RangeError','ReferenceError','SyntaxError','TypeError','URIError'],r=['setInterval','setTimeout','clearInterval','clearTimeout','require','exports','eval','isFinite','isNaN','parseFloat','parseInt','decodeURI','decodeURIComponent','encodeURI','encodeURIComponent','escape','unescape'],c=['arguments','this','super','console','window','document','localStorage','sessionStorage','module','global'],i=[].concat(r,t,s)
+;return o=>{ const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/,
+        end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{
+            const a=e[0].length+e.index,t=e.input[a]
+;if('<'===t||','===t)return void n.ignoreMatch();let s
+;'>'===t&&(((e,{after:n})=>{ const a='</'+e[0].slice(1)
+;return-1!==e.input.indexOf(a,n) })(e,{after:a})||n.ignoreMatch())
+            ;const r=e.input.substring(a)
+;((s=r.match(/^\s*=/))||(s=r.match(/^\s+extends\s+/))&&0===s.index)&&n.ignoreMatch()
+        }},g={$pattern:e,keyword:n,literal:a,built_in:i,'variable.language':c
+    },u='[0-9](_?[0-9])*',m=`\\.(${u})`,E='0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*',A={
+        className:'number',variants:[{
+            begin:`(\\b(${E})((${m})|\\.)?|(${m}))[eE][+-]?(${u})\\b`},{
+            begin:`\\b(${E})\\b((${m})\\b|\\.)?|(${m})\\b`},{
+            begin:'\\b(0|[1-9](_?[0-9])*)n\\b'},{
+            begin:'\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b'},{
+            begin:'\\b0[bB][0-1](_?[0-1])*n?\\b'},{begin:'\\b0[oO][0-7](_?[0-7])*n?\\b'},{
+            begin:'\\b0[0-7]+n?\\b'}],relevance:0},y={className:'subst',begin:'\\$\\{',
+        end:'\\}',keywords:g,contains:[]},h={begin:'html`',end:'',starts:{end:'`',
+        returnEnd:!1,contains:[o.BACKSLASH_ESCAPE,y],subLanguage:'xml'}},N={
+        begin:'css`',end:'',starts:{end:'`',returnEnd:!1,
+            contains:[o.BACKSLASH_ESCAPE,y],subLanguage:'css'}},_={begin:'gql`',end:'',
+        starts:{end:'`',returnEnd:!1,contains:[o.BACKSLASH_ESCAPE,y],
+            subLanguage:'graphql'}},f={className:'string',begin:'`',end:'`',
+        contains:[o.BACKSLASH_ESCAPE,y]},p={className:'comment',
+        variants:[o.COMMENT(/\/\*\*(?!\/)/,'\\*/',{relevance:0,contains:[{
+            begin:'(?=@[A-Za-z]+)',relevance:0,contains:[{className:'doctag',
+                begin:'@[A-Za-z]+'},{className:'type',begin:'\\{',end:'\\}',excludeEnd:!0,
+                excludeBegin:!0,relevance:0},{className:'variable',begin:d+'(?=\\s*(-)|$)',
+                endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]
+        }),o.C_BLOCK_COMMENT_MODE,o.C_LINE_COMMENT_MODE]
+    },v=[o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,h,N,_,f,{match:/\$\d+/},A]
+;y.contains=v.concat({begin:/\{/,end:/\}/,keywords:g,contains:['self'].concat(v)
+});const S=[].concat(p,y.contains),w=S.concat([{begin:/\(/,end:/\)/,keywords:g,
+        contains:['self'].concat(S)}]),R={className:'params',begin:/\(/,end:/\)/,
+        excludeBegin:!0,excludeEnd:!0,keywords:g,contains:w},O={variants:[{
+        match:[/class/,/\s+/,d,/\s+/,/extends/,/\s+/,l.concat(d,'(',l.concat(/\./,d),')*')],
+        scope:{1:'keyword',3:'title.class',5:'keyword',7:'title.class.inherited'}},{
+        match:[/class/,/\s+/,d],scope:{1:'keyword',3:'title.class'}}]},k={relevance:0,
+        match:l.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/),
+        className:'title.class',keywords:{_:[...t,...s]}},I={variants:[{
+        match:[/function/,/\s+/,d,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],
+    className:{1:'keyword',3:'title.function'},label:'func.def',contains:[R],
+    illegal:/%/},x={
+        match:l.concat(/\b/,(T=[...r,'super','import'],l.concat('(?!',T.join('|'),')')),d,l.lookahead(/\(/)),
+        className:'title.function',relevance:0};var T;const C={
+        begin:l.concat(/\./,l.lookahead(l.concat(d,/(?![0-9A-Za-z$_(])/))),end:d,
+        excludeBegin:!0,keywords:'prototype',className:'property',relevance:0},M={
+        match:[/get|set/,/\s+/,d,/(?=\()/],className:{1:'keyword',3:'title.function'},
+        contains:[{begin:/\(\)/},R]
+    },B='(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|'+o.UNDERSCORE_IDENT_RE+')\\s*=>',$={
+        match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)],
+        keywords:'async',className:{1:'keyword',3:'title.function'},contains:[R]}
+;return{name:'JavaScript',aliases:['js','jsx','mjs','cjs'],keywords:g,exports:{
+    PARAMS_CONTAINS:w,CLASS_REFERENCE:k},illegal:/#(?![$_A-z])/,
+contains:[o.SHEBANG({label:'shebang',binary:'node',relevance:5}),{
+    label:'use_strict',className:'meta',relevance:10,
+    begin:/^\s*['"]use (strict|asm)['"]/
+},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,h,N,_,f,p,{match:/\$\d+/},A,k,{
+    className:'attr',begin:d+l.lookahead(':'),relevance:0},$,{
+    begin:'('+o.RE_STARTERS_RE+'|\\b(case|return|throw)\\b)\\s*',
+    keywords:'return throw case',relevance:0,contains:[p,o.REGEXP_MODE,{
+        className:'function',begin:B,returnBegin:!0,end:'\\s*=>',contains:[{
+            className:'params',variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{
+                className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,
+                excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0},{match:/\s+/,
+        relevance:0},{variants:[{begin:'<>',end:'</>'},{
+        match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin,
+        'on:begin':b.isTrulyOpeningTag,end:b.end}],subLanguage:'xml',contains:[{
+        begin:b.begin,end:b.end,skip:!0,contains:['self']}]}]},I,{
+    beginKeywords:'while if switch catch for'},{
+    begin:'\\b(?!function)'+o.UNDERSCORE_IDENT_RE+'\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{',
+    returnBegin:!0,label:'func.def',contains:[R,o.inherit(o.TITLE_MODE,{begin:d,
+        className:'title.function'})]},{match:/\.\.\./,relevance:0},C,{match:'\\$'+d,
+    relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:'title.function'},
+    contains:[R]},x,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
+    className:'variable.constant'},O,M,{match:/\$[(.]/}]} } })()
+;export default hljsGrammar;

+ 1 - 0
src/components/form-create-designer/utils/highlight/style.css

@@ -0,0 +1 @@
+pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#000;background:#fff}.hljs-subst,.hljs-title{font-weight:400;color:#000}.hljs-title.function_{color:#7a7a43}.hljs-code,.hljs-comment,.hljs-quote{color:#8c8c8c;font-style:italic}.hljs-meta{color:#9e880d}.hljs-section{color:#871094}.hljs-built_in,.hljs-keyword,.hljs-literal,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag,.hljs-symbol,.hljs-template-tag,.hljs-type,.hljs-variable.language_{color:#0033b3}.hljs-attr,.hljs-property{color:#871094}.hljs-attribute{color:#174ad4}.hljs-number{color:#1750eb}.hljs-regexp{color:#264eff}.hljs-link{text-decoration:underline;color:#006dcc}.hljs-meta .hljs-string,.hljs-string{color:#067d17}.hljs-char.escape_{color:#0037a6}.hljs-doctag{text-decoration:underline}.hljs-template-variable{color:#248f8f}.hljs-addition{background:#bee6be}.hljs-deletion{background:#d6d6d6}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

+ 29 - 0
src/components/form-create-designer/utils/highlight/xml.min.js

@@ -0,0 +1,29 @@
+/*! `xml` grammar compiled for Highlight.js 11.9.0 */
+var hljsGrammar=(()=>{ 'use strict';return e=>{
+    const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={
+            className:'symbol',begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/,
+            contains:[{className:'keyword',begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
+        },i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{
+            className:'string'}),l=e.inherit(e.QUOTE_STRING_MODE,{className:'string'}),r={
+            endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:'attr',
+                begin:/[\p{L}0-9._:-]+/u,relevance:0},{begin:/=\s*/,relevance:0,contains:[{
+                className:'string',endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[s]},{
+                    begin:/'/,end:/'/,contains:[s]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
+        name:'HTML, XML',
+        aliases:['html','xhtml','rss','atom','xjb','xsd','xsl','plist','wsf','svg'],
+        case_insensitive:!0,unicodeRegex:!0,contains:[{className:'meta',begin:/<![a-z]/,
+            end:/>/,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{
+                className:'meta',begin:/<![a-z]/,end:/>/,contains:[t,i,l,c]}]}]
+        },e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,
+            relevance:10},s,{className:'meta',end:/\?>/,variants:[{begin:/<\?xml/,
+            relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:'tag',
+            begin:/<style(?=\s|>)/,end:/>/,keywords:{name:'style'},contains:[r],starts:{
+                end:/<\/style>/,returnEnd:!0,subLanguage:['css','xml']}},{className:'tag',
+            begin:/<script(?=\s|>)/,end:/>/,keywords:{name:'script'},contains:[r],starts:{
+                end:/<\/script>/,returnEnd:!0,subLanguage:['javascript','handlebars','xml']}},{
+            className:'tag',begin:/<>|<\/>/},{className:'tag',
+            begin:a.concat(/</,a.lookahead(a.concat(n,a.either(/\/>/,/>/,/\s/)))),
+            end:/\/?>/,contains:[{className:'name',begin:n,relevance:0,starts:r}]},{
+            className:'tag',begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{
+                className:'name',begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} }
+})();export default hljsGrammar;

+ 412 - 0
src/components/form-create-designer/utils/index.js

@@ -0,0 +1,412 @@
+import is, {hasProperty} from '@form-create/utils/lib/type';
+import {parseFn} from '@form-create/utils/lib/json';
+import toCase from '@form-create/utils/lib/tocase';
+import {computed, isRef, ref, unref} from 'vue';
+import ZhCn from '../locale/zh-cn';
+import {message} from './message';
+
+export {formTemplate, formTemplateV3, htmlTemplate} from './template';
+
+
+export function makeRequiredRule() {
+    return {
+        type: 'Required', field: 'formCreate$required', title: '是否必填'
+    };
+}
+
+export function addAutoKeyMap(cm) {
+}
+
+export function makeTreeOptions(pre, config, level, data = []) {
+    if (!config.id) {
+        config.id = 1;
+    }
+    level && level--;
+    for (let i = 0; i < 3; i++) {
+        const item = {
+            [config.label]: pre + level * 10 + (i + 1),
+            [config.value]: '' + config.id++,
+        };
+        if (level) {
+            makeTreeOptions(pre, config, level, item.children = []);
+        }
+        data.push(item);
+    }
+    return data;
+}
+
+export function makeOptionsRule(t, to) {
+    const options = [
+        {'label': t('fetch.optionsType.struct'), 'value': 2},
+        {'label': t('fetch.optionsType.fetch'), 'value': 1},
+    ];
+
+    const control = [
+        {
+            value: 1,
+            rule: [
+                {
+                    type: 'FetchConfig',
+                    field: 'formCreateEffect>fetch',
+                    props: {
+                        to
+                    }
+                }
+            ]
+        },
+        {
+            value: 2,
+            rule: [
+                {
+                    type: 'TableOptions',
+                    field: 'formCreate' + upper(to).replace('.', '>'),
+                    props: {
+                        column: [{label: t('props.key'), key: 'label'}, {value: true, label: t('props.value'), key: 'value'}],
+                        keyValue: 'label'
+                    }
+                },
+            ],
+        }
+    ];
+
+    return {
+        type: 'radio',
+        title: t('props.options'),
+        field: '_optionType',
+        value: 2,
+        options,
+        props: {
+            type: 'button'
+        },
+        control
+    };
+}
+
+export function makeTreeOptionsRule(t, to, label, value) {
+    const options = [
+        {'label': t('fetch.optionsType.struct'), 'value': 2},
+        {'label': t('fetch.optionsType.fetch'), 'value': 1},
+    ];
+
+    const control = [
+        {
+            value: 1,
+            rule: [
+                {
+                    type: 'FetchConfig',
+                    field: 'formCreateEffect>fetch',
+                    props: {
+                        to
+                    }
+                }
+            ]
+        },
+        {
+            value: 2,
+            rule: [
+                {
+                    type: 'TreeOptions',
+                    field: 'formCreate' + upper(to).replace('.', '>'),
+                    props: {
+                        columns: {
+                            label,
+                            value
+                        },
+                        keyValue: label,
+                    }
+                },
+            ],
+        }
+    ];
+
+    return {
+        type: 'radio',
+        title: t('props.options'),
+        field: '_optionType',
+        value: 2,
+        options,
+        props: {
+            type: 'button'
+        },
+        control
+    };
+}
+
+export function upper(str) {
+    return str.replace(str[0], str[0].toLocaleUpperCase());
+}
+
+export const toJSON = function (val) {
+    const type = /object ([a-zA-Z]*)/.exec(Object.prototype.toString.call(val));
+    if (type && _toJSON[type[1].toLowerCase()]) {
+        return _toJSON[type[1].toLowerCase()](val);
+    } else {
+        return val;
+    }
+};
+
+const _toJSON = {
+    object: function (val) {
+        var json = [];
+        for (var i in val) {
+            if (!hasProperty(val, i)) continue;
+            json.push(
+                toJSON(i) + ': ' +
+                ((val[i] != null) ? toJSON(val[i]) : 'null')
+            );
+        }
+        return '{\n ' + json.join(',\n ') + '\n}';
+    },
+    function: function (val) {
+        val = '' + val;
+        var exec = (/^ *([\w]+) *\(/).exec(val);
+        if (exec && exec[1] !== 'function') {
+            return 'function ' + val;
+        }
+        return val;
+    },
+    array: function (val) {
+        for (var i = 0, json = []; i < val.length; i++)
+            json[i] = (val[i] != null) ? toJSON(val[i]) : 'null';
+        return '[' + json.join(', ') + ']';
+    },
+    string: function (val) {
+        var tmp = val.split('');
+        for (var i = 0; i < tmp.length; i++) {
+            var c = tmp[i];
+            (c >= ' ') ?
+                (c === '\\') ? (tmp[i] = '\\\\') :
+                    (c === '"') ? (tmp[i] = '\\"') : 0 :
+                (tmp[i] =
+                        (c === '\n') ? '\\n' :
+                            (c === '\r') ? '\\r' :
+                                (c === '\t') ? '\\t' :
+                                    (c === '\b') ? '\\b' :
+                                        (c === '\f') ? '\\f' :
+                                            (c = c.charCodeAt(), ('\\u00' + ((c > 15) ? 1 : 0) + (c % 16)))
+                );
+        }
+        return '"' + tmp.join('') + '"';
+    }
+};
+
+export const deepParseFn = function (target) {
+    if (target && typeof target === 'object') {
+        for (let key in target) {
+            if (Object.prototype.hasOwnProperty.call(target, key)) {
+                let data = target[key];
+                if (Array.isArray(data) || is.Object(data)) {
+                    deepParseFn(data);
+                }
+                if (is.String(data)) {
+                    target[key] = parseFn(data);
+                }
+            }
+        }
+    }
+    return target;
+};
+
+
+export function deepGet(object, path, defaultValue) {
+    path = (path || '').split('.');
+
+    let index = 0,
+        length = path.length;
+
+    while (object != null && index < length) {
+        object = object[path[index++]];
+    }
+    return (index && index === length) ? (object !== undefined ? object : defaultValue) : defaultValue;
+}
+
+export const buildTranslator = (locale) => (path, option) => translate(path, option, unref(locale));
+
+export const translate = (path, option, locale) =>
+    deepGet(locale, path, '').replace(
+        /\{(\w+)\}/g,
+        (_, key) => `${option?.[key] ?? `{${key}}`}`
+    )
+
+export const buildLocaleContext = (locale) => {
+    const lang = computed(() => unref(locale).name)
+    const name = computed(() => upper(toCase(lang.value || '')))
+    const localeRef = isRef(locale) ? locale : ref(locale)
+    return {
+        lang,
+        name,
+        locale: localeRef,
+        t: buildTranslator(locale),
+    }
+}
+
+export const useLocale = (locale) => {
+    return buildLocaleContext(computed(() => locale.value || ZhCn))
+}
+
+export const localeOptions = (t, options, prefix) => {
+    return options.map(opt => {
+        opt.label = t((prefix || 'props') + '.' + opt.label || opt.value) || opt.label;
+        return opt;
+    })
+}
+
+export const localeProps = (t, prefix, rules) => {
+    return rules.map(rule => {
+        if (rule.field === 'formCreate$required') {
+            rule.title = t('validate.required') || rule.title;
+        } else if (rule.field && rule.field !== '_optionType') {
+            rule.title = t('com.' + prefix + '.' + rule.field) || rule.title;
+        }
+        if (rule.type === 'template' && is.trueArray(rule.children)) {
+            rule.children = localeProps(t, prefix, rule.children);
+        }
+        return rule;
+    })
+}
+
+export const getRuleTree = (children) => {
+    const tree = [];
+    children && children.forEach(rule => {
+        if (rule._fc_drag_tag) {
+            const item = {
+                id: rule.__fc__.id,
+                rule,
+                children: getRuleTree(rule.children),
+            };
+            if (!item.children.length) {
+                delete item.children;
+            }
+            tree.push(item);
+        } else {
+            tree.push(...getRuleTree(rule.children));
+        }
+    });
+    return tree;
+}
+
+
+export const getFormRuleDescription = (tree) => {
+    const getTree = (children) => {
+        const tree = [];
+        children && children.forEach(rule => {
+            if (rule.field) {
+                rule.children = getTree(rule.children || []);
+                if (!rule.children.length) {
+                    delete rule.children;
+                }
+                tree.push(rule);
+            } else {
+                tree.push(...getTree(rule.children || []));
+            }
+        });
+        return tree;
+    }
+    return getTree(tree);
+};
+
+export const getRuleDescription = (children) => {
+    const getTree = (children) => {
+        const tree = [];
+        children && children.forEach(rule => {
+            if (typeof rule !== 'object') {
+                return;
+            }
+            if (rule._fc_drag_tag) {
+                const item = {
+                    _fc_id: rule._fc_id,
+                    type: rule.type,
+                    field: rule.field,
+                    title: rule.title,
+                    name: rule.name,
+                    slot: rule.slot,
+                    props: {...rule.props || {}},
+                    children: getTree(rule.children || [])
+                };
+                if (rule.children && typeof rule.children[0] === 'string') {
+                    item.content = rule.children[0];
+                }
+                if (!item.children.length) {
+                    delete item.children;
+                }
+                tree.push(item);
+            } else {
+                tree.push(...getTree(rule.children));
+            }
+        });
+        return tree;
+    }
+    return getTree(children);
+};
+
+export function getInjectArg(t) {
+    return {
+        name: '$inject',
+        columns: [
+            {label: '$inject.api', info: t('event.inject.api'), type: 'Api'},
+            {label: '$inject.rule', info: t('event.inject.rule'), type: 'Rule[]'},
+            {label: '$inject.self', info: t('event.inject.self'), type: 'Rule'},
+            {label: '$inject.option', info: t('event.inject.option'), type: 'Object'},
+            {label: '$inject.args', info: t('event.inject.args'), type: 'Array'},
+        ]
+    }
+}
+
+export function isElementInside(x, y, element) {
+    const rect = element.getBoundingClientRect();
+    return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
+}
+
+export function isNull(v) {
+    return ['', null, undefined].indexOf(v) !== -1;
+}
+
+export function escapeRegExp(str) {
+    return str.replace(/[\ .*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+export function compareVersion(v1, v2) {
+    const a1 = v1.split('.');
+    const a2 = v2.split('.');
+    const minLength = Math.min(a1.length, a2.length);
+
+    for (var i = 0; i < minLength; i++) {
+        var diff = parseInt(a1[i], 10) - parseInt(a2[i], 10);
+        if (diff > 0) {
+            return 1;
+        } else if (diff < 0) {
+            return -1;
+        }
+    }
+
+    return a1.length === a2.length ? 0 : (a1.length < a2.length ? -1 : 1);
+}
+
+export function copyTextToClipboard(text) {
+    const textArea = document.createElement('textarea');
+
+    textArea.style.position = 'fixed';
+    textArea.style.top = 0;
+    textArea.style.left = '-9999px';
+
+    textArea.value = text;
+
+    document.body.appendChild(textArea);
+
+    textArea.focus();
+    textArea.select();
+
+    try {
+        document.execCommand('copy');
+    } catch (err) {
+        console.log('Oops, unable to copy');
+    }
+
+    message('已复制!', 'success');
+
+    document.body.removeChild(textArea);
+}
+
+export function uniqueArray(arr) {
+    return arr.filter((item, index) => arr.indexOf(item) === index);
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов