Bladeren bron

Merge remote-tracking branch 'zkqyOrigin/master'

lucky 3 maanden geleden
bovenliggende
commit
a4e9f985a0
100 gewijzigde bestanden met toevoegingen van 10308 en 18 verwijderingen
  1. 53 3
      zkqy-admin/src/main/resources/sql/initialize_sys_tenant_menu.json
  2. 3 1
      zkqy-ui/babel.config.js
  3. 20 8
      zkqy-ui/package.json
  4. 77 0
      zkqy-ui/src/api/formCreateMange/mobilePageDesignData.js
  5. 57 0
      zkqy-ui/src/api/formCreateMange/mobilePageNavigationBar.js
  6. 14 1
      zkqy-ui/src/mixins/common.js
  7. 6 1
      zkqy-ui/src/store/getters.js
  8. 3 1
      zkqy-ui/src/store/index.js
  9. 84 0
      zkqy-ui/src/store/modules/formCreate.js
  10. 0 3
      zkqy-ui/src/store/modules/permission.js
  11. 24 0
      zkqy-ui/src/views/formCreate/components/DragBox.vue
  12. 201 0
      zkqy-ui/src/views/formCreate/components/DragTool.vue
  13. 52 0
      zkqy-ui/src/views/formCreate/components/DynamicButtonGroup.vue
  14. 230 0
      zkqy-ui/src/views/formCreate/components/DynamicForm.vue
  15. 542 0
      zkqy-ui/src/views/formCreate/components/EventConfig.vue
  16. 182 0
      zkqy-ui/src/views/formCreate/components/FcDesigner.vue
  17. 157 0
      zkqy-ui/src/views/formCreate/components/Fetch.vue
  18. 266 0
      zkqy-ui/src/views/formCreate/components/FetchConfig.vue
  19. 154 0
      zkqy-ui/src/views/formCreate/components/FieldInput.vue
  20. 303 0
      zkqy-ui/src/views/formCreate/components/FnConfig.vue
  21. 251 0
      zkqy-ui/src/views/formCreate/components/FnEditor.vue
  22. 103 0
      zkqy-ui/src/views/formCreate/components/FnInput.vue
  23. 125 0
      zkqy-ui/src/views/formCreate/components/HtmlEditor.vue
  24. 93 0
      zkqy-ui/src/views/formCreate/components/JsonPreview.vue
  25. 76 0
      zkqy-ui/src/views/formCreate/components/PropsInput.vue
  26. 75 0
      zkqy-ui/src/views/formCreate/components/Required.vue
  27. 25 0
      zkqy-ui/src/views/formCreate/components/Row.vue
  28. 145 0
      zkqy-ui/src/views/formCreate/components/Struct.vue
  29. 121 0
      zkqy-ui/src/views/formCreate/components/StructEditor.vue
  30. 167 0
      zkqy-ui/src/views/formCreate/components/TableOptions.vue
  31. 158 0
      zkqy-ui/src/views/formCreate/components/TreeOptions.vue
  32. 140 0
      zkqy-ui/src/views/formCreate/components/TypeSelect.vue
  33. 244 0
      zkqy-ui/src/views/formCreate/components/Validate.vue
  34. 97 0
      zkqy-ui/src/views/formCreate/components/ValueInput.vue
  35. 45 0
      zkqy-ui/src/views/formCreate/components/Warning.vue
  36. 166 0
      zkqy-ui/src/views/formCreate/components/language/LanguageConfig.vue
  37. 192 0
      zkqy-ui/src/views/formCreate/components/language/LanguageInput.vue
  38. 242 0
      zkqy-ui/src/views/formCreate/components/style/BorderInput.vue
  39. 171 0
      zkqy-ui/src/views/formCreate/components/style/BoxSizeInput.vue
  40. 273 0
      zkqy-ui/src/views/formCreate/components/style/BoxSpaceInput.vue
  41. 64 0
      zkqy-ui/src/views/formCreate/components/style/ColorInput.vue
  42. 118 0
      zkqy-ui/src/views/formCreate/components/style/ConfigItem.vue
  43. 179 0
      zkqy-ui/src/views/formCreate/components/style/FontInput.vue
  44. 164 0
      zkqy-ui/src/views/formCreate/components/style/RadiusInput.vue
  45. 92 0
      zkqy-ui/src/views/formCreate/components/style/ShadowInput.vue
  46. 123 0
      zkqy-ui/src/views/formCreate/components/style/SizeInput.vue
  47. 263 0
      zkqy-ui/src/views/formCreate/components/style/StyleConfig.vue
  48. 211 0
      zkqy-ui/src/views/formCreate/components/table/Table.vue
  49. 660 0
      zkqy-ui/src/views/formCreate/components/table/TableView.vue
  50. 392 0
      zkqy-ui/src/views/formCreate/components/tableForm/TableForm.vue
  51. 101 0
      zkqy-ui/src/views/formCreate/components/tableForm/TableFormColumnView.vue
  52. 45 0
      zkqy-ui/src/views/formCreate/components/tableForm/TableFormView.vue
  53. 104 0
      zkqy-ui/src/views/formCreate/components/zkqyMenu/zkqyTable.vue
  54. 55 0
      zkqy-ui/src/views/formCreate/config/base/field.js
  55. 136 0
      zkqy-ui/src/views/formCreate/config/base/form.js
  56. 26 0
      zkqy-ui/src/views/formCreate/config/base/style.js
  57. 15 0
      zkqy-ui/src/views/formCreate/config/base/validate.js
  58. 67 0
      zkqy-ui/src/views/formCreate/config/index.js
  59. 24 0
      zkqy-ui/src/views/formCreate/config/menu.js
  60. 45 0
      zkqy-ui/src/views/formCreate/config/rule/alert.js
  61. 49 0
      zkqy-ui/src/views/formCreate/config/rule/button.js
  62. 40 0
      zkqy-ui/src/views/formCreate/config/rule/card.js
  63. 107 0
      zkqy-ui/src/views/formCreate/config/rule/cascader.js
  64. 67 0
      zkqy-ui/src/views/formCreate/config/rule/checkbox.js
  65. 86 0
      zkqy-ui/src/views/formCreate/config/rule/col.js
  66. 30 0
      zkqy-ui/src/views/formCreate/config/rule/collapse.js
  67. 36 0
      zkqy-ui/src/views/formCreate/config/rule/collapseItem.js
  68. 53 0
      zkqy-ui/src/views/formCreate/config/rule/color.js
  69. 70 0
      zkqy-ui/src/views/formCreate/config/rule/date.js
  70. 64 0
      zkqy-ui/src/views/formCreate/config/rule/dateRange.js
  71. 34 0
      zkqy-ui/src/views/formCreate/config/rule/divider.js
  72. 31 0
      zkqy-ui/src/views/formCreate/config/rule/editor.js
  73. 53 0
      zkqy-ui/src/views/formCreate/config/rule/group.js
  74. 52 0
      zkqy-ui/src/views/formCreate/config/rule/html.js
  75. 32 0
      zkqy-ui/src/views/formCreate/config/rule/image.js
  76. 63 0
      zkqy-ui/src/views/formCreate/config/rule/input.js
  77. 49 0
      zkqy-ui/src/views/formCreate/config/rule/number.js
  78. 52 0
      zkqy-ui/src/views/formCreate/config/rule/password.js
  79. 41 0
      zkqy-ui/src/views/formCreate/config/rule/radio.js
  80. 44 0
      zkqy-ui/src/views/formCreate/config/rule/rate.js
  81. 46 0
      zkqy-ui/src/views/formCreate/config/rule/row.js
  82. 71 0
      zkqy-ui/src/views/formCreate/config/rule/select.js
  83. 53 0
      zkqy-ui/src/views/formCreate/config/rule/slider.js
  84. 44 0
      zkqy-ui/src/views/formCreate/config/rule/space.js
  85. 33 0
      zkqy-ui/src/views/formCreate/config/rule/span.js
  86. 47 0
      zkqy-ui/src/views/formCreate/config/rule/subForm.js
  87. 46 0
      zkqy-ui/src/views/formCreate/config/rule/switch.js
  88. 35 0
      zkqy-ui/src/views/formCreate/config/rule/tab.js
  89. 31 0
      zkqy-ui/src/views/formCreate/config/rule/tabPane.js
  90. 34 0
      zkqy-ui/src/views/formCreate/config/rule/table.js
  91. 79 0
      zkqy-ui/src/views/formCreate/config/rule/tableForm.js
  92. 43 0
      zkqy-ui/src/views/formCreate/config/rule/tableFormColumn.js
  93. 38 0
      zkqy-ui/src/views/formCreate/config/rule/tabs.js
  94. 77 0
      zkqy-ui/src/views/formCreate/config/rule/tag.js
  95. 50 0
      zkqy-ui/src/views/formCreate/config/rule/text.js
  96. 63 0
      zkqy-ui/src/views/formCreate/config/rule/textarea.js
  97. 62 0
      zkqy-ui/src/views/formCreate/config/rule/time.js
  98. 53 0
      zkqy-ui/src/views/formCreate/config/rule/timeRange.js
  99. 59 0
      zkqy-ui/src/views/formCreate/config/rule/transfer.js
  100. 70 0
      zkqy-ui/src/views/formCreate/config/rule/tree.js

