diff --git a/package.json b/package.json index cdd0d13..46aeb1d 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "vue-json-pretty": "^2.2.4", "vue-router": "^4.1.6", "vue-types": "^5.0.2", + "vuedraggable": "^4.1.0", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0d6cc1..06b415f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ dependencies: vue-types: specifier: ^5.0.2 version: 5.0.2(vue@3.2.47) + vuedraggable: + specifier: ^4.1.0 + version: 4.1.0(vue@3.2.47) xlsx: specifier: ^0.18.5 version: 0.18.5 @@ -7582,6 +7585,10 @@ packages: - supports-color dev: true + /sortablejs@1.14.0: + resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} + dev: false + /sortablejs@1.15.0: resolution: {integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==} dev: false @@ -8771,6 +8778,15 @@ packages: '@vue/server-renderer': 3.2.47(vue@3.2.47) '@vue/shared': 3.2.47 + /vuedraggable@4.1.0(vue@3.2.47): + resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} + peerDependencies: + vue: ^3.0.1 + dependencies: + sortablejs: 1.14.0 + vue: 3.2.47 + dev: false + /warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} dependencies: diff --git a/src/components/FormDesign/index.ts b/src/components/FormDesign/index.ts new file mode 100644 index 0000000..50925e3 --- /dev/null +++ b/src/components/FormDesign/index.ts @@ -0,0 +1,4 @@ +import VFormDesign from './src/components/VFormDesign/index.vue' +import VFormCreate from './src/components/VFormCreate/index.vue' + +export { VFormDesign, VFormCreate } diff --git a/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue b/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue new file mode 100644 index 0000000..b7f2536 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue @@ -0,0 +1,71 @@ + + + + diff --git a/src/components/FormDesign/src/components/VFormCreate/index.vue b/src/components/FormDesign/src/components/VFormCreate/index.vue new file mode 100644 index 0000000..170fe59 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormCreate/index.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue b/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue new file mode 100644 index 0000000..35c1c29 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue new file mode 100644 index 0000000..e50a36c --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue @@ -0,0 +1,219 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue new file mode 100644 index 0000000..9361028 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue new file mode 100644 index 0000000..2d51c9a --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue new file mode 100644 index 0000000..8d52ec9 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue new file mode 100644 index 0000000..e9bf0e5 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue @@ -0,0 +1,69 @@ + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue new file mode 100644 index 0000000..d514fdf --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue new file mode 100644 index 0000000..3d1620e --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue b/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue new file mode 100644 index 0000000..5ada14d --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue b/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue new file mode 100644 index 0000000..6d7a230 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue b/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue new file mode 100644 index 0000000..f9b6850 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue b/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue new file mode 100644 index 0000000..a78bac8 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue new file mode 100644 index 0000000..64dccc0 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts b/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts new file mode 100644 index 0000000..09037a5 --- /dev/null +++ b/src/components/FormDesign/src/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/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts b/src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts new file mode 100644 index 0000000..bee2a9c --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts @@ -0,0 +1,349 @@ +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/components/FormDesign/src/components/VFormDesign/index.vue b/src/components/FormDesign/src/components/VFormDesign/index.vue new file mode 100644 index 0000000..61cdf78 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/index.vue @@ -0,0 +1,335 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue b/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue new file mode 100644 index 0000000..a785ff2 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue @@ -0,0 +1,102 @@ + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue b/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue new file mode 100644 index 0000000..0ab94b3 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue b/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue new file mode 100644 index 0000000..7e2b8fb --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue b/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue new file mode 100644 index 0000000..b48704b --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormDesign/styles/drag.less b/src/components/FormDesign/src/components/VFormDesign/styles/drag.less new file mode 100644 index 0000000..e8fb00a --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/styles/drag.less @@ -0,0 +1,226 @@ +.draggable-box { + height: 100%; + overflow: auto; + + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ + :deep(.list-main) { + overflow: hidden; + padding: 5px; + position: relative; + + .moving { + // 拖放移动中; + min-height: 35px; + box-sizing: border-box; + overflow: hidden; + padding: 0 !important; + position: relative; + + &::before { + content: ''; + height: 5px; + width: 100%; + background-color: @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-color: @primary-hover-bg-color; + } + + // 选择时 start + &::before { + content: ''; + height: 5px; + width: 100%; + background-color: @primary-color; + position: absolute; + top: 0; + right: -100%; + transition: all 0.3s; + } + + &.active { + background-color: @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; + } + + .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-color: @primary-color; + } + + .delete { + right: 0; + background-color: @primary-color; + } + } + + .grid-box { + position: relative; + box-sizing: border-box; + padding: 5px; + background-color: @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-color: @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-color: @layout-hover-bg-color; + outline-offset: 0; + + &::before { + background-color: @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-color: @layout-color; + } + + > .delete { + right: 0; + background-color: @layout-color; + } + } + } + } +} diff --git a/src/components/FormDesign/src/components/VFormDesign/styles/variable.less b/src/components/FormDesign/src/components/VFormDesign/styles/variable.less new file mode 100644 index 0000000..8749dce --- /dev/null +++ b/src/components/FormDesign/src/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/components/FormDesign/src/components/VFormItem/index.vue b/src/components/FormDesign/src/components/VFormItem/index.vue new file mode 100644 index 0000000..d6ea536 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormItem/index.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/components/FormDesign/src/components/VFormItem/vFormItem.vue b/src/components/FormDesign/src/components/VFormItem/vFormItem.vue new file mode 100644 index 0000000..24a1760 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormItem/vFormItem.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/src/components/FormDesign/src/components/VFormPreview/index.vue b/src/components/FormDesign/src/components/VFormPreview/index.vue new file mode 100644 index 0000000..5cfee30 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormPreview/index.vue @@ -0,0 +1,96 @@ + + + diff --git a/src/components/FormDesign/src/components/VFormPreview/useForm.vue b/src/components/FormDesign/src/components/VFormPreview/useForm.vue new file mode 100644 index 0000000..d50e810 --- /dev/null +++ b/src/components/FormDesign/src/components/VFormPreview/useForm.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/components/FormDesign/src/components/index.ts b/src/components/FormDesign/src/components/index.ts new file mode 100644 index 0000000..e8f98aa --- /dev/null +++ b/src/components/FormDesign/src/components/index.ts @@ -0,0 +1,69 @@ +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('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/components/FormDesign/src/core/formItemConfig.ts b/src/components/FormDesign/src/core/formItemConfig.ts new file mode 100644 index 0000000..8871e11 --- /dev/null +++ b/src/components/FormDesign/src/core/formItemConfig.ts @@ -0,0 +1,420 @@ +/** + * @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/components/FormDesign/src/core/iconConfig.ts b/src/components/FormDesign/src/core/iconConfig.ts new file mode 100644 index 0000000..19a84e7 --- /dev/null +++ b/src/components/FormDesign/src/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/components/FormDesign/src/hooks/useFormDesignState.ts b/src/components/FormDesign/src/hooks/useFormDesignState.ts new file mode 100644 index 0000000..8e0cdc6 --- /dev/null +++ b/src/components/FormDesign/src/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/components/FormDesign/src/hooks/useFormInstanceMethods.ts b/src/components/FormDesign/src/hooks/useFormInstanceMethods.ts new file mode 100644 index 0000000..fa2d638 --- /dev/null +++ b/src/components/FormDesign/src/hooks/useFormInstanceMethods.ts @@ -0,0 +1,56 @@ +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/components/FormDesign/src/hooks/useVFormMethods.ts b/src/components/FormDesign/src/hooks/useVFormMethods.ts new file mode 100644 index 0000000..62ef621 --- /dev/null +++ b/src/components/FormDesign/src/hooks/useVFormMethods.ts @@ -0,0 +1,188 @@ +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/components/FormDesign/src/typings/base-type.ts b/src/components/FormDesign/src/typings/base-type.ts new file mode 100644 index 0000000..a8a41aa --- /dev/null +++ b/src/components/FormDesign/src/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/components/FormDesign/src/typings/form-type.ts b/src/components/FormDesign/src/typings/form-type.ts new file mode 100644 index 0000000..6e9bf50 --- /dev/null +++ b/src/components/FormDesign/src/typings/form-type.ts @@ -0,0 +1,48 @@ +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/components/FormDesign/src/typings/v-form-component.ts b/src/components/FormDesign/src/typings/v-form-component.ts new file mode 100644 index 0000000..b67712a --- /dev/null +++ b/src/components/FormDesign/src/typings/v-form-component.ts @@ -0,0 +1,344 @@ +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/components/FormDesign/src/utils/index.ts b/src/components/FormDesign/src/utils/index.ts new file mode 100644 index 0000000..859a4f8 --- /dev/null +++ b/src/components/FormDesign/src/utils/index.ts @@ -0,0 +1,195 @@ +// 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/components/FormDesign/src/utils/message.ts b/src/components/FormDesign/src/utils/message.ts new file mode 100644 index 0000000..b32b953 --- /dev/null +++ b/src/components/FormDesign/src/utils/message.ts @@ -0,0 +1,18 @@ +import { useMessage } from '@/hooks/web/useMessage' +const { createMessage } = useMessage() +const message = Object.assign({ + success: (msg: string) => { + createMessage.success(msg) + }, + error: (msg: string) => { + createMessage.error(msg) + }, + warning: (msg: string) => { + createMessage.warning(msg) + }, + info: (msg: string) => { + createMessage.info(msg) + } +}) + +export default message diff --git a/src/views/infra/build/index.vue b/src/views/infra/build/index.vue index 3b64cfc..6046e0c 100644 --- a/src/views/infra/build/index.vue +++ b/src/views/infra/build/index.vue @@ -1,3 +1,10 @@ + +