Ver código fonte

feat: 增加表单设计器 (#2533)

wwsheng009 2 anos atrás
pai
commit
c5b39f2c16
47 arquivos alterados com 7783 adições e 2 exclusões
  1. 2 1
      package.json
  2. 34 0
      src/router/routes/modules/form-design/main.ts
  3. 7 0
      src/views/form-design/assets/iconfont/index.js
  4. 81 0
      src/views/form-design/components/VFormCreate/components/FormRender.vue
  5. 153 0
      src/views/form-design/components/VFormCreate/index.vue
  6. 81 0
      src/views/form-design/components/VFormDesign/components/CodeModal.vue
  7. 245 0
      src/views/form-design/components/VFormDesign/components/ComponentProps.vue
  8. 66 0
      src/views/form-design/components/VFormDesign/components/FormItemColumnProps.vue
  9. 148 0
      src/views/form-design/components/VFormDesign/components/FormItemProps.vue
  10. 55 0
      src/views/form-design/components/VFormDesign/components/FormNode.vue
  11. 76 0
      src/views/form-design/components/VFormDesign/components/FormNodeOperate.vue
  12. 114 0
      src/views/form-design/components/VFormDesign/components/FormOptions.vue
  13. 117 0
      src/views/form-design/components/VFormDesign/components/FormProps.vue
  14. 139 0
      src/views/form-design/components/VFormDesign/components/ImportJsonModal.vue
  15. 66 0
      src/views/form-design/components/VFormDesign/components/JsonModal.vue
  16. 135 0
      src/views/form-design/components/VFormDesign/components/LayoutItem.vue
  17. 97 0
      src/views/form-design/components/VFormDesign/components/PreviewCode.vue
  18. 295 0
      src/views/form-design/components/VFormDesign/components/RuleProps.vue
  19. 1148 0
      src/views/form-design/components/VFormDesign/config/componentPropsConfig.ts
  20. 351 0
      src/views/form-design/components/VFormDesign/config/formItemPropsConfig.ts
  21. 365 0
      src/views/form-design/components/VFormDesign/index.vue
  22. 106 0
      src/views/form-design/components/VFormDesign/modules/CollapseItem.vue
  23. 169 0
      src/views/form-design/components/VFormDesign/modules/FormComponentPanel.vue
  24. 100 0
      src/views/form-design/components/VFormDesign/modules/PropsPanel.vue
  25. 141 0
      src/views/form-design/components/VFormDesign/modules/Toolbar.vue
  26. 231 0
      src/views/form-design/components/VFormDesign/styles/drag.less
  27. 522 0
      src/views/form-design/components/VFormDesign/styles/v-form-design.less
  28. 15 0
      src/views/form-design/components/VFormDesign/styles/variable.less
  29. 228 0
      src/views/form-design/components/VFormItem/index.vue
  30. 79 0
      src/views/form-design/components/VFormItem/vFormItem.vue
  31. 108 0
      src/views/form-design/components/VFormPreview/index.vue
  32. 74 0
      src/views/form-design/components/VFormPreview/useForm.vue
  33. 71 0
      src/views/form-design/components/index.ts
  34. 423 0
      src/views/form-design/core/formItemConfig.ts
  35. 739 0
      src/views/form-design/core/iconConfig.ts
  36. 37 0
      src/views/form-design/examples/baseForm.vue
  37. 18 0
      src/views/form-design/hooks/useFormDesignState.ts
  38. 62 0
      src/views/form-design/hooks/useFormInstanceMethods.ts
  39. 195 0
      src/views/form-design/hooks/useVFormMethods.ts
  40. 9 0
      src/views/form-design/index.vue
  41. 48 0
      src/views/form-design/tests/import1.json
  42. 10 0
      src/views/form-design/typings/base-type.ts
  43. 52 0
      src/views/form-design/typings/form-type.ts
  44. 349 0
      src/views/form-design/typings/v-form-component.ts
  45. 200 0
      src/views/form-design/utils/index.ts
  46. 21 0
      src/views/form-design/utils/message.ts
  47. 1 1
      stylelint.config.js

+ 2 - 1
package.json

@@ -73,7 +73,8 @@
     "vxe-table": "^4.3.9",
     "vxe-table-plugin-export-xlsx": "^3.0.4",
     "xe-utils": "^3.5.7",
-    "xlsx": "^0.18.5"
+    "xlsx": "^0.18.5",
+    "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
     "@commitlint/cli": "^16.2.3",

+ 34 - 0
src/router/routes/modules/form-design/main.ts

@@ -0,0 +1,34 @@
+import type { AppRouteModule } from '/@/router/types';
+
+import { LAYOUT } from '/@/router/constant';
+
+const permission: AppRouteModule = {
+  path: '/form-designer',
+  name: 'Form-designer',
+  component: LAYOUT,
+  meta: {
+    orderNo: 10000,
+    icon: 'icon:add-circle',
+    title: '表单设计',
+  },
+  children: [
+    {
+      path: 'design',
+      name: 'Design',
+      meta: {
+        title: '表单设计',
+      },
+      component: () => import('/@/views/form-design/index.vue'),
+    },
+    {
+      path: 'example1',
+      name: 'Example1',
+      meta: {
+        title: '示例',
+      },
+      component: () => import('/@/views/form-design/examples/baseForm.vue'),
+    },
+  ],
+};
+
+export default permission;

Diferenças do arquivo suprimidas por serem muito extensas
+ 7 - 0
src/views/form-design/assets/iconfont/index.js


+ 81 - 0
src/views/form-design/components/VFormCreate/components/FormRender.vue

@@ -0,0 +1,81 @@
+<template>
+  <!-- <component :is="layoutTag" v-bind="schema.colProps"> -->
+  <template v-if="['Grid'].includes(schema.component)">
+    <Row class="grid-row">
+      <Col
+        class="grid-col"
+        v-for="(colItem, index) in schema.columns"
+        :key="index"
+        :span="colItem.span"
+      >
+        <FormRender
+          v-for="(item, k) in colItem.children"
+          :key="k"
+          :schema="item"
+          :formData="formData"
+          :formConfig="formConfig"
+          :setFormModel="setFormModel"
+        />
+      </Col>
+    </Row>
+  </template>
+  <VFormItem
+    v-else
+    :formConfig="formConfig"
+    :schema="schema"
+    :formData="formData"
+    :setFormModel="setFormModel"
+    @change="$emit('change', { schema: schema, value: $event })"
+    @submit="$emit('submit', schema)"
+    @reset="$emit('reset')"
+  >
+    <template
+      v-if="schema.componentProps && schema.componentProps.slotName"
+      #[schema.componentProps!.slotName]
+    >
+      <slot :name="schema.componentProps!.slotName"></slot>
+    </template>
+  </VFormItem>
+  <!-- </component> -->
+</template>
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+  import { IVFormComponent, IFormConfig } from '../../../typings/v-form-component';
+  import VFormItem from '../../VFormItem/index.vue';
+  import { Row, Col } from 'ant-design-vue';
+
+  export default defineComponent({
+    name: 'FormRender',
+    components: {
+      VFormItem,
+      Row,
+      Col,
+    },
+    props: {
+      formData: {
+        type: Object,
+        default: () => ({}),
+      },
+      schema: {
+        type: Object as PropType<IVFormComponent>,
+        default: () => ({}),
+      },
+      formConfig: {
+        type: Object as PropType<IFormConfig>,
+        default: () => [] as IFormConfig[],
+      },
+      setFormModel: {
+        type: Function as PropType<(key: string, value: any) => void>,
+        default: null,
+      },
+    },
+    emits: ['change', 'submit', 'reset'],
+    setup(_props) {},
+  });
+</script>
+
+<style>
+  .v-form-render-item {
+    overflow: hidden;
+  }
+</style>

+ 153 - 0
src/views/form-design/components/VFormCreate/index.vue

@@ -0,0 +1,153 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/29
+ * @Description: 表单渲染器,根据json生成表单
+-->
+<template>
+  <div class="v-form-container">
+    <Form class="v-form-model" ref="eFormModel" :model="formModel" v-bind="formModelProps">
+      <Row>
+        <!-- <component :is="wrapperComp"> -->
+        <FormRender
+          v-for="(schema, index) of noHiddenList"
+          :key="index"
+          :schema="schema"
+          :formConfig="formConfig"
+          :formData="formModelNew"
+          @change="handleChange"
+          :setFormModel="setFormModel"
+          @submit="handleSubmit"
+          @reset="resetFields"
+        >
+          <template v-if="schema && schema.componentProps" #[`schema.componentProps!.slotName`]>
+            <slot
+              :name="schema.componentProps!.slotName"
+              v-bind="{ formModel: formModel, field: schema.field, schema }"
+            ></slot>
+          </template>
+        </FormRender>
+        <!-- </component> -->
+      </Row>
+    </Form>
+  </div>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, PropType, provide, ref, unref } from 'vue';
+  import FormRender from './components/FormRender.vue';
+  import { IFormConfig, AForm } from '../../typings/v-form-component';
+  import { Form, Row, Col } from 'ant-design-vue';
+  import { useFormInstanceMethods } from '../../hooks/useFormInstanceMethods';
+  import { IProps, IVFormMethods, useVFormMethods } from '../../hooks/useVFormMethods';
+  import { useVModel } from '@vueuse/core';
+  import { omit } from 'lodash-es';
+
+  export default defineComponent({
+    name: 'VFormCreate',
+    components: {
+      FormRender,
+      Form,
+      Row,
+    },
+    props: {
+      fApi: {
+        type: Object,
+      },
+      formModel: {
+        type: Object,
+        default: () => ({}),
+      },
+      formConfig: {
+        type: Object as PropType<IFormConfig>,
+        required: true,
+      },
+    },
+    emits: ['submit', 'change', 'update:fApi', 'update:formModel'],
+    setup(props, context) {
+      const wrapperComp = props.formConfig.layout == 'vertical' ? Col : Row;
+      const { emit } = context;
+      const eFormModel = ref<AForm | null>(null);
+
+      const formModelNew = computed({
+        get: () => props.formModel,
+        set: (value) => emit('update:formModel', value),
+      });
+
+      const noHiddenList = computed(() => {
+        return (
+          props.formConfig.schemas &&
+          props.formConfig.schemas.filter((item) => item.hidden !== true)
+        );
+      });
+
+      const fApi = useVModel(props, 'fApi', emit);
+
+      const { submit, validate, clearValidate, resetFields, validateField } =
+        useFormInstanceMethods(props, formModelNew, context, eFormModel);
+
+      const { linkOn, ...methods } = useVFormMethods(
+        { formConfig: props.formConfig, formData: props.formModel } as unknown as IProps,
+        context,
+        eFormModel,
+        {
+          submit,
+          validate,
+          validateField,
+          resetFields,
+          clearValidate,
+        },
+      );
+
+      fApi.value = methods;
+
+      const handleChange = (_event) => {
+        const { schema, value } = _event;
+        const { field } = unref(schema);
+
+        linkOn[field!]?.forEach((formItem) => {
+          // console.log('handleChange', formItem, field, value);
+          formItem.update?.(value, formItem, fApi.value as IVFormMethods);
+        });
+      };
+      /**
+       * 获取表单属性
+       */
+      const formModelProps = computed(
+        () => omit(props.formConfig, ['disabled', 'labelWidth', 'schemas']) as Recordable,
+      );
+
+      const handleSubmit = () => {
+        submit();
+      };
+
+      provide('formModel', formModelNew);
+      const setFormModel = (key, value) => {
+        formModelNew.value[key] = value;
+      };
+
+      provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
+
+      // 把祖先组件的方法项注入到子组件中,子组件可通过inject获取
+      return {
+        eFormModel,
+        submit,
+        validate,
+        validateField,
+        resetFields,
+        clearValidate,
+        handleChange,
+        formModelProps,
+        handleSubmit,
+        setFormModel,
+        formModelNew,
+        wrapperComp,
+        noHiddenList,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .v-form-model {
+    overflow: hidden;
+  }
+</style>

+ 81 - 0
src/views/form-design/components/VFormDesign/components/CodeModal.vue

@@ -0,0 +1,81 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/12/7
+ * @Description: 渲染代码
+-->
+<template>
+  <Modal
+    title="代码"
+    :footer="null"
+    :visible="visible"
+    @cancel="visible = false"
+    wrapClassName="v-code-modal"
+    style="top: 20px"
+    width="850px"
+    :destroyOnClose="true"
+  >
+    <PreviewCode :editorJson="editorVueJson" fileFormat="vue" />
+  </Modal>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, reactive, toRefs } from 'vue';
+  import { formatRules, removeAttrs } from '../../../utils';
+  import PreviewCode from './PreviewCode.vue';
+  import { IFormConfig } from '../../../typings/v-form-component';
+  import { Modal } from 'ant-design-vue';
+
+  const codeVueFront = `<template>
+  <div>
+    <v-form-create
+      :formConfig="formConfig"
+      :formData="formData"
+      v-model="fApi"
+    />
+    <a-button @click="submit">提交</a-button>
+  </div>
+</template>
+<script>
+
+export default {
+  name: 'Demo',
+  data () {
+    return {
+      fApi:{},
+      formData:{},
+      formConfig: `;
+  /* eslint-disable */
+  let codeVueLast = `
+    }
+  },
+  methods: {
+    async submit() {
+      const data = await this.fApi.submit()
+      console.log(data)
+     }
+  }
+}
+<\/script>`;
+  //
+  export default defineComponent({
+    name: 'CodeModal',
+    components: { PreviewCode, Modal },
+    setup() {
+      const state = reactive({
+        visible: false,
+        jsonData: {} as IFormConfig,
+      });
+
+      const showModal = (formConfig: IFormConfig) => {
+        formConfig.schemas && formatRules(formConfig.schemas);
+        state.visible = true;
+        state.jsonData = formConfig;
+      };
+
+      const editorVueJson = computed(() => {
+        return codeVueFront + JSON.stringify(removeAttrs(state.jsonData), null, '\t') + codeVueLast;
+      });
+
+      return { ...toRefs(state), editorVueJson, showModal };
+    },
+  });
+</script>

+ 245 - 0
src/views/form-design/components/VFormDesign/components/ComponentProps.vue

@@ -0,0 +1,245 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/26
+ * @Description: 组件属性控件
+-->
+<template>
+  <div class="properties-content">
+    <div class="properties-body" v-if="formConfig.currentItem">
+      <Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择组件" />
+
+      <Form label-align="left" layout="vertical">
+        <!--    循环遍历渲染组件属性      -->
+
+        <div v-if="formConfig.currentItem && formConfig.currentItem.componentProps">
+          <FormItem v-for="item in inputOptions" :key="item.name" :label="item.label">
+            <!--     处理数组属性,placeholder       -->
+
+            <div v-if="item.children">
+              <component
+                v-for="(child, index) of item.children"
+                :key="index"
+                v-bind="child.componentProps"
+                :is="child.component"
+                v-model:value="formConfig.currentItem.componentProps[item.name][index]"
+              />
+            </div>
+            <!--     如果不是数组,则正常处理属性值       -->
+            <component
+              v-else
+              class="component-prop"
+              v-bind="item.componentProps"
+              :is="item.component"
+              v-model:value="formConfig.currentItem.componentProps[item.name]"
+            />
+          </FormItem>
+          <!-- </Row> -->
+          <FormItem label="控制属性">
+            <Col v-for="item in controlOptions" :key="item.name">
+              <Checkbox
+                v-if="showControlAttrs(item.includes)"
+                v-bind="item.componentProps"
+                v-model:checked="formConfig.currentItem.componentProps[item.name]"
+              >
+                {{ item.label }}
+              </Checkbox>
+            </Col>
+          </FormItem>
+        </div>
+        <FormItem label="关联字段">
+          <Select
+            mode="multiple"
+            v-model:value="formConfig.currentItem['link']"
+            :options="linkOptions"
+          />
+        </FormItem>
+
+        <FormItem
+          label="选项"
+          v-if="
+            [
+              'Select',
+              'CheckboxGroup',
+              'RadioGroup',
+              'TreeSelect',
+              'Cascader',
+              'AutoComplete',
+            ].includes(formConfig.currentItem.component)
+          "
+        >
+          <FormOptions />
+        </FormItem>
+
+        <FormItem label="栅格" v-if="['Grid'].includes(formConfig.currentItem.component)">
+          <FormOptions />
+        </FormItem>
+      </Form>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import {
+    Empty,
+    Input,
+    Form,
+    FormItem,
+    Switch,
+    Checkbox,
+    Select,
+    InputNumber,
+    RadioGroup,
+  } from 'ant-design-vue';
+  import RadioButtonGroup from '/@/components/Form/src/components/RadioButtonGroup.vue';
+  import { Col, Row } from 'ant-design-vue';
+  import { computed, defineComponent, ref, watch } from 'vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import {
+    baseComponentControlAttrs,
+    baseComponentAttrs,
+    baseComponentCommonAttrs,
+    componentPropsFuncs,
+  } from '../../VFormDesign/config/componentPropsConfig';
+  import FormOptions from './FormOptions.vue';
+  import { formItemsForEach, remove } from '../../../utils';
+  import { IBaseFormAttrs } from '../config/formItemPropsConfig';
+
+  export default defineComponent({
+    name: 'ComponentProps',
+    components: {
+      FormOptions,
+      Empty,
+      Input,
+      Form,
+      FormItem,
+      Switch,
+      Checkbox,
+      Select,
+      InputNumber,
+      RadioGroup,
+      RadioButtonGroup,
+      Col,
+      Row,
+    },
+    setup() {
+      // 让compuated属性自动更新
+      // const dummyUpdate = ref(0);
+
+      const allOptions = ref([] as Omit<IBaseFormAttrs, 'tag'>[]);
+      const showControlAttrs = (includes: string[] | undefined) => {
+        if (!includes) return true;
+        return includes.includes(formConfig.value.currentItem!.component);
+      };
+
+      const { formConfig } = useFormDesignState();
+
+      if (formConfig.value.currentItem) {
+        formConfig.value.currentItem.componentProps =
+          formConfig.value.currentItem.componentProps || {};
+      }
+
+      watch(
+        () => formConfig.value.currentItem?.field,
+        (_newValue, oldValue) => {
+          formConfig.value.schemas &&
+            formItemsForEach(formConfig.value.schemas, (item) => {
+              if (item.link) {
+                const index = item.link.findIndex((linkItem) => linkItem === oldValue);
+                index !== -1 && remove(item.link, index);
+              }
+            });
+        },
+      );
+
+      watch(
+        () => formConfig.value.currentItem && formConfig.value.currentItem.component,
+        () => {
+          allOptions.value = [];
+          baseComponentControlAttrs.forEach((item) => {
+            item.category = 'control';
+            if (!item.includes) {
+              // 如果属性没有include,所有的控件都适用
+
+              allOptions.value.push(item);
+            } else if (item.includes.includes(formConfig.value.currentItem!.component)) {
+              // 如果有include,检查是否包含了当前控件类型
+              allOptions.value.push(item);
+            }
+          });
+
+          baseComponentCommonAttrs.forEach((item) => {
+            item.category = 'input';
+            if (item.includes) {
+              if (item.includes.includes(formConfig.value.currentItem!.component)) {
+                allOptions.value.push(item);
+              }
+            } else if (item.exclude) {
+              if (!item.exclude.includes(formConfig.value.currentItem!.component)) {
+                allOptions.value.push(item);
+              }
+            } else {
+              allOptions.value.push(item);
+            }
+          });
+
+          baseComponentAttrs[formConfig.value.currentItem!.component] &&
+            baseComponentAttrs[formConfig.value.currentItem!.component].forEach(async (item) => {
+              if (item.component) {
+                if (['Switch', 'Checkbox', 'Radio'].includes(item.component)) {
+                  item.category = 'control';
+                  allOptions.value.push(item);
+                } else {
+                  item.category = 'input';
+                  allOptions.value.push(item);
+                }
+              }
+            });
+        },
+        {
+          immediate: true,
+        },
+      );
+      // 控制性的选项
+      const controlOptions = computed(() => {
+        return allOptions.value.filter((item) => {
+          return item.category == 'control';
+        });
+      });
+
+      // 非控制性选择
+      const inputOptions = computed(() => {
+        return allOptions.value.filter((item) => {
+          return item.category == 'input';
+        });
+      });
+
+      watch(
+        () => formConfig.value.currentItem!.componentProps,
+        () => {
+          const func = componentPropsFuncs[formConfig.value.currentItem!.component];
+          if (func) {
+            func(formConfig.value.currentItem!.componentProps, allOptions.value);
+          }
+        },
+        {
+          immediate: true,
+          deep: true,
+        },
+      );
+      const linkOptions = computed(() => {
+        return (
+          formConfig.value.schemas &&
+          formConfig.value.schemas
+            .filter((item) => item.key !== formConfig.value.currentItem!.key)
+            .map(({ label, field }) => ({ label: label + '/' + field, value: field }))
+        );
+      });
+      return {
+        formConfig,
+        showControlAttrs,
+        linkOptions,
+        controlOptions,
+        inputOptions,
+      };
+    },
+  });
+</script>

+ 66 - 0
src/views/form-design/components/VFormDesign/components/FormItemColumnProps.vue

@@ -0,0 +1,66 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/24
+ * @Description: 表单项属性
+-->
+<template>
+  <div class="properties-content">
+    <div class="properties-body" v-if="formConfig.currentItem">
+      <Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
+      <Form v-else label-align="left" layout="vertical">
+        <div v-for="item of baseItemColumnProps" :key="item.name">
+          <FormItem :label="item.label" v-if="showProps(item.exclude)">
+            <component
+              v-if="formConfig.currentItem.colProps"
+              class="component-props"
+              v-bind="item.componentProps"
+              :is="item.component"
+              v-model:value="formConfig.currentItem.colProps[item.name]"
+            />
+          </FormItem>
+        </div>
+      </Form>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { baseItemColumnProps } from '../config/formItemPropsConfig';
+
+  import { Empty, Input, Form, FormItem, Switch, Checkbox, Select, Slider } from 'ant-design-vue';
+  import RuleProps from './RuleProps.vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { isArray } from 'lodash-es';
+
+  export default defineComponent({
+    name: 'FormItemProps',
+    components: {
+      RuleProps,
+      Empty,
+      Input,
+      Form,
+      FormItem,
+      Switch,
+      Checkbox,
+      Select,
+      Slider,
+    },
+    // props: {} as PropsOptions,
+
+    setup() {
+      const { formConfig } = useFormDesignState();
+      const showProps = (exclude: string[] | undefined) => {
+        if (!exclude) {
+          return true;
+        }
+
+        return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true;
+      };
+      return {
+        baseItemColumnProps,
+        formConfig,
+        showProps,
+      };
+    },
+  });
+</script>

+ 148 - 0
src/views/form-design/components/VFormDesign/components/FormItemProps.vue

@@ -0,0 +1,148 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/24
+ * @Description: 表单项属性,控件属性面板
+-->
+<template>
+  <div class="properties-content">
+    <div class="properties-body" v-if="formConfig.currentItem?.itemProps">
+      <Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
+      <Form v-else label-align="left" layout="vertical">
+        <div v-for="item of baseFormItemProps" :key="item.name">
+          <FormItem :label="item.label" v-if="showProps(item.exclude)">
+            <component
+              class="component-props"
+              v-bind="item.componentProps"
+              :is="item.component"
+              v-model:value="formConfig.currentItem[item.name]"
+            />
+          </FormItem>
+        </div>
+        <div v-for="item of advanceFormItemProps" :key="item.name">
+          <FormItem :label="item.label" v-if="showProps(item.exclude)">
+            <component
+              class="component-props"
+              v-bind="item.componentProps"
+              :is="item.component"
+              v-model:value="formConfig.currentItem.itemProps[item.name]"
+            />
+          </FormItem> </div
+        ><div v-for="item of advanceFormItemColProps" :key="item.name">
+          <FormItem :label="item.label" v-if="showProps(item.exclude)">
+            <component
+              class="component-props"
+              v-bind="item.componentProps"
+              :is="item.component"
+              v-model:value="formConfig.currentItem.itemProps[item.name]['span']"
+            />
+          </FormItem>
+        </div>
+        <FormItem label="控制属性" v-if="controlPropsList.length">
+          <Col v-for="item of controlPropsList" :key="item.name">
+            <Checkbox v-model:checked="formConfig.currentItem.itemProps[item.name]">
+              {{ item.label }}
+            </Checkbox>
+          </Col>
+        </FormItem>
+        <FormItem label="是否必选" v-if="!['Grid'].includes(formConfig.currentItem.component)">
+          <Switch v-model:checked="formConfig.currentItem.itemProps['required']" />
+          <Input
+            v-if="formConfig.currentItem.itemProps['required']"
+            v-model:value="formConfig.currentItem.itemProps['message']"
+            placeholder="请输入必选提示"
+          />
+        </FormItem>
+        <FormItem
+          v-if="!['Grid'].includes(formConfig.currentItem.component)"
+          label="校验规则"
+          :class="{ 'form-rule-props': !!formConfig.currentItem.itemProps['rules'] }"
+        >
+          <RuleProps />
+        </FormItem>
+      </Form>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, watch } from 'vue';
+  import {
+    baseFormItemControlAttrs,
+    baseFormItemProps,
+    advanceFormItemProps,
+    advanceFormItemColProps,
+  } from '../../VFormDesign/config/formItemPropsConfig';
+
+  import {
+    Empty,
+    Input,
+    Form,
+    FormItem,
+    Switch,
+    Checkbox,
+    Select,
+    Slider,
+    Col,
+    RadioGroup,
+  } from 'ant-design-vue';
+  import RuleProps from './RuleProps.vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { isArray } from 'lodash-es';
+
+  export default defineComponent({
+    name: 'FormItemProps',
+    components: {
+      RuleProps,
+      Empty,
+      Input,
+      Form,
+      FormItem,
+      Switch,
+      Checkbox,
+      Select,
+      Slider,
+      Col,
+      RadioGroup,
+    },
+    // props: {} as PropsOptions,
+
+    setup() {
+      const { formConfig } = useFormDesignState();
+
+      watch(
+        () => formConfig.value,
+        () => {
+          if (formConfig.value.currentItem) {
+            formConfig.value.currentItem.itemProps = formConfig.value.currentItem.itemProps || {};
+            formConfig.value.currentItem.itemProps.labelCol =
+              formConfig.value.currentItem.itemProps.labelCol || {};
+            formConfig.value.currentItem.itemProps.wrapperCol =
+              formConfig.value.currentItem.itemProps.wrapperCol || {};
+          }
+        },
+        { deep: true, immediate: true },
+      );
+      const showProps = (exclude: string[] | undefined) => {
+        if (!exclude) {
+          return true;
+        }
+        return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true;
+      };
+
+      const controlPropsList = computed(() => {
+        // console.log('const list2 = computed(() => {');
+        return baseFormItemControlAttrs.filter((item) => {
+          return showProps(item.exclude);
+        });
+      });
+
+      return {
+        baseFormItemProps,
+        advanceFormItemProps,
+        advanceFormItemColProps,
+        formConfig,
+        controlPropsList,
+        showProps,
+      };
+    },
+  });
+</script>

+ 55 - 0
src/views/form-design/components/VFormDesign/components/FormNode.vue

@@ -0,0 +1,55 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/19
+ * @Description: 拖拽节点控件
+-->
+<template>
+  <div
+    class="drag-move-box"
+    @click.stop="handleSelectItem"
+    :class="{ active: schema.key === formConfig.currentItem?.key }"
+  >
+    <div class="form-item-box">
+      <VFormItem :formConfig="formConfig" :schema="schema" />
+    </div>
+    <div class="show-key-box">
+      {{ schema.label + (schema.field ? '/' + schema.field : '') }}
+    </div>
+    <FormNodeOperate :schema="schema" :currentItem="formConfig.currentItem" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, toRefs, PropType } from 'vue';
+  import { IVFormComponent } from '../../../typings/v-form-component';
+  import FormNodeOperate from './FormNodeOperate.vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import VFormItem from '../../VFormItem/index.vue';
+  // import VFormItem from '../../VFormItem/vFormItem.vue';
+  export default defineComponent({
+    name: 'FormNode',
+    components: {
+      VFormItem,
+      FormNodeOperate,
+    },
+    props: {
+      schema: {
+        type: Object as PropType<IVFormComponent>,
+        required: true,
+      },
+    },
+    setup(props) {
+      const { formConfig, formDesignMethods } = useFormDesignState();
+      const state = reactive({});
+      // 获取 formDesignMethods
+      const handleSelectItem = () => {
+        // 调用 formDesignMethods
+        formDesignMethods.handleSetSelectItem(props.schema);
+      };
+      return {
+        ...toRefs(state),
+        handleSelectItem,
+        formConfig,
+      };
+    },
+  });
+</script>