+ 53 - 3
zkqy-admin/src/main/resources/sql/initialize_sys_tenant_menu.json

@@ -1530,13 +1530,38 @@
         "updateBy": null,
         "updateTime": null,
         "remark": null,
-        "menuId": 9573,
+        "menuId": 11444,
+        "menuName": "导航条配置",
+        "parentName": null,
+        "parentId": 9574,
+        "orderNum": 1,
+        "path": "navigationBar",
+        "component": "formCreate/layout/home/navigationBar",
+        "query": null,
+        "isFrame": "1",
+        "isCache": "0",
+        "menuType": "C",
+        "visible": "0",
+        "status": "0",
+        "perms": "",
+        "icon": "bpmn-icon-intermediate-event-catch-cancel",
+        "children": [],
+        "tenantName": null,
+        "tenantId": null
+    },
+    {
+        "createBy": null,
+        "createTime": "2023-10-11 01:28:10",
+        "updateBy": null,
+        "updateTime": null,
+        "remark": null,
+        "menuId": 11443,
         "menuName": "页面设计",
         "parentName": null,
         "parentId": 9574,
         "orderNum": 1,
-        "path": "h5Editor",
-        "component": "asEditor/layout/home/index",
+        "path": "formCreate",
+        "component": "formCreate/layout/home/index",
         "query": null,
         "isFrame": "1",
         "isCache": "0",
@@ -1549,6 +1574,31 @@
         "tenantName": null,
         "tenantId": null
     },
+    {
+        "createBy": null,
+        "createTime": "2023-10-11 01:28:10",
+        "updateBy": null,
+        "updateTime": null,
+        "remark": null,
+        "menuId": 11444,
+        "menuName": "移动端建模页",
+        "parentName": null,
+        "parentId": 9574,
+        "orderNum": 1,
+        "path": "FormCreateMange",
+        "component": "formCreate/layout/home/formCreateMange",
+        "query": null,
+        "isFrame": "1",
+        "isCache": "0",
+        "menuType": "C",
+        "visible": "1",
+        "status": "0",
+        "perms": "",
+        "icon": "bpmn-icon-intermediate-event-catch-cancel",
+        "children": [],
+        "tenantName": null,
+        "tenantId": null
+    },
     {
         "createBy": null,
         "createTime": "2023-07-07 06:04:16",

+ 3 - 1
zkqy-ui/babel.config.js

@@ -7,7 +7,7 @@ module.exports = {
     'development': {
       // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
       // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
-      'plugins': ['dynamic-import-node']
+      'plugins': ['dynamic-import-node',]
     }
   },
   plugins: [
@@ -20,6 +20,8 @@ module.exports = {
       },
       'vant',
     ],
+    '@babel/plugin-proposal-optional-chaining',
+    '@babel/plugin-proposal-nullish-coalescing-operator'
   ],
 }
 

+ 20 - 8
zkqy-ui/package.json

@@ -42,10 +42,16 @@
     "@bpmn-io/add-exporter": "^0.2.0",
     "@bpmn-io/element-template-chooser": "^0.0.5",
     "@bpmn-io/properties-panel": "^0.20.3",
+    "@form-create/designer": "^1.1.9",
+    "@form-create/element-ui": "^2.7.4",
+    "@form-create/utils": "^3.2.18",
+    "@form-create/vant": "2.7",
+    "@form-create/vant-designer": "^3.2.8",
     "@jiaminghi/data-view": "^2.10.0",
     "@riophae/vue-treeselect": "0.4.0",
     "@smallwei/avue": "^2.8.23",
     "@tinymce/tinymce-vue": "^3.2.0",
+    "@vue/composition-api": "^1.7.2",
     "amfe-flexible": "^2.2.1",
     "ant-design-vue": "^1.7.8",
     "axios": "0.24.0",
@@ -61,14 +67,15 @@
     "camunda-bpmn-moddle": "^7.0.1",
     "circular-json": "^0.5.9",
     "clipboard": "^2.0.8",
-    "core-js": "3.25.3",
+    "core-js": "^3.8.3",
     "cos-js-sdk-v5": "^1.4.6",
     "crypto-js": "^4.2.0",
     "diagram-js-context-pad": "^1.0.2",
     "diagram-js-grid-bg": "^1.0.3",
     "diagram-js-minimap": "^2.1.1",
     "echarts": "^4.9.0",
-    "element-ui": "2.15.12",
+    "element-plus": "^2.9.7",
+    "element-ui": "^2.15.12",
     "exceljs": "^4.2.0",
     "fast-xml-parser": "^4.3.2",
     "file-saver": "2.0.5",
@@ -80,6 +87,7 @@
     "js-cookie": "3.0.1",
     "js-md5": "^0.8.3",
     "jsencrypt": "3.0.0-rc.1",
+    "jsonlint-mod": "^1.7.6",
     "k-form-design": "^3.8.18",
     "less": "^2.7.3",
     "less-loader": "^4.1.0",
@@ -97,20 +105,21 @@
     "tinymce": "^5.10.0",
     "tinymce-plugin": "^0.0.3-beta.22",
     "uuid": "^9.0.0",
-    "vant": "^2.12.30",
+    "vant": "^2.13.6",
     "vcolorpicker": "^2.0.12",
-    "vue": "2.6.12",
+    "vue": "^2.7.0",
     "vue-codemirror": "^4.0.6",
     "vue-codemirror-lite": "^1.0.4",
     "vue-count-to": "1.0.13",
     "vue-cropper": "0.5.5",
+    "vue-demi": "^0.14.10",
     "vue-echarts": "^5.0.0-beta.0",
     "vue-json-editor": "^1.4.3",
     "vue-json-excel": "^0.3.0",
     "vue-json-viewer": "^2.2.22",
     "vue-meta": "2.4.0",
     "vue-quill-editor": "^3.0.6",
-    "vue-router": "3.4.9",
+    "vue-router": "^3.6.5",
     "vue-ruler-tool": "^1.2.4",
     "vue-superslide": "^0.1.1",
     "vue2-ace-editor": "^0.0.15",
@@ -121,7 +130,9 @@
     "xml2js": "^0.6.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.24.0",
+    "@babel/core": "^7.12.16",
+    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
+    "@babel/plugin-proposal-optional-chaining": "^7.21.0",
     "@babel/preset-env": "^7.24.0",
     "@vue/cli-plugin-babel": "4.4.6",
     "@vue/cli-plugin-eslint": "4.4.6",
@@ -143,7 +154,7 @@
     "sass-loader": "10.1.1",
     "script-ext-html-webpack-plugin": "2.1.5",
     "svg-sprite-loader": "5.1.1",
-    "vue-template-compiler": "2.6.12"
+    "vue-template-compiler": "^2.7.0"
   },
   "engines": {
     "node": ">=8.9",
@@ -151,6 +162,7 @@
   },
   "browserslist": [
     "> 1%",
-    "last 2 versions"
+    "last 2 versions",
+    "not dead"
   ]
 }

+ 77 - 0
zkqy-ui/src/api/formCreateMange/mobilePageDesignData.js

