From c5b39f2c16f9aa56431844e9c01600a2d2f3848c Mon Sep 17 00:00:00 2001 From: wwsheng009 Date: Fri, 10 Feb 2023 07:43:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=99=A8=20(#2533)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- src/router/routes/modules/form-design/main.ts | 34 + .../form-design/assets/iconfont/index.js | 64 + .../VFormCreate/components/FormRender.vue | 81 ++ .../components/VFormCreate/index.vue | 153 +++ .../VFormDesign/components/CodeModal.vue | 81 ++ .../VFormDesign/components/ComponentProps.vue | 245 ++++ .../components/FormItemColumnProps.vue | 66 + .../VFormDesign/components/FormItemProps.vue | 148 +++ .../VFormDesign/components/FormNode.vue | 55 + .../components/FormNodeOperate.vue | 76 ++ .../VFormDesign/components/FormOptions.vue | 114 ++ .../VFormDesign/components/FormProps.vue | 117 ++ .../components/ImportJsonModal.vue | 139 ++ .../VFormDesign/components/JsonModal.vue | 66 + .../VFormDesign/components/LayoutItem.vue | 135 ++ .../VFormDesign/components/PreviewCode.vue | 97 ++ .../VFormDesign/components/RuleProps.vue | 295 +++++ .../config/componentPropsConfig.ts | 1148 +++++++++++++++++ .../VFormDesign/config/formItemPropsConfig.ts | 351 +++++ .../components/VFormDesign/index.vue | 365 ++++++ .../VFormDesign/modules/CollapseItem.vue | 106 ++ .../modules/FormComponentPanel.vue | 169 +++ .../VFormDesign/modules/PropsPanel.vue | 100 ++ .../VFormDesign/modules/Toolbar.vue | 141 ++ .../components/VFormDesign/styles/drag.less | 231 ++++ .../VFormDesign/styles/v-form-design.less | 522 ++++++++ .../VFormDesign/styles/variable.less | 15 + .../components/VFormItem/index.vue | 228 ++++ .../components/VFormItem/vFormItem.vue | 79 ++ .../components/VFormPreview/index.vue | 108 ++ .../components/VFormPreview/useForm.vue | 74 ++ src/views/form-design/components/index.ts | 71 + src/views/form-design/core/formItemConfig.ts | 423 ++++++ src/views/form-design/core/iconConfig.ts | 739 +++++++++++ src/views/form-design/examples/baseForm.vue | 37 + .../form-design/hooks/useFormDesignState.ts | 18 + .../hooks/useFormInstanceMethods.ts | 62 + .../form-design/hooks/useVFormMethods.ts | 195 +++ src/views/form-design/index.vue | 9 + src/views/form-design/tests/import1.json | 48 + src/views/form-design/typings/base-type.ts | 10 + src/views/form-design/typings/form-type.ts | 52 + .../form-design/typings/v-form-component.ts | 349 +++++ src/views/form-design/utils/index.ts | 200 +++ src/views/form-design/utils/message.ts | 21 + stylelint.config.js | 2 +- 47 files changed, 7840 insertions(+), 2 deletions(-) create mode 100644 src/router/routes/modules/form-design/main.ts create mode 100644 src/views/form-design/assets/iconfont/index.js create mode 100644 src/views/form-design/components/VFormCreate/components/FormRender.vue create mode 100644 src/views/form-design/components/VFormCreate/index.vue create mode 100644 src/views/form-design/components/VFormDesign/components/CodeModal.vue create mode 100644 src/views/form-design/components/VFormDesign/components/ComponentProps.vue create mode 100644 src/views/form-design/components/VFormDesign/components/FormItemColumnProps.vue create mode 100644 src/views/form-design/components/VFormDesign/components/FormItemProps.vue create mode 100644 src/views/form-design/components/VFormDesign/components/FormNode.vue create mode 100644 src/views/form-design/components/VFormDesign/components/FormNodeOperate.vue create mode 100644 src/views/form-design/components/VFormDesign/components/FormOptions.vue create mode 100644 src/views/form-design/components/VFormDesign/components/FormProps.vue create mode 100644 src/views/form-design/components/VFormDesign/components/ImportJsonModal.vue create mode 100644 src/views/form-design/components/VFormDesign/components/JsonModal.vue create mode 100644 src/views/form-design/components/VFormDesign/components/LayoutItem.vue create mode 100644 src/views/form-design/components/VFormDesign/components/PreviewCode.vue create mode 100644 src/views/form-design/components/VFormDesign/components/RuleProps.vue create mode 100644 src/views/form-design/components/VFormDesign/config/componentPropsConfig.ts create mode 100644 src/views/form-design/components/VFormDesign/config/formItemPropsConfig.ts create mode 100644 src/views/form-design/components/VFormDesign/index.vue create mode 100644 src/views/form-design/components/VFormDesign/modules/CollapseItem.vue create mode 100644 src/views/form-design/components/VFormDesign/modules/FormComponentPanel.vue create mode 100644 src/views/form-design/components/VFormDesign/modules/PropsPanel.vue create mode 100644 src/views/form-design/components/VFormDesign/modules/Toolbar.vue create mode 100644 src/views/form-design/components/VFormDesign/styles/drag.less create mode 100644 src/views/form-design/components/VFormDesign/styles/v-form-design.less create mode 100644 src/views/form-design/components/VFormDesign/styles/variable.less create mode 100644 src/views/form-design/components/VFormItem/index.vue create mode 100644 src/views/form-design/components/VFormItem/vFormItem.vue create mode 100644 src/views/form-design/components/VFormPreview/index.vue create mode 100644 src/views/form-design/components/VFormPreview/useForm.vue create mode 100644 src/views/form-design/components/index.ts create mode 100644 src/views/form-design/core/formItemConfig.ts create mode 100644 src/views/form-design/core/iconConfig.ts create mode 100644 src/views/form-design/examples/baseForm.vue create mode 100644 src/views/form-design/hooks/useFormDesignState.ts create mode 100644 src/views/form-design/hooks/useFormInstanceMethods.ts create mode 100644 src/views/form-design/hooks/useVFormMethods.ts create mode 100644 src/views/form-design/index.vue create mode 100644 src/views/form-design/tests/import1.json create mode 100644 src/views/form-design/typings/base-type.ts create mode 100644 src/views/form-design/typings/form-type.ts create mode 100644 src/views/form-design/typings/v-form-component.ts create mode 100644 src/views/form-design/utils/index.ts create mode 100644 src/views/form-design/utils/message.ts diff --git a/package.json b/package.json index 55af57e1c..953a23e81 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "vxe-table": "^4.3.9", "vxe-table-plugin-export-xlsx": "^3.0.4", "xe-utils": "^3.5.7", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@commitlint/cli": "^16.2.3", diff --git a/src/router/routes/modules/form-design/main.ts b/src/router/routes/modules/form-design/main.ts new file mode 100644 index 000000000..536fb6955 --- /dev/null +++ b/src/router/routes/modules/form-design/main.ts @@ -0,0 +1,34 @@ +import type { AppRouteModule } from '/@/router/types'; + +import { LAYOUT } from '/@/router/constant'; + +const permission: AppRouteModule = { + path: '/form-designer', + name: 'Form-designer', + component: LAYOUT, + meta: { + orderNo: 10000, + icon: 'icon:add-circle', + title: '表单设计', + }, + children: [ + { + path: 'design', + name: 'Design', + meta: { + title: '表单设计', + }, + component: () => import('/@/views/form-design/index.vue'), + }, + { + path: 'example1', + name: 'Example1', + meta: { + title: '示例', + }, + component: () => import('/@/views/form-design/examples/baseForm.vue'), + }, + ], +}; + +export default permission; diff --git a/src/views/form-design/assets/iconfont/index.js b/src/views/form-design/assets/iconfont/index.js new file mode 100644 index 000000000..1756459e4 --- /dev/null +++ b/src/views/form-design/assets/iconfont/index.js @@ -0,0 +1,64 @@ +!(function (c) { + var h, + l, + a, + t, + v, + o = + '', + i = (i = document.getElementsByTagName('script'))[i.length - 1].getAttribute('data-injectcss'), + s = function (c, h) { + h.parentNode.insertBefore(c, h); + }; + if (i && !c.__iconfont__svg__cssinject__) { + c.__iconfont__svg__cssinject__ = !0; + try { + document.write( + '', + ); + } catch (c) { + console && console.log(c); + } + } + + function e() { + v || ((v = !0), a()); + } + + function z() { + try { + t.documentElement.doScroll('left'); + } catch (c) { + return void setTimeout(z, 50); + } + e(); + } + (h = function () { + var c, h; + ((h = document.createElement('div')).innerHTML = o), + (o = null), + (c = h.getElementsByTagName('svg')[0]) && + (c.setAttribute('aria-hidden', 'true'), + (c.style.position = 'absolute'), + (c.style.width = 0), + (c.style.height = 0), + (c.style.overflow = 'hidden'), + (h = c), + (c = document.body).firstChild ? s(h, c.firstChild) : c.appendChild(h)); + }), + document.addEventListener + ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState) + ? setTimeout(h, 0) + : ((l = function () { + document.removeEventListener('DOMContentLoaded', l, !1), h(); + }), + document.addEventListener('DOMContentLoaded', l, !1)) + : document.attachEvent && + ((a = h), + (t = c.document), + (v = !1), + z(), + (t.onreadystatechange = function () { + 'complete' == t.readyState && ((t.onreadystatechange = null), e()); + })); +})(window); diff --git a/src/views/form-design/components/VFormCreate/components/FormRender.vue b/src/views/form-design/components/VFormCreate/components/FormRender.vue new file mode 100644 index 000000000..e74924c7c --- /dev/null +++ b/src/views/form-design/components/VFormCreate/components/FormRender.vue @@ -0,0 +1,81 @@ + + + + diff --git a/src/views/form-design/components/VFormCreate/index.vue b/src/views/form-design/components/VFormCreate/index.vue new file mode 100644 index 000000000..c8cce5f48 --- /dev/null +++ b/src/views/form-design/components/VFormCreate/index.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/components/CodeModal.vue b/src/views/form-design/components/VFormDesign/components/CodeModal.vue new file mode 100644 index 000000000..b0490e4ce --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/CodeModal.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/ComponentProps.vue b/src/views/form-design/components/VFormDesign/components/ComponentProps.vue new file mode 100644 index 000000000..ee59eda1e --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/ComponentProps.vue @@ -0,0 +1,245 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/FormItemColumnProps.vue b/src/views/form-design/components/VFormDesign/components/FormItemColumnProps.vue new file mode 100644 index 000000000..468d4b2da --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/FormItemColumnProps.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/FormItemProps.vue b/src/views/form-design/components/VFormDesign/components/FormItemProps.vue new file mode 100644 index 000000000..c0c4da59d --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/FormItemProps.vue @@ -0,0 +1,148 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/FormNode.vue b/src/views/form-design/components/VFormDesign/components/FormNode.vue new file mode 100644 index 000000000..80fc37795 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/FormNode.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/FormNodeOperate.vue b/src/views/form-design/components/VFormDesign/components/FormNodeOperate.vue new file mode 100644 index 000000000..7f4d988ad --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/FormNodeOperate.vue @@ -0,0 +1,76 @@ + + + + diff --git a/src/views/form-design/components/VFormDesign/components/FormOptions.vue b/src/views/form-design/components/VFormDesign/components/FormOptions.vue new file mode 100644 index 000000000..b52722e5e --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/FormOptions.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/components/FormProps.vue b/src/views/form-design/components/VFormDesign/components/FormProps.vue new file mode 100644 index 000000000..f9f5438c0 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/FormProps.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/ImportJsonModal.vue b/src/views/form-design/components/VFormDesign/components/ImportJsonModal.vue new file mode 100644 index 000000000..ba0080b48 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/ImportJsonModal.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/components/JsonModal.vue b/src/views/form-design/components/VFormDesign/components/JsonModal.vue new file mode 100644 index 000000000..3480d8a7b --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/JsonModal.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/views/form-design/components/VFormDesign/components/LayoutItem.vue b/src/views/form-design/components/VFormDesign/components/LayoutItem.vue new file mode 100644 index 000000000..4b49a6858 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/LayoutItem.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/components/PreviewCode.vue b/src/views/form-design/components/VFormDesign/components/PreviewCode.vue new file mode 100644 index 000000000..1c97b5b0b --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/PreviewCode.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/components/RuleProps.vue b/src/views/form-design/components/VFormDesign/components/RuleProps.vue new file mode 100644 index 000000000..bb4515fee --- /dev/null +++ b/src/views/form-design/components/VFormDesign/components/RuleProps.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/config/componentPropsConfig.ts b/src/views/form-design/components/VFormDesign/config/componentPropsConfig.ts new file mode 100644 index 000000000..484d23bd9 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/config/componentPropsConfig.ts @@ -0,0 +1,1148 @@ +import { IBaseFormAttrs } from './formItemPropsConfig'; + +interface IBaseComponentProps { + [key: string]: IBaseFormAttrs[]; +} + +type BaseFormAttrs = Omit; + +export const baseComponentControlAttrs: Omit[] = [ + { + // 没有disabled属性的控件不能作为form控件 + name: 'disabled', + label: '禁用', + }, + { + // 没有disabled属性的控件不能作为form控件 + name: 'autofocus', + label: '自动获取焦点', + includes: [ + 'Input', + 'Select', + 'InputTextArea', + 'InputNumber', + 'DatePicker', + 'RangePicker', + 'MonthPicker', + 'TimePicker', + 'Cascader', + 'TreeSelect', + 'Switch', + 'AutoComplete', + 'Slider', + ], + }, + { + name: 'allowClear', + label: '可清除', + includes: [ + 'Input', + 'Select', + 'InputTextArea', + 'InputNumber', + 'DatePicker', + 'RangePicker', + 'MonthPicker', + 'TimePicker', + 'Cascader', + 'TreeSelect', + 'AutoComplete', + ], + }, + { name: 'fullscreen', label: '全屏', includes: ['Calendar'] }, + { + name: 'showSearch', + label: '可搜索', + includes: ['Select', 'TreeSelect', 'Cascader', 'Transfer'], + }, + { + name: 'showTime', + label: '显示时间', + includes: ['DatePicker', 'RangePicker', 'MonthPicker'], + }, + { + name: 'range', + label: '双向滑动', + includes: [], + }, + { + name: 'allowHalf', + label: '允许半选', + includes: ['Rate'], + }, + { + name: 'multiple', + label: '多选', + includes: ['Select', 'TreeSelect', 'Upload'], + }, + { + name: 'directory', + label: '文件夹', + includes: ['Upload'], + }, + { + name: 'withCredentials', + label: '携带cookie', + includes: ['Upload'], + }, + { + name: 'bordered', + label: '是否有边框', + includes: ['Select', 'Input'], + }, + { + name: 'defaultActiveFirstOption', + label: '高亮第一个选项', + component: 'Checkbox', + includes: ['Select', 'AutoComplete'], + }, + { + name: 'dropdownMatchSelectWidth', + label: '下拉菜单和选择器同宽', + component: 'Checkbox', + includes: ['Select', 'TreeSelect', 'AutoComplete'], + }, +]; + +//共用属性 +export const baseComponentCommonAttrs: Omit[] = [ + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default', + }, + { + label: '大', + value: 'large', + }, + { + label: '小', + value: 'small', + }, + ], + }, + includes: ['InputNumber', 'Input', 'Cascader', 'Button'], + }, + { + name: 'placeholder', + label: '占位符', + component: 'Input', + componentProps: { + placeholder: '请输入占位符', + }, + includes: [ + 'AutoComplete', + 'InputTextArea', + 'InputNumber', + 'Input', + 'InputTextArea', + 'Select', + 'DatePicker', + 'MonthPicker', + 'TimePicker', + 'TreeSelect', + 'Cascader', + ], + }, + { + name: 'style', + label: '样式', + component: 'Input', + componentProps: { + placeholder: '请输入样式', + }, + }, + { + name: 'open', + label: '一直展开下拉菜单', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: undefined, + }, + { + label: '是', + value: true, + }, + { + label: '否', + value: false, + }, + ], + }, + includes: ['Select', 'AutoComplete'], + }, +]; + +const componentAttrs: IBaseComponentProps = { + AutoComplete: [ + { + name: 'backfill', + label: '自动回填', + component: 'Switch', + componentProps: { + span: 8, + }, + }, + { + name: 'defaultOpen', + label: '是否默认展开下拉菜单', + component: 'Checkbox', + }, + ], + IconPicker: [ + { + name: 'mode', + label: '模式', + component: 'RadioGroup', + componentProps: { + options: [ + { label: 'ICONIFY', value: null }, + { label: 'SVG', value: 'svg' }, + // { label: '组合', value: 'combobox' }, + ], + }, + }, + ], + + // https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#%3Cinput%3E_types + Input: [ + { + name: 'type', + label: '类型', + component: 'Select', + componentProps: { + options: [ + { value: 'text', label: '文本' }, + { value: 'search', label: '搜索' }, + { value: 'number', label: '数字' }, + { value: 'range', label: '数字范围' }, + { value: 'password', label: '密码' }, + { value: 'date', label: '日期' }, + { value: 'datetime-local', label: '日期-无时区' }, + { value: 'time', label: '时间' }, + { value: 'month', label: '年月' }, + { value: 'week', label: '星期' }, + { value: 'email', label: '邮箱' }, + { value: 'url', label: 'URL' }, + { value: 'tel', label: '电话号码' }, + { value: 'file', label: '文件' }, + { value: 'button', label: '按钮' }, + { value: 'submit', label: '提交按钮' }, + { value: 'reset', label: '重置按钮' }, + { value: 'radio', label: '单选按钮' }, + { value: 'checkbox', label: '复选框' }, + { value: 'color', label: '颜色' }, + { value: 'image', label: '图像' }, + { value: 'hidden', label: '隐藏' }, + ], + }, + }, + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入默认值', + }, + }, + { + name: 'prefix', + label: '前缀', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入前缀', + }, + }, + { + name: 'suffix', + label: '后缀', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入后缀', + }, + }, + { + name: 'addonBefore', + label: '前置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入前置标签', + }, + }, + { + name: 'addonAfter', + label: '后置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入后置标签', + }, + }, + { + name: 'maxLength', + label: '最大长度', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最大长度', + }, + }, + ], + + InputNumber: [ + { + name: 'defaultValue', + label: '默认值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入默认值', + }, + }, + { + name: 'min', + label: '最小值', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最小值', + }, + }, + { + name: 'max', + label: '最大值', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最大值', + }, + }, + { + name: 'precision', + label: '数值精度', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最大值', + }, + }, + { + name: 'step', + label: '步长', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入步长', + }, + }, + { + name: 'decimalSeparator', + label: '小数点', + component: 'Input', + componentProps: { type: 'text', placeholder: '请输入小数点' }, + }, + { + name: 'addonBefore', + label: '前置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入前置标签', + }, + }, + { + name: 'addonAfter', + label: '后置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入后置标签', + }, + }, + { + name: 'controls', + label: '是否显示增减按钮', + component: 'Checkbox', + }, + { + name: 'keyboard', + label: '是否启用键盘快捷行为', + component: 'Checkbox', + }, + { + name: 'stringMode', + label: '字符值模式', + component: 'Checkbox', + }, + { + name: 'bordered', + label: '是否有边框', + component: 'Checkbox', + }, + ], + InputTextArea: [ + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入默认值', + }, + }, + { + name: 'maxlength', + label: '最大长度', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最大长度', + }, + }, + { + name: 'minlength', + label: '最小长度', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最小长度', + }, + }, + { + name: 'cols', + label: '可见列数', + component: 'InputNumber', + componentProps: { + placeholder: '请输入可见列数', + min: 0, + }, + }, + { + name: 'rows', + label: '可见行数', + component: 'InputNumber', + componentProps: { + placeholder: '请输入可见行数', + min: 0, + }, + }, + { + name: 'minlength', + label: '最小长度', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最小长度', + }, + }, + { + name: 'autosize', + label: '自适应内容高度', + component: 'Checkbox', + }, + { + name: 'showCount', + label: '是否展示字数', + component: 'Checkbox', + }, + { + name: 'readonly', + label: '是否只读', + component: 'Checkbox', + }, + { + name: 'spellcheck', + label: '读写检查', + component: 'Checkbox', + }, + { + name: 'autocomplete', + label: '是否自动完成', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '正常', value: null }, + { label: '开', value: 'on' }, + { label: '关', value: 'off' }, + ], + }, + }, + { + name: 'autocorrect', + label: '是否自动纠错', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '正常', value: null }, + { label: '开', value: 'on' }, + { label: '关', value: 'off' }, + ], + }, + }, + ], + Select: [ + { + name: 'mode', + label: '选择模式(默认单选)', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '单选', value: null }, + { label: '多选', value: 'multiple' }, + { label: '标签', value: 'tags' }, + // { label: '组合', value: 'combobox' }, + ], + }, + }, + { + name: 'autoClearSearchValue', + label: '是否在选中项后清空搜索框', + component: 'Checkbox', + }, + { + name: 'labelInValue', + label: '选项的label包装到value中', + component: 'Checkbox', + }, + { + name: 'showArrow', + label: '显示下拉小箭头', + component: 'Checkbox', + }, + { + name: 'defaultOpen', + label: '默认展开下拉菜单', + component: 'Checkbox', + }, + ], + Checkbox: [ + { + name: 'indeterminate', + label: '设置indeterminate状态', + component: 'Checkbox', + }, + ], + CheckboxGroup: [], + RadioGroup: [ + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + placeholder: '请输入默认值', + }, + }, + { + name: 'buttonStyle', + label: 'RadioButton的风格样式', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: 'outline', + value: 'outline', + }, + { + label: 'solid', + value: 'solid', + }, + ], + }, + }, + { + name: 'optionType', + label: 'options类型', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default', + }, + { + label: '按钮', + value: 'button', + }, + ], + //根据其它选项的值更新自身控件配置值 + //compProp当前组件的属性, + //configProps,当且组件的所有配置选项 + //self,当前配置的componentProps属性 + //返回真值进行更新 + // _propsFunc: (compProp, configProps, self) => { + // console.log("i'm called"); + // console.log(compProp, configProps, self); + // if (compProp['buttonStyle'] && compProp['buttonStyle'] == 'outline') { + // if (!self['disabled']) { + // self['disabled'] = true; + // return 1; + // } + // } else { + // if (self['disabled']) { + // self['disabled'] = false; + // return 1; + // } + // } + + // // return prop.optionType == 'button'; + // }, + }, + }, + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default', + }, + { + label: '大', + value: 'large', + }, + { + label: '小', + value: 'small', + }, + ], + }, + }, + ], + DatePicker: [ + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD', + }, + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD', + }, + }, + ], + RangePicker: [ + { + name: 'placeholder', + label: '占位符', + children: [ + { + name: '', + label: '', + component: 'Input', + }, + { + name: '', + label: '', + component: 'Input', + }, + ], + }, + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD HH:mm:ss', + }, + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD', + }, + }, + ], + MonthPicker: [ + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM', + }, + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM', + }, + }, + ], + TimePicker: [ + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM', + }, + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM', + }, + }, + ], + Slider: [ + { + name: 'defaultValue', + label: '默认值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入默认值', + }, + }, + { + name: 'min', + label: '最小值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最小值', + }, + }, + { + name: 'max', + label: '最大值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最大值', + }, + }, + { + name: 'step', + label: '步长', + component: 'InputNumber', + componentProps: { + placeholder: '请输入步长', + }, + }, + { + name: 'tooltipPlacement', + label: 'Tooltip 展示位置', + component: 'Select', + componentProps: { + options: [ + { value: 'top', label: '上' }, + { value: 'left', label: '左' }, + { value: 'right', label: '右' }, + { value: 'bottom', label: '下' }, + { value: 'topLeft', label: '上右' }, + { value: 'topRight', label: '上左' }, + { value: 'bottomLeft', label: '右下' }, + { value: 'bottomRight', label: '左下' }, + { value: 'leftTop', label: '左下' }, + { value: 'leftBottom', label: '左上' }, + { value: 'rightTop', label: '右下' }, + { value: 'rightBottom', label: '右上' }, + ], + }, + }, + { + name: 'tooltipVisible', + label: '始终显示Tooltip', + component: 'Checkbox', + }, + { + name: 'dots', + label: '只能拖拽到刻度上', + component: 'Checkbox', + }, + { + name: 'range', + label: '双滑块模式', + component: 'Checkbox', + }, + { + name: 'reverse', + label: '反向坐标轴', + component: 'Checkbox', + }, + { + name: 'vertical', + label: '垂直方向', + component: 'Checkbox', + }, + { + name: 'included', + label: '值为包含关系', + component: 'Checkbox', + }, + ], + Rate: [ + { + name: 'defaultValue', + label: '默认值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入默认值', + }, + }, + { + name: 'character', + label: '自定义字符', + component: 'Input', + componentProps: { + placeholder: '请输入自定义字符', + }, + }, + { + name: 'count', + label: 'start 总数', + component: 'InputNumber', + componentProps: { + placeholder: '请输入自定义字符', + }, + }, + ], + Switch: [ + { + name: 'checkedChildren', + label: '选中时的内容', + component: 'Input', + componentProps: { + placeholder: '请输入选中时的内容', + }, + }, + { + name: 'checkedValue', + label: '选中时的值', + component: 'Input', + componentProps: { + placeholder: '请输入选中时的值', + }, + }, + { + name: 'unCheckedChildren', + label: '非选中时的内容', + component: 'Input', + componentProps: { + placeholder: '请输入非选中时的内容', + }, + }, + { + name: 'unCheckedValue', + label: '非选中时的值', + component: 'Input', + componentProps: { + placeholder: '请输入非选中时的值', + }, + }, + { + name: 'loading', + label: '加载中的开关', + component: 'Checkbox', + }, + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default', + }, + { + label: '小', + value: 'small', + }, + ], + }, + }, + ], + TreeSelect: [ + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + placeholder: '请输入默认值', + }, + }, + { + name: 'searchPlaceholder', + label: '搜索框默认文字', + component: 'Input', + componentProps: { + placeholder: '请输入搜索框默认文字', + }, + }, + { + name: 'treeNodeFilterProp', + label: '输入项过滤对应的 treeNode 属性', + component: 'Input', + componentProps: { + defaultValue: 'value', + }, + }, + { + name: 'treeNodeLabelProp', + label: '作为显示的 prop 设置', + component: 'Input', + componentProps: { + defaultValue: 'title', + }, + }, + { + name: 'dropdownClassName', + label: '下拉菜单的 className 属性', + component: 'Input', + componentProps: { + placeholder: '请输入下拉菜单的 className 属性', + }, + }, + + { + name: 'labelInValue', + label: '选项的label包装到value中', + component: 'Checkbox', + }, + { + name: 'treeIcon', + label: '展示TreeNode title前的图标', + component: 'Checkbox', + }, + { + name: 'treeCheckable', + label: '选项可勾选', + component: 'Checkbox', + }, + { + name: 'treeCheckStrictly', + label: '节点选择完全受控', + component: 'Checkbox', + }, + { + name: 'treeDefaultExpandAll', + label: '默认展开所有', + component: 'Checkbox', + }, + { + name: 'treeLine', + label: '是否展示线条样式', + component: 'Checkbox', + }, + { + name: 'maxTagCount', + label: '最多显示多少个 tag', + component: 'InputNumber', + componentProps: { + placeholder: '最多显示多少个 tag', + }, + }, + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default', + }, + { + label: '小', + value: 'small', + }, + ], + }, + }, + ], + Cascader: [ + { + name: 'expandTrigger', + label: '次级展开方式(默认click)', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: 'click', + value: 'click', + }, + { + label: 'hover', + value: 'hover', + }, + ], + }, + }, + ], + Button: [ + { + name: 'type', + label: '类型', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: 'default', + value: 'default', + }, + { + label: 'primary', + value: 'primary', + }, + { + label: 'danger', + value: 'danger', + }, + { + label: 'dashed', + value: 'dashed', + }, + ], + }, + }, + { + name: 'handle', + label: '操作', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '提交', + value: 'submit', + }, + { + label: '重置', + value: 'reset', + }, + ], + }, + }, + ], + Upload: [ + { + name: 'action', + label: '上传地址', + component: 'Input', + }, + { + name: 'name', + label: '附件参数名(name)', + component: 'Input', + }, + ], + ColorPicker: [ + { + name: 'defaultValue', + label: '默认值', + component: 'AColorPicker', + }, + ], + slot: [ + { + name: 'slotName', + label: '插槽名称', + component: 'Input', + }, + ], + Transfer: [ + // { + // name: 'operations', + // label: '操作文案集合,顺序从上至下', + // component: 'Input', + // componentProps: { + // type: 'text', + // // defaultValue: ['>', '<'], + // }, + // }, + // { + // name: 'titles', + // label: '标题集合,顺序从左至右', + // component: 'Input', + // componentProps: { + // type: 'text', + // // defaultValue: ['', ''], + // }, + // }, + { + name: 'oneWay', + label: '展示为单向样式', + component: 'Checkbox', + }, + { + name: 'pagination', + label: '使用分页样式', + component: 'Checkbox', + }, + { + name: 'showSelectAll', + label: '展示全选勾选框', + component: 'Checkbox', + }, + ], +}; + +function deleteProps(list: Omit[], key: string) { + list.forEach((element, index) => { + if (element.name == key) { + list.splice(index, 1); + } + }); +} + +componentAttrs['StrengthMeter'] = componentAttrs['Input']; +componentAttrs['StrengthMeter'].push({ + name: 'visibilityToggle', + label: '是否显示切换按钮', + component: 'Checkbox', +}); + +deleteProps(componentAttrs['StrengthMeter'], 'type'); +deleteProps(componentAttrs['StrengthMeter'], 'prefix'); +deleteProps(componentAttrs['StrengthMeter'], 'defaultValue'); +deleteProps(componentAttrs['StrengthMeter'], 'suffix'); +//组件属性 +// name 控件的属性 +export const baseComponentAttrs: IBaseComponentProps = componentAttrs; + +//在所有的选项中查找需要配置项 +const findCompoentProps = (props, name) => { + const idx = props.findIndex((value: BaseFormAttrs, _index) => { + return value.name == name; + }); + if (idx) { + if (props[idx].componentProps) { + return props[idx].componentProps; + } + } +}; + +// 根据其它选项的值更新自身控件配置值 +export const componentPropsFuncs = { + RadioGroup: (compProp, options: BaseFormAttrs[]) => { + const props = findCompoentProps(options, 'size'); + if (props) { + if (compProp['optionType'] && compProp['optionType'] != 'button') { + props['disabled'] = true; + compProp['size'] = null; + } else { + props['disabled'] = false; + } + } + }, +}; diff --git a/src/views/form-design/components/VFormDesign/config/formItemPropsConfig.ts b/src/views/form-design/components/VFormDesign/config/formItemPropsConfig.ts new file mode 100644 index 000000000..99b4f76dc --- /dev/null +++ b/src/views/form-design/components/VFormDesign/config/formItemPropsConfig.ts @@ -0,0 +1,351 @@ +import { IAnyObject } from '../../../typings/base-type'; +import { baseComponents, customComponents } from '../../../core/formItemConfig'; + +export const globalConfigState: { span: number } = { + span: 24, +}; +export interface IBaseFormAttrs { + name: string; // 字段名 + label: string; // 字段标签 + component?: string; // 属性控件 + componentProps?: IAnyObject; // 传递给控件的属性 + exclude?: string[]; // 需要排除的控件 + includes?: string[]; // 符合条件的组件 + on?: IAnyObject; + children?: IBaseFormAttrs[]; + category?: 'control' | 'input'; +} + +export interface IBaseFormItemControlAttrs extends IBaseFormAttrs { + target?: 'props' | 'options'; // 绑定到对象下的某个目标key中 +} + +export const baseItemColumnProps: IBaseFormAttrs[] = [ + { + name: 'span', + label: '栅格数', + component: 'Slider', + on: { + change(value: number) { + globalConfigState.span = value; + }, + }, + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + + { + name: 'offset', + label: '栅格左侧的间隔格数', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'order', + label: '栅格顺序,flex 布局模式下有效', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'pull', + label: '栅格向左移动格数', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'push', + label: '栅格向右移动格数', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'xs', + label: '<576px 响应式栅格', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'sm', + label: '≥576px 响应式栅格', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'md', + label: '≥768p 响应式栅格', + component: 'Slider', + + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'lg', + label: '≥992px 响应式栅格', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'xl', + label: '≥1200px 响应式栅格', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: 'xxl', + label: '≥1600px 响应式栅格', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, + { + name: '≥2000px', + label: '≥1600px 响应式栅格', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + }, +]; + +// 控件属性面板的配置项 +export const advanceFormItemColProps: IBaseFormAttrs[] = [ + { + name: 'labelCol', + label: '标签col', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + exclude: ['Grid'], + }, + { + name: 'wrapperCol', + label: '控件-span', + component: 'Slider', + componentProps: { + max: 24, + min: 0, + marks: { 12: '' }, + }, + exclude: ['Grid'], + }, +]; +// 控件属性面板的配置项 +export const baseFormItemProps: IBaseFormAttrs[] = [ + { + // 动态的切换控件的类型 + name: 'component', + label: '控件-FormItem', + component: 'Select', + componentProps: { + options: baseComponents + .concat(customComponents) + .map((item) => ({ value: item.component, label: item.label })), + }, + }, + { + name: 'label', + label: '标签', + component: 'Input', + componentProps: { + type: 'Input', + placeholder: '请输入标签', + }, + exclude: ['Grid'], + }, + { + name: 'field', + label: '字段标识', + component: 'Input', + componentProps: { + type: 'InputTextArea', + placeholder: '请输入字段标识', + }, + exclude: ['Grid'], + }, + { + name: 'helpMessage', + label: 'helpMessage', + component: 'Input', + componentProps: { + placeholder: '请输入提示信息', + }, + exclude: ['Grid'], + }, +]; + +// 控件属性面板的配置项 +export const advanceFormItemProps: IBaseFormAttrs[] = [ + { + name: 'labelAlign', + label: '标签对齐', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '靠左', + value: 'left', + }, + { + label: '靠右', + value: 'right', + }, + ], + }, + exclude: ['Grid'], + }, + + { + name: 'help', + label: 'help', + component: 'Input', + componentProps: { + placeholder: '请输入提示信息', + }, + exclude: ['Grid'], + }, + { + name: 'extra', + label: '额外消息', + component: 'Input', + componentProps: { + type: 'InputTextArea', + placeholder: '请输入额外消息', + }, + exclude: ['Grid'], + }, + { + name: 'validateTrigger', + label: 'validateTrigger', + component: 'Input', + componentProps: { + type: 'InputTextArea', + placeholder: '请输入validateTrigger', + }, + exclude: ['Grid'], + }, + { + name: 'validateStatus', + label: '校验状态', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: '', + }, + { + label: '成功', + value: 'success', + }, + { + label: '警告', + value: 'warning', + }, + { + label: '错误', + value: 'error', + }, + { + label: '校验中', + value: 'validating', + }, + ], + }, + exclude: ['Grid'], + }, +]; + +export const baseFormItemControlAttrs: IBaseFormItemControlAttrs[] = [ + { + name: 'required', + label: '必填项', + component: 'Checkbox', + exclude: ['alert'], + }, + { + name: 'hidden', + label: '隐藏', + component: 'Checkbox', + exclude: ['alert'], + }, + { + name: 'hiddenLabel', + component: 'Checkbox', + exclude: ['Grid'], + label: '隐藏标签', + }, + { + name: 'colon', + label: 'label后面显示冒号', + component: 'Checkbox', + componentProps: {}, + exclude: ['Grid'], + }, + { + name: 'hasFeedback', + label: '输入反馈', + component: 'Checkbox', + componentProps: {}, + includes: ['Input'], + }, + { + name: 'autoLink', + label: '自动关联', + component: 'Checkbox', + componentProps: {}, + includes: ['Input'], + }, + { + name: 'validateFirst', + label: '检验证错误停止', + component: 'Checkbox', + componentProps: {}, + includes: ['Input'], + }, +]; diff --git a/src/views/form-design/components/VFormDesign/index.vue b/src/views/form-design/components/VFormDesign/index.vue new file mode 100644 index 000000000..39e5dc3c4 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/index.vue @@ -0,0 +1,365 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/modules/CollapseItem.vue b/src/views/form-design/components/VFormDesign/modules/CollapseItem.vue new file mode 100644 index 000000000..2104554c4 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/modules/CollapseItem.vue @@ -0,0 +1,106 @@ + + + + diff --git a/src/views/form-design/components/VFormDesign/modules/FormComponentPanel.vue b/src/views/form-design/components/VFormDesign/modules/FormComponentPanel.vue new file mode 100644 index 000000000..1c51fed30 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/modules/FormComponentPanel.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/modules/PropsPanel.vue b/src/views/form-design/components/VFormDesign/modules/PropsPanel.vue new file mode 100644 index 000000000..72e677bb5 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/modules/PropsPanel.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/modules/Toolbar.vue b/src/views/form-design/components/VFormDesign/modules/Toolbar.vue new file mode 100644 index 000000000..fb43378d3 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/modules/Toolbar.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/views/form-design/components/VFormDesign/styles/drag.less b/src/views/form-design/components/VFormDesign/styles/drag.less new file mode 100644 index 000000000..e46785768 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/styles/drag.less @@ -0,0 +1,231 @@ +.draggable-box { + height: 100%; + overflow: auto; + + :deep(.list-main) { + overflow: hidden; + min-height: 100%; + padding: 5px; + position: relative; + background: #fafafa; + // border : 1px #ccc dashed; + + .moving { + // 拖放移动中 + // outline-width: 0; + min-height: 35px; + box-sizing: border-box; + overflow: hidden; + padding: 0 !important; + // margin : 3px 0; + position: relative; + + &::before { + content: ''; + height: 5px; + width: 100%; + background: @primary-color; + position: absolute; + top: 0; + right: 0; + } + } + + .drag-move-box { + position: relative; + box-sizing: border-box; + padding: 8px; + overflow: hidden; + transition: all 0.3s; + min-height: 60px; + + &:hover { + background: @primary-hover-bg-color; + } + + // 选择时 start + &::before { + content: ''; + height: 5px; + width: 100%; + background: @primary-color; + position: absolute; + top: 0; + right: -100%; + transition: all 0.3s; + } + + &.active { + background: @primary-hover-bg-color; + outline-offset: 0; + + &::before { + right: 0; + } + } + + // 选择时 end + .form-item-box { + position: relative; + box-sizing: border-box; + word-wrap: break-word; + + &::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + // z-index: 888; + } + + .ant-form-item { + // 修改ant form-item的margin为padding + margin: 0; + padding-bottom: 6px; + } + } + + .show-key-box { + // 显示key + position: absolute; + bottom: 2px; + right: 5px; + font-size: 14px; + // z-index: 999; + color: @primary-color; + } + + .copy, + .delete { + position: absolute; + top: 0; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; + color: #fff; + // z-index: 989; + transition: all 0.3s; + + &.unactivated { + opacity: 0 !important; + pointer-events: none; + } + + &.active { + opacity: 1 !important; + } + } + + .copy { + border-radius: 0 0 0 8px; + right: 30px; + background: @primary-color; + } + + .delete { + right: 0; + background: @primary-color; + } + } + + .grid-box { + position: relative; + box-sizing: border-box; + padding: 5px; + background: @layout-background-color; + width: 100%; + transition: all 0.3s; + overflow: hidden; + + .form-item-box { + position: relative; + box-sizing: border-box; + + .ant-form-item { + // 修改ant form-item的margin为padding + margin: 0; + padding-bottom: 15px; + } + } + + .grid-row { + background: @layout-background-color; + + .grid-col { + .draggable-box { + min-height: 80px; + min-width: 50px; + border: 1px #ccc dashed; + background: #fff; + + .list-main { + min-height: 83px; + position: relative; + border: 1px #ccc dashed; + } + } + } + } + + // 选择时 start + &::before { + content: ''; + height: 5px; + width: 100%; + background: transparent; + position: absolute; + top: 0; + right: -100%; + transition: all 0.3s; + } + + &.active { + background: @layout-hover-bg-color; + outline-offset: 0; + + &::before { + background: @layout-color; + right: 0; + } + } + // 选择时 end + > .copy-delete-box { + > .copy, + > .delete { + position: absolute; + top: 0; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; + color: #fff; + // z-index: 989; + transition: all 0.3s; + + &.unactivated { + opacity: 0 !important; + pointer-events: none; + } + + &.active { + opacity: 1 !important; + } + } + + > .copy { + border-radius: 0 0 0 8px; + right: 30px; + background: @layout-color; + } + + > .delete { + right: 0; + background: @layout-color; + } + } + } + } +} diff --git a/src/views/form-design/components/VFormDesign/styles/v-form-design.less b/src/views/form-design/components/VFormDesign/styles/v-form-design.less new file mode 100644 index 000000000..0e6ce86b4 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/styles/v-form-design.less @@ -0,0 +1,522 @@ +.v-form-design-container { + // height: 100%; + width: 100%; + // overflow: hidden; + display: flex; + flex-direction: column; + + & > .v-form-design-header { + height: @header-height; + line-height: @header-height; + background: @primary-color; + text-align: center; + font-size: 20px; + color: #fff; + } + + :deep(.icon) { + width: 1em; + height: 1em; + vertical-align: -0.15em; + fill: currentcolor; + // overflow: hidden; + } + + .content { + position: relative; + flex: 1; + // margin-top: 5px; + display: flex; + // flex-flow: row nowrap; + // height: 100%; + // overflow: hidden; + box-sizing: border-box; + + .left, + .right { + width: @left-right-width; + box-shadow: 0 0 1px 1px #ccc; + // overflow: hidden; + // height: 100%; + // height: 600px; + // + border: 1px solid green; + // overflow-y: auto; + // :deep(.ant-tabs) { + // height: 100%; + // .ant-tabs-content-holder { + // // display: flex; + // // flex-flow: column; + // height: 100%; + // .ant-tabs-content { + // // flex:1; + // // height: 0; + // // overflow-y: auto; + // // overflow-x: hidden; + // display: flex; + // flex-flow: column; + // height: 100%; + // } + // } + // } + } + + :deep(.right) { + // & > div { + // height: 100%; + // } + // overflow: hidden; + // & > div { + // height: 100%; + // .ant-tabs-content-holder{ + // height: 100%; + // .ant-tabs-content{ + // height: 100%; + // .ant-tabs-tabpane{ + // height: 100%; + // } + // } + // } + // } + + .ant-tabs { + width: 280px; + height: 100%; + // overflow: hidden; + + .ant-tabs-content-holder { + // display: flex; + // flex-flow: column; + // height: 100%; + // overflow: hidden; + + .ant-tabs-content { + // flex:1; + // height: 0; + // overflow-y: auto; + // overflow-x: hidden; + + height: 100%; + // overflow: hidden; + .ant-tabs-tabpane { + .properties-content { + // height: 100%; + + // overflow: auto; + // overflow: hidden; + + // background: #fff; + .properties-body { + box-sizing: border-box; + // height: 100%; + + // display: flex; + // flex-flow: column; + + form { + position: absolute; + // height: 400px; + height: calc(100% - 50px); + // height: 100%; + // flex: 1; + // height: 0; + margin-right: 10px; + // overflow: auto; + overflow-y: auto; + overflow-x: hidden; + // overflow: auto; + } + // overflow: auto; + // height: 100%; + // padding: 8px 16px; + .hint-box { + margin-top: 200px; + } + + .ant-form-item, + .ant-slider-with-marks { + margin-left: 10px; + margin-right: 20px; + margin-bottom: 0; + } + + .ant-form-item { + // box-sizing: border-box; + width: 100%; + margin-bottom: 0; + // padding: 2px 0; + border-bottom: 1px solid @border-color; + + .ant-form-item-label { + line-height: 2; + vertical-align: text-top; + } + } + } + } + } + } + } + } + } + + :deep(.left) { + .ant-collapse { + border: 0; + + .ant-collapse-header { + padding: 7px 0 7px 40px; + } + + .ant-collapse-content-box { + padding: 0; + } + } + + ul { + padding: 5px; + list-style: none; + display: flex; + margin-bottom: 0; + flex-wrap: wrap; + // background: #efefef; + + li { + padding: 8px 12px; + transition: all 0.3s; + width: calc(50% - 6px); + margin: 2.7px; + height: 36px; + line-height: 20px; + cursor: move; + border: 1px solid @border-color; + border-radius: 3px; + + &:hover { + color: @primary-color; + border: 1px solid @primary-color; + position: relative; + // z-index: 1; + box-shadow: 0 2px 6px @primary-color; + } + } + } + } + + :deep(.node-panel) { + box-shadow: 0 0 1px 1px #ccc; + flex: 1; + margin: 0 8px; + overflow: hidden; + + .draggable-box { + height: 100%; + overflow: auto; + + .list-main { + overflow: hidden; + min-height: 100%; + padding: 5px; + position: relative; + background: #fafafa; + // border : 1px #ccc dashed; + + .moving { + // 拖放移动中 + // outline-width: 0; + min-height: 35px; + box-sizing: border-box; + overflow: hidden; + padding: 0 !important; + // margin : 3px 0; + position: relative; + + &::before { + content: ''; + height: 5px; + width: 100%; + background: @primary-color; + position: absolute; + top: 0; + right: 0; + } + } + + .drag-move-box { + position: relative; + box-sizing: border-box; + padding: 8px; + overflow: hidden; + transition: all 0.3s; + min-height: 60px; + + &:hover { + background: @primary-hover-bg-color; + } + + // 选择时 start + &::before { + content: ''; + height: 5px; + width: 100%; + background: @primary-color; + position: absolute; + top: 0; + right: -100%; + transition: all 0.3s; + } + + &.active { + background: @primary-hover-bg-color; + outline-offset: 0; + + &::before { + right: 0; + } + } + + // 选择时 end + .form-item-box { + position: relative; + box-sizing: border-box; + word-wrap: break-word; + + &::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + // z-index: 888; + } + + .ant-form-item { + // 修改ant form-item的margin为padding + margin: 0; + padding-bottom: 6px; + } + } + + .show-key-box { + // 显示key + position: absolute; + bottom: 2px; + right: 5px; + font-size: 14px; + // z-index: 999; + color: @primary-color; + } + + .copy, + .delete { + position: absolute; + top: 0; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; + color: #fff; + // z-index: 989; + transition: all 0.3s; + + &.unactivated { + opacity: 0 !important; + pointer-events: none; + } + + &.active { + opacity: 1 !important; + } + } + + .copy { + border-radius: 0 0 0 8px; + right: 30px; + background: @primary-color; + } + + .delete { + right: 0; + background: @primary-color; + } + } + + .grid-box { + position: relative; + box-sizing: border-box; + padding: 5px; + background: @layout-background-color; + width: 100%; + transition: all 0.3s; + overflow: hidden; + + .form-item-box { + position: relative; + box-sizing: border-box; + + .ant-form-item { + // 修改ant form-item的margin为padding + margin: 0; + padding-bottom: 15px; + } + } + + .grid-row { + background: @layout-background-color; + + .grid-col { + .draggable-box { + min-height: 80px; + min-width: 50px; + border: 1px #ccc dashed; + background: #fff; + + .list-main { + min-height: 83px; + position: relative; + border: 1px #ccc dashed; + } + } + } + } + + // 选择时 start + &::before { + content: ''; + height: 5px; + width: 100%; + background: transparent; + position: absolute; + top: 0; + right: -100%; + transition: all 0.3s; + } + + &.active { + background: @layout-hover-bg-color; + outline-offset: 0; + + &::before { + background: @layout-color; + right: 0; + } + } + // 选择时 end + > .copy-delete-box { + > .copy, + > .delete { + position: absolute; + top: 0; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; + color: #fff; + // z-index: 989; + transition: all 0.3s; + + &.unactivated { + opacity: 0 !important; + pointer-events: none; + } + + &.active { + opacity: 1 !important; + } + } + + > .copy { + border-radius: 0 0 0 8px; + right: 30px; + background: @layout-color; + } + + > .delete { + right: 0; + background: @layout-color; + } + } + } + } + } + } + } + + ::-webkit-scrollbar { + /* 滚动条整体样式 */ + width: 6px; + + /* 高宽分别对应横竖滚动条的尺寸 */ + height: 6px; + scrollbar-arrow-color: red; + } + + ::-webkit-scrollbar-thumb { + /* 滚动条里面小方块 */ + border-radius: 5px; + box-shadow: inset 0 0 5px rgb(0 0 0 / 20%); + background: rgb(0 0 0 / 20%); + scrollbar-arrow-color: red; + } + + ::-webkit-scrollbar-track { + /* 滚动条里面轨道 */ + box-shadow: inset 0 0 5px rgb(0 0 0 / 20%); + border-radius: 0; + background: rgb(0 0 0 / 10%); + } +} + +// code盒子样式 +.v-json-box { + height: 570px; + overflow: auto; + + .vue-codemirror-wrap { + height: 100%; + + .CodeMirror-wrap { + height: 100%; + background: #f6f6f6; + + .CodeMirror-scroll { + height: 100%; + width: 100%; + } + + pre.CodeMirror-line, + .CodeMirror-linenumber { + min-height: 21px; + line-height: 21px; + } + } + } +} + +// code-modal盒子样式 +.v-code-modal { + .ant-modal-body { + padding: 12px; + } +} + +.v-form-container { + // 内联布局样式 + .ant-form-inline { + .list-main { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + + .layout-width { + width: 100%; + } + } + + .ant-form-item-control-wrapper { + min-width: 175px !important; + } + } +} diff --git a/src/views/form-design/components/VFormDesign/styles/variable.less b/src/views/form-design/components/VFormDesign/styles/variable.less new file mode 100644 index 000000000..8749dce12 --- /dev/null +++ b/src/views/form-design/components/VFormDesign/styles/variable.less @@ -0,0 +1,15 @@ +// 表单设计器样式 +@primary-color: #13c2c2; +@layout-color: #9867f7; + +@primary-background-color: fade(@primary-color, 6%); +@primary-hover-bg-color: fade(@primary-color, 20%); +@layout-background-color: fade(@layout-color, 12%); +@layout-hover-bg-color: fade(@layout-color, 24%); + +@title-text-color: #fff; +@border-color: #ccc; + +@left-right-width: 280px; +@header-height: 56px; +@operating-area-height: 45px; diff --git a/src/views/form-design/components/VFormItem/index.vue b/src/views/form-design/components/VFormItem/index.vue new file mode 100644 index 000000000..e96635c16 --- /dev/null +++ b/src/views/form-design/components/VFormItem/index.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/views/form-design/components/VFormItem/vFormItem.vue b/src/views/form-design/components/VFormItem/vFormItem.vue new file mode 100644 index 000000000..252dc2d52 --- /dev/null +++ b/src/views/form-design/components/VFormItem/vFormItem.vue @@ -0,0 +1,79 @@ + + + + + + diff --git a/src/views/form-design/components/VFormPreview/index.vue b/src/views/form-design/components/VFormPreview/index.vue new file mode 100644 index 000000000..da56ad040 --- /dev/null +++ b/src/views/form-design/components/VFormPreview/index.vue @@ -0,0 +1,108 @@ + + + diff --git a/src/views/form-design/components/VFormPreview/useForm.vue b/src/views/form-design/components/VFormPreview/useForm.vue new file mode 100644 index 000000000..9d4d76af8 --- /dev/null +++ b/src/views/form-design/components/VFormPreview/useForm.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/views/form-design/components/index.ts b/src/views/form-design/components/index.ts new file mode 100644 index 000000000..4bf4f45b4 --- /dev/null +++ b/src/views/form-design/components/index.ts @@ -0,0 +1,71 @@ +import type { Component } from 'vue'; +import { ComponentType } from '/@/components/Form/src/types'; +import { IconPicker } from '/@/components/Icon/index'; +/** + * Component list, register here to setting it in the form + */ +import { + Input, + Button, + Select, + Radio, + Checkbox, + AutoComplete, + Cascader, + DatePicker, + InputNumber, + Switch, + TimePicker, + // ColorPicker, + TreeSelect, + Slider, + Rate, + Divider, + Calendar, + Transfer, +} from 'ant-design-vue'; + +//ant-desing本身的Form控件库 + +const componentMap = new Map(); +componentMap.set('Radio', Radio); +componentMap.set('Button', Button); +componentMap.set('Calendar', Calendar); +componentMap.set('Input', Input); +componentMap.set('InputGroup', Input.Group); +componentMap.set('InputPassword', Input.Password); +componentMap.set('InputSearch', Input.Search); +componentMap.set('InputTextArea', Input.TextArea); +componentMap.set('InputNumber', InputNumber); +componentMap.set('AutoComplete', AutoComplete); + +componentMap.set('Select', Select); +componentMap.set('TreeSelect', TreeSelect); +componentMap.set('Switch', Switch); +componentMap.set('RadioGroup', Radio.Group); +componentMap.set('Checkbox', Checkbox); +componentMap.set('CheckboxGroup', Checkbox.Group); +componentMap.set('Cascader', Cascader); +componentMap.set('Slider', Slider); +componentMap.set('Rate', Rate); +componentMap.set('Transfer', Transfer); +componentMap.set('DatePicker', DatePicker); +componentMap.set('MonthPicker', DatePicker.MonthPicker); +componentMap.set('RangePicker', DatePicker.RangePicker); +componentMap.set('WeekPicker', DatePicker.WeekPicker); +componentMap.set('TimePicker', TimePicker); + +componentMap.set('ColorPicker', TimePicker); + +componentMap.set('IconPicker', IconPicker); +componentMap.set('Divider', Divider); + +export function add(compName: ComponentType, component: Component) { + componentMap.set(compName, component); +} + +export function del(compName: ComponentType) { + componentMap.delete(compName); +} + +export { componentMap }; diff --git a/src/views/form-design/core/formItemConfig.ts b/src/views/form-design/core/formItemConfig.ts new file mode 100644 index 000000000..7ae9e5f85 --- /dev/null +++ b/src/views/form-design/core/formItemConfig.ts @@ -0,0 +1,423 @@ +/** + * @name: formItemConfig + * @author: ypt + * @date: 2021/11/18 16:25 + * @description:表单配置 + */ +import { IVFormComponent } from '../typings/v-form-component'; +import { isArray } from 'lodash-es'; +import { componentMap as VbenCmp, add } from '/@/components/Form/src/componentMap'; +import { ComponentType } from '/@/components/Form/src/types'; + +import { componentMap as Cmp } from '../components'; +import { Component } from 'vue'; + +const componentMap = new Map(); + +//如果有其它控件,可以在这里初始化 + +//注册Ant控件库 +Cmp.forEach((value, key) => { + componentMap.set(key, value); + if (VbenCmp[key] == null) { + add(key as ComponentType, value); + } +}); +//注册vben控件库 +VbenCmp.forEach((value, key) => { + componentMap.set(key, value); +}); + +export { componentMap }; + +/** + * 设置自定义表单控件 + * @param {IVFormComponent | IVFormComponent[]} config + */ +export function setFormDesignComponents(config: IVFormComponent | IVFormComponent[]) { + if (isArray(config)) { + config.forEach((item) => { + const { componentInstance: component, ...rest } = item; + componentMap[item.component] = component; + customComponents.push(Object.assign({ props: {} }, rest)); + }); + } else { + const { componentInstance: component, ...rest } = config; + componentMap[config.component] = component; + customComponents.push(Object.assign({ props: {} }, rest)); + } +} + +//外部设置的自定义控件 +export const customComponents: IVFormComponent[] = []; + +// 左侧控件列表与初始化的控件属性 +// props.slotName,会在formitem级别生成一个slot,并绑定当前record值 +// 属性props,类型为对象,不能为undefined或是null。 +export const baseComponents: IVFormComponent[] = [ + { + component: 'InputCountDown', + label: '倒计时输入', + icon: 'line-md:iconify2', + colProps: { span: 24 }, + field: '', + componentProps: {}, + }, + { + component: 'IconPicker', + label: '图标选择器', + icon: 'line-md:iconify2', + colProps: { span: 24 }, + field: '', + componentProps: {}, + }, + { + component: 'StrengthMeter', + label: '密码强度', + icon: 'wpf:password1', + colProps: { span: 24 }, + field: '', + componentProps: {}, + }, + { + component: 'AutoComplete', + label: '自动完成', + icon: 'wpf:password1', + colProps: { span: 24 }, + field: '', + componentProps: { + placeholder: '请输入正则表达式', + options: [ + { + value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/', + label: '手机号码', + }, + { + value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/', + label: '网址带端口号', + }, + ], + }, + }, + { + component: 'Divider', + label: '分割线', + icon: 'radix-icons:divider-horizontal', + colProps: { span: 24 }, + field: '', + componentProps: { + orientation: 'center', + dashed: true, + }, + }, + { + component: 'Checkbox', + label: '复选框', + icon: 'ant-design:check-circle-outlined', + colProps: { span: 24 }, + field: '', + }, + { + component: 'CheckboxGroup', + label: '复选框-组', + icon: 'ant-design:check-circle-filled', + field: '', + colProps: { span: 24 }, + componentProps: { + options: [ + { + label: '选项1', + value: '1', + }, + { + label: '选项2', + value: '2', + }, + ], + }, + }, + { + component: 'Input', + label: '输入框', + icon: 'bi:input-cursor-text', + field: '', + colProps: { span: 24 }, + componentProps: { + type: 'text', + }, + }, + { + component: 'InputNumber', + label: '数字输入框', + icon: 'ant-design:field-number-outlined', + field: '', + colProps: { span: 24 }, + componentProps: { style: 'width:200px' }, + }, + { + component: 'InputTextArea', + label: '文本域', + icon: 'ant-design:file-text-filled', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'Select', + label: '下拉选择', + icon: 'gg:select', + field: '', + colProps: { span: 24 }, + componentProps: { + options: [ + { + label: '选项1', + value: '1', + }, + { + label: '选项2', + value: '2', + }, + ], + }, + }, + + { + component: 'Radio', + label: '单选框', + icon: 'ant-design:check-circle-outlined', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'RadioGroup', + label: '单选框-组', + icon: 'carbon:radio-button-checked', + field: '', + colProps: { span: 24 }, + componentProps: { + options: [ + { + label: '选项1', + value: '1', + }, + { + label: '选项2', + value: '2', + }, + ], + }, + }, + { + component: 'DatePicker', + label: '日期选择', + icon: 'healthicons:i-schedule-school-date-time-outline', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'RangePicker', + label: '日期范围', + icon: 'healthicons:i-schedule-school-date-time-outline', + field: '', + colProps: { span: 24 }, + componentProps: { + placeholder: ['开始日期', '结束日期'], + }, + }, + { + component: 'MonthPicker', + label: '月份选择', + icon: 'healthicons:i-schedule-school-date-time-outline', + field: '', + colProps: { span: 24 }, + componentProps: { + placeholder: '请选择月份', + }, + }, + { + component: 'TimePicker', + label: '时间选择', + icon: 'healthicons:i-schedule-school-date-time', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'Slider', + label: '滑动输入条', + icon: 'vaadin:slider', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'Rate', + label: '评分', + icon: 'ic:outline-star-rate', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'Switch', + label: '开关', + icon: 'entypo:switch', + field: '', + colProps: { span: 24 }, + componentProps: {}, + }, + { + component: 'TreeSelect', + label: '树形选择', + icon: 'clarity:tree-view-line', + field: '', + colProps: { span: 24 }, + componentProps: { + treeData: [ + { + label: '选项1', + value: '1', + children: [ + { + label: '选项三', + value: '1-1', + }, + ], + }, + { + label: '选项2', + value: '2', + }, + ], + }, + }, + { + component: 'Upload', + label: '上传', + icon: 'ant-design:upload-outlined', + field: '', + colProps: { span: 24 }, + componentProps: { + api: () => 1, + }, + }, + { + component: 'Cascader', + label: '级联选择', + icon: 'ant-design:check-outlined', + field: '', + colProps: { span: 24 }, + componentProps: { + options: [ + { + label: '选项1', + value: '1', + children: [ + { + label: '选项三', + value: '1-1', + }, + ], + }, + { + label: '选项2', + value: '2', + }, + ], + }, + }, + // { + // component: 'Button', + // label: '按钮', + // icon: 'dashicons:button', + // field: '', + // colProps: { span: 24 }, + // hiddenLabel: true, + // componentProps: {}, + // }, + { + component: 'ColorPicker', + label: '颜色选择器', + icon: 'carbon:color-palette', + field: '', + colProps: { span: 24 }, + componentProps: { + defaultValue: '', + value: '', + }, + }, + + { + component: 'slot', + label: '插槽', + icon: 'vs:timeslot-question', + field: '', + colProps: { span: 24 }, + componentProps: { + slotName: 'slotName', + }, + }, +]; + +// https://next.antdv.com/components/transfer-cn +const transferControl = { + component: 'Transfer', + label: '穿梭框', + icon: 'bx:bx-transfer-alt', + field: '', + colProps: { span: 24 }, + componentProps: { + render: (item) => item.title, + dataSource: [ + { + key: 'key-1', + title: '标题1', + description: '描述', + disabled: false, + chosen: true, + }, + { + key: 'key-2', + title: 'title2', + description: 'description2', + disabled: true, + }, + { + key: 'key-3', + title: '标题3', + description: '描述3', + disabled: false, + chosen: true, + }, + ], + }, +}; + +baseComponents.push(transferControl); + +export const layoutComponents: IVFormComponent[] = [ + { + field: '', + component: 'Grid', + label: '栅格布局', + icon: 'icon-grid', + componentProps: {}, + columns: [ + { + span: 12, + children: [], + }, + { + span: 12, + children: [], + }, + ], + colProps: { span: 24 }, + options: { + gutter: 0, + }, + }, +]; diff --git a/src/views/form-design/core/iconConfig.ts b/src/views/form-design/core/iconConfig.ts new file mode 100644 index 000000000..2588b4ade --- /dev/null +++ b/src/views/form-design/core/iconConfig.ts @@ -0,0 +1,739 @@ +const iconConfig = { + filled: [ + 'account-book', + 'alert', + 'alipay-circle', + 'alipay-square', + 'aliwangwang', + 'amazon-circle', + 'android', + 'amazon-square', + 'api', + 'appstore', + 'audio', + 'apple', + 'backward', + 'bank', + 'behance-circle', + 'bell', + 'behance-square', + 'book', + 'box-plot', + 'bug', + 'bulb', + 'calculator', + 'build', + 'calendar', + 'camera', + 'car', + 'caret-down', + 'caret-left', + 'caret-right', + 'carry-out', + 'caret-up', + 'check-circle', + 'check-square', + 'chrome', + 'ci-circle', + 'clock-circle', + 'close-circle', + 'cloud', + 'close-square', + 'code-sandbox-square', + 'code-sandbox-circle', + 'code', + 'codepen-circle', + 'compass', + 'codepen-square', + 'contacts', + 'container', + 'control', + 'copy', + 'copyright-circle', + 'credit-card', + 'crown', + 'customer-service', + 'dashboard', + 'delete', + 'diff', + 'dingtalk-circle', + 'database', + 'dingtalk-square', + 'dislike', + 'dollar-circle', + 'down-circle', + 'down-square', + 'dribbble-circle', + 'dribbble-square', + 'dropbox-circle', + 'dropbox-square', + 'environment', + 'edit', + 'exclamation-circle', + 'euro-circle', + 'experiment', + 'eye-invisible', + 'eye', + 'facebook', + 'fast-backward', + 'fast-forward', + 'file-add', + 'file-excel', + 'file-exclamation', + 'file-image', + 'file-markdown', + 'file-pdf', + 'file-ppt', + 'file-text', + 'file-unknown', + 'file-word', + 'file-zip', + 'file', + 'filter', + 'fire', + 'flag', + 'folder-add', + 'folder', + 'folder-open', + 'forward', + 'frown', + 'fund', + 'funnel-plot', + 'gift', + 'github', + 'gitlab', + 'golden', + 'google-circle', + 'google-plus-circle', + 'google-plus-square', + 'google-square', + 'hdd', + 'heart', + 'highlight', + 'home', + 'hourglass', + 'html5', + 'idcard', + 'ie-circle', + 'ie-square', + 'info-circle', + 'instagram', + 'insurance', + 'interaction', + 'interation', + 'layout', + 'left-circle', + 'left-square', + 'like', + 'linkedin', + 'lock', + 'mail', + 'medicine-box', + 'medium-circle', + 'medium-square', + 'meh', + 'message', + 'minus-circle', + 'minus-square', + 'mobile', + 'money-collect', + 'pause-circle', + 'pay-circle', + 'notification', + 'phone', + 'picture', + 'pie-chart', + 'play-circle', + 'play-square', + 'plus-circle', + 'plus-square', + 'pound-circle', + 'printer', + 'profile', + 'project', + 'pushpin', + 'property-safety', + 'qq-circle', + 'qq-square', + 'question-circle', + 'read', + 'reconciliation', + 'red-envelope', + 'reddit-circle', + 'reddit-square', + 'rest', + 'right-circle', + 'rocket', + 'right-square', + 'safety-certificate', + 'save', + 'schedule', + 'security-scan', + 'setting', + 'shop', + 'shopping', + 'sketch-circle', + 'sketch-square', + 'skin', + 'slack-circle', + 'skype', + 'slack-square', + 'sliders', + 'smile', + 'snippets', + 'sound', + 'star', + 'step-backward', + 'step-forward', + 'stop', + 'switcher', + 'tablet', + 'tag', + 'tags', + 'taobao-circle', + 'taobao-square', + 'tool', + 'thunderbolt', + 'trademark-circle', + 'twitter-circle', + 'trophy', + 'twitter-square', + 'unlock', + 'up-circle', + 'up-square', + 'usb', + 'video-camera', + 'wallet', + 'warning', + 'wechat', + 'weibo-circle', + 'windows', + 'yahoo', + 'weibo-square', + 'yuque', + 'youtube', + 'zhihu-circle', + 'zhihu-square', + ], + outlined: [ + 'account-book', + 'alert', + 'alipay-circle', + 'aliwangwang', + 'android', + 'api', + 'appstore', + 'audio', + 'apple', + 'backward', + 'bank', + 'bell', + 'behance-square', + 'book', + 'box-plot', + 'bug', + 'bulb', + 'calculator', + 'build', + 'calendar', + 'camera', + 'car', + 'caret-down', + 'caret-left', + 'caret-right', + 'carry-out', + 'caret-up', + 'check-circle', + 'check-square', + 'chrome', + 'clock-circle', + 'close-circle', + 'cloud', + 'close-square', + 'code', + 'codepen-circle', + 'compass', + 'contacts', + 'container', + 'control', + 'copy', + 'credit-card', + 'crown', + 'customer-service', + 'dashboard', + 'delete', + 'diff', + 'database', + 'dislike', + 'down-circle', + 'down-square', + 'dribbble-square', + 'environment', + 'edit', + 'exclamation-circle', + 'experiment', + 'eye-invisible', + 'eye', + 'facebook', + 'fast-backward', + 'fast-forward', + 'file-add', + 'file-excel', + 'file-exclamation', + 'file-image', + 'file-markdown', + 'file-pdf', + 'file-ppt', + 'file-text', + 'file-unknown', + 'file-word', + 'file-zip', + 'file', + 'filter', + 'fire', + 'flag', + 'folder-add', + 'folder', + 'folder-open', + 'forward', + 'frown', + 'fund', + 'funnel-plot', + 'gift', + 'github', + 'gitlab', + 'hdd', + 'heart', + 'highlight', + 'home', + 'hourglass', + 'html5', + 'idcard', + 'info-circle', + 'instagram', + 'insurance', + 'interaction', + 'interation', + 'layout', + 'left-circle', + 'left-square', + 'like', + 'linkedin', + 'lock', + 'mail', + 'medicine-box', + 'meh', + 'message', + 'minus-circle', + 'minus-square', + 'mobile', + 'money-collect', + 'pause-circle', + 'pay-circle', + 'notification', + 'phone', + 'picture', + 'pie-chart', + 'play-circle', + 'play-square', + 'plus-circle', + 'plus-square', + 'printer', + 'profile', + 'project', + 'pushpin', + 'property-safety', + 'question-circle', + 'read', + 'reconciliation', + 'red-envelope', + 'rest', + 'right-circle', + 'rocket', + 'right-square', + 'safety-certificate', + 'save', + 'schedule', + 'security-scan', + 'setting', + 'shop', + 'shopping', + 'skin', + 'skype', + 'slack-square', + 'sliders', + 'smile', + 'snippets', + 'sound', + 'star', + 'step-backward', + 'step-forward', + 'stop', + 'switcher', + 'tablet', + 'tag', + 'tags', + 'taobao-circle', + 'tool', + 'thunderbolt', + 'trophy', + 'unlock', + 'up-circle', + 'up-square', + 'usb', + 'video-camera', + 'wallet', + 'warning', + 'wechat', + 'weibo-circle', + 'windows', + 'yahoo', + 'weibo-square', + 'yuque', + 'youtube', + 'alibaba', + 'align-center', + 'align-left', + 'align-right', + 'alipay', + 'aliyun', + 'amazon', + 'ant-cloud', + 'apartment', + 'ant-design', + 'area-chart', + 'arrow-left', + 'arrow-down', + 'arrow-up', + 'arrows-alt', + 'arrow-right', + 'audit', + 'bar-chart', + 'barcode', + 'bars', + 'behance', + 'bg-colors', + 'block', + 'bold', + 'border-bottom', + 'border-left', + 'border-outer', + 'border-inner', + 'border-right', + 'border-horizontal', + 'border-top', + 'border-verticle', + 'border', + 'branches', + 'check', + 'ci', + 'close', + 'cloud-download', + 'cloud-server', + 'cloud-sync', + 'cloud-upload', + 'cluster', + 'codepen', + 'code-sandbox', + 'colum-height', + 'column-width', + 'column-height', + 'coffee', + 'copyright', + 'dash', + 'deployment-unit', + 'desktop', + 'dingding', + 'disconnect', + 'dollar', + 'double-left', + 'dot-chart', + 'double-right', + 'down', + 'drag', + 'download', + 'dribbble', + 'dropbox', + 'ellipsis', + 'enter', + 'euro', + 'exception', + 'exclamation', + 'export', + 'fall', + 'file-done', + 'file-jpg', + 'file-protect', + 'file-sync', + 'file-search', + 'font-colors', + 'font-size', + 'fork', + 'form', + 'fullscreen-exit', + 'fullscreen', + 'gateway', + 'global', + 'google-plus', + 'gold', + 'google', + 'heat-map', + 'history', + 'ie', + 'import', + 'inbox', + 'info', + 'italic', + 'key', + 'issues-close', + 'laptop', + 'left', + 'line-chart', + 'link', + 'line-height', + 'line', + 'loading-3-quarters', + 'loading', + 'login', + 'logout', + 'man', + 'medium', + 'medium-workmark', + 'menu-unfold', + 'menu-fold', + 'menu', + 'minus', + 'monitor', + 'more', + 'ordered-list', + 'number', + 'pause', + 'percentage', + 'paper-clip', + 'pic-center', + 'pic-left', + 'pic-right', + 'plus', + 'pound', + 'poweroff', + 'pull-request', + 'qq', + 'question', + 'radar-chart', + 'qrcode', + 'radius-bottomleft', + 'radius-bottomright', + 'radius-upleft', + 'radius-setting', + 'radius-upright', + 'reddit', + 'redo', + 'reload', + 'retweet', + 'right', + 'rise', + 'rollback', + 'safety', + 'robot', + 'scan', + 'search', + 'scissor', + 'select', + 'shake', + 'share-alt', + 'shopping-cart', + 'shrink', + 'sketch', + 'slack', + 'small-dash', + 'solution', + 'sort-descending', + 'sort-ascending', + 'stock', + 'swap-left', + 'swap-right', + 'strikethrough', + 'swap', + 'sync', + 'table', + 'team', + 'taobao', + 'to-top', + 'trademark', + 'transaction', + 'twitter', + 'underline', + 'undo', + 'unordered-list', + 'up', + 'upload', + 'user-add', + 'user-delete', + 'usergroup-add', + 'user', + 'usergroup-delete', + 'vertical-align-bottom', + 'vertical-align-middle', + 'vertical-align-top', + 'vertical-left', + 'vertical-right', + 'weibo', + 'wifi', + 'zhihu', + 'woman', + 'zoom-out', + 'zoom-in', + ], + twoTone: [ + 'account-book', + 'alert', + 'api', + 'appstore', + 'audio', + 'bank', + 'bell', + 'book', + 'box-plot', + 'bug', + 'bulb', + 'calculator', + 'build', + 'calendar', + 'camera', + 'car', + 'carry-out', + 'check-circle', + 'check-square', + 'clock-circle', + 'close-circle', + 'cloud', + 'close-square', + 'code', + 'compass', + 'contacts', + 'container', + 'control', + 'copy', + 'credit-card', + 'crown', + 'customer-service', + 'dashboard', + 'delete', + 'diff', + 'database', + 'dislike', + 'down-circle', + 'down-square', + 'environment', + 'edit', + 'exclamation-circle', + 'experiment', + 'eye-invisible', + 'eye', + 'file-add', + 'file-excel', + 'file-exclamation', + 'file-image', + 'file-markdown', + 'file-pdf', + 'file-ppt', + 'file-text', + 'file-unknown', + 'file-word', + 'file-zip', + 'file', + 'filter', + 'fire', + 'flag', + 'folder-add', + 'folder', + 'folder-open', + 'frown', + 'fund', + 'funnel-plot', + 'gift', + 'hdd', + 'heart', + 'highlight', + 'home', + 'hourglass', + 'html5', + 'idcard', + 'info-circle', + 'insurance', + 'interaction', + 'interation', + 'layout', + 'left-circle', + 'left-square', + 'like', + 'lock', + 'mail', + 'medicine-box', + 'meh', + 'message', + 'minus-circle', + 'minus-square', + 'mobile', + 'money-collect', + 'pause-circle', + 'notification', + 'phone', + 'picture', + 'pie-chart', + 'play-circle', + 'play-square', + 'plus-circle', + 'plus-square', + 'pound-circle', + 'printer', + 'profile', + 'project', + 'pushpin', + 'property-safety', + 'question-circle', + 'reconciliation', + 'red-envelope', + 'rest', + 'right-circle', + 'rocket', + 'right-square', + 'safety-certificate', + 'save', + 'schedule', + 'security-scan', + 'setting', + 'shop', + 'shopping', + 'skin', + 'sliders', + 'smile', + 'snippets', + 'sound', + 'star', + 'stop', + 'switcher', + 'tablet', + 'tag', + 'tags', + 'tool', + 'thunderbolt', + 'trademark-circle', + 'trophy', + 'unlock', + 'up-circle', + 'up-square', + 'usb', + 'video-camera', + 'wallet', + 'warning', + 'ci', + 'copyright', + 'dollar', + 'euro', + 'gold', + 'canlendar', + ], +}; + +export default iconConfig; diff --git a/src/views/form-design/examples/baseForm.vue b/src/views/form-design/examples/baseForm.vue new file mode 100644 index 000000000..de19ec58b --- /dev/null +++ b/src/views/form-design/examples/baseForm.vue @@ -0,0 +1,37 @@ + + diff --git a/src/views/form-design/hooks/useFormDesignState.ts b/src/views/form-design/hooks/useFormDesignState.ts new file mode 100644 index 000000000..865016e64 --- /dev/null +++ b/src/views/form-design/hooks/useFormDesignState.ts @@ -0,0 +1,18 @@ +import { inject, Ref } from 'vue'; +import { IFormDesignMethods } from '../typings/form-type'; +import { IFormConfig } from '../typings/v-form-component'; + +/** + * 获取formDesign状态 + */ +export function useFormDesignState() { + const formConfig = inject('formConfig') as Ref; + const formDesignMethods = inject('formDesignMethods') as IFormDesignMethods; + return { formConfig, formDesignMethods }; +} + +export function useFormModelState() { + const formModel = inject('formModel') as Ref<{}>; + const setFormModel = inject('setFormModelMethod') as (key: String, value: any) => void; + return { formModel, setFormModel }; +} diff --git a/src/views/form-design/hooks/useFormInstanceMethods.ts b/src/views/form-design/hooks/useFormInstanceMethods.ts new file mode 100644 index 000000000..b29eb7909 --- /dev/null +++ b/src/views/form-design/hooks/useFormInstanceMethods.ts @@ -0,0 +1,62 @@ +import { IAnyObject } from '../typings/base-type'; +import { Ref, SetupContext } from 'vue'; +import { cloneDeep, forOwn, isFunction } from 'lodash-es'; +import { AForm, IVFormComponent } from '../typings/v-form-component'; +import { getCurrentInstance } from 'vue'; +import { Form } from 'ant-design-vue'; +import { toRaw } from 'vue'; + +export function useFormInstanceMethods( + props: IAnyObject, + formdata, + context: Partial, + _formInstance: Ref, +) { + /** + * 绑定props和on中的上下文为parent + */ + const bindContext = () => { + const instance = getCurrentInstance(); + const vm = instance?.parent; + if (!vm) return; + + (props.formConfig.schemas as IVFormComponent[]).forEach((item) => { + // 绑定 props 中的上下文 + forOwn(item.componentProps, (value: any, key) => { + if (isFunction(value)) { + item.componentProps![key] = value.bind(vm); + } + }); + // 绑定事件监听(v-on)的上下文 + forOwn(item.on, (value: any, key) => { + if (isFunction(value)) { + item.componentProps![key] = value.bind(vm); + } + }); + }); + }; + bindContext(); + + const { emit } = context; + + const useForm = Form.useForm; + + const { resetFields, validate, clearValidate, validateField } = useForm(formdata, []); + + const submit = async () => { + //const _result = await validate(); + + const data = cloneDeep(toRaw(formdata.value)); + emit?.('submit', data); + props.formConfig.submit?.(data); + return data; + }; + + return { + validate, + validateField, + resetFields, + clearValidate, + submit, + }; +} diff --git a/src/views/form-design/hooks/useVFormMethods.ts b/src/views/form-design/hooks/useVFormMethods.ts new file mode 100644 index 000000000..8325e9063 --- /dev/null +++ b/src/views/form-design/hooks/useVFormMethods.ts @@ -0,0 +1,195 @@ +import { Ref, SetupContext } from 'vue'; +import { IVFormComponent, IFormConfig, AForm } from '../typings/v-form-component'; +import { findFormItem, formItemsForEach } from '../utils'; +import { cloneDeep, isFunction } from 'lodash-es'; +import { IAnyObject } from '../typings/base-type'; + +interface IFormInstanceMethods extends AForm { + submit: () => Promise; +} + +export interface IProps { + formConfig: IFormConfig; + formModel: IAnyObject; +} + +type ISet = ( + field: string, + key: T, + value: IVFormComponent[T], +) => void; +// 获取当前field绑定的表单项 +type IGet = (field: string) => IVFormComponent | undefined; +// 获取field在formData中的值 +type IGetValue = (field: string) => any; +// 设置field在formData中的值并且触发校验 +type ISetValue = (field: string | IAnyObject, value?: any) => void; +// 隐藏field对应的表单项 +type IHidden = (field: string) => void; +// 显示field对应的表单项 +type IShow = (field: string) => void; +// 设置field对应的表单项绑定的props属性 +type ISetProps = (field: string, key: string, value: any) => void; +// 获取formData中的值 +type IGetData = () => Promise; +// 禁用表单,如果field为空,则禁用整个表单 +type IDisable = (field?: string | boolean) => void; +// 设置表单配置方法 +type ISetFormConfig = (key: string, value: any) => void; +interface ILinkOn { + [key: string]: Set; +} + +export interface IVFormMethods extends Partial { + set: ISet; + get: IGet; + getValue: IGetValue; + setValue: ISetValue; + hidden: IHidden; + show: IShow; + setProps: ISetProps; + linkOn: ILinkOn; + getData: IGetData; + disable: IDisable; +} +export function useVFormMethods( + props: IProps, + _context: Partial, + formInstance: Ref, + formInstanceMethods: Partial, +): IVFormMethods { + /** + * 根据field获取表单项 + * @param {string} field + * @return {IVFormComponent | undefined} + */ + const get: IGet = (field) => + findFormItem(props.formConfig.schemas, (item) => item.field === field); + + /** + * 根据表单field设置表单项字段值 + * @param {string} field + * @param {keyof IVFormComponent} key + * @param {never} value + */ + const set: ISet = (field, key, value) => { + const formItem = get(field); + if (formItem) formItem[key] = value; + }; + + /** + * 设置表单项的props + * @param {string} field 需要设置的表单项field + * @param {string} key 需要设置的key + * @param value 需要设置的值 + */ + const setProps: ISetProps = (field, key, value) => { + const formItem = get(field); + if (formItem?.componentProps) { + ['options', 'treeData'].includes(key) && setValue(field, undefined); + + formItem.componentProps[key] = value; + } + }; + /** + * 设置字段的值,设置后触发校验 + * @param {string} field 需要设置的字段 + * @param value 需要设置的值 + */ + const setValue: ISetValue = (field, value) => { + if (typeof field === 'string') { + // props.formData[field] = value + props.formModel[field] = value; + formInstance.value?.validateField(field, value, []); + } else { + const keys = Object.keys(field); + keys.forEach((key) => { + props.formModel[key] = field[key]; + formInstance.value?.validateField(key, field[key], []); + }); + } + }; + /** + * 设置表单配置方法 + * @param {string} key + * @param value + */ + const setFormConfig: ISetFormConfig = (key, value) => { + props.formConfig[key] = value; + }; + /** + * 根据表单项field获取字段值,如果field为空,则 + * @param {string} field 需要设置的字段 + */ + const getValue: IGetValue = (field) => { + const formData = cloneDeep(props.formModel); + return formData[field]; + }; + + /** + * 获取formData中的值 + * @return {Promise>} + */ + const getData: IGetData = async () => { + return cloneDeep(props.formModel); + }; + /** + * 隐藏指定表单项 + * @param {string} field 需要隐藏的表单项的field + */ + const hidden: IHidden = (field) => { + set(field, 'hidden', true); + }; + + /** + * 禁用表单 + * @param {string | undefined} field + */ + const disable: IDisable = (field) => { + typeof field === 'string' + ? setProps(field, 'disabled', true) + : setFormConfig('disabled', field !== false); + }; + + /** + * 显示表单项 + * @param {string} field 需要显示的表单项的field + */ + const show: IShow = (field) => { + set(field, 'hidden', false); + }; + + /** + * 监听表单字段联动时触发 + * @type {ILinkOn} + */ + const linkOn: ILinkOn = {}; + const initLink = (schemas: IVFormComponent[]) => { + // 首次遍历,查找需要关联字段的表单 + formItemsForEach(schemas, (formItem) => { + // 如果需要关联,则进行第二层遍历,查找表单中关联的字段,存到Set中 + formItemsForEach(schemas, (item) => { + if (!linkOn[item.field!]) linkOn[item.field!] = new Set(); + if (formItem.link?.includes(item.field!) && isFunction(formItem.update)) { + linkOn[item.field!].add(formItem); + } + }); + linkOn[formItem.field!].add(formItem); + }); + }; + initLink(props.formConfig.schemas); + + return { + linkOn, + setValue, + getValue, + hidden, + show, + set, + get, + setProps, + getData, + disable, + ...formInstanceMethods, + }; +} diff --git a/src/views/form-design/index.vue b/src/views/form-design/index.vue new file mode 100644 index 000000000..f953bc178 --- /dev/null +++ b/src/views/form-design/index.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/src/views/form-design/tests/import1.json b/src/views/form-design/tests/import1.json new file mode 100644 index 000000000..77d1a7eec --- /dev/null +++ b/src/views/form-design/tests/import1.json @@ -0,0 +1,48 @@ +{ + "schemas": [{ + "field": "filename", + "component": "Input", + "label": "component.excel.fileName", + "rules": [{ + "required": true + }] + }, + { + "field": "bookType", + "component": "Select", + "label": "component.excel.fileType", + "defaultValue": "xlsx", + "rules": [{ + "required": true + }], + "componentProps": { + "options": [{ + "label": "xlsx", + "value": "xlsx", + "key": "xlsx" + }, + { + "label": "html", + "value": "html", + "key": "html" + }, + { + "label": "csv", + "value": "csv", + "key": "csv" + }, + { + "label": "txt", + "value": "txt", + "key": "txt" + } + ] + } + } + ], + "layout": "horizontal", + "labelLayout": "flex", + "labelWidth": 100, + "labelCol": {}, + "wrapperCol": {} +} \ No newline at end of file diff --git a/src/views/form-design/typings/base-type.ts b/src/views/form-design/typings/base-type.ts new file mode 100644 index 000000000..94f5d8c88 --- /dev/null +++ b/src/views/form-design/typings/base-type.ts @@ -0,0 +1,10 @@ +export interface IAnyObject { + [key: string]: T; +} + +export interface IInputEvent { + target: { + value: any; + checked: boolean; + }; +} diff --git a/src/views/form-design/typings/form-type.ts b/src/views/form-design/typings/form-type.ts new file mode 100644 index 000000000..5f4539467 --- /dev/null +++ b/src/views/form-design/typings/form-type.ts @@ -0,0 +1,52 @@ +import { Ref } from 'vue'; +import { IAnyObject } from './base-type'; +import { IFormConfig, IVFormComponent } from './v-form-component'; + +export interface IToolbarMethods { + showModal: (jsonData: IAnyObject) => void; +} + +type ChangeTabKey = 1 | 2; +export interface IPropsPanel { + changeTab: (key: ChangeTabKey) => void; +} +export interface IState { + // 语言 + locale: any; + // 公用组件 + baseComponents: IVFormComponent[]; + // 自定义组件 + customComponents: IVFormComponent[]; + // 布局组件 + layoutComponents: IVFormComponent[]; + // 属性面板实例 + propsPanel: Ref; + // json模态框实例 + jsonModal: Ref; + // 导入json数据模态框 + importJsonModal: Ref; + // 代码预览模态框 + codeModal: Ref; + // 预览模态框 + eFormPreview: Ref; + + eFormPreview2: Ref; +} + +export interface IFormDesignMethods { + // 设置当前选中的控件 + handleSetSelectItem(item: IVFormComponent): void; + // 添加控件到formConfig.formItems中 + handleListPush(item: IVFormComponent): void; + // 复制控件 + handleCopy(item?: IVFormComponent, isCopy?: boolean): void; + // 添加控件属性 + handleAddAttrs(schemas: IVFormComponent[], index: number): void; + setFormConfig(config: IFormConfig): void; + // 添加到表单中之前触发 + handleBeforeColAdd( + event: { newIndex: string }, + schemas: IVFormComponent[], + isCopy?: boolean, + ): void; +} diff --git a/src/views/form-design/typings/v-form-component.ts b/src/views/form-design/typings/v-form-component.ts new file mode 100644 index 000000000..af632aa04 --- /dev/null +++ b/src/views/form-design/typings/v-form-component.ts @@ -0,0 +1,349 @@ +import { IAnyObject } from './base-type'; +// import { ComponentOptions } from 'vue/types/options'; +import { ComponentOptions } from 'vue'; +import { IVFormMethods } from '../hooks/useVFormMethods'; +import { ColEx } from '/@/components/Form/src/types'; + +import { SelectValue } from 'ant-design-vue/lib/select'; +import { validateOptions } from 'ant-design-vue/lib/form/useForm'; +import { RuleError } from 'ant-design-vue/lib/form/interface'; +import { FormItem } from '/@/components/Form'; +type LayoutType = 'horizontal' | 'vertical' | 'inline'; +type labelLayout = 'flex' | 'Grid'; +export type PropsTabKey = 1 | 2 | 3; +type ColSpanType = number | string; + +declare type Value = [number, number] | number; +/** + * 组件属性 + */ +export interface IVFormComponent { + // extends Omit { + // 对应的字段 + field?: string; + // 组件类型 + component: string; + // 组件label + label?: string; + // 自定义组件控件实例 + componentInstance?: ComponentOptions; + // 组件icon + icon?: string; + // 组件校验规则 + rules?: Partial[]; + // 是否隐藏 + hidden?: boolean; + // 隐藏label + hiddenLabel?: boolean; + // 组件宽度 + width?: string; + // 是否必选 + required?: boolean; + // 必选提示 + message?: string; + // 提示信息 + helpMessage?: string; + // 传给给组件的属性,默认会吧所有的props都传递给控件 + componentProps?: IAnyObject; + // 监听组件事件对象,以v-on方式传递给控件 + on?: IAnyObject<(...any: []) => void>; + // 组件选项 + options?: IAnyObject; + // 唯一标识 + key?: string; + // Reference formModelItem + itemProps?: Partial; + + colProps?: Partial; + // 联动字段 + link?: string[]; + // 联动属性变化的回调 + update?: (value: any, formItem: IVFormComponent, fApi: IVFormMethods) => void; + // 控件栅格数 + // span?: number; + // 标签布局 + labelCol?: IAnyObject; + // 组件布局 + wrapperCol?: IAnyObject; + // 子控件 + columns?: Array<{ span: number; children: any[] }>; +} + +declare type namesType = string | string[]; + +/** + * 表单配置 + */ +export interface IFormConfig { + // 表单项配置列表 + // schemas: IVFormComponent[]; + // 表单配置 + // config: { + layout?: LayoutType; + labelLayout?: labelLayout; + labelWidth?: number; + labelCol?: Partial; + wrapperCol?: Partial; + hideRequiredMark?: boolean; + // Whether to disable + schemas: IVFormComponent[]; + disabled?: boolean; + labelAlign?: 'left' | 'right'; + // Internal component size of the form + size?: 'default' | 'small' | 'large'; + // }; + // 当前选中项 + currentItem?: IVFormComponent; + activeKey?: PropsTabKey; + colon?: boolean; +} + +export interface AForm { + /** + * Hide required mark of all form items + * @default false + * @type boolean + */ + hideRequiredMark: boolean; + + /** + * The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with + * @type IACol + */ + labelCol: IACol; + + /** + * Define form layout + * @default 'horizontal' + * @type string + */ + layout: 'horizontal' | 'inline' | 'vertical'; + + /** + * The layout for input controls, same as labelCol + * @type IACol + */ + wrapperCol: IACol; + + /** + * change default props colon value of Form.Item (only effective when prop layout is horizontal) + * @type boolean + * @default true + */ + colon: boolean; + + /** + * text align of label of all items + * @type 'left' | 'right' + * @default 'left' + */ + labelAlign: 'left' | 'right'; + + /** + * data of form component + * @type object + */ + model: IAnyObject; + + /** + * validation rules of form + * @type object + */ + rules: IAnyObject; + + /** + * Default validate message. And its format is similar with newMessages's returned value + * @type any + */ + validateMessages?: any; + + /** + * whether to trigger validation when the rules prop is changed + * @type Boolean + * @default true + */ + validateOnRuleChange: boolean; + + /** + * validate the whole form. Takes a callback as a param. After validation, + * the callback will be executed with two params: a boolean indicating if the validation has passed, + * and an object containing all fields that fail the validation. Returns a promise if callback is omitted + * @type Function + */ + validate: (names?: namesType, option?: validateOptions) => Promise; + + /** + * validate one or several form items + * @type Function + */ + validateField: ( + name: string, + value: any, + rules: Record[], + option?: validateOptions, + ) => Promise; + /** + * reset all the fields and remove validation result + */ + resetFields: () => void; + + /** + * clear validation message for certain fields. + * The parameter is prop name or an array of prop names of the form items whose validation messages will be removed. + * When omitted, all fields' validation messages will be cleared + * @type string[] | string + */ + clearValidate: (props: string[] | string) => void; +} + +interface IACol { + /** + * raster number of cells to occupy, 0 corresponds to display: none + * @default none (0) + * @type ColSpanType + */ + span: Value; + + /** + * raster order, used in flex layout mode + * @default 0 + * @type ColSpanType + */ + order: ColSpanType; + + /** + * the layout fill of flex + * @default none + * @type ColSpanType + */ + flex: ColSpanType; + + /** + * the number of cells to offset Col from the left + * @default 0 + * @type ColSpanType + */ + offset: ColSpanType; + + /** + * the number of cells that raster is moved to the right + * @default 0 + * @type ColSpanType + */ + push: ColSpanType; + + /** + * the number of cells that raster is moved to the left + * @default 0 + * @type ColSpanType + */ + pull: ColSpanType; + + /** + * <576px and also default setting, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + xs: { span: ColSpanType; offset: ColSpanType } | ColSpanType; + + /** + * ≥576px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + sm: { span: ColSpanType; offset: ColSpanType } | ColSpanType; + + /** + * ≥768px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + md: { span: ColSpanType; offset: ColSpanType } | ColSpanType; + + /** + * ≥992px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + lg: { span: ColSpanType; offset: ColSpanType } | ColSpanType; + + /** + * ≥1200px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + xl: { span: ColSpanType; offset: ColSpanType } | ColSpanType; + + /** + * ≥1600px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + xxl: { span: ColSpanType; offset: ColSpanType } | ColSpanType; +} + +export interface IValidationRule { + trigger?: 'change' | 'blur' | ['change', 'blur']; + /** + * validation error message + * @type string | Function + */ + message?: string | number; + + /** + * built-in validation type, available options: https://github.com/yiminghe/async-validator#type + * @default 'string' + * @type string + */ + type?: string; + + /** + * indicates whether field is required + * @default false + * @type boolean + */ + required?: boolean; + + /** + * treat required fields that only contain whitespace as errors + * @default false + * @type boolean + */ + whitespace?: boolean; + + /** + * validate the exact length of a field + * @type number + */ + len?: number; + + /** + * validate the min length of a field + * @type number + */ + min?: number; + + /** + * validate the max length of a field + * @type number + */ + max?: number; + + /** + * validate the value from a list of possible values + * @type string | string[] + */ + enum?: string | string[]; + + /** + * validate from a regular expression + * @type boolean + */ + pattern?: SelectValue; + + /** + * transform a value before validation + * @type Function + */ + transform?: (value: any) => any; + + /** + * custom validate function (Note: callback must be called) + * @type Function + */ + validator?: (rule: any, value: any, callback: () => void) => any; +} diff --git a/src/views/form-design/utils/index.ts b/src/views/form-design/utils/index.ts new file mode 100644 index 000000000..e26028fba --- /dev/null +++ b/src/views/form-design/utils/index.ts @@ -0,0 +1,200 @@ +// import { VueConstructor } from 'vue'; +import { IVFormComponent, IFormConfig, IValidationRule } from '../typings/v-form-component'; +import { cloneDeep, isArray, isFunction, isNumber, uniqueId } from 'lodash-es'; +// import { del } from '@vue/composition-api'; +// import { withInstall } from '/@/utils'; + +/** + * 组件install方法 + * @param comp 需要挂载install方法的组件 + */ +// export function withInstall(comp: T) { +// return Object.assign(comp, { +// install(Vue: VueConstructor) { +// Vue.component(comp.name, comp); +// }, +// }); +// } + +/** + * 生成key + * @param [formItem] 需要生成 key 的控件,可选,如果不传,默认返回一个唯一 key + * @returns {string|boolean} 返回一个唯一 id 或者 false + */ +export function generateKey(formItem?: IVFormComponent): string | boolean { + if (formItem && formItem.component) { + const key = uniqueId(`${toLine(formItem.component)}_`); + formItem.key = key; + formItem.field = key; + + return true; + } + return uniqueId('key_'); +} + +/** + * 移除数组中指定元素,value可以是一个数字下标,也可以是一个函数,删除函数第一个返回true的元素 + * @param array {Array} 需要移除元素的数组 + * @param value {number | ((item: T, index: number, array: Array) => boolean} + * @returns {T} 返回删除的数组项 + */ +export function remove( + array: Array, + value: number | ((item: T, index: number, array: Array) => boolean), +): T | undefined { + let removeVal: Array = []; + if (!isArray(array)) return undefined; + if (isNumber(value)) { + removeVal = array.splice(value, 1); + } else { + const index = array.findIndex(value); + if (index !== -1) { + removeVal = array.splice(index, 1); + } + } + return removeVal.shift(); +} + +/** + * 判断数据类型 + * @param value + */ +export function getType(value: any): string { + return Object.prototype.toString.call(value).slice(8, -1); +} + +/** + * 生成唯一guid + * @returns {String} 唯一id标识符 + */ +export function randomUUID(): string { + function S4() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + } + return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4() + S4() + S4()}`; +} + +/** + * 驼峰转下划线 + * @param str + */ +export function toLine(str: string) { + return str.replace(/([A-Z])/g, '_$1').toLowerCase(); +} + +/** + * 遍历表单项 + * @param array + * @param cb + */ +export function formItemsForEach(array: IVFormComponent[], cb: (item: IVFormComponent) => void) { + if (!isArray(array)) return; + const traverse = (schemas: IVFormComponent[]) => { + schemas.forEach((formItem: IVFormComponent) => { + if (['Grid'].includes(formItem.component)) { + // 栅格布局 + formItem.columns?.forEach((item) => traverse(item.children)); + } else { + cb(formItem); + } + }); + }; + traverse(array); +} + +/** + * 查找表单项 + */ +export const findFormItem: ( + schemas: IVFormComponent[], + cb: (formItem: IVFormComponent) => boolean, +) => IVFormComponent | undefined = (schemas, cb) => { + let res; + const traverse = (schemas: IVFormComponent[]): boolean => { + return schemas.some((formItem: IVFormComponent) => { + const { component: type } = formItem; + // 处理栅格 + if (['Grid'].includes(type)) { + return formItem.columns?.some((item) => traverse(item.children)); + } + if (cb(formItem)) res = formItem; + return cb(formItem); + }); + }; + traverse(schemas); + return res; +}; + +/** + * 打开json模态框时删除当前项属性 + * @param formConfig {IFormConfig} + * @returns {IFormConfig} + */ +export const removeAttrs = (formConfig: IFormConfig): IFormConfig => { + const copyFormConfig = cloneDeep(formConfig); + delete copyFormConfig.currentItem; + delete copyFormConfig.activeKey; + copyFormConfig.schemas && + formItemsForEach(copyFormConfig.schemas, (item) => { + delete item.icon; + delete item.key; + }); + return copyFormConfig; +}; + +/** + * 处理异步选项属性,如 select treeSelect 等选项属性如果传递为函数并且返回Promise对象,获取异步返回的选项属性 + * @param {(() => Promise) | any[]} options + * @return {Promise} + */ +export const handleAsyncOptions = async ( + options: (() => Promise) | any[], +): Promise => { + try { + if (isFunction(options)) return await options(); + return options; + } catch { + return []; + } +}; + +/** + * 格式化表单项校验规则配置 + * @param {IVFormComponent[]} schemas + */ +export const formatRules = (schemas: IVFormComponent[]) => { + formItemsForEach(schemas, (item) => { + if ('required' in item) { + !isArray(item.rules) && (item.rules = []); + item.rules.push({ required: true, message: item.message }); + delete item['required']; + delete item['message']; + } + }); +}; + +/** + * 将校验规则中的正则字符串转换为正则对象 + * @param {IValidationRule[]} rules + * @return {IValidationRule[]} + */ +export const strToReg = (rules: IValidationRule[]) => { + const newRules = cloneDeep(rules); + return newRules.map((item) => { + if (item.pattern) item.pattern = runCode(item.pattern); + return item; + }); +}; + +/** + * 执行一段字符串代码,并返回执行结果,如果执行出错,则返回该参数 + * @param code + * @return {any} + */ +export const runCode = (code: any): T => { + try { + return new Function(`return ${code}`)(); + } catch { + return code; + } +}; diff --git a/src/views/form-design/utils/message.ts b/src/views/form-design/utils/message.ts new file mode 100644 index 000000000..35321ca0e --- /dev/null +++ b/src/views/form-design/utils/message.ts @@ -0,0 +1,21 @@ +// import Vue from 'vue'; + +const message = Object.assign( + { + success: (msg: string) => { + console.log(msg); + }, + error: (msg: string) => { + console.error(msg); + }, + warning: (msg: string) => { + console.warn(msg); + }, + info: (msg: string) => { + console.info(msg); + }, + }, + // Vue.prototype.$message, +); + +export default message; diff --git a/stylelint.config.js b/stylelint.config.js index 4b3501d84..3f10b86a9 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -9,7 +9,7 @@ module.exports = { 'selector-pseudo-class-no-unknown': [ true, { - ignorePseudoClasses: ['global'], + ignorePseudoClasses: ['global', 'deep'], }, ], 'selector-pseudo-element-no-unknown': [