+ 76 - 0
src/views/form-design/components/VFormDesign/components/FormNodeOperate.vue

@@ -0,0 +1,76 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/11
+ * @Description: 节点操作复制删除控件
+-->
+<template>
+  <div class="copy-delete-box">
+    <a class="copy" :class="activeClass" @click.stop="handleCopy">
+      <Icon icon="ant-design:copy-outlined" />
+    </a>
+    <a class="delete" :class="activeClass" @click.stop="handleDelete">
+      <Icon icon="ant-design:delete-outlined" />
+    </a>
+  </div>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent } from 'vue';
+  import { IVFormComponent } from '../../../typings/v-form-component';
+  import { remove } from '../../../utils';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import Icon from '/@/components/Icon/index';
+
+  export default defineComponent({
+    name: 'FormNodeOperate',
+    components: {
+      Icon,
+    },
+    props: {
+      schema: {
+        type: Object,
+        default: () => ({}),
+      },
+      currentItem: {
+        type: Object,
+        default: () => ({}),
+      },
+    },
+    setup(props) {
+      const { formConfig, formDesignMethods } = useFormDesignState();
+      const activeClass = computed(() => {
+        return props.schema.key === props.currentItem.key ? 'active' : 'unactivated';
+      });
+      /**
+       * 删除当前项
+       */
+      const handleDelete = () => {
+        const traverse = (schemas: IVFormComponent[]) => {
+          schemas.some((formItem, index) => {
+            const { component, key } = formItem;
+            // 处理栅格和标签页布局
+            ['Grid', 'Tabs'].includes(component) &&
+              formItem.columns?.forEach((item) => traverse(item.children));
+            if (key === props.currentItem.key) {
+              let params: IVFormComponent =
+                schemas.length === 1
+                  ? { component: '' }
+                  : schemas.length - 1 > index
+                  ? schemas[index + 1]
+                  : schemas[index - 1];
+              formDesignMethods.handleSetSelectItem(params);
+              remove(schemas, index);
+              return true;
+            }
+          });
+        };
+        traverse(formConfig.value!.schemas);
+      };
+
+      const handleCopy = () => {
+        formDesignMethods.handleCopy();
+      };
+      return { activeClass, handleDelete, handleCopy };
+    },
+  });
+</script>

+ 114 - 0
src/views/form-design/components/VFormDesign/components/FormOptions.vue