@@ -0,0 +1,77 @@
+import request from '@/utils/request'
+
+// 查询新新页面设计列表 name
+export function listMobilePageDesignData(query) {
+  return request({
+    url: '/system/mobilePageDesignData/list',
+    method: 'get',
+    params: query,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 查询新新页面设计详细
+export function getMobilePageDesignData(id) {
+  return request({
+    url: '/system/mobilePageDesignData/' + id,
+    method: 'get',
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 新增新新页面设计 
+export function addMobilePageDesignData(data) { 
+  return request({
+    url: '/system/mobilePageDesignData',
+    method: 'post',
+    data: data,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 修改新新页面设计
+export function updateMobilePageDesignData(data) {
+  return request({
+    url: '/system/mobilePageDesignData',
+    method: 'put',
+    data: data,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 删除新新页面设计
+export function delMobilePageDesignData(id) {
+  return request({
+    url: '/system/mobilePageDesignData/' + id,
+    method: 'delete',
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+// 获取所有表: http://192.168.10.103:8088/dataSource/getAllTable
+export function getAllTable(data) {
+  return request({
+    url: '/dataSource/getAllTable',
+    method: 'post',
+    data: data,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+// 获取所有表(带值)
+export function tableLimitInfo(params) {
+  return request({
+    url: '/system/mobilePageDesignData/tableLimitInfo',
+    method: 'get',
+    params: params,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 获取表里的所有字段:http://192.168.10.103:8088/dataSource/getInfoTable
+export function getListName(data) {
+  return request({
+    url: '/dataSource/getInfoTable',
+    method: 'post',
+    data: data,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}

+ 57 - 0
zkqy-ui/src/api/formCreateMange/mobilePageNavigationBar.js

@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+// 查询所有的页面数据  页面id的查询
+export function listAllMobilePageDesignData() {
+    return request({
+      url: '/system/mobilePageDesignData/list/all',
+      method: 'get',
+      baseURL: process.env.VUE_APP_BASE_API3
+    })
+  }
+
+// 查询移动端页面导航条设计列表
+export function listMobilePageNavigationBar(query) {
+  return request({
+    url: '/system/mobilePageNavigationBar/list',
+    method: 'get',
+    params: query,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 查询移动端页面导航条设计详细
+export function getMobilePageNavigationBar(id) {
+  return request({
+    url: '/system/mobilePageNavigationBar/' + id,
+    method: 'get',
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 新增移动端页面导航条设计
+export function addMobilePageNavigationBar(data) {
+  return request({
+    url: '/system/mobilePageNavigationBar',
+    method: 'post',
+    data: data,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 修改移动端页面导航条设计
+export function updateMobilePageNavigationBar(data) {
+  return request({
+    url: '/system/mobilePageNavigationBar',
+    method: 'put',
+    data: data,
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}
+
+// 删除移动端页面导航条设计
+export function delMobilePageNavigationBar(id) {
+  return request({
+    url: '/system/mobilePageNavigationBar/' + id,
+    method: 'delete',
+    baseURL: process.env.VUE_APP_BASE_API3
+  })
+}

+ 14 - 1
zkqy-ui/src/mixins/common.js

@@ -32,7 +32,20 @@ export default {
       this.$router.go(-1)
     },
     refresh() {
-      this.$router.go(0)
+      try {
+        // 获取调用堆栈
+        const stack = new Error().stack || '';
+        // 如果调用链包含 form-create.esm.js 或 dragMenu,跳过执行
+        if (stack.includes('form-create.esm.js') || stack.includes('dragMenu')) {
+          this.$forceUpdate()
+          return;
+        }
+        // 执行刷新操作
+        this.$router.go(0);
+      } catch (error) {
+        // 记录错误日志
+        console.error('刷新页面时发生错误:', error);
+      }
     },
     parseString(object) {
       if (typeof object === 'undefined' || object == null) {

+ 6 - 1
zkqy-ui/src/store/getters.js

@@ -36,7 +36,12 @@ const getters = {
   // modeler
   getModeler: (state) => state.bpmn.bpmn._modeler,
   getModeling: (state) => (state.bpmn.bpmn._modeler ? state.bpmn.bpmn._modeler.get("modeling") : undefined),
-  getActive: (state) => state.bpmn.bpmn.activeElement
+  getActive: (state) => state.bpmn.bpmn.activeElement,
 
+  formData: state => state.formCreate.formData,
+  formCachedData: state => state.formCreate.cachedData,
+  formLastUpdated: state => state.formCreate.lastUpdated,
+  // 新增:label 数据
+  labelData: state => state.formCreate.labelData || {},
 }
 export default getters

+ 3 - 1
zkqy-ui/src/store/index.js

@@ -9,6 +9,7 @@ import settings from './modules/settings'
 import getters from './getters'
 import flow from './modules/flow'
 import bpmn from './modules/bpmn'
+import formCreate from './modules/formCreate' // 新增的导入
 import createPersistedstate from 'vuex-persistedstate'
 Vue.use(Vuex)
 const PERSIST_PATHS = ['tagsView.visitedViews'];
@@ -21,7 +22,8 @@ const store = new Vuex.Store({
     permission,
     settings,
     flow,
-    bpmn
+    bpmn,
+    formCreate 
   },
   getters,
   plugins: [

+ 84 - 0
zkqy-ui/src/store/modules/formCreate.js

@@ -0,0 +1,84 @@
+const state = {
+  formData: null,    // 存储表单数据
+  labelData: null,  // 存储label映射数据
+  cachedData: {},    // 可选:缓存多组表单数据(如需要存储多个表单)
+  lastUpdated: null  // 最后更新时间(可选)
+}
+
+const mutations = {
+  // 设置表单数据
+  SET_FORM_DATA(state, payload) {
+    state.formData = payload.data
+    state.lastUpdated = new Date().toISOString()
+  },
+   // 新增:设置label数据
+   SET_LABEL_DATA(state, payload) {
+    state.labelData = payload.data
+  },
+  // 更新部分表单字段(适用于局部更新)
+  UPDATE_FORM_FIELD(state, { field, value }) {
+    if (state.formData) {
+      state.formData = { 
+        ...state.formData,
+        [field]: value 
+      }
+    }
+  },
+  
+  // 缓存数据(可选)
+  CACHE_FORM_DATA(state, { key, data }) {
+    state.cachedData = {
+      ...state.cachedData,
+      [key]: data
+    }
+  },
+  
+  // 清空数据
+  CLEAR_FORM_DATA(state) {
+    state.formData = null
+  },
+  // 新增:清空label数据
+  CLEAR_LABEL_DATA(state) {
+    state.labelData = null
+  },
+}
+
+const actions = {
+  // 从组件接收数据并存入Vuex
+  saveFormData({ commit }, formData) {
+    commit('SET_FORM_DATA', { data: formData })
+  },
+   // 新增:保存label数据
+   saveLabelData({ commit }, labelData) {
+    commit('SET_LABEL_DATA', { data: labelData })
+  },
+  
+  // 局部更新字段(示例)
+  updateField({ commit }, payload) {
+    commit('UPDATE_FORM_FIELD', payload)
+  },
+  
+  // 清空数据
+  clearFormData({ commit }) {
+    commit('CLEAR_FORM_DATA')
+  },
+    // 新增:清空label数据
+    clearLabelData({ commit }) {
+      commit('CLEAR_LABEL_DATA')
+    },
+}
+
+const getters = {
+  getFormData: state => state.formData,
+  getCachedData: state => key => state.cachedData[key], // 通过key获取缓存
+  getLastUpdated: state => state.lastUpdated,
+  getLabelData: state => state.labelData,  // 新增:获取label数据
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+  getters
+}

+ 0 - 3
zkqy-ui/src/store/modules/permission.js

@@ -36,7 +36,6 @@ const permission = {
       return new Promise(resolve => {
         // 向后端请求路由数据
         getRouters().then(res => {
-         
           // console.log('[sdata]',sdata)
           // 在这里处理sdata,查找path为"/system/fromModel/index"的路由,并添加子路由
           // res.data.forEach(route => {
@@ -71,10 +70,8 @@ const permission = {
           const sdata = JSON.parse(JSON.stringify(res.data))
           const rdata = JSON.parse(JSON.stringify(res.data))
           const sidebarRoutes = filterAsyncRouter(sdata)
-          // console.log('sidebarRoutes', sidebarRoutes);
           const rewriteRoutes = filterAsyncRouter(rdata, false, true)
           const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
-          // console.log('[asyncRoutes]',asyncRoutes)
           rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
           router.addRoutes(asyncRoutes);
           commit('SET_ROUTES', rewriteRoutes)

+ 24 - 0
zkqy-ui/src/views/formCreate/components/DragBox.vue

@@ -0,0 +1,24 @@
+<script>
+import draggable from 'vuedraggable';
+
+export default {
+    name: 'DragBox',
+    props: ['rule', 'subRule', 'tag', 'list'],
+    components: {
+        draggable
+    },
+    render(h) {
+        const subRule = this.$props.rule || {};
+        let _class = '_fd-' + this.$props.tag + '-drag _fd-drag-box';
+        if (!this.$slots.default || !this.$slots.default.length) {
+            _class += ' drag-holder';
+        }
+        subRule.class = _class;
+        const props = {...this.$props.rule, on: this.$listeners};
+        if (this.$props.list) {
+            props.props.list = this.$props.list;
+        }
+        return h(draggable, props, this.$slots.default);
+    }
+};
+</script>

+ 201 - 0
zkqy-ui/src/views/formCreate/components/DragTool.vue

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

+ 52 - 0
zkqy-ui/src/views/formCreate/components/DynamicButtonGroup.vue

@@ -0,0 +1,52 @@
+<template>
+  <el-row :gutter="10" class="mb8">
+    <el-col
+      v-for="(button, index) in buttons"
+      :key="index"
+      :span="button.span || 1.5"
+    >
+      <el-button
+        :type="button.type"
+        :plain="button.plain"
+        :icon="button.icon"
+        :size="button.size"
+        :disabled="button.disabled"
+        @click="button.click"
+      >
+        {{ button.label }}
+      </el-button>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+/**
+ * buttons: 每个按钮必须包含 label(按钮文本)和 click(点击事件)属性
+ * label:按钮显示的文本
+ * type:按钮类型(如 primary、danger 等)
+ * plain:是否为朴素按钮(true/false)
+ * icon:按钮图标(如 el-icon-plus、el-icon-delete 等)
+ * size:按钮大小(如 mini、small 等)
+ * disabled:是否禁用按钮(true/false)
+ * click:按钮点击事件的处理函数
+ * span:按钮所占的列宽(默认为 1.5)
+ */
+export default {
+  name: 'DynamicButtonGroup',
+  props: {
+    buttons: {
+      type: Array,
+      required: true,
+      validator: (value) => {
+        return value.every((button) => button.label && button.click);
+      },
+    },
+  },
+};
+</script>
+
+<style scoped>
+.mb8 {
+  margin-bottom: 8px;
+}
+</style>

+ 230 - 0
zkqy-ui/src/views/formCreate/components/DynamicForm.vue

@@ -0,0 +1,230 @@
+<template>
+
+  <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch"
+    :label-width="labelWidth" >
+    <!-- 动态渲染表单项 -->
+    <div v-for="(item, index) in formItems" :key="index" class="el-form-div">
+      <el-form-item :label="(item.component === 'el-radio-group' ? (item.options.length > 0 ? item.label : '') : item.label)" :prop="item.prop" :label-width="item.labelWidth || labelWidth"
+        :rules="item.rules" :class="item.prop" >
+        <!-- 动态渲染输入组件 -->
+        <component v-if="!item.type && item.type != 'tree'" :is="item.component || 'el-input'" :style="{ width: item.styleWidth }"
+          v-model="queryParams[item.prop]" v-bind="item.attrs || {}" v-on="item.listeners || {}">
+          <template v-if="item.component === 'el-select'">
+            <el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value">
+              <span style="float: left" v-if="item.selectSlot" class="discribe">{{ option.label }}</span>
+              <span style="float: right; color: #8492a6; font-size: 13px" v-if="item.selectSlot">{{ option.value
+                }}</span>
+            </el-option>
+          </template>
+          <template v-if="item.component === 'el-radio-group' && item.options && item.options.length > 0">
+            <el-radio-group v-model="queryParams[item.prop]" >
+              <el-radio v-for="option in item.options" :key="option.value" :label="option.value">
+                {{ option.label }}
+              </el-radio>
+            </el-radio-group>
+          </template>
+        </component>
+        <div v-if="item.type && item.type == 'tree'" class="treeselect">
+          <treeselect :append-to-body="true" v-model="queryParams[item.prop]" :options="item.menus"
+            :placeholder="item.placeholder" :normalizer="normalizer" :show-count="true"
+            :openDirection="item.openDirection" />
+        </div>
+      </el-form-item>
+
+      <!-- 插入父组件提供的插槽内容 -->
+      <el-form-item>
+        <slot :name="`${item.prop}Slot`"></slot>
+      </el-form-item>
+    </div>
+
+    <!-- 动态渲染按钮 -->
+    <el-form-item v-if="Showbuttons" class="el-form-btn">
+      <el-button v-for="(button, idx) in buttonsWithDefaults" :key="idx" :type="button.type || 'default'"
+        :icon="button.icon || ''" :size="button.size || 'mini'" @click="handleButtonClick(button)">
+        {{ button.label }}
+      </el-button>
+    </el-form-item>
+
+  </el-form>
+</template>
+
+<script>
+/**
+ * showSearch: 控制搜索表单是否显示。
+ * formItems: 表单项配置数组,每个元素代表一个表单项。
+ *            必需字段:label(表单项标签文本),prop(绑定的模型数据键名)
+ *            校验规则:每个元素必须包含 label 和 prop 属性
+ * labelWidth: 表单项标签的宽度。
+ * formButtons: 按钮配置, 不传值时默认搜索和重置,传值时传出表单数据,父组件定义逻辑。
+ */
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import Treeselect from "@riophae/vue-treeselect";
+export default {
+  name: 'DynamicForm',
+  components: {
+    Treeselect,
+  },
+  props: {
+    showSearch: {
+      type: Boolean,
+      default: true,
+    },
+    Showbuttons: {
+      type: Boolean,
+      default: true,
+    },
+    formItems: {
+      type: Array,
+      required: true,
+      validator: (value) => {
+        return value.every((item) => item.label && item.prop);
+      },
+    },
+    labelWidth: {
+      type: String,
+      default: '85px',
+    },
+    formButtons: {
+      type: Array,
+      default: () => [],
+      validator: (value) => value.every((button) => button.label && button.event),
+    },
+  },
+  data() {
+    return {
+      // 表单数据
+      queryParams: {},
+    };
+  },
+  watch: {
+  queryParams: {
+    handler(newVal) {
+      // 当 queryParams 变化时,通知父组件
+      this.$emit('query-params-change', newVal);
+    },
+    deep: true, // 深度监听,确保嵌套属性的变化也能触发
+  },
+},
+  created() {
+    this.initQueryParams();
+  },
+  computed: {
+    // 如果没有传入按钮,则提供默认的搜索和重置按钮
+    buttonsWithDefaults() {
+      if (this.formButtons.length === 0) {
+        return [
+          { label: '搜索', event: 'search', eventName: 'query', icon: 'el-icon-search', type: 'primary', size: 'mini' },
+          { label: '重置', event: 'reset', eventName: 'reset', icon: 'el-icon-refresh', size: 'mini' },
+        ];
+      }
+      return this.formButtons;
+    },
+  },
+  methods: {
+    /**
+     * 初始化 queryParams 对象,根据 formItems 的配置动态设置表单的初始值。
+     * item.defaultValue || '':如果表单项配置了 defaultValue,则使用该值;否则使用空字符串 '' 作为默认值。
+     */
+    initQueryParams() {
+      this.formItems.forEach((item) => {
+        // console.log('defaultValue', item)
+
+        this.$set(this.queryParams, item.prop, item.radioDefaultValue ? true : item.treeDefaultValue?null:"");
+        // console.log('this.queryParams', this.queryParams)
+      });
+    },
+    // 查询
+    handleQuery() {
+      // console.log('[点击查询]')
+      this.$emit('query', this.queryParams);
+    },
+    // 重置
+    resetQuery() {
+      this.initQueryParams();
+      this.$emit('reset');
+    },
+    // 处理按钮点击事件
+    handleButtonClick(button) {
+      if (button.event === 'search') {
+        this.handleQuery();
+      } else if (button.event === 'reset') {
+        this.resetQuery();
+      } else {
+        // 触发自定义事件,传递表单数据、事件类型以及可能的额外参数
+        const payload = { event: button.event, data: this.queryParams };
+        if (button.extraData) {
+          payload.extraData = button.extraData;
+        }
+        this.$emit(button.eventName || 'button-click', payload);
+      }
+    },
+    /** 转换菜单数据结构 */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.menuId,
+        label: node.menuName,
+        children: node.children,
+      };
+    },
+  },
+};
+</script>
+
+<style scoped>
+.el-form {
+  display: flex;
+  flex-wrap: wrap;
+  /* 允许换行 */
+}
+
+.el-form-div {
+  min-width: 200px;
+  /* 设置最小宽度,防止过窄 */
+}
+
+.discribe {
+  display: block;
+  max-width: 200px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+
+</style>
+<style lang="scss">
+::v-deep .el-select-dropdown {  
+/* transform-origin: center top !important; */
+z-index: 9999 !important;
+
+}
+::v-deep .vue-treeselect--has-value .vue-treeselect__input {
+  vertical-align: middle !important;
+}
+
+::v-deep .vue-treeselect--has-value .vue-treeselect__input {
+  vertical-align: middle !important;
+}
+
+.treeselect {
+  .vue-treeselect {
+    width: 205px !important;
+  }
+}
+
+.treeselect {
+  .vue-treeselect .vue-treeselect__control {
+    height: 32px;
+    line-height: 32px;
+    border-radius: 4px;
+    font-size: 13px;
+  }
+}
+
+.vue-treeselect {
+  z-index: 100 !important;
+}
+</style>

+ 542 - 0
zkqy-ui/src/views/formCreate/components/EventConfig.vue

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

File diff suppressed because it is too large
+ 182 - 0
zkqy-ui/src/views/formCreate/components/FcDesigner.vue


+ 157 - 0
zkqy-ui/src/views/formCreate/components/Fetch.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="_fc_fetch">
+    <component :is="FormCreate" v-model="api" :value="formValue" :rule="rule" :option="option" @change="input"/>
+  </div>
+</template>
+
+<script>
+import debounce from '@form-create/utils/lib/debounce';
+import is from '@form-create/utils/lib/type';
+import {designerForm} from '../utils/form';
+
+export default {
+    name: 'Fetch',
+    props: {
+        value: [Object, String],
+        to: String,
+    },
+    computed: {
+        formValue() {
+            const val = this.value;
+            if (!val) return {};
+            if (is.String(val)) {
+                return {
+                    action: val
+                };
+            }
+            if (!val._parse && val.parse) {
+                return {...val, _parse: '' + val.parse};
+            } else if(is.Function(val._parse)){
+                return {...val, _parse: '' + val._parse};
+            }
+            return val;
+        }
+    },
+    data() {
+        return {
+            FormCreate: designerForm.$form(),
+            api: {},
+            fetch: {},
+            option: {
+                form: {
+                    labelPosition: 'right',
+                    size: 'mini',
+                    labelWidth: '90px'
+                },
+                submitBtn: false,
+            },
+            rule: [
+                {
+                    type: 'input',
+                    field: 'action',
+                    title: '接口: ',
+                    validate: [{required: true, message: '请数据接口'}]
+                },
+                {
+                    type: 'select',
+                    field: 'method',
+                    title: '请求方式: ',
+                    value: 'GET',
+                    options: [
+                        {label: 'GET', value: 'GET'},
+                        {label: 'POST', value: 'POST'},
+                    ],
+                    control: [
+                        {
+                            value:'POST',
+                            rule: [
+                                {
+                                    type: 'select',
+                                    field: 'dataType',
+                                    title: '提交方式: ',
+                                    value: 'FormData',
+                                    options: [
+                                        {label: 'FormData', value: 'FormData'},
+                                        {label: 'JSON', value: 'JSON'},
+                                    ]
+                                },
+                            ]
+                        }
+                    ]
+                },
+                {
+                    type: 'Struct',
+                    field: 'data',
+                    title: '附带数据: ',
+                    value: {},
+                    props: {
+                        defaultValue: {},
+                    }
+                },
+                {
+                    type: 'Struct',
+                    field: 'headers',
+                    title: 'header信息: ',
+                    value: {},
+                    props: {
+                        defaultValue: {},
+                    }
+                },
+                {
+                    type: 'input',
+                    field: '_parse',
+                    title: '解析函数',
+                    info: '解析接口数据,返回组件所需的数据结构',
+                    value: 'function (res){\n   return res.data;\n}',
+                    props: {
+                        type: 'textarea',
+                        rows: 8,
+                    },
+                    validate: [{
+                        validator: (_, v, cb) => {
+                            if (!v) return cb();
+                            try {
+                                this.parseFn(v);
+                            } catch (e) {
+                                return cb(false);
+                            }
+                            cb();
+                        }, message: '请输入正确的解析函数'
+                    }]
+                },
+            ]
+        };
+    },
+    methods: {
+        parseFn(v){
+            return eval(`(function(){return ${v} })()`);
+        },
+        _input() {
+            this.api.submit((formData) => {
+                formData.to = this.to || 'options';
+                if (formData._parse) formData.parse = this.parseFn(formData._parse);
+                this.$emit('input', formData);
+            });
+        },
+        input: debounce(function () {
+            this._input();
+        }, 1000),
+    },
+    mounted() {
+        this._input();
+    }
+};
+</script>
+<style>
+._fc_fetch .el-form-item__label {
+  float: left;
+  display: inline-block;
+  text-align: right;
+  padding-right: 5px;
+}
+
+._fc_fetch {
+  background-color: #bfdaf7;
+  padding: 10px;
+}
+</style>

+ 266 - 0
zkqy-ui/src/views/formCreate/components/FetchConfig.vue

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

+ 154 - 0
zkqy-ui/src/views/formCreate/components/FieldInput.vue

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

+ 303 - 0
zkqy-ui/src/views/formCreate/components/FnConfig.vue

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

+ 251 - 0
zkqy-ui/src/views/formCreate/components/FnEditor.vue

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

+ 103 - 0
zkqy-ui/src/views/formCreate/components/FnInput.vue

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

+ 125 - 0
zkqy-ui/src/views/formCreate/components/HtmlEditor.vue

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

+ 93 - 0
zkqy-ui/src/views/formCreate/components/JsonPreview.vue

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

+ 76 - 0
zkqy-ui/src/views/formCreate/components/PropsInput.vue

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

+ 75 - 0
zkqy-ui/src/views/formCreate/components/Required.vue

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

+ 25 - 0
zkqy-ui/src/views/formCreate/components/Row.vue

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

+ 145 - 0
zkqy-ui/src/views/formCreate/components/Struct.vue

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

+ 121 - 0
zkqy-ui/src/views/formCreate/components/StructEditor.vue

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

+ 167 - 0
zkqy-ui/src/views/formCreate/components/TableOptions.vue

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

+ 158 - 0
zkqy-ui/src/views/formCreate/components/TreeOptions.vue

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

+ 140 - 0
zkqy-ui/src/views/formCreate/components/TypeSelect.vue

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

+ 244 - 0
zkqy-ui/src/views/formCreate/components/Validate.vue

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

+ 97 - 0
zkqy-ui/src/views/formCreate/components/ValueInput.vue

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

+ 45 - 0
zkqy-ui/src/views/formCreate/components/Warning.vue

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

+ 166 - 0
zkqy-ui/src/views/formCreate/components/language/LanguageConfig.vue

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

+ 192 - 0
zkqy-ui/src/views/formCreate/components/language/LanguageInput.vue

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

+ 242 - 0
zkqy-ui/src/views/formCreate/components/style/BorderInput.vue

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

+ 171 - 0
zkqy-ui/src/views/formCreate/components/style/BoxSizeInput.vue

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

+ 273 - 0
zkqy-ui/src/views/formCreate/components/style/BoxSpaceInput.vue

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

+ 64 - 0
zkqy-ui/src/views/formCreate/components/style/ColorInput.vue

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

+ 118 - 0
zkqy-ui/src/views/formCreate/components/style/ConfigItem.vue

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

+ 179 - 0
zkqy-ui/src/views/formCreate/components/style/FontInput.vue

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

+ 164 - 0
zkqy-ui/src/views/formCreate/components/style/RadiusInput.vue

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

+ 92 - 0
zkqy-ui/src/views/formCreate/components/style/ShadowInput.vue

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

+ 123 - 0
zkqy-ui/src/views/formCreate/components/style/SizeInput.vue

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

+ 263 - 0
zkqy-ui/src/views/formCreate/components/style/StyleConfig.vue

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

+ 211 - 0
zkqy-ui/src/views/formCreate/components/table/Table.vue

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

+ 660 - 0
zkqy-ui/src/views/formCreate/components/table/TableView.vue

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

+ 392 - 0
zkqy-ui/src/views/formCreate/components/tableForm/TableForm.vue

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

+ 101 - 0
zkqy-ui/src/views/formCreate/components/tableForm/TableFormColumnView.vue

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

+ 45 - 0
zkqy-ui/src/views/formCreate/components/tableForm/TableFormView.vue

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

+ 104 - 0
zkqy-ui/src/views/formCreate/components/zkqyMenu/zkqyTable.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="zkqy-table">
+    <div class="table-container">
+      <div class="zkqy-table-btn" style="margin: 5px 0 10px 0;">
+        <el-button type="primary" plain @click="add">新增</el-button>
+        <el-button type="primary" plain @click="edit">修改</el-button>
+      </div>
+      <el-table :data="tableData" style="width: 100%"  :fit="true"
+  border  >
+        <!-- 单选 -->
+        <el-table-column  v-if="columns && columns.length > 0" :width="40" >
+          <template slot-scope="scope">
+            <el-radio class="radio" :label="scope.row" v-model="radio" @change.native="getCurrentRow(scope.row)">
+              &nbsp;</el-radio>
+          </template>
+        </el-table-column>
+        <!-- 列定义 -->
+        <el-table-column v-for="column in columns" 
+        :key="column.prop" :prop="column.prop" :label="column.label" flex="1">
+        </el-table-column>
+        <!-- 操作列(删除) -->
+        <el-table-column label="操作" width="120" align="center" v-if="columns && columns.length > 0" >
+            <template slot-scope="scope">
+                 <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
+            </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters, mapActions } from 'vuex'
+export default {
+  name: "zkqyTable",
+  emits: ['change', 'add', 'delete', 'input'],
+  computed: {
+    ...mapGetters('formCreate', ['getFormData', 'getLabelData']),
+  },
+  props: {
+    rule: {
+      type: Object,
+      default: () => ({})
+    },
+  },
+  data() {
+    return {
+      radio: '',
+      policyData: {},
+
+    }
+  },
+  watch: {
+  },
+  computed: {
+    formData() {
+    return this.$store.state.formCreate.formData || [];
+  },
+  labelData() {
+    return this.$store.state.formCreate.labelData || [];
+  },
+  tableData() {
+    return this.formData;
+  },
+  columns() {
+    return (this.labelData || []).map(item => ({
+      prop: item.value,
+      label: item.label
+    }));
+  }
+  },
+  methods: {
+    add() {
+            console.log("点击新增");
+        },
+        edit() {
+            if (!this.radio) {
+                this.$message.warning("请先选择一行数据");
+                return;
+            }
+            console.log("修改选中行:", this.radio);
+        },
+    getCurrentRow(val) {
+      this.policyData = val;
+      console.log('this.policyData', this.policyData)
+    },
+    handleDelete(index, row) {
+            // 删除确认弹窗
+            this.$msgbox.confirm('确认删除该行数据?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                this.tableData.splice(index, 1);
+                this.$message.success("删除成功");
+            }).catch(() => {
+                this.$message.info("已取消删除");
+            });
+        }
+  },
+
+}
+</script>
+<style></style>

+ 55 - 0
zkqy-ui/src/views/formCreate/config/base/field.js

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

+ 136 - 0
zkqy-ui/src/views/formCreate/config/base/form.js

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

+ 26 - 0
zkqy-ui/src/views/formCreate/config/base/style.js

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

+ 15 - 0
zkqy-ui/src/views/formCreate/config/base/validate.js

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

+ 67 - 0
zkqy-ui/src/views/formCreate/config/index.js

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

+ 24 - 0
zkqy-ui/src/views/formCreate/config/menu.js

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

+ 45 - 0
zkqy-ui/src/views/formCreate/config/rule/alert.js

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

+ 49 - 0
zkqy-ui/src/views/formCreate/config/rule/button.js

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

+ 40 - 0
zkqy-ui/src/views/formCreate/config/rule/card.js

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

+ 107 - 0
zkqy-ui/src/views/formCreate/config/rule/cascader.js

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

+ 67 - 0
zkqy-ui/src/views/formCreate/config/rule/checkbox.js

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

+ 86 - 0
zkqy-ui/src/views/formCreate/config/rule/col.js

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

+ 30 - 0
zkqy-ui/src/views/formCreate/config/rule/collapse.js

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

+ 36 - 0
zkqy-ui/src/views/formCreate/config/rule/collapseItem.js

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

+ 53 - 0
zkqy-ui/src/views/formCreate/config/rule/color.js

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

+ 70 - 0
zkqy-ui/src/views/formCreate/config/rule/date.js

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

+ 64 - 0
zkqy-ui/src/views/formCreate/config/rule/dateRange.js

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

+ 34 - 0
zkqy-ui/src/views/formCreate/config/rule/divider.js

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

+ 31 - 0
zkqy-ui/src/views/formCreate/config/rule/editor.js

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

+ 53 - 0
zkqy-ui/src/views/formCreate/config/rule/group.js

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

+ 52 - 0
zkqy-ui/src/views/formCreate/config/rule/html.js

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

+ 32 - 0
zkqy-ui/src/views/formCreate/config/rule/image.js

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

+ 63 - 0
zkqy-ui/src/views/formCreate/config/rule/input.js

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

+ 49 - 0
zkqy-ui/src/views/formCreate/config/rule/number.js

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

+ 52 - 0
zkqy-ui/src/views/formCreate/config/rule/password.js

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

+ 41 - 0
zkqy-ui/src/views/formCreate/config/rule/radio.js

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

+ 44 - 0
zkqy-ui/src/views/formCreate/config/rule/rate.js

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

+ 46 - 0
zkqy-ui/src/views/formCreate/config/rule/row.js

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

+ 71 - 0
zkqy-ui/src/views/formCreate/config/rule/select.js

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

+ 53 - 0
zkqy-ui/src/views/formCreate/config/rule/slider.js

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

+ 44 - 0
zkqy-ui/src/views/formCreate/config/rule/space.js

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

+ 33 - 0
zkqy-ui/src/views/formCreate/config/rule/span.js

@@ -0,0 +1,33 @@
+const label = '文字';
+const name = 'span';
+
+export default {
+    icon: 'icon-span',
+    label,
+    name,
+    rule() {
+        return {
+            type: name,
+            title: '文字',
+            native: false,
+            children: ['这是一段文字'],
+        };
+    },
+    props() {
+        return [
+            {
+                type: 'input',
+                field: 'formCreateTitle',
+                title: 'title',
+            },
+            {
+                type: 'input',
+                field: 'formCreateChild',
+                title: '内容',
+                props: {
+                    type: 'textarea'
+                }
+            }
+        ];
+    }
+};

+ 47 - 0
zkqy-ui/src/views/formCreate/config/rule/subForm.js

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

+ 46 - 0
zkqy-ui/src/views/formCreate/config/rule/switch.js

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

+ 35 - 0
zkqy-ui/src/views/formCreate/config/rule/tab.js

@@ -0,0 +1,35 @@
+const label = '标签页';
+const name = 'tab';
+
+export default {
+    icon: 'icon-tab',
+    label,
+    name,
+    children: 'tab-pane',
+    mask: false,
+    rule() {
+        return {
+            type: 'el-tabs',
+            children: []
+        };
+    },
+    props() {
+        return [{
+            type: 'select',
+            field: 'type',
+            title: '风格类型',
+            options: [{label: 'default', value: 'default'}, {
+                label: 'card',
+                value: 'card'
+            }, {label: 'border-card', value: 'border-card'}]
+        }, {type: 'switch', field: 'closable', title: '标签是否可关闭'}, {
+            type: 'select',
+            field: 'tabPosition',
+            title: '选项卡所在位置',
+            options: [{label: 'top', value: 'top'}, {label: 'right', value: 'right'}, {
+                label: 'left',
+                value: 'left'
+            }]
+        }, {type: 'switch', field: 'stretch', title: '标签的宽度是否自撑开'}];
+    }
+};

+ 31 - 0
zkqy-ui/src/views/formCreate/config/rule/tabPane.js

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

+ 34 - 0
zkqy-ui/src/views/formCreate/config/rule/table.js

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

+ 79 - 0
zkqy-ui/src/views/formCreate/config/rule/tableForm.js

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

+ 43 - 0
zkqy-ui/src/views/formCreate/config/rule/tableFormColumn.js

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

+ 38 - 0
zkqy-ui/src/views/formCreate/config/rule/tabs.js

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

+ 77 - 0
zkqy-ui/src/views/formCreate/config/rule/tag.js

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

+ 50 - 0
zkqy-ui/src/views/formCreate/config/rule/text.js

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

+ 63 - 0
zkqy-ui/src/views/formCreate/config/rule/textarea.js

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

+ 62 - 0
zkqy-ui/src/views/formCreate/config/rule/time.js

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

+ 53 - 0
zkqy-ui/src/views/formCreate/config/rule/timeRange.js

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

+ 59 - 0
zkqy-ui/src/views/formCreate/config/rule/transfer.js

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

+ 70 - 0
zkqy-ui/src/views/formCreate/config/rule/tree.js

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

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