@@ -0,0 +1,114 @@
+<template>
+  <div>
+    <div v-if="['Grid'].includes(formConfig.currentItem!.component)">
+      <div v-for="(item, index) of formConfig.currentItem!['columns']" :key="index">
+        <div class="options-box">
+          <Input v-model:value="item.span" class="options-value" />
+          <a class="options-delete" @click="deleteGridOptions(index)">
+            <!-- <a-icon type="delete" /> -->
+            <Icon icon="ant-design:delete-outlined" />
+          </a>
+        </div>
+      </div>
+      <a @click="addGridOptions">
+        <Icon icon="ant-design:file-add-outlined" />
+        添加栅格
+      </a>
+    </div>
+    <div v-else>
+      <div v-for="(item, index) of formConfig.currentItem!.componentProps![key]" :key="index">
+        <div class="options-box">
+          <Input v-model:value="item.label" />
+          <Input v-model:value="item.value" class="options-value" />
+          <a class="options-delete" @click="deleteOptions(index)">
+            <!-- <a-icon type="delete" /> -->
+            <Icon icon="ant-design:delete-outlined" />
+          </a>
+        </div>
+      </div>
+      <a @click="addOptions">
+        <Icon icon="ant-design:file-add-outlined" />
+        添加选项
+      </a>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, reactive, toRefs } from 'vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { remove } from '../../../utils';
+  import message from '../../../utils/message';
+  import { Input } from 'ant-design-vue';
+  import Icon from '/@/components/Icon/index';
+  export default defineComponent({
+    name: 'FormOptions',
+    components: { Input, Icon },
+    // props: {},
+    setup() {
+      const state = reactive({});
+      const { formConfig } = useFormDesignState();
+      const key = formConfig.value.currentItem?.component === 'TreeSelect' ? 'treeData' : 'options';
+      const addOptions = () => {
+        if (!formConfig.value.currentItem?.componentProps?.[key])
+          formConfig.value.currentItem!.componentProps![key] = [];
+        const len = formConfig.value.currentItem?.componentProps?.[key].length + 1;
+        formConfig.value.currentItem!.componentProps![key].push({
+          label: `选项${len}`,
+          value: '' + len,
+        });
+      };
+      const deleteOptions = (index: number) => {
+        remove(formConfig.value.currentItem?.componentProps?.[key], index);
+      };
+
+      const addGridOptions = () => {
+        formConfig.value.currentItem?.['columns']?.push({
+          span: 12,
+          children: [],
+        });
+      };
+      const deleteGridOptions = (index: number) => {
+        if (index === 0) return message.warning('请至少保留一个栅格');
+
+        remove(formConfig.value.currentItem!['columns']!, index);
+      };
+      return {
+        ...toRefs(state),
+        formConfig,
+        addOptions,
+        deleteOptions,
+        key,
+        deleteGridOptions,
+        addGridOptions,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .options-box {
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+
+    .options-value {
+      margin: 0 8px;
+    }
+
+    .options-delete {
+      width: 30px;
+      height: 30px;
+      flex-shrink: 0;
+      line-height: 30px;
+      text-align: center;
+      border-radius: 50%;
+      background: #f5f5f5;
+      color: #666;
+
+      &:hover {
+        background: #ff4d4f;
+      }
+    }
+  }
+</style>

+ 117 - 0
src/views/form-design/components/VFormDesign/components/FormProps.vue

@@ -0,0 +1,117 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/23
+ * @Description: 右侧属性面板控件 表单属性面板
+-->
+<template>
+  <div class="properties-content">
+    <Form class="properties-body" label-align="left" layout="vertical">
+      <!--      <e-upload v-model="fileList"></e-upload>-->
+
+      <FormItem label="表单布局">
+        <RadioGroup button-style="solid" v-model:value="formConfig.layout">
+          <RadioButton value="horizontal">水平</RadioButton>
+          <RadioButton value="vertical" :disabled="formConfig.labelLayout === 'Grid'">
+            垂直
+          </RadioButton>
+          <RadioButton value="inline" :disabled="formConfig.labelLayout === 'Grid'">
+            行内
+          </RadioButton>
+        </RadioGroup>
+      </FormItem>
+
+      <!-- <Row> -->
+      <FormItem label="标签布局">
+        <RadioGroup
+          buttonStyle="solid"
+          v-model:value="formConfig.labelLayout"
+          @change="lableLayoutChange"
+        >
+          <RadioButton value="flex">固定</RadioButton>
+          <RadioButton value="Grid" :disabled="formConfig.layout !== 'horizontal'">
+            栅格
+          </RadioButton>
+        </RadioGroup>
+      </FormItem>
+      <!-- </Row> -->
+      <FormItem label="标签宽度(px)" v-show="formConfig.labelLayout === 'flex'">
+        <InputNumber
+          :style="{ width: '100%' }"
+          v-model:value="formConfig.labelWidth"
+          :min="0"
+          :step="1"
+        />
+      </FormItem>
+      <div v-if="formConfig.labelLayout === 'Grid'">
+        <FormItem label="labelCol">
+          <Slider v-model:value="formConfig.labelCol!.span" :max="24" />
+        </FormItem>
+        <FormItem label="wrapperCol">
+          <Slider v-model:value="formConfig.wrapperCol!.span" :max="24" />
+        </FormItem>
+
+        <FormItem label="标签对齐">
+          <RadioGroup button-style="solid" v-model:value="formConfig.labelAlign">
+            <RadioButton value="left">靠左</RadioButton>
+            <RadioButton value="right">靠右</RadioButton>
+          </RadioGroup>
+        </FormItem>
+
+        <FormItem label="控件大小">
+          <RadioGroup button-style="solid" v-model:value="formConfig.size">
+            <RadioButton value="default">默认</RadioButton>
+            <RadioButton value="small">小</RadioButton>
+            <RadioButton value="large">大</RadioButton>
+          </RadioGroup>
+        </FormItem>
+      </div>
+      <FormItem label="表单属性">
+        <Col
+          ><Checkbox v-model:checked="formConfig.colon" v-if="formConfig.layout == 'horizontal'"
+            >label后面显示冒号</Checkbox
+          ></Col
+        >
+        <Col><Checkbox v-model:checked="formConfig.disabled">禁用</Checkbox></Col>
+        <Col><Checkbox v-model:checked="formConfig.hideRequiredMark">隐藏必选标记</Checkbox></Col>
+      </FormItem>
+    </Form>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { InputNumber, Slider, Checkbox, Col, RadioChangeEvent } from 'ant-design-vue';
+  // import RadioButtonGroup from '/@/components/RadioButtonGroup.vue';
+  import { Form, FormItem, Radio } from 'ant-design-vue';
+  export default defineComponent({
+    name: 'FormProps',
+    components: {
+      InputNumber,
+      Slider,
+      Checkbox,
+      // RadioButtonGroup,
+      RadioGroup: Radio.Group,
+      RadioButton: Radio.Button,
+      Form,
+      FormItem,
+      Col,
+    },
+    setup() {
+      // const labelColspan = computed(()=>)
+      const { formConfig } = useFormDesignState();
+
+      formConfig.value = formConfig.value || {
+        labelCol: { span: 24 },
+        wrapperCol: { span: 24 },
+      };
+
+      const lableLayoutChange = (e: RadioChangeEvent) => {
+        if (e.target.value === 'Grid') {
+          formConfig.value.layout = 'horizontal';
+        }
+      };
+
+      return { formConfig, lableLayoutChange };
+    },
+  });
+</script>

+ 139 - 0
src/views/form-design/components/VFormDesign/components/ImportJsonModal.vue

@@ -0,0 +1,139 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/12/7
+ * @Description: 导入JSON模板
+-->
+<template>
+  <Modal
+    title="JSON数据"
+    :visible="visible"
+    @ok="handleImportJson"
+    @cancel="handleCancel"
+    cancelText="关闭"
+    :destroyOnClose="true"
+    wrapClassName="v-code-modal"
+    style="top: 20px"
+    :width="850"
+  >
+    <p class="hint-box">导入格式如下:</p>
+    <div class="v-json-box">
+      <!-- <CodeEditor style="height: 100%" ref="myEditor" v-model="json"></CodeEditor> -->
+      <CodeEditor v-model:value="json" ref="myEditor" :mode="MODE.JSON" />
+    </div>
+
+    <template #footer>
+      <a-button @click="handleCancel">取消</a-button>
+      <Upload
+        class="upload-button"
+        :beforeUpload="beforeUpload"
+        :showUploadList="false"
+        accept="application/json"
+      >
+        <a-button type="primary">导入json文件</a-button>
+      </Upload>
+      <a-button type="primary" @click="handleImportJson">确定</a-button>
+    </template>
+  </Modal>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, toRefs } from 'vue';
+  // import message from '../../../utils/message';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  // import { codemirror } from 'vue-codemirror-lite';
+  import { IFormConfig } from '../../../typings/v-form-component';
+  import { formItemsForEach, generateKey } from '../../../utils';
+  import { CodeEditor, MODE } from '/@/components/CodeEditor';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { Upload, Modal } from 'ant-design-vue';
+
+  export default defineComponent({
+    name: 'ImportJsonModal',
+    components: {
+      CodeEditor,
+      Upload,
+      Modal,
+    },
+    setup() {
+      const { createMessage } = useMessage();
+
+      const state = reactive({
+        visible: false,
+        json: `{
+  "schemas": [
+    {
+      "component": "input",
+      "label": "输入框",
+      "field": "input_2",
+      "span": 24,
+      "props": {
+        "type": "text"
+      }
+    }
+  ],
+  "layout": "horizontal",
+  "labelLayout": "flex",
+  "labelWidth": 100,
+  "labelCol": {},
+  "wrapperCol": {}
+}`,
+        jsonData: {
+          schemas: {},
+          config: {},
+        },
+        handleSetSelectItem: null,
+      });
+      const { formDesignMethods } = useFormDesignState();
+      const handleCancel = () => {
+        state.visible = false;
+      };
+      const showModal = () => {
+        state.visible = true;
+      };
+      const handleImportJson = () => {
+        // 导入JSON
+        console.log(state.json);
+        try {
+          const editorJsonData = JSON.parse(state.json) as IFormConfig;
+          editorJsonData.schemas &&
+            formItemsForEach(editorJsonData.schemas, (formItem) => {
+              generateKey(formItem);
+            });
+          formDesignMethods.setFormConfig({
+            ...editorJsonData,
+            activeKey: 1,
+            currentItem: { component: '' },
+          });
+          handleCancel();
+          createMessage.success('导入成功');
+        } catch {
+          createMessage.error('导入失败,数据格式不对');
+        }
+      };
+      const beforeUpload = (e: File) => {
+        // 通过json文件导入
+        const reader = new FileReader();
+        reader.readAsText(e);
+        reader.onload = function () {
+          state.json = this.result as string;
+          handleImportJson();
+        };
+        return false;
+      };
+
+      return {
+        handleImportJson,
+        beforeUpload,
+        handleCancel,
+        showModal,
+        ...toRefs(state),
+        MODE,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .upload-button {
+    margin: 0 10px;
+  }
+</style>

+ 66 - 0
src/views/form-design/components/VFormDesign/components/JsonModal.vue

@@ -0,0 +1,66 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/23
+ * @Description: 渲染JSON数据
+-->
+<template>
+  <Modal
+    title="JSON数据"
+    :footer="null"
+    :visible="visible"
+    @cancel="handleCancel"
+    :destroyOnClose="true"
+    wrapClassName="v-code-modal"
+    style="top: 20px"
+    width="850px"
+  >
+    <PreviewCode :editorJson="editorJson" />
+  </Modal>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, reactive, toRefs } from 'vue';
+  import PreviewCode from './PreviewCode.vue';
+  import { IFormConfig } from '../../../typings/v-form-component';
+  import { formatRules, removeAttrs } from '../../../utils';
+  import { Modal } from 'ant-design-vue';
+
+  export default defineComponent({
+    name: 'JsonModal',
+    components: {
+      PreviewCode,
+      Modal,
+    },
+    emits: ['cancel'],
+    setup(_props, { emit }) {
+      const state = reactive<{
+        visible: boolean;
+        jsonData: IFormConfig;
+      }>({
+        visible: false, // 控制json数据弹框显示
+        jsonData: {} as IFormConfig, // json数据
+      });
+      /**
+       * 显示Json数据弹框
+       * @param jsonData
+       */
+      const showModal = (jsonData: IFormConfig) => {
+        formatRules(jsonData.schemas);
+        state.jsonData = jsonData;
+        state.visible = true;
+      };
+
+      // 计算json数据
+      const editorJson = computed(() => {
+        return JSON.stringify(removeAttrs(state.jsonData), null, '\t');
+      });
+
+      // 关闭弹框
+      const handleCancel = () => {
+        state.visible = false;
+        emit('cancel');
+      };
+
+      return { ...toRefs(state), editorJson, handleCancel, showModal };
+    },
+  });
+</script>

+ 135 - 0
src/views/form-design/components/VFormDesign/components/LayoutItem.vue

@@ -0,0 +1,135 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/19
+ * @Description: 表单项布局控件
+ * 千万不要在template下面的第一行加注释,因为这里拖动的第一个元素
+-->
+
+<template>
+  <Col v-bind="colPropsComputed">
+    <template v-if="['Grid'].includes(schema.component)">
+      <div
+        class="grid-box"
+        :class="{ active: schema.key === currentItem.key }"
+        @click.stop="handleSetSelectItem(schema)"
+      >
+        <Row class="grid-row" v-bind="schema.componentProps">
+          <Col
+            class="grid-col"
+            v-for="(colItem, index) in schema.columns"
+            :key="index"
+            :span="colItem.span"
+          >
+            <!-- <div class="draggable-box"> -->
+            <!-- <div class="list-main"> -->
+            <draggable
+              class="list-main draggable-box"
+              :component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
+              v-bind="{
+                group: 'form-draggable',
+                ghostClass: 'moving',
+                animation: 180,
+                handle: '.drag-move',
+              }"
+              item-key="key"
+              v-model="colItem.children"
+              @start="$emit('dragStart', $event, colItem.children)"
+              @add="$emit('handleColAdd', $event, colItem.children)"
+            >
+              <!-- <transition-group tag="div" name="list" class="list-main"> -->
+              <template #item="{ element }">
+                <LayoutItem
+                  class="drag-move"
+                  :schema="element"
+                  :current-item="currentItem"
+                  @handle-copy="$emit('handle-copy')"
+                  @handle-delete="$emit('handle-delete')"
+                />
+              </template>
+              <!-- </transition-group> -->
+            </draggable>
+            <!-- </div> -->
+            <!-- </div> -->
+          </Col>
+        </Row>
+        <FormNodeOperate :schema="schema" :currentItem="currentItem" />
+      </div>
+    </template>
+    <FormNode
+      v-else
+      :key="schema.key"
+      :schema="schema"
+      :current-item="currentItem"
+      @handle-copy="$emit('handle-copy')"
+      @handle-delete="$emit('handle-delete')"
+    />
+  </Col>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, PropType, reactive, toRefs } from 'vue';
+  import draggable from 'vuedraggable';
+  import FormNode from './FormNode.vue';
+  import FormNodeOperate from './FormNodeOperate.vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { IVFormComponent } from '../../../typings/v-form-component';
+  import { Row, Col } from 'ant-design-vue';
+  export default defineComponent({
+    name: 'LayoutItem',
+    components: {
+      FormNode,
+      FormNodeOperate,
+      draggable,
+      Row,
+      Col,
+    },
+    props: {
+      schema: {
+        type: Object as PropType<IVFormComponent>,
+        required: true,
+      },
+      currentItem: {
+        type: Object,
+        required: true,
+      },
+    },
+    emits: ['dragStart', 'handleColAdd', 'handle-copy', 'handle-delete'],
+    setup(props) {
+      const {
+        formDesignMethods: { handleSetSelectItem },
+        formConfig,
+      } = useFormDesignState();
+      const state = reactive({});
+      const colPropsComputed = computed(() => {
+        const { colProps = {} } = props.schema;
+        return colProps;
+      });
+
+      const list1 = computed(() => props.schema.columns);
+
+      // 计算布局元素,水平模式下为ACol,非水平模式下为div
+      const layoutTag = computed(() => {
+        return formConfig.value.layout === 'horizontal' ? 'Col' : 'div';
+      });
+
+      return {
+        ...toRefs(state),
+        colPropsComputed,
+        handleSetSelectItem,
+        layoutTag,
+        list1,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @import url(../styles/variable.less);
+
+  .layout-width {
+    width: 100%;
+  }
+
+  .hidden-item {
+    background-color: rgb(240, 191, 195);
+    //opacity: 0.5;
+  }
+</style>

+ 97 - 0
src/views/form-design/components/VFormDesign/components/PreviewCode.vue

@@ -0,0 +1,97 @@
+<template>
+  <div>
+    <div class="v-json-box">
+      <CodeEditor :value="editorJson" ref="myEditor" :mode="MODE.JSON" />
+    </div>
+    <div class="copy-btn-box">
+      <a-button
+        @click="handleCopyJson"
+        type="primary"
+        class="copy-btn"
+        data-clipboard-action="copy"
+        :data-clipboard-text="editorJson"
+      >
+        复制数据
+      </a-button>
+      <a-button @click="handleExportJson" type="primary">导出代码</a-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, reactive, toRefs, unref } from 'vue';
+  import { CodeEditor, MODE } from '/@/components/CodeEditor';
+
+  import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  export default defineComponent({
+    name: 'PreviewCode',
+    components: {
+      CodeEditor,
+    },
+    props: {
+      fileFormat: {
+        type: String,
+        default: 'json',
+      },
+      editorJson: {
+        type: String,
+        default: '',
+      },
+    },
+    setup(props) {
+      const state = reactive({
+        visible: false,
+      });
+
+      const exportData = (data: string, fileName = `file.${props.fileFormat}`) => {
+        let content = 'data:text/csv;charset=utf-8,';
+        content += data;
+        const encodedUri = encodeURI(content);
+        const actions = document.createElement('a');
+        actions.setAttribute('href', encodedUri);
+        actions.setAttribute('download', fileName);
+        actions.click();
+      };
+
+      const handleExportJson = () => {
+        exportData(props.editorJson);
+      };
+      const { clipboardRef, copiedRef } = useCopyToClipboard();
+      const { createMessage } = useMessage();
+
+      const handleCopyJson = () => {
+        // 复制数据
+        const value = props.editorJson;
+        if (!value) {
+          createMessage.warning('代码为空!');
+          return;
+        }
+        clipboardRef.value = value;
+        if (unref(copiedRef)) {
+          createMessage.warning('复制成功!');
+        }
+      };
+
+      return {
+        ...toRefs(state),
+        exportData,
+        handleCopyJson,
+        handleExportJson,
+        MODE,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  // modal复制按钮样式
+  .copy-btn-box {
+    padding-top: 8px;
+    text-align: center;
+
+    .copy-btn {
+      margin-right: 8px;
+    }
+  }
+</style>

+ 295 - 0
src/views/form-design/components/VFormDesign/components/RuleProps.vue

@@ -0,0 +1,295 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/25
+ * @Description: 正则校验选项组件
+-->
+<template>
+  <div class="rule-props-content">
+    <Form v-if="formConfig.currentItem && formConfig.currentItem['rules']">
+      <div
+        v-for="(item, index) of formConfig.currentItem['rules']"
+        :key="index"
+        class="rule-props-item"
+      >
+        <Icon
+          icon="ant-design:close-circle-filled"
+          class="rule-props-item-close"
+          @click="removeRule(index)"
+        />
+        <FormItem label="正则" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+          <AutoComplete
+            v-model:value="item.pattern"
+            placeholder="请输入正则表达式"
+            :dataSource="patternDataSource"
+          />
+        </FormItem>
+        <FormItem label="文案" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
+          <Input v-model:value="item.message" placeholder="请输入提示文案" />
+        </FormItem>
+      </div>
+    </Form>
+    <a @click="addRules">
+      <Icon icon="ant-design:file-add-outlined" />
+      添加正则
+    </a>
+  </div>
+</template>
+<script lang="ts">
+  import { ref, defineComponent } from 'vue';
+  import { remove } from '../../../utils';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { isArray } from 'lodash-es';
+  import { Form, FormItem, AutoComplete, Input } from 'ant-design-vue';
+  import Icon from '/@/components/Icon';
+
+  export default defineComponent({
+    name: 'RuleProps',
+    components: {
+      Form,
+      FormItem,
+      AutoComplete,
+      Input,
+      Icon,
+    },
+    setup() {
+      // 获取祖先组件的状态
+      const { formConfig } = useFormDesignState();
+      // 抽离 currentItem
+      /**
+       * 添加正则校验,判断当前组件的rules是不是数组,如果不是数组,使用set方法重置成数组,然后添加正则校验
+       */
+      const addRules = () => {
+        if (!isArray(formConfig.value.currentItem!.rules))
+          formConfig.value.currentItem!['rules'] = [];
+        formConfig.value.currentItem!.rules?.push({ pattern: '', message: '' });
+      };
+      /**
+       * 删除正则校验,当正则规则为0时,删除rules属性
+       * @param index {number} 需要删除的规则下标
+       */
+      const removeRule = (index: number) => {
+        remove(formConfig.value.currentItem!.rules as Array<any>, index);
+        if (formConfig.value.currentItem!.rules?.length === 0)
+          delete formConfig.value.currentItem!['rules'];
+      };
+
+      const patternDataSource = ref([
+        {
+          value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
+          text: '手机号码',
+        },
+        {
+          value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
+          text: '网址带端口号',
+        },
+        {
+          value:
+            '/^(((ht|f)tps?):\\/\\/)?[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-\\(\\)]*[\\w@?^=%&/~+#-\\(\\)])?$/',
+          text: '网址带参数',
+        },
+        {
+          value: '/^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/',
+          text: '统一社会信用代码',
+        },
+        {
+          value: '/^(s[hz]|S[HZ])(000[\\d]{3}|002[\\d]{3}|300[\\d]{3}|600[\\d]{3}|60[\\d]{4})$/',
+          text: '股票代码',
+        },
+        {
+          value: '/^([a-f\\d]{32}|[A-F\\d]{32})$/',
+          text: 'md5格式(32位)',
+        },
+        {
+          value: '/^[a-f\\d]{4}(?:[a-f\\d]{4}-){4}[a-f\\d]{12}$/i',
+          text: 'GUID/UUID',
+        },
+        {
+          value: '/^\\d+(?:\\.\\d+){2}$/',
+          text: '版本号(x.y.z)格式',
+        },
+        {
+          value:
+            '/^https?:\\/\\/(.+\\/)+.+(\\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i',
+          text: '视频链接地址',
+        },
+        {
+          value: '/^https?:\\/\\/(.+\\/)+.+(\\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i',
+          text: '图片链接地址',
+        },
+        {
+          value: '/^-?\\d+(,\\d{3})*(\\.\\d{1,2})?$/',
+          text: '数字/货币金额(支持负数、千分位分隔符)',
+        },
+        {
+          value:
+            '/(?:^[1-9]([0-9]+)?(?:\\.[0-9]{1,2})?$)|(?:^(?:0)$)|(?:^[0-9]\\.[0-9](?:[0-9])?$)/',
+          text: '数字/货币金额',
+        },
+        {
+          value: '/^[1-9]\\d{9,29}$/',
+          text: '银行卡号',
+        },
+        {
+          value: '/^(?:[\u4e00-\u9fa5·]{2,16})$/',
+          text: '中文姓名',
+        },
+        {
+          value: '/(^[a-zA-Z][a-zA-Z\\s]{0,20}[a-zA-Z]$)/',
+          text: '英文姓名',
+        },
+        {
+          value:
+            '/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:((\\d{5}[A-HJK])|([A-HJK][A-HJ-NP-Z0-9][0-9]{4}))|[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])$/',
+          text: '车牌号(新能源)',
+        },
+        {
+          value:
+            '/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/',
+          text: '车牌号(非新能源)',
+        },
+        {
+          value:
+            '/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/',
+          text: '车牌号(新能源+非新能源)',
+        },
+        {
+          value:
+            '/^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/',
+          text: 'email(邮箱)',
+        },
+        {
+          value: '/^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$/',
+          text: '座机',
+        },
+        {
+          value:
+            '/^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$/',
+          text: '身份证号',
+        },
+        {
+          value:
+            '/(^[EeKkGgDdSsPpHh]\\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\\d{7}$)/',
+          text: '护照',
+        },
+        {
+          value:
+            '/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/',
+          text: '中文汉字',
+        },
+        {
+          value: '/^\\d+\\.\\d+$/',
+          text: '小数',
+        },
+        {
+          value: '/^\\d{1,}$/',
+          text: '数字',
+        },
+        {
+          value: '/^[1-9][0-9]{4,10}$/',
+          text: 'qq号',
+        },
+        {
+          value: '/^[A-Za-z0-9]+$/',
+          text: '数字字母组合',
+        },
+        {
+          value: '/^[a-zA-Z]+$/',
+          text: '英文字母',
+        },
+        {
+          value: '/^[a-z]+$/',
+          text: '小写英文字母',
+        },
+        {
+          value: '/^[A-Z]+$/',
+          text: '大写英文字母',
+        },
+        {
+          value: '/^[a-zA-Z0-9_-]{4,16}$/',
+          text: '用户名校验,4到16位(字母,数字,下划线,减号)',
+        },
+        {
+          value: '/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/',
+          text: '16进制颜色',
+        },
+        {
+          value: '/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/',
+          text: '微信号',
+        },
+        {
+          value: '/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$/',
+          text: '邮政编码(中国)',
+        },
+        {
+          value: '/^[^A-Za-z]*$/',
+          text: '不能包含字母',
+        },
+        {
+          value: '/^\\+?[1-9]\\d*$/',
+          text: '正整数,不包含0',
+        },
+        {
+          value: '/^-[1-9]\\d*$/',
+          text: '负整数,不包含0',
+        },
+        {
+          value: '/^-?[0-9]\\d*$/',
+          text: '整数',
+        },
+        {
+          value: '/^(-?\\d+)(\\.\\d+)?$/',
+          text: '浮点数',
+        },
+        {
+          value: '/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/',
+          text: 'email(支持中文邮箱)',
+        },
+      ]);
+
+      return { addRules, removeRule, formConfig, patternDataSource };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  :deep(.icon) {
+    width: 1em;
+    height: 1em;
+    vertical-align: -0.15em;
+    fill: currentColor;
+    overflow: hidden;
+  }
+
+  .rule-props-content {
+    :deep(.ant-form-item) {
+      margin-bottom: 0;
+    }
+
+    .rule-props-item {
+      position: relative;
+      background-color: #f0eded;
+      padding: 3px 2px;
+      border-radius: 5px;
+      margin-bottom: 5px;
+
+      :deep(.ant-form-item) {
+        border: 0 !important;
+      }
+
+      &-close {
+        position: absolute;
+        top: -5px;
+        right: -5px;
+        color: #ccc;
+        cursor: pointer;
+        border-radius: 7px;
+        background-color: #a3a0a0;
+        z-index: 999;
+
+        &:hover {
+          color: #00c;
+        }
+      }
+    }
+  }
+</style>

+ 1148 - 0
src/views/form-design/components/VFormDesign/config/componentPropsConfig.ts

@@ -0,0 +1,1148 @@
+import { IBaseFormAttrs } from './formItemPropsConfig';
+
+interface IBaseComponentProps {
+  [key: string]: IBaseFormAttrs[];
+}
+
+type BaseFormAttrs = Omit<IBaseFormAttrs, 'tag'>;
+
+export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
+  {
+    // 没有disabled属性的控件不能作为form控件
+    name: 'disabled',
+    label: '禁用',
+  },
+  {
+    // 没有disabled属性的控件不能作为form控件
+    name: 'autofocus',
+    label: '自动获取焦点',
+    includes: [
+      'Input',
+      'Select',
+      'InputTextArea',
+      'InputNumber',
+      'DatePicker',
+      'RangePicker',
+      'MonthPicker',
+      'TimePicker',
+      'Cascader',
+      'TreeSelect',
+      'Switch',
+      'AutoComplete',
+      'Slider',
+    ],
+  },
+  {
+    name: 'allowClear',
+    label: '可清除',
+    includes: [
+      'Input',
+      'Select',
+      'InputTextArea',
+      'InputNumber',
+      'DatePicker',
+      'RangePicker',
+      'MonthPicker',
+      'TimePicker',
+      'Cascader',
+      'TreeSelect',
+      'AutoComplete',
+    ],
+  },
+  { name: 'fullscreen', label: '全屏', includes: ['Calendar'] },
+  {
+    name: 'showSearch',
+    label: '可搜索',
+    includes: ['Select', 'TreeSelect', 'Cascader', 'Transfer'],
+  },
+  {
+    name: 'showTime',
+    label: '显示时间',
+    includes: ['DatePicker', 'RangePicker', 'MonthPicker'],
+  },
+  {
+    name: 'range',
+    label: '双向滑动',
+    includes: [],
+  },
+  {
+    name: 'allowHalf',
+    label: '允许半选',
+    includes: ['Rate'],
+  },
+  {
+    name: 'multiple',
+    label: '多选',
+    includes: ['Select', 'TreeSelect', 'Upload'],
+  },
+  {
+    name: 'directory',
+    label: '文件夹',
+    includes: ['Upload'],
+  },
+  {
+    name: 'withCredentials',
+    label: '携带cookie',
+    includes: ['Upload'],
+  },
+  {
+    name: 'bordered',
+    label: '是否有边框',
+    includes: ['Select', 'Input'],
+  },
+  {
+    name: 'defaultActiveFirstOption',
+    label: '高亮第一个选项',
+    component: 'Checkbox',
+    includes: ['Select', 'AutoComplete'],
+  },
+  {
+    name: 'dropdownMatchSelectWidth',
+    label: '下拉菜单和选择器同宽',
+    component: 'Checkbox',
+    includes: ['Select', 'TreeSelect', 'AutoComplete'],
+  },
+];
+
+//共用属性
+export const baseComponentCommonAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [
+  {
+    name: 'size',
+    label: '尺寸',
+    component: 'RadioGroup',
+    componentProps: {
+      options: [
+        {
+          label: '默认',
+          value: 'default',
+        },
+        {
+          label: '大',
+          value: 'large',
+        },
+        {
+          label: '小',
+          value: 'small',
+        },
+      ],
+    },
+    includes: ['InputNumber', 'Input', 'Cascader', 'Button'],
+  },
+  {
+    name: 'placeholder',
+    label: '占位符',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入占位符',
+    },
+    includes: [
+      'AutoComplete',
+      'InputTextArea',
+      'InputNumber',
+      'Input',
+      'InputTextArea',
+      'Select',
+      'DatePicker',
+      'MonthPicker',
+      'TimePicker',
+      'TreeSelect',
+      'Cascader',
+    ],
+  },
+  {
+    name: 'style',
+    label: '样式',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入样式',
+    },
+  },
+  {
+    name: 'open',
+    label: '一直展开下拉菜单',
+    component: 'RadioGroup',
+    componentProps: {
+      options: [
+        {
+          label: '默认',
+          value: undefined,
+        },
+        {
+          label: '是',
+          value: true,
+        },
+        {
+          label: '否',
+          value: false,
+        },
+      ],
+    },
+    includes: ['Select', 'AutoComplete'],
+  },
+];
+
+const componentAttrs: IBaseComponentProps = {
+  AutoComplete: [
+    {
+      name: 'backfill',
+      label: '自动回填',
+      component: 'Switch',
+      componentProps: {
+        span: 8,
+      },
+    },
+    {
+      name: 'defaultOpen',
+      label: '是否默认展开下拉菜单',
+      component: 'Checkbox',
+    },
+  ],
+  IconPicker: [
+    {
+      name: 'mode',
+      label: '模式',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: 'ICONIFY', value: null },
+          { label: 'SVG', value: 'svg' },
+          // { label: '组合', value: 'combobox' },
+        ],
+      },
+    },
+  ],
+
+  // https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#%3Cinput%3E_types
+  Input: [
+    {
+      name: 'type',
+      label: '类型',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { value: 'text', label: '文本' },
+          { value: 'search', label: '搜索' },
+          { value: 'number', label: '数字' },
+          { value: 'range', label: '数字范围' },
+          { value: 'password', label: '密码' },
+          { value: 'date', label: '日期' },
+          { value: 'datetime-local', label: '日期-无时区' },
+          { value: 'time', label: '时间' },
+          { value: 'month', label: '年月' },
+          { value: 'week', label: '星期' },
+          { value: 'email', label: '邮箱' },
+          { value: 'url', label: 'URL' },
+          { value: 'tel', label: '电话号码' },
+          { value: 'file', label: '文件' },
+          { value: 'button', label: '按钮' },
+          { value: 'submit', label: '提交按钮' },
+          { value: 'reset', label: '重置按钮' },
+          { value: 'radio', label: '单选按钮' },
+          { value: 'checkbox', label: '复选框' },
+          { value: 'color', label: '颜色' },
+          { value: 'image', label: '图像' },
+          { value: 'hidden', label: '隐藏' },
+        ],
+      },
+    },
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'prefix',
+      label: '前缀',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入前缀',
+      },
+    },
+    {
+      name: 'suffix',
+      label: '后缀',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入后缀',
+      },
+    },
+    {
+      name: 'addonBefore',
+      label: '前置标签',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入前置标签',
+      },
+    },
+    {
+      name: 'addonAfter',
+      label: '后置标签',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入后置标签',
+      },
+    },
+    {
+      name: 'maxLength',
+      label: '最大长度',
+      component: 'InputNumber',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入最大长度',
+      },
+    },
+  ],
+
+  InputNumber: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'min',
+      label: '最小值',
+      component: 'InputNumber',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入最小值',
+      },
+    },
+    {
+      name: 'max',
+      label: '最大值',
+      component: 'InputNumber',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入最大值',
+      },
+    },
+    {
+      name: 'precision',
+      label: '数值精度',
+      component: 'InputNumber',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入最大值',
+      },
+    },
+    {
+      name: 'step',
+      label: '步长',
+      component: 'InputNumber',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入步长',
+      },
+    },
+    {
+      name: 'decimalSeparator',
+      label: '小数点',
+      component: 'Input',
+      componentProps: { type: 'text', placeholder: '请输入小数点' },
+    },
+    {
+      name: 'addonBefore',
+      label: '前置标签',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入前置标签',
+      },
+    },
+    {
+      name: 'addonAfter',
+      label: '后置标签',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入后置标签',
+      },
+    },
+    {
+      name: 'controls',
+      label: '是否显示增减按钮',
+      component: 'Checkbox',
+    },
+    {
+      name: 'keyboard',
+      label: '是否启用键盘快捷行为',
+      component: 'Checkbox',
+    },
+    {
+      name: 'stringMode',
+      label: '字符值模式',
+      component: 'Checkbox',
+    },
+    {
+      name: 'bordered',
+      label: '是否有边框',
+      component: 'Checkbox',
+    },
+  ],
+  InputTextArea: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'Input',
+      componentProps: {
+        type: 'text',
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'maxlength',
+      label: '最大长度',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最大长度',
+      },
+    },
+    {
+      name: 'minlength',
+      label: '最小长度',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最小长度',
+      },
+    },
+    {
+      name: 'cols',
+      label: '可见列数',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入可见列数',
+        min: 0,
+      },
+    },
+    {
+      name: 'rows',
+      label: '可见行数',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入可见行数',
+        min: 0,
+      },
+    },
+    {
+      name: 'minlength',
+      label: '最小长度',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最小长度',
+      },
+    },
+    {
+      name: 'autosize',
+      label: '自适应内容高度',
+      component: 'Checkbox',
+    },
+    {
+      name: 'showCount',
+      label: '是否展示字数',
+      component: 'Checkbox',
+    },
+    {
+      name: 'readonly',
+      label: '是否只读',
+      component: 'Checkbox',
+    },
+    {
+      name: 'spellcheck',
+      label: '读写检查',
+      component: 'Checkbox',
+    },
+    {
+      name: 'autocomplete',
+      label: '是否自动完成',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '正常', value: null },
+          { label: '开', value: 'on' },
+          { label: '关', value: 'off' },
+        ],
+      },
+    },
+    {
+      name: 'autocorrect',
+      label: '是否自动纠错',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '正常', value: null },
+          { label: '开', value: 'on' },
+          { label: '关', value: 'off' },
+        ],
+      },
+    },
+  ],
+  Select: [
+    {
+      name: 'mode',
+      label: '选择模式(默认单选)',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '单选', value: null },
+          { label: '多选', value: 'multiple' },
+          { label: '标签', value: 'tags' },
+          // { label: '组合', value: 'combobox' },
+        ],
+      },
+    },
+    {
+      name: 'autoClearSearchValue',
+      label: '是否在选中项后清空搜索框',
+      component: 'Checkbox',
+    },
+    {
+      name: 'labelInValue',
+      label: '选项的label包装到value中',
+      component: 'Checkbox',
+    },
+    {
+      name: 'showArrow',
+      label: '显示下拉小箭头',
+      component: 'Checkbox',
+    },
+    {
+      name: 'defaultOpen',
+      label: '默认展开下拉菜单',
+      component: 'Checkbox',
+    },
+  ],
+  Checkbox: [
+    {
+      name: 'indeterminate',
+      label: '设置indeterminate状态',
+      component: 'Checkbox',
+    },
+  ],
+  CheckboxGroup: [],
+  RadioGroup: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'buttonStyle',
+      label: 'RadioButton的风格样式',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: 'outline',
+            value: 'outline',
+          },
+          {
+            label: 'solid',
+            value: 'solid',
+          },
+        ],
+      },
+    },
+    {
+      name: 'optionType',
+      label: 'options类型',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: '默认',
+            value: 'default',
+          },
+          {
+            label: '按钮',
+            value: 'button',
+          },
+        ],
+        //根据其它选项的值更新自身控件配置值
+        //compProp当前组件的属性,
+        //configProps,当且组件的所有配置选项
+        //self,当前配置的componentProps属性
+        //返回真值进行更新
+        // _propsFunc: (compProp, configProps, self) => {
+        //   console.log("i'm called");
+        //   console.log(compProp, configProps, self);
+        //   if (compProp['buttonStyle'] && compProp['buttonStyle'] == 'outline') {
+        //     if (!self['disabled']) {
+        //       self['disabled'] = true;
+        //       return 1;
+        //     }
+        //   } else {
+        //     if (self['disabled']) {
+        //       self['disabled'] = false;
+        //       return 1;
+        //     }
+        //   }
+
+        //   // return prop.optionType == 'button';
+        // },
+      },
+    },
+    {
+      name: 'size',
+      label: '尺寸',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: '默认',
+            value: 'default',
+          },
+          {
+            label: '大',
+            value: 'large',
+          },
+          {
+            label: '小',
+            value: 'small',
+          },
+        ],
+      },
+    },
+  ],
+  DatePicker: [
+    {
+      name: 'format',
+      label: '展示格式(format)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM-DD',
+      },
+    },
+    {
+      name: 'valueFormat',
+      label: '绑定值格式(valueFormat)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM-DD',
+      },
+    },
+  ],
+  RangePicker: [
+    {
+      name: 'placeholder',
+      label: '占位符',
+      children: [
+        {
+          name: '',
+          label: '',
+          component: 'Input',
+        },
+        {
+          name: '',
+          label: '',
+          component: 'Input',
+        },
+      ],
+    },
+    {
+      name: 'format',
+      label: '展示格式(format)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM-DD HH:mm:ss',
+      },
+    },
+    {
+      name: 'valueFormat',
+      label: '绑定值格式(valueFormat)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM-DD',
+      },
+    },
+  ],
+  MonthPicker: [
+    {
+      name: 'format',
+      label: '展示格式(format)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM',
+      },
+    },
+    {
+      name: 'valueFormat',
+      label: '绑定值格式(valueFormat)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM',
+      },
+    },
+  ],
+  TimePicker: [
+    {
+      name: 'format',
+      label: '展示格式(format)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM',
+      },
+    },
+    {
+      name: 'valueFormat',
+      label: '绑定值格式(valueFormat)',
+      component: 'Input',
+      componentProps: {
+        placeholder: 'YYYY-MM',
+      },
+    },
+  ],
+  Slider: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'min',
+      label: '最小值',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最小值',
+      },
+    },
+    {
+      name: 'max',
+      label: '最大值',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最大值',
+      },
+    },
+    {
+      name: 'step',
+      label: '步长',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入步长',
+      },
+    },
+    {
+      name: 'tooltipPlacement',
+      label: 'Tooltip 展示位置',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { value: 'top', label: '上' },
+          { value: 'left', label: '左' },
+          { value: 'right', label: '右' },
+          { value: 'bottom', label: '下' },
+          { value: 'topLeft', label: '上右' },
+          { value: 'topRight', label: '上左' },
+          { value: 'bottomLeft', label: '右下' },
+          { value: 'bottomRight', label: '左下' },
+          { value: 'leftTop', label: '左下' },
+          { value: 'leftBottom', label: '左上' },
+          { value: 'rightTop', label: '右下' },
+          { value: 'rightBottom', label: '右上' },
+        ],
+      },
+    },
+    {
+      name: 'tooltipVisible',
+      label: '始终显示Tooltip',
+      component: 'Checkbox',
+    },
+    {
+      name: 'dots',
+      label: '只能拖拽到刻度上',
+      component: 'Checkbox',
+    },
+    {
+      name: 'range',
+      label: '双滑块模式',
+      component: 'Checkbox',
+    },
+    {
+      name: 'reverse',
+      label: '反向坐标轴',
+      component: 'Checkbox',
+    },
+    {
+      name: 'vertical',
+      label: '垂直方向',
+      component: 'Checkbox',
+    },
+    {
+      name: 'included',
+      label: '值为包含关系',
+      component: 'Checkbox',
+    },
+  ],
+  Rate: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'character',
+      label: '自定义字符',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入自定义字符',
+      },
+    },
+    {
+      name: 'count',
+      label: 'start 总数',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入自定义字符',
+      },
+    },
+  ],
+  Switch: [
+    {
+      name: 'checkedChildren',
+      label: '选中时的内容',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入选中时的内容',
+      },
+    },
+    {
+      name: 'checkedValue',
+      label: '选中时的值',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入选中时的值',
+      },
+    },
+    {
+      name: 'unCheckedChildren',
+      label: '非选中时的内容',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入非选中时的内容',
+      },
+    },
+    {
+      name: 'unCheckedValue',
+      label: '非选中时的值',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入非选中时的值',
+      },
+    },
+    {
+      name: 'loading',
+      label: '加载中的开关',
+      component: 'Checkbox',
+    },
+    {
+      name: 'size',
+      label: '尺寸',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: '默认',
+            value: 'default',
+          },
+          {
+            label: '小',
+            value: 'small',
+          },
+        ],
+      },
+    },
+  ],
+  TreeSelect: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入默认值',
+      },
+    },
+    {
+      name: 'searchPlaceholder',
+      label: '搜索框默认文字',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入搜索框默认文字',
+      },
+    },
+    {
+      name: 'treeNodeFilterProp',
+      label: '输入项过滤对应的 treeNode 属性',
+      component: 'Input',
+      componentProps: {
+        defaultValue: 'value',
+      },
+    },
+    {
+      name: 'treeNodeLabelProp',
+      label: '作为显示的 prop 设置',
+      component: 'Input',
+      componentProps: {
+        defaultValue: 'title',
+      },
+    },
+    {
+      name: 'dropdownClassName',
+      label: '下拉菜单的 className 属性',
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入下拉菜单的 className 属性',
+      },
+    },
+
+    {
+      name: 'labelInValue',
+      label: '选项的label包装到value中',
+      component: 'Checkbox',
+    },
+    {
+      name: 'treeIcon',
+      label: '展示TreeNode title前的图标',
+      component: 'Checkbox',
+    },
+    {
+      name: 'treeCheckable',
+      label: '选项可勾选',
+      component: 'Checkbox',
+    },
+    {
+      name: 'treeCheckStrictly',
+      label: '节点选择完全受控',
+      component: 'Checkbox',
+    },
+    {
+      name: 'treeDefaultExpandAll',
+      label: '默认展开所有',
+      component: 'Checkbox',
+    },
+    {
+      name: 'treeLine',
+      label: '是否展示线条样式',
+      component: 'Checkbox',
+    },
+    {
+      name: 'maxTagCount',
+      label: '最多显示多少个 tag',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '最多显示多少个 tag',
+      },
+    },
+    {
+      name: 'size',
+      label: '尺寸',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: '默认',
+            value: 'default',
+          },
+          {
+            label: '小',
+            value: 'small',
+          },
+        ],
+      },
+    },
+  ],
+  Cascader: [
+    {
+      name: 'expandTrigger',
+      label: '次级展开方式(默认click)',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: 'click',
+            value: 'click',
+          },
+          {
+            label: 'hover',
+            value: 'hover',
+          },
+        ],
+      },
+    },
+  ],
+  Button: [
+    {
+      name: 'type',
+      label: '类型',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: 'default',
+            value: 'default',
+          },
+          {
+            label: 'primary',
+            value: 'primary',
+          },
+          {
+            label: 'danger',
+            value: 'danger',
+          },
+          {
+            label: 'dashed',
+            value: 'dashed',
+          },
+        ],
+      },
+    },
+    {
+      name: 'handle',
+      label: '操作',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          {
+            label: '提交',
+            value: 'submit',
+          },
+          {
+            label: '重置',
+            value: 'reset',
+          },
+        ],
+      },
+    },
+  ],
+  Upload: [
+    {
+      name: 'action',
+      label: '上传地址',
+      component: 'Input',
+    },
+    {
+      name: 'name',
+      label: '附件参数名(name)',
+      component: 'Input',
+    },
+  ],
+  ColorPicker: [
+    {
+      name: 'defaultValue',
+      label: '默认值',
+      component: 'AColorPicker',
+    },
+  ],
+  slot: [
+    {
+      name: 'slotName',
+      label: '插槽名称',
+      component: 'Input',
+    },
+  ],
+  Transfer: [
+    // {
+    //   name: 'operations',
+    //   label: '操作文案集合,顺序从上至下',
+    //   component: 'Input',
+    //   componentProps: {
+    //     type: 'text',
+    //     // defaultValue: ['>', '<'],
+    //   },
+    // },
+    // {
+    //   name: 'titles',
+    //   label: '标题集合,顺序从左至右',
+    //   component: 'Input',
+    //   componentProps: {
+    //     type: 'text',
+    //     // defaultValue: ['', ''],
+    //   },
+    // },
+    {
+      name: 'oneWay',
+      label: '展示为单向样式',
+      component: 'Checkbox',
+    },
+    {
+      name: 'pagination',
+      label: '使用分页样式',
+      component: 'Checkbox',
+    },
+    {
+      name: 'showSelectAll',
+      label: '展示全选勾选框',
+      component: 'Checkbox',
+    },
+  ],
+};
+
+function deleteProps(list: Omit<IBaseFormAttrs, 'tag'>[], key: string) {
+  list.forEach((element, index) => {
+    if (element.name == key) {
+      list.splice(index, 1);
+    }
+  });
+}
+
+componentAttrs['StrengthMeter'] = componentAttrs['Input'];
+componentAttrs['StrengthMeter'].push({
+  name: 'visibilityToggle',
+  label: '是否显示切换按钮',
+  component: 'Checkbox',
+});
+
+deleteProps(componentAttrs['StrengthMeter'], 'type');
+deleteProps(componentAttrs['StrengthMeter'], 'prefix');
+deleteProps(componentAttrs['StrengthMeter'], 'defaultValue');
+deleteProps(componentAttrs['StrengthMeter'], 'suffix');
+//组件属性
+// name 控件的属性
+export const baseComponentAttrs: IBaseComponentProps = componentAttrs;
+
+//在所有的选项中查找需要配置项
+const findCompoentProps = (props, name) => {
+  const idx = props.findIndex((value: BaseFormAttrs, _index) => {
+    return value.name == name;
+  });
+  if (idx) {
+    if (props[idx].componentProps) {
+      return props[idx].componentProps;
+    }
+  }
+};
+
+// 根据其它选项的值更新自身控件配置值
+export const componentPropsFuncs = {
+  RadioGroup: (compProp, options: BaseFormAttrs[]) => {
+    const props = findCompoentProps(options, 'size');
+    if (props) {
+      if (compProp['optionType'] && compProp['optionType'] != 'button') {
+        props['disabled'] = true;
+        compProp['size'] = null;
+      } else {
+        props['disabled'] = false;
+      }
+    }
+  },
+};

+ 351 - 0
src/views/form-design/components/VFormDesign/config/formItemPropsConfig.ts

@@ -0,0 +1,351 @@
+import { IAnyObject } from '../../../typings/base-type';
+import { baseComponents, customComponents } from '../../../core/formItemConfig';
+
+export const globalConfigState: { span: number } = {
+  span: 24,
+};
+export interface IBaseFormAttrs {
+  name: string; // 字段名
+  label: string; // 字段标签
+  component?: string; // 属性控件
+  componentProps?: IAnyObject; // 传递给控件的属性
+  exclude?: string[]; // 需要排除的控件
+  includes?: string[]; // 符合条件的组件
+  on?: IAnyObject;
+  children?: IBaseFormAttrs[];
+  category?: 'control' | 'input';
+}
+
+export interface IBaseFormItemControlAttrs extends IBaseFormAttrs {
+  target?: 'props' | 'options'; // 绑定到对象下的某个目标key中
+}
+
+export const baseItemColumnProps: IBaseFormAttrs[] = [
+  {
+    name: 'span',
+    label: '栅格数',
+    component: 'Slider',
+    on: {
+      change(value: number) {
+        globalConfigState.span = value;
+      },
+    },
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+
+  {
+    name: 'offset',
+    label: '栅格左侧的间隔格数',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'order',
+    label: '栅格顺序,flex 布局模式下有效',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'pull',
+    label: '栅格向左移动格数',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'push',
+    label: '栅格向右移动格数',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'xs',
+    label: '<576px 响应式栅格',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'sm',
+    label: '≥576px 响应式栅格',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'md',
+    label: '≥768p 响应式栅格',
+    component: 'Slider',
+
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'lg',
+    label: '≥992px 响应式栅格',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'xl',
+    label: '≥1200px 响应式栅格',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: 'xxl',
+    label: '≥1600px 响应式栅格',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+  {
+    name: '≥2000px',
+    label: '≥1600px 响应式栅格',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+  },
+];
+
+// 控件属性面板的配置项
+export const advanceFormItemColProps: IBaseFormAttrs[] = [
+  {
+    name: 'labelCol',
+    label: '标签col',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+    exclude: ['Grid'],
+  },
+  {
+    name: 'wrapperCol',
+    label: '控件-span',
+    component: 'Slider',
+    componentProps: {
+      max: 24,
+      min: 0,
+      marks: { 12: '' },
+    },
+    exclude: ['Grid'],
+  },
+];
+// 控件属性面板的配置项
+export const baseFormItemProps: IBaseFormAttrs[] = [
+  {
+    // 动态的切换控件的类型
+    name: 'component',
+    label: '控件-FormItem',
+    component: 'Select',
+    componentProps: {
+      options: baseComponents
+        .concat(customComponents)
+        .map((item) => ({ value: item.component, label: item.label })),
+    },
+  },
+  {
+    name: 'label',
+    label: '标签',
+    component: 'Input',
+    componentProps: {
+      type: 'Input',
+      placeholder: '请输入标签',
+    },
+    exclude: ['Grid'],
+  },
+  {
+    name: 'field',
+    label: '字段标识',
+    component: 'Input',
+    componentProps: {
+      type: 'InputTextArea',
+      placeholder: '请输入字段标识',
+    },
+    exclude: ['Grid'],
+  },
+  {
+    name: 'helpMessage',
+    label: 'helpMessage',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入提示信息',
+    },
+    exclude: ['Grid'],
+  },
+];
+
+// 控件属性面板的配置项
+export const advanceFormItemProps: IBaseFormAttrs[] = [
+  {
+    name: 'labelAlign',
+    label: '标签对齐',
+    component: 'RadioGroup',
+    componentProps: {
+      options: [
+        {
+          label: '靠左',
+          value: 'left',
+        },
+        {
+          label: '靠右',
+          value: 'right',
+        },
+      ],
+    },
+    exclude: ['Grid'],
+  },
+
+  {
+    name: 'help',
+    label: 'help',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入提示信息',
+    },
+    exclude: ['Grid'],
+  },
+  {
+    name: 'extra',
+    label: '额外消息',
+    component: 'Input',
+    componentProps: {
+      type: 'InputTextArea',
+      placeholder: '请输入额外消息',
+    },
+    exclude: ['Grid'],
+  },
+  {
+    name: 'validateTrigger',
+    label: 'validateTrigger',
+    component: 'Input',
+    componentProps: {
+      type: 'InputTextArea',
+      placeholder: '请输入validateTrigger',
+    },
+    exclude: ['Grid'],
+  },
+  {
+    name: 'validateStatus',
+    label: '校验状态',
+    component: 'RadioGroup',
+    componentProps: {
+      options: [
+        {
+          label: '默认',
+          value: '',
+        },
+        {
+          label: '成功',
+          value: 'success',
+        },
+        {
+          label: '警告',
+          value: 'warning',
+        },
+        {
+          label: '错误',
+          value: 'error',
+        },
+        {
+          label: '校验中',
+          value: 'validating',
+        },
+      ],
+    },
+    exclude: ['Grid'],
+  },
+];
+
+export const baseFormItemControlAttrs: IBaseFormItemControlAttrs[] = [
+  {
+    name: 'required',
+    label: '必填项',
+    component: 'Checkbox',
+    exclude: ['alert'],
+  },
+  {
+    name: 'hidden',
+    label: '隐藏',
+    component: 'Checkbox',
+    exclude: ['alert'],
+  },
+  {
+    name: 'hiddenLabel',
+    component: 'Checkbox',
+    exclude: ['Grid'],
+    label: '隐藏标签',
+  },
+  {
+    name: 'colon',
+    label: 'label后面显示冒号',
+    component: 'Checkbox',
+    componentProps: {},
+    exclude: ['Grid'],
+  },
+  {
+    name: 'hasFeedback',
+    label: '输入反馈',
+    component: 'Checkbox',
+    componentProps: {},
+    includes: ['Input'],
+  },
+  {
+    name: 'autoLink',
+    label: '自动关联',
+    component: 'Checkbox',
+    componentProps: {},
+    includes: ['Input'],
+  },
+  {
+    name: 'validateFirst',
+    label: '检验证错误停止',
+    component: 'Checkbox',
+    componentProps: {},
+    includes: ['Input'],
+  },
+];

+ 365 - 0
src/views/form-design/components/VFormDesign/index.vue

@@ -0,0 +1,365 @@
+<template>
+  <!-- <div class="v-form-design-container"> -->
+  <!-- <header class="v-form-design-header">{{ title }}</header> -->
+
+  <Layout>
+    <LayoutSider
+      class="left"
+      theme="light"
+      collapsible
+      collapsedWidth="0"
+      width="270"
+      :zeroWidthTriggerStyle="{ 'margin-top': '-70px' }"
+      breakpoint="md"
+    >
+      <CollapseContainer title="基础控件">
+        <CollapseItem
+          :list="baseComponents"
+          :handleListPush="handleListPushDrag"
+          @add-attrs="handleAddAttrs"
+          @handle-list-push="handleListPush"
+        />
+      </CollapseContainer>
+      <CollapseContainer title="自定义控件">
+        <CollapseItem
+          :list="customComponents"
+          @add-attrs="handleAddAttrs"
+          :handleListPush="handleListPushDrag"
+          @handle-list-push="handleListPush"
+        />
+      </CollapseContainer>
+      <CollapseContainer title="布局控件">
+        <CollapseItem
+          :list="layoutComponents"
+          :handleListPush="handleListPushDrag"
+          @add-attrs="handleAddAttrs"
+          @handle-list-push="handleListPush"
+        />
+      </CollapseContainer>
+    </LayoutSider>
+    <LayoutContent>
+      <Toolbar
+        @handle-open-json-modal="handleOpenModal(jsonModal!)"
+        @handle-open-import-json-modal="handleOpenModal(importJsonModal!)"
+        @handle-preview="handleOpenModal(eFormPreview!)"
+        @handle-preview2="handleOpenModal(eFormPreview2!)"
+        @handle-open-code-modal="handleOpenModal(codeModal!)"
+        @handle-clear-form-items="handleClearFormItems"
+      />
+      <FormComponentPanel
+        :current-item="formConfig.currentItem"
+        :data="formConfig"
+        @handle-set-select-item="handleSetSelectItem"
+      />
+    </LayoutContent>
+    <LayoutSider
+      class="right"
+      collapsible
+      :reverseArrow="true"
+      theme="light"
+      collapsedWidth="0"
+      width="270"
+      :zeroWidthTriggerStyle="{ 'margin-top': '-70px' }"
+      breakpoint="lg"
+    >
+      <!-- <div class="right" onselectstart="return false"> -->
+      <PropsPanel ref="propsPanel" :activeKey="formConfig.activeKey">
+        <template v-for="item of formConfig.schemas" #[`${item.component}Props`]="data">
+          <slot
+            :name="`${item.component}Props`"
+            v-bind="{ formItem: data, props: data.componentProps }"
+          ></slot>
+        </template>
+      </PropsPanel>
+      <!-- </div> -->
+    </LayoutSider>
+  </Layout>
+
+  <JsonModal ref="jsonModal" />
+  <CodeModal ref="codeModal" />
+  <ImportJsonModal ref="importJsonModal" />
+  <VFormPreview ref="eFormPreview" :formConfig="formConfig" />
+  <VFormPreview2 ref="eFormPreview2" :formConfig="formConfig" />
+  <!-- </div> -->
+</template>
+
+<script lang="ts" setup>
+  import CollapseItem from './modules/CollapseItem.vue';
+  import FormComponentPanel from './modules/FormComponentPanel.vue';
+  import JsonModal from './components/JsonModal.vue';
+  import VFormPreview from '../VFormPreview/index.vue';
+  import VFormPreview2 from '../VFormPreview/useForm.vue';
+
+  import Toolbar from './modules/Toolbar.vue';
+  import PropsPanel from './modules/PropsPanel.vue';
+  import ImportJsonModal from './components/ImportJsonModal.vue';
+  import CodeModal from './components/CodeModal.vue';
+
+  import 'codemirror/mode/javascript/javascript';
+
+  import { ref, provide, Ref } from 'vue';
+  import { Layout, LayoutContent, LayoutSider } from 'ant-design-vue';
+
+  // import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN';
+  import { IVFormComponent, IFormConfig, PropsTabKey } from '../../typings/v-form-component';
+  import { formItemsForEach, generateKey } from '../../utils';
+  import { cloneDeep } from 'lodash-es';
+  import { baseComponents, customComponents, layoutComponents } from '../../core/formItemConfig';
+  import { useRefHistory, UseRefHistoryReturn } from '@vueuse/core';
+  // import { IAnyObject } from '../../typings/base-type';
+  import { globalConfigState } from './config/formItemPropsConfig';
+  import { IFormDesignMethods, IPropsPanel, IToolbarMethods } from '../../typings/form-type';
+
+  import { CollapseContainer } from '/@/components/Container/index';
+  defineProps({
+    title: {
+      type: String,
+      default: 'v-form-antd表单设计器',
+    },
+  });
+  // 子组件实例
+  const propsPanel = ref<null | IPropsPanel>(null);
+  const jsonModal = ref<null | IToolbarMethods>(null);
+  const importJsonModal = ref<null | IToolbarMethods>(null);
+  const eFormPreview = ref<null | IToolbarMethods>(null);
+  const eFormPreview2 = ref<null | IToolbarMethods>(null);
+
+  const codeModal = ref<null | IToolbarMethods>(null);
+
+  const formModel = ref({});
+  // endregion
+  const formConfig = ref<IFormConfig>({
+    // 表单配置
+    schemas: [],
+    layout: 'horizontal',
+    labelLayout: 'flex',
+    labelWidth: 100,
+    labelCol: {},
+    wrapperCol: {},
+    currentItem: {
+      component: '',
+      componentProps: {},
+    },
+    activeKey: 1,
+  });
+  // const _state = reactive<IState>({
+  //   locale: zhCN, // 国际化
+  //   baseComponents, // 基础控件列表
+  //   layoutComponents, // 布局组件列表
+  //   customComponents,
+  //   propsPanel,
+  //   jsonModal,
+  //   eFormPreview,
+  //   eFormPreview2,
+  //   importJsonModal,
+  //   codeModal,
+  // });
+  const setFormConfig = (config: IFormConfig) => {
+    //外部导入时,可能会缺少必要的信息。
+    config.schemas = config.schemas || [];
+    config.schemas.forEach((item) => {
+      item.colProps = item.colProps || { span: 24 };
+      item.componentProps = item.componentProps || {};
+      item.itemProps = item.itemProps || {};
+    });
+    formConfig.value = config;
+  };
+  // 获取历史记录,用于撤销和重构
+  const historyReturn = useRefHistory(formConfig, {
+    deep: true,
+    capacity: 20,
+    parse: (val: IFormConfig) => {
+      // 使用lodash.cloneDeep重新拷贝数据,把currentItem指向选中项
+      const formConfig = cloneDeep(val);
+      const { currentItem, schemas } = formConfig;
+      // 从formItems中查找选中项
+
+      const item = schemas && schemas.find((item) => item.key === currentItem?.key);
+      // 如果有,则赋值给当前项,如果没有,则切换属性面板
+      if (item) {
+        formConfig.currentItem = item;
+      }
+      return formConfig;
+    },
+  });
+
+  /**
+   * 选中表单项
+   * @param schema 当前选中的表单项
+   */
+  const handleSetSelectItem = (schema: IVFormComponent) => {
+    formConfig.value.currentItem = schema;
+    handleChangePropsTabs(
+      schema.key ? (formConfig.value.activeKey! === 1 ? 2 : formConfig.value.activeKey!) : 1,
+    );
+  };
+
+  const setGlobalConfigState = (formItem: IVFormComponent) => {
+    formItem.colProps = formItem.colProps || {};
+    formItem.colProps.span = globalConfigState.span;
+    // console.log('setGlobalConfigState', formItem);
+  };
+
+  /**
+   * 添加属性
+   * @param schemas
+   * @param index
+   */
+  const handleAddAttrs = (_formItems: IVFormComponent[], _index: number) => {
+    // const item = schemas[index];
+    // setGlobalConfigState(item);
+    // generateKey(item);
+    // handleListPush(item);
+  };
+
+  const handleListPushDrag = (item: IVFormComponent) => {
+    const formItem = cloneDeep(item);
+    setGlobalConfigState(formItem);
+    generateKey(formItem);
+    // if (!formConfig.value.currentItem?.key) {
+    //   formConfig.value.schemas.push(formItem);
+    //   handleSetSelectItem(formItem);
+    //   return formItem;
+    // }
+    // handleCopy(formItem, false);
+    // handleCopy(formItem, false);
+    return formItem;
+  };
+  /**
+   * 单击控件时添加到面板中
+   * @param item {IVFormComponent} 当前点击的组件
+   */
+  const handleListPush = (item: IVFormComponent) => {
+    // console.log('handleListPush', item);
+    const formItem = cloneDeep(item);
+    setGlobalConfigState(formItem);
+    generateKey(formItem);
+    if (!formConfig.value.currentItem?.key) {
+      handleSetSelectItem(formItem);
+      formConfig.value.schemas && formConfig.value.schemas.push(formItem);
+
+      return;
+    }
+    handleCopy(formItem, false);
+  };
+
+  /**
+   * 复制表单项,如果表单项为栅格布局,则遍历所有自表单项重新生成key
+   * @param {IVFormComponent} formItem
+   * @return {IVFormComponent}
+   */
+  const copyFormItem = (formItem: IVFormComponent) => {
+    const newFormItem = cloneDeep(formItem);
+    if (newFormItem.component === 'Grid') {
+      formItemsForEach([formItem], (item) => {
+        generateKey(item);
+      });
+    }
+    return newFormItem;
+  };
+  /**
+   * 复制或者添加表单,isCopy为true时则复制表单
+   * @param item {IVFormComponent} 当前点击的组件
+   * @param isCopy {boolean} 是否复制
+   */
+  const handleCopy = (
+    item: IVFormComponent = formConfig.value.currentItem as IVFormComponent,
+    isCopy = true,
+  ) => {
+    const key = formConfig.value.currentItem?.key;
+    /**
+     * 遍历当表单项配置,如果是复制,则复制一份表单项,如果不是复制,则直接添加到表单项中
+     * @param schemas
+     */
+    const traverse = (schemas: IVFormComponent[]) => {
+      // 使用some遍历,找到目标后停止遍历
+      schemas.some((formItem: IVFormComponent, index: number) => {
+        if (formItem.key === key) {
+          // 判断是不是复制
+          isCopy
+            ? schemas.splice(index, 0, copyFormItem(formItem))
+            : schemas.splice(index + 1, 0, item);
+          const event = {
+            newIndex: index + 1,
+          };
+          // 添加到表单项中
+          handleBeforeColAdd(event, schemas, isCopy);
+          return true;
+        }
+        if (['Grid', 'Tabs'].includes(formItem.component)) {
+          // 栅格布局
+          formItem.columns?.forEach((item) => {
+            traverse(item.children);
+          });
+        }
+      });
+    };
+    if (formConfig.value.schemas) {
+      traverse(formConfig.value.schemas);
+    }
+  };
+
+  /**
+   * 添加到表单中
+   * @param newIndex {object} 事件对象
+   * @param schemas {IVFormComponent[]} 表单项列表
+   * @param isCopy {boolean} 是否复制
+   */
+  const handleBeforeColAdd = ({ newIndex }: any, schemas: IVFormComponent[], isCopy = false) => {
+    const item = schemas[newIndex];
+    isCopy && generateKey(item);
+    handleSetSelectItem(item);
+  };
+
+  /**
+   * 打开模态框
+   * @param Modal {IToolbarMethods}
+   */
+  const handleOpenModal = (Modal: IToolbarMethods) => {
+    const config = cloneDeep(formConfig.value);
+    Modal?.showModal(config);
+  };
+  /**
+   * 切换属性面板
+   * @param key
+   */
+  const handleChangePropsTabs = (key: PropsTabKey) => {
+    formConfig.value.activeKey = key;
+  };
+  /**
+   * 清空表单项列表
+   */
+  const handleClearFormItems = () => {
+    formConfig.value.schemas = [];
+    handleSetSelectItem({ component: '' });
+  };
+
+  const setFormModel = (key, value) => (formModel.value[key] = value);
+  provide('formModel', formModel);
+  // 把祖先组件的方法项注入到子组件中,子组件可通过inject获取
+  provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
+  // region 注入给子组件的属性
+  // provide('currentItem', formConfig.value.currentItem)
+
+  // 把表单配置项注入到子组件中,子组件可通过inject获取,获取到的数据为响应式
+  provide<Ref<IFormConfig>>('formConfig', formConfig);
+
+  // 注入历史记录
+  provide<UseRefHistoryReturn<any, any>>('historyReturn', historyReturn);
+
+  // 把祖先组件的方法项注入到子组件中,子组件可通过inject获取
+  provide<IFormDesignMethods>('formDesignMethods', {
+    handleBeforeColAdd,
+    handleCopy,
+    handleListPush,
+    handleSetSelectItem,
+    handleAddAttrs,
+    setFormConfig,
+  });
+
+  // endregion
+</script>
+
+<style lang="less" scoped>
+  // @import url(./styles/variable.less);
+</style>

+ 106 - 0
src/views/form-design/components/VFormDesign/modules/CollapseItem.vue

@@ -0,0 +1,106 @@
+<template>
+  <div>
+    <draggable
+      tag="ul"
+      :model-value="list"
+      v-bind="{
+        group: { name: 'form-draggable', pull: 'clone', put: false },
+        sort: false,
+        clone: cloneItem,
+        animation: 180,
+        ghostClass: 'moving',
+      }"
+      item-key="type"
+      @start="handleStart($event, list)"
+      @add="handleAdd"
+    >
+      <template #item="{ element, index }">
+        <li
+          class="bs-box text-ellipsis"
+          @dragstart="$emit('add-attrs', list, index)"
+          @click="$emit('handle-list-push', element)"
+        >
+          <!-- <svg v-if="element.icon.indexOf('icon-') > -1" class="icon" aria-hidden="true">
+            <use :xlink:href="`#${element.icon}`" />
+          </svg> -->
+          <Icon :icon="element.icon" />
+          {{ element.label }}</li
+        ></template
+      >
+    </draggable>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive } from 'vue';
+  import { IVFormComponent } from '../../../typings/v-form-component';
+  import draggable from 'vuedraggable';
+  // import { toRefs } from '@vueuse/core';
+  import { Icon } from '/@/components/Icon';
+
+  export default defineComponent({
+    name: 'CollapseItem',
+    components: { draggable, Icon },
+    props: {
+      list: {
+        type: [Array] as PropType<IVFormComponent[]>,
+        default: () => [],
+      },
+      handleListPush: {
+        type: Function as PropType<(item: IVFormComponent) => void>,
+        default: null,
+      },
+    },
+    setup(props, { emit }) {
+      const state = reactive({});
+      const handleStart = (e: any, list1: IVFormComponent[]) => {
+        emit('start', list1[e.oldIndex].component);
+      };
+      const handleAdd = (e: any) => {
+        console.log(e);
+      };
+      // https://github.com/SortableJS/vue.draggable.next
+      // https://github.com/SortableJS/vue.draggable.next/blob/master/example/components/custom-clone.vue
+      const cloneItem = (one) => {
+        return props.handleListPush(one);
+      };
+      return { state, handleStart, handleAdd, cloneItem };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  @import url(../styles/variable.less);
+
+  ul {
+    padding: 5px;
+    list-style: none;
+    display: flex;
+    margin-bottom: 0;
+    flex-wrap: wrap;
+    // background: #efefef;
+
+    li {
+      padding: 8px 12px;
+      transition: all 0.3s;
+      width: calc(50% - 6px);
+      margin: 2.7px;
+      height: 36px;
+      line-height: 20px;
+      cursor: move;
+      border: 1px solid @border-color;
+      border-radius: 3px;
+
+      &:hover {
+        color: @primary-color;
+        border: 1px solid @primary-color;
+        position: relative;
+        // z-index: 1;
+        box-shadow: 0 2px 6px @primary-color;
+      }
+    }
+  }
+
+  svg {
+    display: inline !important;
+  }
+</style>

+ 169 - 0
src/views/form-design/components/VFormDesign/modules/FormComponentPanel.vue

@@ -0,0 +1,169 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/18
+ * @Description: 中间表单布局面板
+ * https://github.com/SortableJS/vue.draggable.next/issues/138
+-->
+<template>
+  <div class="form-panel v-form-container">
+    <Empty
+      class="empty-text"
+      v-show="formConfig.schemas.length === 0"
+      description="从左侧选择控件添加"
+    />
+    <Form v-bind="formConfig">
+      <div class="draggable-box">
+        <draggable
+          class="list-main ant-row"
+          group="form-draggable"
+          :component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
+          ghostClass="moving"
+          :animation="180"
+          handle=".drag-move"
+          v-model="formConfig.schemas"
+          item-key="key"
+          @add="addItem"
+          @start="handleDragStart"
+        >
+          <template #item="{ element }">
+            <LayoutItem
+              class="drag-move"
+              :schema="element"
+              :data="formConfig"
+              :current-item="formConfig.currentItem || {}"
+            />
+          </template>
+        </draggable>
+      </div>
+    </Form>
+  </div>
+</template>
+<script lang="ts">
+  import draggable from 'vuedraggable';
+  import { defineComponent, computed } from 'vue';
+  import LayoutItem from '../components/LayoutItem.vue';
+  import { cloneDeep } from 'lodash-es';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { Form, Empty } from 'ant-design-vue';
+
+  export default defineComponent({
+    name: 'FormComponentPanel',
+    components: {
+      LayoutItem,
+      draggable,
+      Form,
+      Empty,
+    },
+    emits: ['handleSetSelectItem'],
+    setup(_, { emit }) {
+      const { formConfig } = useFormDesignState() as Recordable;
+
+      /**
+       * 拖拽完成事件
+       * @param newIndex
+       */
+      const addItem = ({ newIndex }: any) => {
+        formConfig.value.schemas = formConfig.value.schemas || [];
+
+        const schemas = formConfig.value.schemas;
+        schemas[newIndex] = cloneDeep(schemas[newIndex]);
+        emit('handleSetSelectItem', schemas[newIndex]);
+      };
+
+      /**
+       * 拖拽开始事件
+       * @param e {Object} 事件对象
+       */
+      const handleDragStart = (e: any) => {
+        emit('handleSetSelectItem', formConfig.value.schemas[e.oldIndex]);
+      };
+
+      // 获取祖先组件传递的currentItem
+
+      // 计算布局元素,水平模式下为ACol,非水平模式下为div
+      const layoutTag = computed(() => {
+        return formConfig.value.layout === 'horizontal' ? 'Col' : 'div';
+      });
+
+      return {
+        addItem,
+        handleDragStart,
+        formConfig,
+        layoutTag,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  @import url(../styles/variable.less);
+  @import url(../styles/drag.less);
+
+  .v-form-container {
+    // 内联布局样式
+    .ant-form-inline {
+      .list-main {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: flex-start;
+        align-content: flex-start;
+
+        .layout-width {
+          width: 100%;
+        }
+      }
+
+      .ant-form-item-control-wrapper {
+        min-width: 175px !important;
+      }
+    }
+  }
+
+  .form-panel {
+    position: relative;
+    height: 100%;
+
+    .empty-text {
+      color: #aaa;
+      height: 150px;
+      top: -10%;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      margin: auto;
+      position: absolute;
+      z-index: 100;
+    }
+
+    .draggable-box {
+      // width: 100%;
+      .drag-move {
+        cursor: move;
+        min-height: 62px;
+      }
+
+      .list-main {
+        overflow: auto;
+        height: 100%;
+        // 列表动画
+        .list-enter-active {
+          transition: all 0.5s;
+        }
+
+        .list-leave-active {
+          transition: all 0.3s;
+        }
+
+        .list-enter,
+        .list-leave-to {
+          opacity: 0;
+          transform: translateX(-100px);
+        }
+
+        .list-enter {
+          height: 30px;
+        }
+      }
+    }
+  }
+</style>

+ 100 - 0
src/views/form-design/components/VFormDesign/modules/PropsPanel.vue

@@ -0,0 +1,100 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/19
+ * @Description: 右侧属性配置面板
+-->
+<template>
+  <div>
+    <Tabs v-model:activeKey="formConfig.activeKey" :tabBarStyle="{ margin: 0 }">
+      <TabPane :key="1" tab="表单">
+        <FormProps />
+      </TabPane>
+      <TabPane :key="2" tab="控件">
+        <FormItemProps />
+      </TabPane>
+      <TabPane :key="3" tab="栅格">
+        <ComponentColumnProps />
+      </TabPane>
+      <TabPane :key="4" tab="组件">
+        <slot v-if="slotProps" :name="slotProps.component + 'Props'"></slot>
+        <ComponentProps v-else />
+      </TabPane>
+    </Tabs>
+  </div>
+</template>
+<script lang="ts">
+  import { computed, defineComponent } from 'vue';
+  import FormProps from '../components/FormProps.vue';
+  import FormItemProps from '../components/FormItemProps.vue';
+  import ComponentProps from '../components/ComponentProps.vue';
+  import ComponentColumnProps from '../components/FormItemColumnProps.vue';
+  import { useFormDesignState } from '../../../hooks/useFormDesignState';
+  import { customComponents } from '../../../core/formItemConfig';
+  import { TabPane, Tabs } from 'ant-design-vue';
+  type ChangeTabKey = 1 | 2;
+  export interface IPropsPanel {
+    changeTab: (key: ChangeTabKey) => void;
+  }
+  export default defineComponent({
+    name: 'PropsPanel',
+    components: {
+      FormProps,
+      FormItemProps,
+      ComponentProps,
+      ComponentColumnProps,
+      Tabs,
+      TabPane,
+    },
+    setup() {
+      const { formConfig } = useFormDesignState();
+      const slotProps = computed(() => {
+        return customComponents.find(
+          (item) => item.component === formConfig.value.currentItem?.component,
+        );
+      });
+      return { formConfig, customComponents, slotProps };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  @import url(../styles/variable.less);
+
+  :deep(.ant-tabs) {
+    box-sizing: border-box;
+
+    form {
+      width: 100%;
+      position: absolute;
+      height: calc(100% - 50px);
+      margin-right: 10px;
+      overflow-y: auto;
+      overflow-x: hidden;
+    }
+
+    .hint-box {
+      margin-top: 200px;
+    }
+
+    .ant-form-item,
+    .ant-slider-with-marks {
+      margin-left: 10px;
+      margin-right: 20px;
+      margin-bottom: 0;
+    }
+
+    .ant-form-item {
+      // width: 100%;
+      margin-bottom: 0;
+
+      .ant-form-item-label {
+        line-height: 2;
+        vertical-align: text-top;
+      }
+    }
+
+    .ant-input-number {
+      width: 100%;
+    }
+  }
+</style>

+ 141 - 0
src/views/form-design/components/VFormDesign/modules/Toolbar.vue

@@ -0,0 +1,141 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/23
+ * @Description: 工具栏
+-->
+<template>
+  <div class="operating-area">
+    <!-- 头部操作按钮区域 start -->
+    <!-- 操作左侧区域 start -->
+    <div class="left-btn-box">
+      <Tooltip v-for="item in toolbarsConfigs" :title="item.title" :key="item.icon">
+        <a @click="$emit(item.event)" class="toolbar-text">
+          <!-- <a-icon :type="item.icon" /> -->
+          <Icon :icon="item.icon" />
+        </a>
+      </Tooltip>
+      <Divider type="vertical" />
+      <Tooltip title="撤销">
+        <a :class="{ disabled: !canUndo }" :disabled="!canUndo" @click="undo">
+          <!-- <a-icon type="undo" /> -->
+          <Icon icon="ant-design:undo-outlined" />
+        </a>
+      </Tooltip>
+      <Tooltip title="重做">
+        <a :class="{ disabled: !canRedo }" :disabled="!canRedo" @click="redo">
+          <!-- <a-icon type="redo" /> -->
+          <Icon icon="ant-design:redo-outlined" />
+        </a>
+      </Tooltip>
+    </div>
+  </div>
+  <!-- 操作区域 start -->
+</template>
+<script lang="ts">
+  import { defineComponent, inject, reactive, toRefs } from 'vue';
+  import { UseRefHistoryReturn } from '@vueuse/core';
+  import { IFormConfig } from '../../../typings/v-form-component';
+  import { Tooltip, Divider } from 'ant-design-vue';
+  import Icon from '/@/components/Icon/index';
+
+  interface IToolbarsConfig {
+    type: string;
+    title: string;
+    icon: string;
+    event: string;
+  }
+
+  export default defineComponent({
+    name: 'OperatingArea',
+    components: {
+      Tooltip,
+      Icon,
+      Divider,
+    },
+    setup() {
+      const state = reactive<{
+        toolbarsConfigs: IToolbarsConfig[];
+      }>({
+        toolbarsConfigs: [
+          {
+            title: '预览',
+            type: 'preview',
+            event: 'handlePreview',
+            icon: 'ant-design:chrome-filled',
+          },
+          {
+            title: '预览2',
+            type: 'preview',
+            event: 'handlePreview2',
+            icon: 'ant-design:chrome-filled',
+          },
+          {
+            title: '导入',
+            type: 'importJson',
+            event: 'handleOpenImportJsonModal',
+            icon: 'ant-design:import-outlined',
+          },
+          {
+            title: '生成JSON',
+            type: 'exportJson',
+            event: 'handleOpenJsonModal',
+            icon: 'ant-design:export-outlined',
+          },
+          {
+            title: '生成代码',
+            type: 'exportCode',
+            event: 'handleOpenCodeModal',
+            icon: 'ant-design:code-filled',
+          },
+          {
+            title: '清空',
+            type: 'reset',
+            event: 'handleClearFormItems',
+            icon: 'ant-design:clear-outlined',
+          },
+        ],
+      });
+      const historyRef = inject('historyReturn') as UseRefHistoryReturn<IFormConfig, IFormConfig>;
+
+      const { undo, redo, canUndo, canRedo } = historyRef;
+      return { ...toRefs(state), undo, redo, canUndo, canRedo };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  //noinspection CssUnknownTarget
+  @import url('../styles/variable.less');
+
+  .operating-area {
+    border-bottom: 2px solid @border-color;
+    font-size: 16px;
+    text-align: left;
+    height: @operating-area-height;
+    line-height: @operating-area-height;
+    padding: 0 12px;
+    display: flex;
+    justify-content: space-between;
+    align-content: center;
+    padding-left: 30px;
+
+    a {
+      color: #666;
+      margin: 0 5px;
+
+      &.disabled,
+      &.disabled:hover {
+        color: #ccc;
+      }
+
+      &:hover {
+        color: @primary-color;
+      }
+
+      > span {
+        font-size: 14px;
+        padding-left: 2px;
+      }
+    }
+  }
+</style>

+ 231 - 0
src/views/form-design/components/VFormDesign/styles/drag.less

@@ -0,0 +1,231 @@
+.draggable-box {
+  height: 100%;
+  overflow: auto;
+
+  :deep(.list-main) {
+    overflow: hidden;
+    min-height: 100%;
+    padding: 5px;
+    position: relative;
+    background: #fafafa;
+    // border    : 1px #ccc dashed;
+
+    .moving {
+      // 拖放移动中
+      // outline-width: 0;
+      min-height: 35px;
+      box-sizing: border-box;
+      overflow: hidden;
+      padding: 0 !important;
+      // margin       : 3px 0;
+      position: relative;
+
+      &::before {
+        content: '';
+        height: 5px;
+        width: 100%;
+        background: @primary-color;
+        position: absolute;
+        top: 0;
+        right: 0;
+      }
+    }
+
+    .drag-move-box {
+      position: relative;
+      box-sizing: border-box;
+      padding: 8px;
+      overflow: hidden;
+      transition: all 0.3s;
+      min-height: 60px;
+
+      &:hover {
+        background: @primary-hover-bg-color;
+      }
+
+      // 选择时 start
+      &::before {
+        content: '';
+        height: 5px;
+        width: 100%;
+        background: @primary-color;
+        position: absolute;
+        top: 0;
+        right: -100%;
+        transition: all 0.3s;
+      }
+
+      &.active {
+        background: @primary-hover-bg-color;
+        outline-offset: 0;
+
+        &::before {
+          right: 0;
+        }
+      }
+
+      // 选择时 end
+      .form-item-box {
+        position: relative;
+        box-sizing: border-box;
+        word-wrap: break-word;
+
+        &::before {
+          content: '';
+          position: absolute;
+          width: 100%;
+          height: 100%;
+          top: 0;
+          left: 0;
+          // z-index: 888;
+        }
+
+        .ant-form-item {
+          // 修改ant form-item的margin为padding
+          margin: 0;
+          padding-bottom: 6px;
+        }
+      }
+
+      .show-key-box {
+        // 显示key
+        position: absolute;
+        bottom: 2px;
+        right: 5px;
+        font-size: 14px;
+        // z-index: 999;
+        color: @primary-color;
+      }
+
+      .copy,
+      .delete {
+        position: absolute;
+        top: 0;
+        width: 30px;
+        height: 30px;
+        line-height: 30px;
+        text-align: center;
+        color: #fff;
+        // z-index: 989;
+        transition: all 0.3s;
+
+        &.unactivated {
+          opacity: 0 !important;
+          pointer-events: none;
+        }
+
+        &.active {
+          opacity: 1 !important;
+        }
+      }
+
+      .copy {
+        border-radius: 0 0 0 8px;
+        right: 30px;
+        background: @primary-color;
+      }
+
+      .delete {
+        right: 0;
+        background: @primary-color;
+      }
+    }
+
+    .grid-box {
+      position: relative;
+      box-sizing: border-box;
+      padding: 5px;
+      background: @layout-background-color;
+      width: 100%;
+      transition: all 0.3s;
+      overflow: hidden;
+
+      .form-item-box {
+        position: relative;
+        box-sizing: border-box;
+
+        .ant-form-item {
+          // 修改ant form-item的margin为padding
+          margin: 0;
+          padding-bottom: 15px;
+        }
+      }
+
+      .grid-row {
+        background: @layout-background-color;
+
+        .grid-col {
+          .draggable-box {
+            min-height: 80px;
+            min-width: 50px;
+            border: 1px #ccc dashed;
+            background: #fff;
+
+            .list-main {
+              min-height: 83px;
+              position: relative;
+              border: 1px #ccc dashed;
+            }
+          }
+        }
+      }
+
+      // 选择时 start
+      &::before {
+        content: '';
+        height: 5px;
+        width: 100%;
+        background: transparent;
+        position: absolute;
+        top: 0;
+        right: -100%;
+        transition: all 0.3s;
+      }
+
+      &.active {
+        background: @layout-hover-bg-color;
+        outline-offset: 0;
+
+        &::before {
+          background: @layout-color;
+          right: 0;
+        }
+      }
+      // 选择时 end
+      > .copy-delete-box {
+        > .copy,
+        > .delete {
+          position: absolute;
+          top: 0;
+          width: 30px;
+          height: 30px;
+          line-height: 30px;
+          text-align: center;
+          color: #fff;
+          // z-index: 989;
+          transition: all 0.3s;
+
+          &.unactivated {
+            opacity: 0 !important;
+            pointer-events: none;
+          }
+
+          &.active {
+            opacity: 1 !important;
+          }
+        }
+
+        > .copy {
+          border-radius: 0 0 0 8px;
+          right: 30px;
+          background: @layout-color;
+        }
+
+        > .delete {
+          right: 0;
+          background: @layout-color;
+        }
+      }
+    }
+  }
+}

+ 522 - 0
src/views/form-design/components/VFormDesign/styles/v-form-design.less

@@ -0,0 +1,522 @@
+.v-form-design-container {
+  // height: 100%;
+  width: 100%;
+  // overflow: hidden;
+  display: flex;
+  flex-direction: column;
+
+  & > .v-form-design-header {
+    height: @header-height;
+    line-height: @header-height;
+    background: @primary-color;
+    text-align: center;
+    font-size: 20px;
+    color: #fff;
+  }
+
+  :deep(.icon) {
+    width: 1em;
+    height: 1em;
+    vertical-align: -0.15em;
+    fill: currentcolor;
+    // overflow: hidden;
+  }
+
+  .content {
+    position: relative;
+    flex: 1;
+    // margin-top: 5px;
+    display: flex;
+    // flex-flow: row nowrap;
+    // height: 100%;
+    // overflow: hidden;
+    box-sizing: border-box;
+
+    .left,
+    .right {
+      width: @left-right-width;
+      box-shadow: 0 0 1px 1px #ccc;
+      // overflow: hidden;
+      // height: 100%;
+      // height: 600px;
+      //
+      border: 1px solid green;
+      // overflow-y: auto;
+      // :deep(.ant-tabs) {
+      //   height: 100%;
+      //   .ant-tabs-content-holder {
+      //     // display: flex;
+      //     // flex-flow: column;
+      //     height: 100%;
+      //     .ant-tabs-content {
+      //       // flex:1;
+      //       // height: 0;
+      //       // overflow-y: auto;
+      //       // overflow-x: hidden;
+      //       display: flex;
+      //       flex-flow: column;
+      //       height: 100%;
+      //     }
+      //   }
+      // }
+    }
+
+    :deep(.right) {
+      // & > div {
+      // height: 100%;
+      // }
+      // overflow: hidden;
+      // & > div {
+      //   height: 100%;
+      //   .ant-tabs-content-holder{
+      //     height: 100%;
+      //     .ant-tabs-content{
+      //       height: 100%;
+      //       .ant-tabs-tabpane{
+      //         height: 100%;
+      //       }
+      //     }
+      //   }
+      // }
+
+      .ant-tabs {
+        width: 280px;
+        height: 100%;
+        // overflow: hidden;
+
+        .ant-tabs-content-holder {
+          // display: flex;
+          // flex-flow: column;
+          // height: 100%;
+          // overflow: hidden;
+
+          .ant-tabs-content {
+            // flex:1;
+            // height: 0;
+            // overflow-y: auto;
+            // overflow-x: hidden;
+
+            height: 100%;
+            // overflow: hidden;
+            .ant-tabs-tabpane {
+              .properties-content {
+                // height: 100%;
+
+                // overflow: auto;
+                // overflow: hidden;
+
+                // background: #fff;
+                .properties-body {
+                  box-sizing: border-box;
+                  // height: 100%;
+
+                  // display: flex;
+                  // flex-flow: column;
+
+                  form {
+                    position: absolute;
+                    // height: 400px;
+                    height: calc(100% - 50px);
+                    // height: 100%;
+                    // flex: 1;
+                    // height: 0;
+                    margin-right: 10px;
+                    // overflow: auto;
+                    overflow-y: auto;
+                    overflow-x: hidden;
+                    // overflow: auto;
+                  }
+                  // overflow: auto;
+                  // height: 100%;
+                  // padding: 8px 16px;
+                  .hint-box {
+                    margin-top: 200px;
+                  }
+
+                  .ant-form-item,
+                  .ant-slider-with-marks {
+                    margin-left: 10px;
+                    margin-right: 20px;
+                    margin-bottom: 0;
+                  }
+
+                  .ant-form-item {
+                    // box-sizing: border-box;
+                    width: 100%;
+                    margin-bottom: 0;
+                    // padding: 2px 0;
+                    border-bottom: 1px solid @border-color;
+
+                    .ant-form-item-label {
+                      line-height: 2;
+                      vertical-align: text-top;
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    :deep(.left) {
+      .ant-collapse {
+        border: 0;
+
+        .ant-collapse-header {
+          padding: 7px 0 7px 40px;
+        }
+
+        .ant-collapse-content-box {
+          padding: 0;
+        }
+      }
+
+      ul {
+        padding: 5px;
+        list-style: none;
+        display: flex;
+        margin-bottom: 0;
+        flex-wrap: wrap;
+        // background: #efefef;
+
+        li {
+          padding: 8px 12px;
+          transition: all 0.3s;
+          width: calc(50% - 6px);
+          margin: 2.7px;
+          height: 36px;
+          line-height: 20px;
+          cursor: move;
+          border: 1px solid @border-color;
+          border-radius: 3px;
+
+          &:hover {
+            color: @primary-color;
+            border: 1px solid @primary-color;
+            position: relative;
+            // z-index: 1;
+            box-shadow: 0 2px 6px @primary-color;
+          }
+        }
+      }
+    }
+
+    :deep(.node-panel) {
+      box-shadow: 0 0 1px 1px #ccc;
+      flex: 1;
+      margin: 0 8px;
+      overflow: hidden;
+
+      .draggable-box {
+        height: 100%;
+        overflow: auto;
+
+        .list-main {
+          overflow: hidden;
+          min-height: 100%;
+          padding: 5px;
+          position: relative;
+          background: #fafafa;
+          // border    : 1px #ccc dashed;
+
+          .moving {
+            // 拖放移动中
+            // outline-width: 0;
+            min-height: 35px;
+            box-sizing: border-box;
+            overflow: hidden;
+            padding: 0 !important;
+            // margin       : 3px 0;
+            position: relative;
+
+            &::before {
+              content: '';
+              height: 5px;
+              width: 100%;
+              background: @primary-color;
+              position: absolute;
+              top: 0;
+              right: 0;
+            }
+          }
+
+          .drag-move-box {
+            position: relative;
+            box-sizing: border-box;
+            padding: 8px;
+            overflow: hidden;
+            transition: all 0.3s;
+            min-height: 60px;
+
+            &:hover {
+              background: @primary-hover-bg-color;
+            }
+
+            // 选择时 start
+            &::before {
+              content: '';
+              height: 5px;
+              width: 100%;
+              background: @primary-color;
+              position: absolute;
+              top: 0;
+              right: -100%;
+              transition: all 0.3s;
+            }
+
+            &.active {
+              background: @primary-hover-bg-color;
+              outline-offset: 0;
+
+              &::before {
+                right: 0;
+              }
+            }
+
+            // 选择时 end
+            .form-item-box {
+              position: relative;
+              box-sizing: border-box;
+              word-wrap: break-word;
+
+              &::before {
+                content: '';
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                top: 0;
+                left: 0;
+                // z-index: 888;
+              }
+
+              .ant-form-item {
+                // 修改ant form-item的margin为padding
+                margin: 0;
+                padding-bottom: 6px;
+              }
+            }
+
+            .show-key-box {
+              // 显示key
+              position: absolute;
+              bottom: 2px;
+              right: 5px;
+              font-size: 14px;
+              // z-index: 999;
+              color: @primary-color;
+            }
+
+            .copy,
+            .delete {
+              position: absolute;
+              top: 0;
+              width: 30px;
+              height: 30px;
+              line-height: 30px;
+              text-align: center;
+              color: #fff;
+              // z-index: 989;
+              transition: all 0.3s;
+
+              &.unactivated {
+                opacity: 0 !important;
+                pointer-events: none;
+              }
+
+              &.active {
+                opacity: 1 !important;
+              }
+            }
+
+            .copy {
+              border-radius: 0 0 0 8px;
+              right: 30px;
+              background: @primary-color;
+            }
+
+            .delete {
+              right: 0;
+              background: @primary-color;
+            }
+          }
+
+          .grid-box {
+            position: relative;
+            box-sizing: border-box;
+            padding: 5px;
+            background: @layout-background-color;
+            width: 100%;
+            transition: all 0.3s;
+            overflow: hidden;
+
+            .form-item-box {
+              position: relative;
+              box-sizing: border-box;
+
+              .ant-form-item {
+                // 修改ant form-item的margin为padding
+                margin: 0;
+                padding-bottom: 15px;
+              }
+            }
+
+            .grid-row {
+              background: @layout-background-color;
+
+              .grid-col {
+                .draggable-box {
+                  min-height: 80px;
+                  min-width: 50px;
+                  border: 1px #ccc dashed;
+                  background: #fff;
+
+                  .list-main {
+                    min-height: 83px;
+                    position: relative;
+                    border: 1px #ccc dashed;
+                  }
+                }
+              }
+            }
+
+            // 选择时 start
+            &::before {
+              content: '';
+              height: 5px;
+              width: 100%;
+              background: transparent;
+              position: absolute;
+              top: 0;
+              right: -100%;
+              transition: all 0.3s;
+            }
+
+            &.active {
+              background: @layout-hover-bg-color;
+              outline-offset: 0;
+
+              &::before {
+                background: @layout-color;
+                right: 0;
+              }
+            }
+            // 选择时 end
+            > .copy-delete-box {
+              > .copy,
+              > .delete {
+                position: absolute;
+                top: 0;
+                width: 30px;
+                height: 30px;
+                line-height: 30px;
+                text-align: center;
+                color: #fff;
+                // z-index: 989;
+                transition: all 0.3s;
+
+                &.unactivated {
+                  opacity: 0 !important;
+                  pointer-events: none;
+                }
+
+                &.active {
+                  opacity: 1 !important;
+                }
+              }
+
+              > .copy {
+                border-radius: 0 0 0 8px;
+                right: 30px;
+                background: @layout-color;
+              }
+
+              > .delete {
+                right: 0;
+                background: @layout-color;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  ::-webkit-scrollbar {
+    /* 滚动条整体样式 */
+    width: 6px;
+
+    /* 高宽分别对应横竖滚动条的尺寸 */
+    height: 6px;
+    scrollbar-arrow-color: red;
+  }
+
+  ::-webkit-scrollbar-thumb {
+    /* 滚动条里面小方块 */
+    border-radius: 5px;
+    box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
+    background: rgb(0 0 0 / 20%);
+    scrollbar-arrow-color: red;
+  }
+
+  ::-webkit-scrollbar-track {
+    /* 滚动条里面轨道 */
+    box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
+    border-radius: 0;
+    background: rgb(0 0 0 / 10%);
+  }
+}
+
+// code盒子样式
+.v-json-box {
+  height: 570px;
+  overflow: auto;
+
+  .vue-codemirror-wrap {
+    height: 100%;
+
+    .CodeMirror-wrap {
+      height: 100%;
+      background: #f6f6f6;
+
+      .CodeMirror-scroll {
+        height: 100%;
+        width: 100%;
+      }
+
+      pre.CodeMirror-line,
+      .CodeMirror-linenumber {
+        min-height: 21px;
+        line-height: 21px;
+      }
+    }
+  }
+}
+
+// code-modal盒子样式
+.v-code-modal {
+  .ant-modal-body {
+    padding: 12px;
+  }
+}
+
+.v-form-container {
+  // 内联布局样式
+  .ant-form-inline {
+    .list-main {
+      display: flex;
+      flex-wrap: wrap;
+      justify-content: flex-start;
+      align-content: flex-start;
+
+      .layout-width {
+        width: 100%;
+      }
+    }
+
+    .ant-form-item-control-wrapper {
+      min-width: 175px !important;
+    }
+  }
+}

+ 15 - 0
src/views/form-design/components/VFormDesign/styles/variable.less

@@ -0,0 +1,15 @@
+// 表单设计器样式
+@primary-color: #13c2c2;
+@layout-color: #9867f7;
+
+@primary-background-color: fade(@primary-color, 6%);
+@primary-hover-bg-color: fade(@primary-color, 20%);
+@layout-background-color: fade(@layout-color, 12%);
+@layout-hover-bg-color: fade(@layout-color, 24%);
+
+@title-text-color: #fff;
+@border-color: #ccc;
+
+@left-right-width: 280px;
+@header-height: 56px;
+@operating-area-height: 45px;

+ 228 - 0
src/views/form-design/components/VFormItem/index.vue

@@ -0,0 +1,228 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/19
+ * @Description:
+-->
+<template>
+  <Col v-bind="colPropsComputed">
+    <FormItem v-bind="{ ...formItemProps }">
+      <template #label v-if="!formItemProps.hiddenLabel && schema.component !== 'Divider'">
+        <Tooltip>
+          <span>{{ schema.label }}</span>
+          <template #title v-if="schema.helpMessage"
+            ><span>{{ schema.helpMessage }}</span></template
+          >
+          <Icon v-if="schema.helpMessage" class="ml-5" icon="ant-design:question-circle-outlined" />
+        </Tooltip>
+      </template>
+
+      <slot
+        v-if="schema.componentProps && schema.componentProps?.slotName"
+        :name="schema.componentProps.slotName"
+        v-bind="schema"
+      ></slot>
+      <Divider
+        v-else-if="schema.component == 'Divider' && schema.label && !formItemProps.hiddenLabel"
+        >{{ schema.label }}</Divider
+      >
+      <!-- 部分控件需要一个空div -->
+      <div
+        ><component
+          class="v-form-item-wrapper"
+          :is="componentItem"
+          v-bind="{ ...cmpProps, ...asyncProps }"
+          :schema="schema"
+          :style="schema.width ? { width: schema.width } : {}"
+          @change="handleChange"
+          @click="handleClick(schema)"
+      /></div>
+
+      <span v-if="['Button'].includes(schema.component)">{{ schema.label }}</span>
+    </FormItem>
+  </Col>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, toRefs, computed, PropType, unref } from 'vue';
+  import { componentMap } from '../../core/formItemConfig';
+  import { IVFormComponent, IFormConfig } from '../../typings/v-form-component';
+  import { asyncComputed } from '@vueuse/core';
+  import { handleAsyncOptions } from '../../utils';
+  import { omit } from 'lodash-es';
+  import { Tooltip, FormItem, Divider, Col } from 'ant-design-vue';
+
+  // import FormItem from '/@/components/Form/src/components/FormItem.vue';
+
+  import { Icon } from '/@/components/Icon';
+  import { useFormModelState } from '../../hooks/useFormDesignState';
+  export default defineComponent({
+    name: 'VFormItem',
+    components: {
+      Tooltip,
+      Icon,
+      FormItem,
+      Divider,
+      Col,
+    },
+
+    props: {
+      formData: {
+        type: Object,
+        default: () => ({}),
+      },
+      schema: {
+        type: Object as PropType<IVFormComponent>,
+        required: true,
+      },
+      formConfig: {
+        type: Object as PropType<IFormConfig>,
+        required: true,
+      },
+    },
+    emits: ['update:form-data', 'change'],
+    setup(props, { emit }) {
+      const state = reactive({
+        componentMap,
+      });
+
+      const { formModel: formData1, setFormModel } = useFormModelState();
+      const colPropsComputed = computed(() => {
+        const { colProps = {} } = props.schema;
+        return colProps;
+      });
+      const formItemProps = computed(() => {
+        const { formConfig } = unref(props);
+        let { field, required, rules, labelCol, wrapperCol } = unref(props.schema);
+        const { colon } = props.formConfig;
+
+        const { itemProps } = unref(props.schema);
+
+        //<editor-fold desc="布局属性">
+        labelCol = labelCol
+          ? labelCol
+          : formConfig.layout === 'horizontal'
+          ? formConfig.labelLayout === 'flex'
+            ? { style: `width:${formConfig.labelWidth}px` }
+            : formConfig.labelCol
+          : {};
+
+        wrapperCol = wrapperCol
+          ? wrapperCol
+          : formConfig.layout === 'horizontal'
+          ? formConfig.labelLayout === 'flex'
+            ? { style: 'width:auto;flex:1' }
+            : formConfig.wrapperCol
+          : {};
+
+        const style =
+          formConfig.layout === 'horizontal' && formConfig.labelLayout === 'flex'
+            ? { display: 'flex' }
+            : {};
+
+        /**
+         * 将字符串正则格式化成正则表达式
+         */
+
+        const newConfig = Object.assign(
+          {},
+          {
+            name: field,
+            style: { ...style },
+            colon,
+            required,
+            rules,
+            labelCol,
+            wrapperCol,
+          },
+          itemProps,
+        );
+        if (!itemProps?.labelCol?.span) {
+          newConfig.labelCol = labelCol;
+        }
+        if (!itemProps?.wrapperCol?.span) {
+          newConfig.wrapperCol = wrapperCol;
+        }
+        if (!itemProps?.rules) {
+          newConfig.rules = rules;
+        }
+        return newConfig;
+      }) as Recordable;
+
+      const componentItem = computed(() => componentMap.get(props.schema.component as string));
+
+      // console.log('component change:', props.schema.component, componentItem.value);
+      const handleClick = (schema: IVFormComponent) => {
+        if (schema.component === 'Button' && schema.componentProps?.handle)
+          emit(schema.componentProps?.handle);
+      };
+      /**
+       * 处理异步属性,异步属性会导致一些属性渲染错误,如defaultValue异步加载会导致渲染不出来,故而此处只处理options,treeData,同步属性在cmpProps中处理
+       */
+      const asyncProps = asyncComputed(async () => {
+        let { options, treeData } = props.schema.componentProps ?? {};
+        if (options) options = await handleAsyncOptions(options);
+        if (treeData) treeData = await handleAsyncOptions(treeData);
+        return {
+          options,
+          treeData,
+        };
+      });
+
+      /**
+       * 处理同步属性
+       */
+      const cmpProps = computed(() => {
+        const isCheck =
+          props.schema && ['Switch', 'Checkbox', 'Radio'].includes(props.schema.component);
+        let { field } = props.schema;
+
+        let { disabled, ...attrs } =
+          omit(props.schema.componentProps, ['options', 'treeData']) ?? {};
+
+        disabled = props.formConfig.disabled || disabled;
+
+        return {
+          ...attrs,
+          disabled,
+          [isCheck ? 'checked' : 'value']: formData1.value[field!],
+        };
+      });
+
+      const handleChange = function (e) {
+        const isCheck = ['Switch', 'Checkbox', 'Radio'].includes(props.schema.component);
+        const target = e ? e.target : null;
+        const value = target ? (isCheck ? target.checked : target.value) : e;
+        setFormModel(props.schema.field!, value);
+        emit('change', value);
+      };
+      return {
+        ...toRefs(state),
+        componentItem,
+        formItemProps,
+        handleClick,
+        asyncProps,
+        cmpProps,
+        handleChange,
+        colPropsComputed,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .ml-5 {
+    margin-left: 5px;
+  }
+
+  // form字段中的标签有ant-col,不能使用width:100%
+  :deep(.ant-col) {
+    width: auto;
+  }
+
+  .ant-form-item:not(.ant-form-item-with-help) {
+    margin-bottom: 20px;
+  }
+
+  // .w-full {
+  //   width: 100% !important;
+  // }
+</style>

+ 79 - 0
src/views/form-design/components/VFormItem/vFormItem.vue

@@ -0,0 +1,79 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/19
+ * @Description:
+   `<FormItem`
+    :tableAction="tableAction"
+    :formActionType="formActionType"
+    :schema="schema2"
+    :formProps="getProps"
+    :allDefaultValues="defaultValueRef"
+    :formModel="formModel"
+    :setFormModel="setFormModel"
+  >
+
+  <FormItem
+  :tableAction="tableAction"
+  :formActionType="formActionType"
+  :schema="schemaNew"
+  :formProps="getProps"
+  :allDefaultValues="defaultValueRef"
+  :formModel="formModel"
+  >
+-->
+
+<template>
+  <FormItem :schema="schemaNew" :formProps="getProps">
+    <template #[item]="data" v-for="item in Object.keys($slots)">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+  </FormItem>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, unref } from 'vue';
+  import { IFormConfig, IVFormComponent } from '../../typings/v-form-component';
+  import { FormProps, FormSchema } from '/@/components/Form';
+
+  import FormItem from '/@/components/Form/src/components/FormItem.vue';
+
+  export default defineComponent({
+    name: 'VFormItem',
+    components: {
+      FormItem,
+    },
+    props: {
+      formData: {
+        type: Object,
+        default: () => ({}),
+      },
+      schema: {
+        type: Object as PropType<IVFormComponent>,
+        required: true,
+      },
+      formConfig: {
+        type: Object as PropType<IFormConfig>,
+        required: true,
+      },
+    },
+    setup(props) {
+      const schema = computed(() => {
+        const schema: FormSchema = {
+          ...unref(props.schema),
+        } as FormSchema;
+
+        return schema;
+      });
+
+      // Get the basic configuration of the form
+      const getProps = computed((): FormProps => {
+        return { ...unref(props.formConfig) } as FormProps;
+      });
+      return {
+        schemaNew: schema,
+        getProps,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped></style>

+ 108 - 0
src/views/form-design/components/VFormPreview/index.vue

@@ -0,0 +1,108 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/29
+ * @Description: 渲染组件,无法使用Vben的组件
+-->
+<template>
+  <Modal
+    title="预览(标准Ant控件)"
+    :visible="visible"
+    @ok="handleGetData"
+    @cancel="handleCancel"
+    okText="获取数据"
+    cancelText="关闭"
+    style="top: 20px"
+    :destroyOnClose="true"
+    :width="900"
+  >
+    <VFormCreate
+      :form-config="formConfig"
+      v-model:fApi="fApi"
+      v-model:formModel="formModel"
+      @submit="onSubmit"
+    >
+      <template #slotName="{ formModel, field }">
+        <a-input v-model:value="formModel[field]" placeholder="我是插槽传递的输入框" />
+      </template>
+    </VFormCreate>
+    <JsonModal ref="jsonModal" />
+  </Modal>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive, ref, toRaw, toRefs } from 'vue';
+  import { IFormConfig } from '../../typings/v-form-component';
+  import { IAnyObject } from '../../typings/base-type';
+  import VFormCreate from '../VFormCreate/index.vue';
+  import { formatRules } from '../../utils';
+  import { IVFormMethods } from '../../hooks/useVFormMethods';
+  import JsonModal from '../VFormDesign/components/JsonModal.vue';
+  import { IToolbarMethods } from '../../typings/form-type';
+  import { Modal } from 'ant-design-vue';
+  export default defineComponent({
+    name: 'VFormPreview',
+    components: {
+      JsonModal,
+      VFormCreate,
+      Modal,
+    },
+    setup() {
+      const jsonModal = ref<IToolbarMethods | null>(null);
+      const state = reactive<{
+        formModel: IAnyObject;
+        visible: boolean;
+        formConfig: IFormConfig;
+        fApi: IVFormMethods;
+      }>({
+        formModel: {},
+        formConfig: {} as IFormConfig,
+        visible: false,
+        fApi: {} as IVFormMethods,
+      });
+
+      /**
+       * 显示Json数据弹框
+       * @param jsonData
+       */
+      const showModal = (jsonData: IFormConfig) => {
+        // console.log('showModal-', jsonData);
+        formatRules(jsonData.schemas);
+        state.formConfig = jsonData;
+        state.visible = true;
+      };
+
+      /**
+       * 获取表单数据
+       * @return {Promise<void>}
+       */
+      const handleCancel = () => {
+        // console.log('handleCancel');
+        state.visible = false;
+        state.formModel = {};
+      };
+      const handleGetData = async () => {
+        // console.log('handleGetData');
+        console.log(toRaw(state.formModel));
+        const _data = await state.fApi.submit?.();
+        // console.log('handleGetData', 'end');
+        jsonModal.value?.showModal?.(_data);
+        // jsonModal.value?.showModal?.(toRaw(state.formModel));
+      };
+
+      const onSubmit = (_data: IAnyObject) => {
+        // console.log('-> data', data);
+      };
+      const onCancel = () => {
+        state.formModel = {};
+      };
+      return {
+        handleGetData,
+        handleCancel,
+        ...toRefs(state),
+        showModal,
+        jsonModal,
+        onSubmit,
+        onCancel,
+      };
+    },
+  });
+</script>

+ 74 - 0
src/views/form-design/components/VFormPreview/useForm.vue

@@ -0,0 +1,74 @@
+<!--
+ * @Author: ypt
+ * @Date: 2021/11/29
+ * @Description: 使用vbenForm的功能进行渲染
+-->
+<template>
+  <Modal
+    title="预览(VbenForm)"
+    :visible="state.visible"
+    @ok="handleGetData"
+    @cancel="handleCancel"
+    okText="获取数据"
+    cancelText="关闭"
+    style="top: 20px"
+    :destroyOnClose="true"
+    :width="900"
+  >
+    <BasicForm v-bind="attrs" @register="registerForm" />
+    <JsonModal ref="jsonModal" />
+  </Modal>
+</template>
+<script lang="ts" setup>
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { reactive, ref, computed } from 'vue';
+  import { IFormConfig } from '../../typings/v-form-component';
+  import { IAnyObject } from '../../typings/base-type';
+  import JsonModal from '../VFormDesign/components/JsonModal.vue';
+  import { IToolbarMethods } from '../../typings/form-type';
+  import { Modal } from 'ant-design-vue';
+
+  const jsonModal = ref<IToolbarMethods | null>(null);
+  const state = reactive<{
+    formModel: IAnyObject;
+    visible: boolean;
+    formConfig: IFormConfig;
+  }>({
+    formModel: {},
+    formConfig: {} as IFormConfig,
+    visible: false,
+  });
+
+  const attrs = computed(() => {
+    return {
+      ...state.formConfig,
+    } as Recordable;
+  });
+
+  /**
+   * 显示Json数据弹框
+   * @param jsonData
+   */
+  const showModal = (jsonData: IFormConfig) => {
+    state.formConfig = jsonData;
+    state.visible = true;
+  };
+
+  //表单
+  const [registerForm, { validate }] = useForm();
+
+  const handleCancel = () => {
+    state.visible = false;
+  };
+  /**
+   * 获取表单数据
+   * @return {Promise<void>}
+   */
+  const handleGetData = async () => {
+    let data = await validate();
+    console.log(data);
+    jsonModal.value?.showModal?.(data);
+  };
+
+  defineExpose({ showModal });
+</script>

+ 71 - 0
src/views/form-design/components/index.ts

@@ -0,0 +1,71 @@
+import type { Component } from 'vue';
+import { ComponentType } from '/@/components/Form/src/types';
+import { IconPicker } from '/@/components/Icon/index';
+/**
+ * Component list, register here to setting it in the form
+ */
+import {
+  Input,
+  Button,
+  Select,
+  Radio,
+  Checkbox,
+  AutoComplete,
+  Cascader,
+  DatePicker,
+  InputNumber,
+  Switch,
+  TimePicker,
+  // ColorPicker,
+  TreeSelect,
+  Slider,
+  Rate,
+  Divider,
+  Calendar,
+  Transfer,
+} from 'ant-design-vue';
+
+//ant-desing本身的Form控件库
+
+const componentMap = new Map<string, Component>();
+componentMap.set('Radio', Radio);
+componentMap.set('Button', Button);
+componentMap.set('Calendar', Calendar);
+componentMap.set('Input', Input);
+componentMap.set('InputGroup', Input.Group);
+componentMap.set('InputPassword', Input.Password);
+componentMap.set('InputSearch', Input.Search);
+componentMap.set('InputTextArea', Input.TextArea);
+componentMap.set('InputNumber', InputNumber);
+componentMap.set('AutoComplete', AutoComplete);
+
+componentMap.set('Select', Select);
+componentMap.set('TreeSelect', TreeSelect);
+componentMap.set('Switch', Switch);
+componentMap.set('RadioGroup', Radio.Group);
+componentMap.set('Checkbox', Checkbox);
+componentMap.set('CheckboxGroup', Checkbox.Group);
+componentMap.set('Cascader', Cascader);
+componentMap.set('Slider', Slider);
+componentMap.set('Rate', Rate);
+componentMap.set('Transfer', Transfer);
+componentMap.set('DatePicker', DatePicker);
+componentMap.set('MonthPicker', DatePicker.MonthPicker);
+componentMap.set('RangePicker', DatePicker.RangePicker);
+componentMap.set('WeekPicker', DatePicker.WeekPicker);
+componentMap.set('TimePicker', TimePicker);
+
+componentMap.set('ColorPicker', TimePicker);
+
+componentMap.set('IconPicker', IconPicker);
+componentMap.set('Divider', Divider);
+
+export function add(compName: ComponentType, component: Component) {
+  componentMap.set(compName, component);
+}
+
+export function del(compName: ComponentType) {
+  componentMap.delete(compName);
+}
+
+export { componentMap };

+ 423 - 0
src/views/form-design/core/formItemConfig.ts

@@ -0,0 +1,423 @@
+/**
+ * @name: formItemConfig
+ * @author: ypt
+ * @date: 2021/11/18 16:25
+ * @description:表单配置
+ */
+import { IVFormComponent } from '../typings/v-form-component';
+import { isArray } from 'lodash-es';
+import { componentMap as VbenCmp, add } from '/@/components/Form/src/componentMap';
+import { ComponentType } from '/@/components/Form/src/types';
+
+import { componentMap as Cmp } from '../components';
+import { Component } from 'vue';
+
+const componentMap = new Map<string, Component>();
+
+//如果有其它控件,可以在这里初始化
+
+//注册Ant控件库
+Cmp.forEach((value, key) => {
+  componentMap.set(key, value);
+  if (VbenCmp[key] == null) {
+    add(key as ComponentType, value);
+  }
+});
+//注册vben控件库
+VbenCmp.forEach((value, key) => {
+  componentMap.set(key, value);
+});
+
+export { componentMap };
+
+/**
+ * 设置自定义表单控件
+ * @param {IVFormComponent | IVFormComponent[]} config
+ */
+export function setFormDesignComponents(config: IVFormComponent | IVFormComponent[]) {
+  if (isArray(config)) {
+    config.forEach((item) => {
+      const { componentInstance: component, ...rest } = item;
+      componentMap[item.component] = component;
+      customComponents.push(Object.assign({ props: {} }, rest));
+    });
+  } else {
+    const { componentInstance: component, ...rest } = config;
+    componentMap[config.component] = component;
+    customComponents.push(Object.assign({ props: {} }, rest));
+  }
+}
+
+//外部设置的自定义控件
+export const customComponents: IVFormComponent[] = [];
+
+// 左侧控件列表与初始化的控件属性
+// props.slotName,会在formitem级别生成一个slot,并绑定当前record值
+// 属性props,类型为对象,不能为undefined或是null。
+export const baseComponents: IVFormComponent[] = [
+  {
+    component: 'InputCountDown',
+    label: '倒计时输入',
+    icon: 'line-md:iconify2',
+    colProps: { span: 24 },
+    field: '',
+    componentProps: {},
+  },
+  {
+    component: 'IconPicker',
+    label: '图标选择器',
+    icon: 'line-md:iconify2',
+    colProps: { span: 24 },
+    field: '',
+    componentProps: {},
+  },
+  {
+    component: 'StrengthMeter',
+    label: '密码强度',
+    icon: 'wpf:password1',
+    colProps: { span: 24 },
+    field: '',
+    componentProps: {},
+  },
+  {
+    component: 'AutoComplete',
+    label: '自动完成',
+    icon: 'wpf:password1',
+    colProps: { span: 24 },
+    field: '',
+    componentProps: {
+      placeholder: '请输入正则表达式',
+      options: [
+        {
+          value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
+          label: '手机号码',
+        },
+        {
+          value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
+          label: '网址带端口号',
+        },
+      ],
+    },
+  },
+  {
+    component: 'Divider',
+    label: '分割线',
+    icon: 'radix-icons:divider-horizontal',
+    colProps: { span: 24 },
+    field: '',
+    componentProps: {
+      orientation: 'center',
+      dashed: true,
+    },
+  },
+  {
+    component: 'Checkbox',
+    label: '复选框',
+    icon: 'ant-design:check-circle-outlined',
+    colProps: { span: 24 },
+    field: '',
+  },
+  {
+    component: 'CheckboxGroup',
+    label: '复选框-组',
+    icon: 'ant-design:check-circle-filled',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      options: [
+        {
+          label: '选项1',
+          value: '1',
+        },
+        {
+          label: '选项2',
+          value: '2',
+        },
+      ],
+    },
+  },
+  {
+    component: 'Input',
+    label: '输入框',
+    icon: 'bi:input-cursor-text',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      type: 'text',
+    },
+  },
+  {
+    component: 'InputNumber',
+    label: '数字输入框',
+    icon: 'ant-design:field-number-outlined',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: { style: 'width:200px' },
+  },
+  {
+    component: 'InputTextArea',
+    label: '文本域',
+    icon: 'ant-design:file-text-filled',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'Select',
+    label: '下拉选择',
+    icon: 'gg:select',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      options: [
+        {
+          label: '选项1',
+          value: '1',
+        },
+        {
+          label: '选项2',
+          value: '2',
+        },
+      ],
+    },
+  },
+
+  {
+    component: 'Radio',
+    label: '单选框',
+    icon: 'ant-design:check-circle-outlined',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'RadioGroup',
+    label: '单选框-组',
+    icon: 'carbon:radio-button-checked',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      options: [
+        {
+          label: '选项1',
+          value: '1',
+        },
+        {
+          label: '选项2',
+          value: '2',
+        },
+      ],
+    },
+  },
+  {
+    component: 'DatePicker',
+    label: '日期选择',
+    icon: 'healthicons:i-schedule-school-date-time-outline',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'RangePicker',
+    label: '日期范围',
+    icon: 'healthicons:i-schedule-school-date-time-outline',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      placeholder: ['开始日期', '结束日期'],
+    },
+  },
+  {
+    component: 'MonthPicker',
+    label: '月份选择',
+    icon: 'healthicons:i-schedule-school-date-time-outline',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      placeholder: '请选择月份',
+    },
+  },
+  {
+    component: 'TimePicker',
+    label: '时间选择',
+    icon: 'healthicons:i-schedule-school-date-time',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'Slider',
+    label: '滑动输入条',
+    icon: 'vaadin:slider',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'Rate',
+    label: '评分',
+    icon: 'ic:outline-star-rate',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'Switch',
+    label: '开关',
+    icon: 'entypo:switch',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {},
+  },
+  {
+    component: 'TreeSelect',
+    label: '树形选择',
+    icon: 'clarity:tree-view-line',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      treeData: [
+        {
+          label: '选项1',
+          value: '1',
+          children: [
+            {
+              label: '选项三',
+              value: '1-1',
+            },
+          ],
+        },
+        {
+          label: '选项2',
+          value: '2',
+        },
+      ],
+    },
+  },
+  {
+    component: 'Upload',
+    label: '上传',
+    icon: 'ant-design:upload-outlined',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      api: () => 1,
+    },
+  },
+  {
+    component: 'Cascader',
+    label: '级联选择',
+    icon: 'ant-design:check-outlined',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      options: [
+        {
+          label: '选项1',
+          value: '1',
+          children: [
+            {
+              label: '选项三',
+              value: '1-1',
+            },
+          ],
+        },
+        {
+          label: '选项2',
+          value: '2',
+        },
+      ],
+    },
+  },
+  // {
+  //   component: 'Button',
+  //   label: '按钮',
+  //   icon: 'dashicons:button',
+  //   field: '',
+  //   colProps: { span: 24 },
+  //   hiddenLabel: true,
+  //   componentProps: {},
+  // },
+  {
+    component: 'ColorPicker',
+    label: '颜色选择器',
+    icon: 'carbon:color-palette',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      defaultValue: '',
+      value: '',
+    },
+  },
+
+  {
+    component: 'slot',
+    label: '插槽',
+    icon: 'vs:timeslot-question',
+    field: '',
+    colProps: { span: 24 },
+    componentProps: {
+      slotName: 'slotName',
+    },
+  },
+];
+
+// https://next.antdv.com/components/transfer-cn
+const transferControl = {
+  component: 'Transfer',
+  label: '穿梭框',
+  icon: 'bx:bx-transfer-alt',
+  field: '',
+  colProps: { span: 24 },
+  componentProps: {
+    render: (item) => item.title,
+    dataSource: [
+      {
+        key: 'key-1',
+        title: '标题1',
+        description: '描述',
+        disabled: false,
+        chosen: true,
+      },
+      {
+        key: 'key-2',
+        title: 'title2',
+        description: 'description2',
+        disabled: true,
+      },
+      {
+        key: 'key-3',
+        title: '标题3',
+        description: '描述3',
+        disabled: false,
+        chosen: true,
+      },
+    ],
+  },
+};
+
+baseComponents.push(transferControl);
+
+export const layoutComponents: IVFormComponent[] = [
+  {
+    field: '',
+    component: 'Grid',
+    label: '栅格布局',
+    icon: 'icon-grid',
+    componentProps: {},
+    columns: [
+      {
+        span: 12,
+        children: [],
+      },
+      {
+        span: 12,
+        children: [],
+      },
+    ],
+    colProps: { span: 24 },
+    options: {
+      gutter: 0,
+    },
+  },
+];

+ 739 - 0
src/views/form-design/core/iconConfig.ts

@@ -0,0 +1,739 @@
+const iconConfig = {
+  filled: [
+    'account-book',
+    'alert',
+    'alipay-circle',
+    'alipay-square',
+    'aliwangwang',
+    'amazon-circle',
+    'android',
+    'amazon-square',
+    'api',
+    'appstore',
+    'audio',
+    'apple',
+    'backward',
+    'bank',
+    'behance-circle',
+    'bell',
+    'behance-square',
+    'book',
+    'box-plot',
+    'bug',
+    'bulb',
+    'calculator',
+    'build',
+    'calendar',
+    'camera',
+    'car',
+    'caret-down',
+    'caret-left',
+    'caret-right',
+    'carry-out',
+    'caret-up',
+    'check-circle',
+    'check-square',
+    'chrome',
+    'ci-circle',
+    'clock-circle',
+    'close-circle',
+    'cloud',
+    'close-square',
+    'code-sandbox-square',
+    'code-sandbox-circle',
+    'code',
+    'codepen-circle',
+    'compass',
+    'codepen-square',
+    'contacts',
+    'container',
+    'control',
+    'copy',
+    'copyright-circle',
+    'credit-card',
+    'crown',
+    'customer-service',
+    'dashboard',
+    'delete',
+    'diff',
+    'dingtalk-circle',
+    'database',
+    'dingtalk-square',
+    'dislike',
+    'dollar-circle',
+    'down-circle',
+    'down-square',
+    'dribbble-circle',
+    'dribbble-square',
+    'dropbox-circle',
+    'dropbox-square',
+    'environment',
+    'edit',
+    'exclamation-circle',
+    'euro-circle',
+    'experiment',
+    'eye-invisible',
+    'eye',
+    'facebook',
+    'fast-backward',
+    'fast-forward',
+    'file-add',
+    'file-excel',
+    'file-exclamation',
+    'file-image',
+    'file-markdown',
+    'file-pdf',
+    'file-ppt',
+    'file-text',
+    'file-unknown',
+    'file-word',
+    'file-zip',
+    'file',
+    'filter',
+    'fire',
+    'flag',
+    'folder-add',
+    'folder',
+    'folder-open',
+    'forward',
+    'frown',
+    'fund',
+    'funnel-plot',
+    'gift',
+    'github',
+    'gitlab',
+    'golden',
+    'google-circle',
+    'google-plus-circle',
+    'google-plus-square',
+    'google-square',
+    'hdd',
+    'heart',
+    'highlight',
+    'home',
+    'hourglass',
+    'html5',
+    'idcard',
+    'ie-circle',
+    'ie-square',
+    'info-circle',
+    'instagram',
+    'insurance',
+    'interaction',
+    'interation',
+    'layout',
+    'left-circle',
+    'left-square',
+    'like',
+    'linkedin',
+    'lock',
+    'mail',
+    'medicine-box',
+    'medium-circle',
+    'medium-square',
+    'meh',
+    'message',
+    'minus-circle',
+    'minus-square',
+    'mobile',
+    'money-collect',
+    'pause-circle',
+    'pay-circle',
+    'notification',
+    'phone',
+    'picture',
+    'pie-chart',
+    'play-circle',
+    'play-square',
+    'plus-circle',
+    'plus-square',
+    'pound-circle',
+    'printer',
+    'profile',
+    'project',
+    'pushpin',
+    'property-safety',
+    'qq-circle',
+    'qq-square',
+    'question-circle',
+    'read',
+    'reconciliation',
+    'red-envelope',
+    'reddit-circle',
+    'reddit-square',
+    'rest',
+    'right-circle',
+    'rocket',
+    'right-square',
+    'safety-certificate',
+    'save',
+    'schedule',
+    'security-scan',
+    'setting',
+    'shop',
+    'shopping',
+    'sketch-circle',
+    'sketch-square',
+    'skin',
+    'slack-circle',
+    'skype',
+    'slack-square',
+    'sliders',
+    'smile',
+    'snippets',
+    'sound',
+    'star',
+    'step-backward',
+    'step-forward',
+    'stop',
+    'switcher',
+    'tablet',
+    'tag',
+    'tags',
+    'taobao-circle',
+    'taobao-square',
+    'tool',
+    'thunderbolt',
+    'trademark-circle',
+    'twitter-circle',
+    'trophy',
+    'twitter-square',
+    'unlock',
+    'up-circle',
+    'up-square',
+    'usb',
+    'video-camera',
+    'wallet',
+    'warning',
+    'wechat',
+    'weibo-circle',
+    'windows',
+    'yahoo',
+    'weibo-square',
+    'yuque',
+    'youtube',
+    'zhihu-circle',
+    'zhihu-square',
+  ],
+  outlined: [
+    'account-book',
+    'alert',
+    'alipay-circle',
+    'aliwangwang',
+    'android',
+    'api',
+    'appstore',
+    'audio',
+    'apple',
+    'backward',
+    'bank',
+    'bell',
+    'behance-square',
+    'book',
+    'box-plot',
+    'bug',
+    'bulb',
+    'calculator',
+    'build',
+    'calendar',
+    'camera',
+    'car',
+    'caret-down',
+    'caret-left',
+    'caret-right',
+    'carry-out',
+    'caret-up',
+    'check-circle',
+    'check-square',
+    'chrome',
+    'clock-circle',
+    'close-circle',
+    'cloud',
+    'close-square',
+    'code',
+    'codepen-circle',
+    'compass',
+    'contacts',
+    'container',
+    'control',
+    'copy',
+    'credit-card',
+    'crown',
+    'customer-service',
+    'dashboard',
+    'delete',
+    'diff',
+    'database',
+    'dislike',
+    'down-circle',
+    'down-square',
+    'dribbble-square',
+    'environment',
+    'edit',
+    'exclamation-circle',
+    'experiment',
+    'eye-invisible',
+    'eye',
+    'facebook',
+    'fast-backward',
+    'fast-forward',
+    'file-add',
+    'file-excel',
+    'file-exclamation',
+    'file-image',
+    'file-markdown',
+    'file-pdf',
+    'file-ppt',
+    'file-text',
+    'file-unknown',
+    'file-word',
+    'file-zip',
+    'file',
+    'filter',
+    'fire',
+    'flag',
+    'folder-add',
+    'folder',
+    'folder-open',
+    'forward',
+    'frown',
+    'fund',
+    'funnel-plot',
+    'gift',
+    'github',
+    'gitlab',
+    'hdd',
+    'heart',
+    'highlight',
+    'home',
+    'hourglass',
+    'html5',
+    'idcard',
+    'info-circle',
+    'instagram',
+    'insurance',
+    'interaction',
+    'interation',
+    'layout',
+    'left-circle',
+    'left-square',
+    'like',
+    'linkedin',
+    'lock',
+    'mail',
+    'medicine-box',
+    'meh',
+    'message',
+    'minus-circle',
+    'minus-square',
+    'mobile',
+    'money-collect',
+    'pause-circle',
+    'pay-circle',
+    'notification',
+    'phone',
+    'picture',
+    'pie-chart',
+    'play-circle',
+    'play-square',
+    'plus-circle',
+    'plus-square',
+    'printer',
+    'profile',
+    'project',
+    'pushpin',
+    'property-safety',
+    'question-circle',
+    'read',
+    'reconciliation',
+    'red-envelope',
+    'rest',
+    'right-circle',
+    'rocket',
+    'right-square',
+    'safety-certificate',
+    'save',
+    'schedule',
+    'security-scan',
+    'setting',
+    'shop',
+    'shopping',
+    'skin',
+    'skype',
+    'slack-square',
+    'sliders',
+    'smile',
+    'snippets',
+    'sound',
+    'star',
+    'step-backward',
+    'step-forward',
+    'stop',
+    'switcher',
+    'tablet',
+    'tag',
+    'tags',
+    'taobao-circle',
+    'tool',
+    'thunderbolt',
+    'trophy',
+    'unlock',
+    'up-circle',
+    'up-square',
+    'usb',
+    'video-camera',
+    'wallet',
+    'warning',
+    'wechat',
+    'weibo-circle',
+    'windows',
+    'yahoo',
+    'weibo-square',
+    'yuque',
+    'youtube',
+    'alibaba',
+    'align-center',
+    'align-left',
+    'align-right',
+    'alipay',
+    'aliyun',
+    'amazon',
+    'ant-cloud',
+    'apartment',
+    'ant-design',
+    'area-chart',
+    'arrow-left',
+    'arrow-down',
+    'arrow-up',
+    'arrows-alt',
+    'arrow-right',
+    'audit',
+    'bar-chart',
+    'barcode',
+    'bars',
+    'behance',
+    'bg-colors',
+    'block',
+    'bold',
+    'border-bottom',
+    'border-left',
+    'border-outer',
+    'border-inner',
+    'border-right',
+    'border-horizontal',
+    'border-top',
+    'border-verticle',
+    'border',
+    'branches',
+    'check',
+    'ci',
+    'close',
+    'cloud-download',
+    'cloud-server',
+    'cloud-sync',
+    'cloud-upload',
+    'cluster',
+    'codepen',
+    'code-sandbox',
+    'colum-height',
+    'column-width',
+    'column-height',
+    'coffee',
+    'copyright',
+    'dash',
+    'deployment-unit',
+    'desktop',
+    'dingding',
+    'disconnect',
+    'dollar',
+    'double-left',
+    'dot-chart',
+    'double-right',
+    'down',
+    'drag',
+    'download',
+    'dribbble',
+    'dropbox',
+    'ellipsis',
+    'enter',
+    'euro',
+    'exception',
+    'exclamation',
+    'export',
+    'fall',
+    'file-done',
+    'file-jpg',
+    'file-protect',
+    'file-sync',
+    'file-search',
+    'font-colors',
+    'font-size',
+    'fork',
+    'form',
+    'fullscreen-exit',
+    'fullscreen',
+    'gateway',
+    'global',
+    'google-plus',
+    'gold',
+    'google',
+    'heat-map',
+    'history',
+    'ie',
+    'import',
+    'inbox',
+    'info',
+    'italic',
+    'key',
+    'issues-close',
+    'laptop',
+    'left',
+    'line-chart',
+    'link',
+    'line-height',
+    'line',
+    'loading-3-quarters',
+    'loading',
+    'login',
+    'logout',
+    'man',
+    'medium',
+    'medium-workmark',
+    'menu-unfold',
+    'menu-fold',
+    'menu',
+    'minus',
+    'monitor',
+    'more',
+    'ordered-list',
+    'number',
+    'pause',
+    'percentage',
+    'paper-clip',
+    'pic-center',
+    'pic-left',
+    'pic-right',
+    'plus',
+    'pound',
+    'poweroff',
+    'pull-request',
+    'qq',
+    'question',
+    'radar-chart',
+    'qrcode',
+    'radius-bottomleft',
+    'radius-bottomright',
+    'radius-upleft',
+    'radius-setting',
+    'radius-upright',
+    'reddit',
+    'redo',
+    'reload',
+    'retweet',
+    'right',
+    'rise',
+    'rollback',
+    'safety',
+    'robot',
+    'scan',
+    'search',
+    'scissor',
+    'select',
+    'shake',
+    'share-alt',
+    'shopping-cart',
+    'shrink',
+    'sketch',
+    'slack',
+    'small-dash',
+    'solution',
+    'sort-descending',
+    'sort-ascending',
+    'stock',
+    'swap-left',
+    'swap-right',
+    'strikethrough',
+    'swap',
+    'sync',
+    'table',
+    'team',
+    'taobao',
+    'to-top',
+    'trademark',
+    'transaction',
+    'twitter',
+    'underline',
+    'undo',
+    'unordered-list',
+    'up',
+    'upload',
+    'user-add',
+    'user-delete',
+    'usergroup-add',
+    'user',
+    'usergroup-delete',
+    'vertical-align-bottom',
+    'vertical-align-middle',
+    'vertical-align-top',
+    'vertical-left',
+    'vertical-right',
+    'weibo',
+    'wifi',
+    'zhihu',
+    'woman',
+    'zoom-out',
+    'zoom-in',
+  ],
+  twoTone: [
+    'account-book',
+    'alert',
+    'api',
+    'appstore',
+    'audio',
+    'bank',
+    'bell',
+    'book',
+    'box-plot',
+    'bug',
+    'bulb',
+    'calculator',
+    'build',
+    'calendar',
+    'camera',
+    'car',
+    'carry-out',
+    'check-circle',
+    'check-square',
+    'clock-circle',
+    'close-circle',
+    'cloud',
+    'close-square',
+    'code',
+    'compass',
+    'contacts',
+    'container',
+    'control',
+    'copy',
+    'credit-card',
+    'crown',
+    'customer-service',
+    'dashboard',
+    'delete',
+    'diff',
+    'database',
+    'dislike',
+    'down-circle',
+    'down-square',
+    'environment',
+    'edit',
+    'exclamation-circle',
+    'experiment',
+    'eye-invisible',
+    'eye',
+    'file-add',
+    'file-excel',
+    'file-exclamation',
+    'file-image',
+    'file-markdown',
+    'file-pdf',
+    'file-ppt',
+    'file-text',
+    'file-unknown',
+    'file-word',
+    'file-zip',
+    'file',
+    'filter',
+    'fire',
+    'flag',
+    'folder-add',
+    'folder',
+    'folder-open',
+    'frown',
+    'fund',
+    'funnel-plot',
+    'gift',
+    'hdd',
+    'heart',
+    'highlight',
+    'home',
+    'hourglass',
+    'html5',
+    'idcard',
+    'info-circle',
+    'insurance',
+    'interaction',
+    'interation',
+    'layout',
+    'left-circle',
+    'left-square',
+    'like',
+    'lock',
+    'mail',
+    'medicine-box',
+    'meh',
+    'message',
+    'minus-circle',
+    'minus-square',
+    'mobile',
+    'money-collect',
+    'pause-circle',
+    'notification',
+    'phone',
+    'picture',
+    'pie-chart',
+    'play-circle',
+    'play-square',
+    'plus-circle',
+    'plus-square',
+    'pound-circle',
+    'printer',
+    'profile',
+    'project',
+    'pushpin',
+    'property-safety',
+    'question-circle',
+    'reconciliation',
+    'red-envelope',
+    'rest',
+    'right-circle',
+    'rocket',
+    'right-square',
+    'safety-certificate',
+    'save',
+    'schedule',
+    'security-scan',
+    'setting',
+    'shop',
+    'shopping',
+    'skin',
+    'sliders',
+    'smile',
+    'snippets',
+    'sound',
+    'star',
+    'stop',
+    'switcher',
+    'tablet',
+    'tag',
+    'tags',
+    'tool',
+    'thunderbolt',
+    'trademark-circle',
+    'trophy',
+    'unlock',
+    'up-circle',
+    'up-square',
+    'usb',
+    'video-camera',
+    'wallet',
+    'warning',
+    'ci',
+    'copyright',
+    'dollar',
+    'euro',
+    'gold',
+    'canlendar',
+  ],
+};
+
+export default iconConfig;

+ 37 - 0
src/views/form-design/examples/baseForm.vue

@@ -0,0 +1,37 @@
+<template>
+  <BasicForm @register="register" />
+</template>
+<script lang="ts" setup>
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+
+  const schemas: FormSchema[] = [
+    {
+      field: 'field1',
+      component: 'Input',
+      label: '字段1',
+      span: 8,
+      // colProps: {
+      //   span: 8,
+      // },
+      componentProps: {
+        placeholder: '自定义placeholder',
+        onChange: (e: any) => {
+          console.log(e);
+        },
+      },
+    },
+    {
+      field: 'field2',
+      component: 'Input',
+      label: '字段2',
+      span: 8,
+      // colProps: {
+      //   span: 8,
+      // },
+    },
+  ];
+
+  const [register] = useForm({
+    schemas,
+  });
+</script>

+ 18 - 0
src/views/form-design/hooks/useFormDesignState.ts

@@ -0,0 +1,18 @@
+import { inject, Ref } from 'vue';
+import { IFormDesignMethods } from '../typings/form-type';
+import { IFormConfig } from '../typings/v-form-component';
+
+/**
+ * 获取formDesign状态
+ */
+export function useFormDesignState() {
+  const formConfig = inject('formConfig') as Ref<IFormConfig>;
+  const formDesignMethods = inject('formDesignMethods') as IFormDesignMethods;
+  return { formConfig, formDesignMethods };
+}
+
+export function useFormModelState() {
+  const formModel = inject('formModel') as Ref<{}>;
+  const setFormModel = inject('setFormModelMethod') as (key: String, value: any) => void;
+  return { formModel, setFormModel };
+}

+ 62 - 0
src/views/form-design/hooks/useFormInstanceMethods.ts

@@ -0,0 +1,62 @@
+import { IAnyObject } from '../typings/base-type';
+import { Ref, SetupContext } from 'vue';
+import { cloneDeep, forOwn, isFunction } from 'lodash-es';
+import { AForm, IVFormComponent } from '../typings/v-form-component';
+import { getCurrentInstance } from 'vue';
+import { Form } from 'ant-design-vue';
+import { toRaw } from 'vue';
+
+export function useFormInstanceMethods(
+  props: IAnyObject,
+  formdata,
+  context: Partial<SetupContext>,
+  _formInstance: Ref<AForm | null>,
+) {
+  /**
+   * 绑定props和on中的上下文为parent
+   */
+  const bindContext = () => {
+    const instance = getCurrentInstance();
+    const vm = instance?.parent;
+    if (!vm) return;
+
+    (props.formConfig.schemas as IVFormComponent[]).forEach((item) => {
+      // 绑定 props 中的上下文
+      forOwn(item.componentProps, (value: any, key) => {
+        if (isFunction(value)) {
+          item.componentProps![key] = value.bind(vm);
+        }
+      });
+      // 绑定事件监听(v-on)的上下文
+      forOwn(item.on, (value: any, key) => {
+        if (isFunction(value)) {
+          item.componentProps![key] = value.bind(vm);
+        }
+      });
+    });
+  };
+  bindContext();
+
+  const { emit } = context;
+
+  const useForm = Form.useForm;
+
+  const { resetFields, validate, clearValidate, validateField } = useForm(formdata, []);
+
+  const submit = async () => {
+    //const _result = await validate();
+
+    const data = cloneDeep(toRaw(formdata.value));
+    emit?.('submit', data);
+    props.formConfig.submit?.(data);
+    return data;
+  };
+
+  return {
+    validate,
+    validateField,
+    resetFields,
+    clearValidate,
+    submit,
+  };
+}

+ 195 - 0
src/views/form-design/hooks/useVFormMethods.ts

@@ -0,0 +1,195 @@
+import { Ref, SetupContext } from 'vue';
+import { IVFormComponent, IFormConfig, AForm } from '../typings/v-form-component';
+import { findFormItem, formItemsForEach } from '../utils';
+import { cloneDeep, isFunction } from 'lodash-es';
+import { IAnyObject } from '../typings/base-type';
+
+interface IFormInstanceMethods extends AForm {
+  submit: () => Promise<any>;
+}
+
+export interface IProps {
+  formConfig: IFormConfig;
+  formModel: IAnyObject;
+}
+
+type ISet = <T extends keyof IVFormComponent>(
+  field: string,
+  key: T,
+  value: IVFormComponent[T],
+) => void;
+// 获取当前field绑定的表单项
+type IGet = (field: string) => IVFormComponent | undefined;
+// 获取field在formData中的值
+type IGetValue = (field: string) => any;
+// 设置field在formData中的值并且触发校验
+type ISetValue = (field: string | IAnyObject, value?: any) => void;
+// 隐藏field对应的表单项
+type IHidden = (field: string) => void;
+// 显示field对应的表单项
+type IShow = (field: string) => void;
+// 设置field对应的表单项绑定的props属性
+type ISetProps = (field: string, key: string, value: any) => void;
+// 获取formData中的值
+type IGetData = () => Promise<IAnyObject>;
+// 禁用表单,如果field为空,则禁用整个表单
+type IDisable = (field?: string | boolean) => void;
+// 设置表单配置方法
+type ISetFormConfig = (key: string, value: any) => void;
+interface ILinkOn {
+  [key: string]: Set<IVFormComponent>;
+}
+
+export interface IVFormMethods extends Partial<IFormInstanceMethods> {
+  set: ISet;
+  get: IGet;
+  getValue: IGetValue;
+  setValue: ISetValue;
+  hidden: IHidden;
+  show: IShow;
+  setProps: ISetProps;
+  linkOn: ILinkOn;
+  getData: IGetData;
+  disable: IDisable;
+}
+export function useVFormMethods(
+  props: IProps,
+  _context: Partial<SetupContext>,
+  formInstance: Ref<AForm | null>,
+  formInstanceMethods: Partial<IFormInstanceMethods>,
+): IVFormMethods {
+  /**
+   * 根据field获取表单项
+   * @param {string} field
+   * @return {IVFormComponent | undefined}
+   */
+  const get: IGet = (field) =>
+    findFormItem(props.formConfig.schemas, (item) => item.field === field);
+
+  /**
+   * 根据表单field设置表单项字段值
+   * @param {string} field
+   * @param {keyof IVFormComponent} key
+   * @param {never} value
+   */
+  const set: ISet = (field, key, value) => {
+    const formItem = get(field);
+    if (formItem) formItem[key] = value;
+  };
+
+  /**
+   * 设置表单项的props
+   * @param {string} field 需要设置的表单项field
+   * @param {string} key 需要设置的key
+   * @param value 需要设置的值
+   */
+  const setProps: ISetProps = (field, key, value) => {
+    const formItem = get(field);
+    if (formItem?.componentProps) {
+      ['options', 'treeData'].includes(key) && setValue(field, undefined);
+
+      formItem.componentProps[key] = value;
+    }
+  };
+  /**
+   * 设置字段的值,设置后触发校验
+   * @param {string} field  需要设置的字段
+   * @param value  需要设置的值
+   */
+  const setValue: ISetValue = (field, value) => {
+    if (typeof field === 'string') {
+      // props.formData[field] = value
+      props.formModel[field] = value;
+      formInstance.value?.validateField(field, value, []);
+    } else {
+      const keys = Object.keys(field);
+      keys.forEach((key) => {
+        props.formModel[key] = field[key];
+        formInstance.value?.validateField(key, field[key], []);
+      });
+    }
+  };
+  /**
+   * 设置表单配置方法
+   * @param {string} key
+   * @param value
+   */
+  const setFormConfig: ISetFormConfig = (key, value) => {
+    props.formConfig[key] = value;
+  };
+  /**
+   * 根据表单项field获取字段值,如果field为空,则
+   * @param {string} field  需要设置的字段
+   */
+  const getValue: IGetValue = (field) => {
+    const formData = cloneDeep(props.formModel);
+    return formData[field];
+  };
+
+  /**
+   * 获取formData中的值
+   * @return {Promise<IAnyObject<any>>}
+   */
+  const getData: IGetData = async () => {
+    return cloneDeep(props.formModel);
+  };
+  /**
+   * 隐藏指定表单项
+   * @param {string} field 需要隐藏的表单项的field
+   */
+  const hidden: IHidden = (field) => {
+    set(field, 'hidden', true);
+  };
+
+  /**
+   * 禁用表单
+   * @param {string | undefined} field
+   */
+  const disable: IDisable = (field) => {
+    typeof field === 'string'
+      ? setProps(field, 'disabled', true)
+      : setFormConfig('disabled', field !== false);
+  };
+
+  /**
+   * 显示表单项
+   * @param {string} field 需要显示的表单项的field
+   */
+  const show: IShow = (field) => {
+    set(field, 'hidden', false);
+  };
+
+  /**
+   * 监听表单字段联动时触发
+   * @type {ILinkOn}
+   */
+  const linkOn: ILinkOn = {};
+  const initLink = (schemas: IVFormComponent[]) => {
+    // 首次遍历,查找需要关联字段的表单
+    formItemsForEach(schemas, (formItem) => {
+      // 如果需要关联,则进行第二层遍历,查找表单中关联的字段,存到Set中
+      formItemsForEach(schemas, (item) => {
+        if (!linkOn[item.field!]) linkOn[item.field!] = new Set<IVFormComponent>();
+        if (formItem.link?.includes(item.field!) && isFunction(formItem.update)) {
+          linkOn[item.field!].add(formItem);
+        }
+      });
+      linkOn[formItem.field!].add(formItem);
+    });
+  };
+  initLink(props.formConfig.schemas);
+
+  return {
+    linkOn,
+    setValue,
+    getValue,
+    hidden,
+    show,
+    set,
+    get,
+    setProps,
+    getData,
+    disable,
+    ...formInstanceMethods,
+  };
+}

+ 9 - 0
src/views/form-design/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <VFormDesign />
+</template>
+
+<script lang="ts" setup>
+  import VFormDesign from './components/VFormDesign/index.vue';
+</script>
+
+<style lang="less" scoped></style>

+ 48 - 0
src/views/form-design/tests/import1.json

@@ -0,0 +1,48 @@
+{
+    "schemas": [{
+            "field": "filename",
+            "component": "Input",
+            "label": "component.excel.fileName",
+            "rules": [{
+                "required": true
+            }]
+        },
+        {
+            "field": "bookType",
+            "component": "Select",
+            "label": "component.excel.fileType",
+            "defaultValue": "xlsx",
+            "rules": [{
+                "required": true
+            }],
+            "componentProps": {
+                "options": [{
+                        "label": "xlsx",
+                        "value": "xlsx",
+                        "key": "xlsx"
+                    },
+                    {
+                        "label": "html",
+                        "value": "html",
+                        "key": "html"
+                    },
+                    {
+                        "label": "csv",
+                        "value": "csv",
+                        "key": "csv"
+                    },
+                    {
+                        "label": "txt",
+                        "value": "txt",
+                        "key": "txt"
+                    }
+                ]
+            }
+        }
+    ],
+    "layout": "horizontal",
+    "labelLayout": "flex",
+    "labelWidth": 100,
+    "labelCol": {},
+    "wrapperCol": {}
+}

+ 10 - 0
src/views/form-design/typings/base-type.ts

@@ -0,0 +1,10 @@
+export interface IAnyObject<T = any> {
+  [key: string]: T;
+}
+
+export interface IInputEvent {
+  target: {
+    value: any;
+    checked: boolean;
+  };
+}

+ 52 - 0
src/views/form-design/typings/form-type.ts

@@ -0,0 +1,52 @@
+import { Ref } from 'vue';
+import { IAnyObject } from './base-type';
+import { IFormConfig, IVFormComponent } from './v-form-component';
+
+export interface IToolbarMethods {
+  showModal: (jsonData: IAnyObject) => void;
+}
+
+type ChangeTabKey = 1 | 2;
+export interface IPropsPanel {
+  changeTab: (key: ChangeTabKey) => void;
+}
+export interface IState {
+  // 语言
+  locale: any;
+  // 公用组件
+  baseComponents: IVFormComponent[];
+  // 自定义组件
+  customComponents: IVFormComponent[];
+  // 布局组件
+  layoutComponents: IVFormComponent[];
+  // 属性面板实例
+  propsPanel: Ref<null | IPropsPanel>;
+  // json模态框实例
+  jsonModal: Ref<null | IToolbarMethods>;
+  // 导入json数据模态框
+  importJsonModal: Ref<null | IToolbarMethods>;
+  // 代码预览模态框
+  codeModal: Ref<null | IToolbarMethods>;
+  // 预览模态框
+  eFormPreview: Ref<null | IToolbarMethods>;
+
+  eFormPreview2: Ref<null | IToolbarMethods>;
+}
+
+export interface IFormDesignMethods {
+  // 设置当前选中的控件
+  handleSetSelectItem(item: IVFormComponent): void;
+  // 添加控件到formConfig.formItems中
+  handleListPush(item: IVFormComponent): void;
+  // 复制控件
+  handleCopy(item?: IVFormComponent, isCopy?: boolean): void;
+  // 添加控件属性
+  handleAddAttrs(schemas: IVFormComponent[], index: number): void;
+  setFormConfig(config: IFormConfig): void;
+  // 添加到表单中之前触发
+  handleBeforeColAdd(
+    event: { newIndex: string },
+    schemas: IVFormComponent[],
+    isCopy?: boolean,
+  ): void;
+}

+ 349 - 0
src/views/form-design/typings/v-form-component.ts

@@ -0,0 +1,349 @@
+import { IAnyObject } from './base-type';
+// import { ComponentOptions } from 'vue/types/options';
+import { ComponentOptions } from 'vue';
+import { IVFormMethods } from '../hooks/useVFormMethods';
+import { ColEx } from '/@/components/Form/src/types';
+
+import { SelectValue } from 'ant-design-vue/lib/select';
+import { validateOptions } from 'ant-design-vue/lib/form/useForm';
+import { RuleError } from 'ant-design-vue/lib/form/interface';
+import { FormItem } from '/@/components/Form';
+type LayoutType = 'horizontal' | 'vertical' | 'inline';
+type labelLayout = 'flex' | 'Grid';
+export type PropsTabKey = 1 | 2 | 3;
+type ColSpanType = number | string;
+
+declare type Value = [number, number] | number;
+/**
+ * 组件属性
+ */
+export interface IVFormComponent {
+  // extends Omit<FormSchema, 'component' | 'label' | 'field' | 'rules'> {
+  // 对应的字段
+  field?: string;
+  // 组件类型
+  component: string;
+  // 组件label
+  label?: string;
+  // 自定义组件控件实例
+  componentInstance?: ComponentOptions<any>;
+  // 组件icon
+  icon?: string;
+  // 组件校验规则
+  rules?: Partial<IValidationRule>[];
+  // 是否隐藏
+  hidden?: boolean;
+  // 隐藏label
+  hiddenLabel?: boolean;
+  // 组件宽度
+  width?: string;
+  // 是否必选
+  required?: boolean;
+  // 必选提示
+  message?: string;
+  // 提示信息
+  helpMessage?: string;
+  // 传给给组件的属性,默认会吧所有的props都传递给控件
+  componentProps?: IAnyObject;
+  // 监听组件事件对象,以v-on方式传递给控件
+  on?: IAnyObject<(...any: []) => void>;
+  // 组件选项
+  options?: IAnyObject;
+  // 唯一标识
+  key?: string;
+  // Reference formModelItem
+  itemProps?: Partial<FormItem>;
+
+  colProps?: Partial<ColEx>;
+  // 联动字段
+  link?: string[];
+  // 联动属性变化的回调
+  update?: (value: any, formItem: IVFormComponent, fApi: IVFormMethods) => void;
+  // 控件栅格数
+  // span?: number;
+  // 标签布局
+  labelCol?: IAnyObject;
+  // 组件布局
+  wrapperCol?: IAnyObject;
+  // 子控件
+  columns?: Array<{ span: number; children: any[] }>;
+}
+
+declare type namesType = string | string[];
+
+/**
+ * 表单配置
+ */
+export interface IFormConfig {
+  // 表单项配置列表
+  // schemas: IVFormComponent[];
+  // 表单配置
+  // config: {
+  layout?: LayoutType;
+  labelLayout?: labelLayout;
+  labelWidth?: number;
+  labelCol?: Partial<IACol>;
+  wrapperCol?: Partial<IACol>;
+  hideRequiredMark?: boolean;
+  // Whether to disable
+  schemas: IVFormComponent[];
+  disabled?: boolean;
+  labelAlign?: 'left' | 'right';
+  // Internal component size of the form
+  size?: 'default' | 'small' | 'large';
+  // };
+  // 当前选中项
+  currentItem?: IVFormComponent;
+  activeKey?: PropsTabKey;
+  colon?: boolean;
+}
+
+export interface AForm {
+  /**
+   * Hide required mark of all form items
+   * @default false
+   * @type boolean
+   */
+  hideRequiredMark: boolean;
+
+  /**
+   * The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with <Col>
+   * @type IACol
+   */
+  labelCol: IACol;
+
+  /**
+   * Define form layout
+   * @default 'horizontal'
+   * @type string
+   */
+  layout: 'horizontal' | 'inline' | 'vertical';
+
+  /**
+   * The layout for input controls, same as labelCol
+   * @type IACol
+   */
+  wrapperCol: IACol;
+
+  /**
+   * change default props colon value of Form.Item (only effective when prop layout is horizontal)
+   * @type boolean
+   * @default true
+   */
+  colon: boolean;
+
+  /**
+   * text align of label of all items
+   * @type 'left' | 'right'
+   * @default 'left'
+   */
+  labelAlign: 'left' | 'right';
+
+  /**
+   * data of form component
+   * @type object
+   */
+  model: IAnyObject;
+
+  /**
+   * validation rules of form
+   * @type object
+   */
+  rules: IAnyObject;
+
+  /**
+   * Default validate message. And its format is similar with newMessages's returned value
+   * @type any
+   */
+  validateMessages?: any;
+
+  /**
+   * whether to trigger validation when the rules prop is changed
+   * @type Boolean
+   * @default true
+   */
+  validateOnRuleChange: boolean;
+
+  /**
+   * validate the whole form. Takes a callback as a param. After validation,
+   * the callback will be executed with two params: a boolean indicating if the validation has passed,
+   * and an object containing all fields that fail the validation. Returns a promise if callback is omitted
+   * @type Function
+   */
+  validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>;
+
+  /**
+   * validate one or several form items
+   * @type Function
+   */
+  validateField: (
+    name: string,
+    value: any,
+    rules: Record<string, unknown>[],
+    option?: validateOptions,
+  ) => Promise<RuleError[]>;
+  /**
+   * reset all the fields and remove validation result
+   */
+  resetFields: () => void;
+
+  /**
+   * clear validation message for certain fields.
+   * The parameter is prop name or an array of prop names of the form items whose validation messages will be removed.
+   * When omitted, all fields' validation messages will be cleared
+   * @type string[] | string
+   */
+  clearValidate: (props: string[] | string) => void;
+}
+
+interface IACol {
+  /**
+   * raster number of cells to occupy, 0 corresponds to display: none
+   * @default none (0)
+   * @type ColSpanType
+   */
+  span: Value;
+
+  /**
+   * raster order, used in flex layout mode
+   * @default 0
+   * @type ColSpanType
+   */
+  order: ColSpanType;
+
+  /**
+   * the layout fill of flex
+   * @default none
+   * @type ColSpanType
+   */
+  flex: ColSpanType;
+
+  /**
+   * the number of cells to offset Col from the left
+   * @default 0
+   * @type ColSpanType
+   */
+  offset: ColSpanType;
+
+  /**
+   * the number of cells that raster is moved to the right
+   * @default 0
+   * @type ColSpanType
+   */
+  push: ColSpanType;
+
+  /**
+   * the number of cells that raster is moved to the left
+   * @default 0
+   * @type ColSpanType
+   */
+  pull: ColSpanType;
+
+  /**
+   * <576px and also default setting, could be a span value or an object containing above props
+   * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
+   */
+  xs: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
+
+  /**
+   * ≥576px, could be a span value or an object containing above props
+   * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
+   */
+  sm: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
+
+  /**
+   * ≥768px, could be a span value or an object containing above props
+   * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
+   */
+  md: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
+
+  /**
+   * ≥992px, could be a span value or an object containing above props
+   * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
+   */
+  lg: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
+
+  /**
+   * ≥1200px, could be a span value or an object containing above props
+   * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
+   */
+  xl: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
+
+  /**
+   * ≥1600px, could be a span value or an object containing above props
+   * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
+   */
+  xxl: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
+}
+
+export interface IValidationRule {
+  trigger?: 'change' | 'blur' | ['change', 'blur'];
+  /**
+   * validation error message
+   * @type string | Function
+   */
+  message?: string | number;
+
+  /**
+   * built-in validation type, available options: https://github.com/yiminghe/async-validator#type
+   * @default 'string'
+   * @type string
+   */
+  type?: string;
+
+  /**
+   * indicates whether field is required
+   * @default false
+   * @type boolean
+   */
+  required?: boolean;
+
+  /**
+   * treat required fields that only contain whitespace as errors
+   * @default false
+   * @type boolean
+   */
+  whitespace?: boolean;
+
+  /**
+   * validate the exact length of a field
+   * @type number
+   */
+  len?: number;
+
+  /**
+   * validate the min length of a field
+   * @type number
+   */
+  min?: number;
+
+  /**
+   * validate the max length of a field
+   * @type number
+   */
+  max?: number;
+
+  /**
+   * validate the value from a list of possible values
+   * @type string | string[]
+   */
+  enum?: string | string[];
+
+  /**
+   * validate from a regular expression
+   * @type boolean
+   */
+  pattern?: SelectValue;
+
+  /**
+   * transform a value before validation
+   * @type Function
+   */
+  transform?: (value: any) => any;
+
+  /**
+   * custom validate function (Note: callback must be called)
+   * @type Function
+   */
+  validator?: (rule: any, value: any, callback: () => void) => any;
+}

+ 200 - 0
src/views/form-design/utils/index.ts

@@ -0,0 +1,200 @@
+// import { VueConstructor } from 'vue';
+import { IVFormComponent, IFormConfig, IValidationRule } from '../typings/v-form-component';
+import { cloneDeep, isArray, isFunction, isNumber, uniqueId } from 'lodash-es';
+// import { del } from '@vue/composition-api';
+// import { withInstall } from '/@/utils';
+
+/**
+ * 组件install方法
+ * @param comp 需要挂载install方法的组件
+ */
+// export function withInstall<T extends { name: string }>(comp: T) {
+//   return Object.assign(comp, {
+//     install(Vue: VueConstructor) {
+//       Vue.component(comp.name, comp);
+//     },
+//   });
+// }
+
+/**
+ * 生成key
+ * @param [formItem] 需要生成 key 的控件,可选,如果不传,默认返回一个唯一 key
+ * @returns {string|boolean} 返回一个唯一 id 或者 false
+ */
+export function generateKey(formItem?: IVFormComponent): string | boolean {
+  if (formItem && formItem.component) {
+    const key = uniqueId(`${toLine(formItem.component)}_`);
+    formItem.key = key;
+    formItem.field = key;
+
+    return true;
+  }
+  return uniqueId('key_');
+}
+
+/**
+ * 移除数组中指定元素,value可以是一个数字下标,也可以是一个函数,删除函数第一个返回true的元素
+ * @param array {Array<T>} 需要移除元素的数组
+ * @param value {number | ((item: T, index: number, array: Array<T>) => boolean}
+ * @returns {T} 返回删除的数组项
+ */
+export function remove<T>(
+  array: Array<T>,
+  value: number | ((item: T, index: number, array: Array<T>) => boolean),
+): T | undefined {
+  let removeVal: Array<T | undefined> = [];
+  if (!isArray(array)) return undefined;
+  if (isNumber(value)) {
+    removeVal = array.splice(value, 1);
+  } else {
+    const index = array.findIndex(value);
+    if (index !== -1) {
+      removeVal = array.splice(index, 1);
+    }
+  }
+  return removeVal.shift();
+}
+
+/**
+ * 判断数据类型
+ * @param value
+ */
+export function getType(value: any): string {
+  return Object.prototype.toString.call(value).slice(8, -1);
+}
+
+/**
+ * 生成唯一guid
+ * @returns {String} 唯一id标识符
+ */
+export function randomUUID(): string {
+  function S4() {
+    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+  }
+  return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4() + S4() + S4()}`;
+}
+
+/**
+ * 驼峰转下划线
+ * @param str
+ */
+export function toLine(str: string) {
+  return str.replace(/([A-Z])/g, '_$1').toLowerCase();
+}
+
+/**
+ * 遍历表单项
+ * @param array
+ * @param cb
+ */
+export function formItemsForEach(array: IVFormComponent[], cb: (item: IVFormComponent) => void) {
+  if (!isArray(array)) return;
+  const traverse = (schemas: IVFormComponent[]) => {
+    schemas.forEach((formItem: IVFormComponent) => {
+      if (['Grid'].includes(formItem.component)) {
+        // 栅格布局
+        formItem.columns?.forEach((item) => traverse(item.children));
+      } else {
+        cb(formItem);
+      }
+    });
+  };
+  traverse(array);
+}
+
+/**
+ * 查找表单项
+ */
+export const findFormItem: (
+  schemas: IVFormComponent[],
+  cb: (formItem: IVFormComponent) => boolean,
+) => IVFormComponent | undefined = (schemas, cb) => {
+  let res;
+  const traverse = (schemas: IVFormComponent[]): boolean => {
+    return schemas.some((formItem: IVFormComponent) => {
+      const { component: type } = formItem;
+      // 处理栅格
+      if (['Grid'].includes(type)) {
+        return formItem.columns?.some((item) => traverse(item.children));
+      }
+      if (cb(formItem)) res = formItem;
+      return cb(formItem);
+    });
+  };
+  traverse(schemas);
+  return res;
+};
+
+/**
+ * 打开json模态框时删除当前项属性
+ * @param formConfig {IFormConfig}
+ * @returns {IFormConfig}
+ */
+export const removeAttrs = (formConfig: IFormConfig): IFormConfig => {
+  const copyFormConfig = cloneDeep(formConfig);
+  delete copyFormConfig.currentItem;
+  delete copyFormConfig.activeKey;
+  copyFormConfig.schemas &&
+    formItemsForEach(copyFormConfig.schemas, (item) => {
+      delete item.icon;
+      delete item.key;
+    });
+  return copyFormConfig;
+};
+
+/**
+ * 处理异步选项属性,如 select treeSelect 等选项属性如果传递为函数并且返回Promise对象,获取异步返回的选项属性
+ * @param {(() => Promise<any[]>) | any[]} options
+ * @return {Promise<any[]>}
+ */
+export const handleAsyncOptions = async (
+  options: (() => Promise<any[]>) | any[],
+): Promise<any[]> => {
+  try {
+    if (isFunction(options)) return await options();
+    return options;
+  } catch {
+    return [];
+  }
+};
+
+/**
+ * 格式化表单项校验规则配置
+ * @param {IVFormComponent[]} schemas
+ */
+export const formatRules = (schemas: IVFormComponent[]) => {
+  formItemsForEach(schemas, (item) => {
+    if ('required' in item) {
+      !isArray(item.rules) && (item.rules = []);
+      item.rules.push({ required: true, message: item.message });
+      delete item['required'];
+      delete item['message'];
+    }
+  });
+};
+
+/**
+ * 将校验规则中的正则字符串转换为正则对象
+ * @param {IValidationRule[]} rules
+ * @return {IValidationRule[]}
+ */
+export const strToReg = (rules: IValidationRule[]) => {
+  const newRules = cloneDeep(rules);
+  return newRules.map((item) => {
+    if (item.pattern) item.pattern = runCode(item.pattern);
+    return item;
+  });
+};
+
+/**
+ * 执行一段字符串代码,并返回执行结果,如果执行出错,则返回该参数
+ * @param code
+ * @return {any}
+ */
+export const runCode = <T>(code: any): T => {
+  try {
+    return new Function(`return ${code}`)();
+  } catch {
+    return code;
+  }
+};

+ 21 - 0
src/views/form-design/utils/message.ts

@@ -0,0 +1,21 @@
+// import Vue from 'vue';
+
+const message = Object.assign(
+  {
+    success: (msg: string) => {
+      console.log(msg);
+    },
+    error: (msg: string) => {
+      console.error(msg);
+    },
+    warning: (msg: string) => {
+      console.warn(msg);
+    },
+    info: (msg: string) => {
+      console.info(msg);
+    },
+  },
+  // Vue.prototype.$message,
+);
+
+export default message;

+ 1 - 1
stylelint.config.js

@@ -9,7 +9,7 @@ module.exports = {
     'selector-pseudo-class-no-unknown': [
       true,
       {
-        ignorePseudoClasses: ['global'],
+        ignorePseudoClasses: ['global', 'deep'],
       },
     ],
     'selector-pseudo-element-no-unknown': [

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff