Browse Source

refactor(form): enhanced form customization and dynamic capabilities

vben 4 years ago
parent
commit
1d45617e4a

+ 7 - 0
CHANGELOG.zh_CN.md

@@ -4,6 +4,7 @@
 
 - 重构 hook,引入 `@vueuse`,删除其中已有的`hook`,优化现有的 hook
 - `useEvent` 更名->`useEventListener`
+- 表单`ComponentType`删除 `SelectOptGroup`,`SelectOption`,`Transfer`,`Radio`,四个类型。修改`RadioButtonGroup`组件
 
 ### ✨ Features
 
@@ -12,10 +13,15 @@
 - 新增菜单及顶栏颜色选择配色
 - 增加示例结果页
 
+### ⚡ Wip
+
+- 上传组件(未完成,测试中...)
+
 ### ⚡ Performance Improvements
 
 - 优化 settingDrawer 代码
 - 优化多标签页切换速度
+- 增加表单自定义及动态能力
 
 ### 🐛 Bug Fixes
 
@@ -23,6 +29,7 @@
 - 修复登录过期后重新登录未跳转原来页面的
 - 修复 window 系统动态引入错误
 - 修复页面类型错误
+- 修复表单 switch 和 checkBox 单独使用报错
 
 ## 2.0.0-rc.9 (2020-11-9)
 

+ 5 - 3
src/components/Form/src/BasicForm.vue

@@ -5,6 +5,7 @@
       <template v-for="schema in getSchema" :key="schema.field">
         <FormItem
           :tableAction="tableAction"
+          :formActionType="formActionType"
           :schema="schema"
           :formProps="getProps"
           :allDefaultValues="defaultValueRef"
@@ -164,7 +165,7 @@
         propsRef.value = mergeProps;
       }
 
-      const methods: Partial<FormActionType> = {
+      const formActionType: Partial<FormActionType> = {
         getFieldsValue,
         setFieldsValue,
         resetFields,
@@ -179,7 +180,7 @@
 
       onMounted(() => {
         initDefault();
-        emit('register', methods);
+        emit('register', formActionType);
       });
 
       return {
@@ -191,7 +192,8 @@
         getProps,
         formElRef,
         getSchema,
-        ...methods,
+        formActionType,
+        ...formActionType,
       };
     },
   });

+ 28 - 12
src/components/Form/src/FormItem.tsx

@@ -1,5 +1,5 @@
 import type { PropType } from 'vue';
-import type { FormProps } from './types/form';
+import type { FormActionType, FormProps } from './types/form';
 import type { FormSchema } from './types/form';
 import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
 import type { TableActionType } from '/@/components/Table';
@@ -41,6 +41,9 @@ export default defineComponent({
     tableAction: {
       type: Object as PropType<TableActionType>,
     },
+    formActionType: {
+      type: Object as PropType<FormActionType>,
+    },
   },
   setup(props, { slots }) {
     const itemLabelWidthRef = useItemLabelWidth(toRef(props, 'schema'), toRef(props, 'formProps'));
@@ -61,12 +64,12 @@ export default defineComponent({
     });
 
     const getComponentsPropsRef = computed(() => {
-      const { schema, tableAction, formModel } = props;
+      const { schema, tableAction, formModel, formActionType } = props;
       const { componentProps = {} } = schema;
       if (!isFunction(componentProps)) {
         return componentProps;
       }
-      return componentProps({ schema, tableAction, formModel }) || {};
+      return componentProps({ schema, tableAction, formModel, formActionType }) || {};
     });
 
     const getDisableRef = computed(() => {
@@ -179,17 +182,27 @@ export default defineComponent({
     }
 
     function renderComponent() {
-      const { renderComponentContent, component, field, changeEvent = 'change' } = props.schema;
+      const {
+        renderComponentContent,
+        component,
+        field,
+        changeEvent = 'change',
+        valueField,
+      } = props.schema;
 
-      const isCheck = component && ['Switch'].includes(component);
+      const isCheck = component && ['Switch', 'Checkbox'].includes(component);
 
       const eventKey = `on${upperFirst(changeEvent)}`;
+
       const on = {
         [eventKey]: (e: any) => {
           if (propsData[eventKey]) {
             propsData[eventKey](e);
           }
-          (props.formModel as any)[field] = e && e.target ? e.target.value : e;
+
+          const target = e ? e.target : null;
+          const value = target ? (isCheck ? target.checked : target.value) : e;
+          (props.formModel as any)[field] = value;
         },
       };
       const Comp = componentMap.get(component);
@@ -215,17 +228,20 @@ export default defineComponent({
       propsData.codeField = field;
       propsData.formValues = unref(getValuesRef);
       const bindValue = {
-        [isCheck ? 'checked' : 'value']: handleValue(component, field),
+        [valueField || (isCheck ? 'checked' : 'value')]: handleValue(component, field),
       };
       if (!renderComponentContent) {
         return <Comp {...propsData} {...on} {...bindValue} />;
       }
+      const compSlot = isFunction(renderComponentContent)
+        ? { ...renderComponentContent(unref(getValuesRef)) }
+        : {
+            default: () => renderComponentContent,
+          };
 
       return (
         <Comp {...propsData} {...on} {...bindValue}>
-          {{
-            ...renderComponentContent(unref(getValuesRef)),
-          }}
+          {compSlot}
         </Comp>
       );
     }
@@ -249,7 +265,7 @@ export default defineComponent({
       const { colon } = props.formProps;
       const getContent = () => {
         return slot
-          ? getSlot(slots, slot)
+          ? getSlot(slots, slot, unref(getValuesRef))
           : render
           ? render(unref(getValuesRef))
           : renderComponent();
@@ -276,7 +292,7 @@ export default defineComponent({
       const { isIfShow, isShow } = getShow();
       const getContent = () => {
         return colSlot
-          ? getSlot(slots, colSlot)
+          ? getSlot(slots, colSlot, unref(getValuesRef))
           : renderColContent
           ? renderColContent(unref(getValuesRef))
           : renderItem();

+ 6 - 6
src/components/Form/src/componentMap.ts

@@ -14,8 +14,8 @@ import {
   Switch,
   TimePicker,
   TreeSelect,
-  Transfer,
 } from 'ant-design-vue';
+import RadioButtonGroup from './components/RadioButtonGroup.vue';
 
 import { ComponentType } from './types/index';
 
@@ -30,13 +30,13 @@ componentMap.set('InputNumber', InputNumber);
 componentMap.set('AutoComplete', AutoComplete);
 
 componentMap.set('Select', Select);
-componentMap.set('SelectOptGroup', Select.OptGroup);
-componentMap.set('SelectOption', Select.Option);
+// componentMap.set('SelectOptGroup', Select.OptGroup);
+// componentMap.set('SelectOption', Select.Option);
 componentMap.set('TreeSelect', TreeSelect);
-componentMap.set('Transfer', Transfer);
-componentMap.set('Radio', Radio);
+// componentMap.set('Transfer', Transfer);
+// componentMap.set('Radio', Radio);
 componentMap.set('Switch', Switch);
-componentMap.set('RadioButton', Radio.Button);
+componentMap.set('RadioButtonGroup', RadioButtonGroup);
 componentMap.set('RadioGroup', Radio.Group);
 componentMap.set('Checkbox', Checkbox);
 componentMap.set('CheckboxGroup', Checkbox.Group);

+ 61 - 0
src/components/Form/src/components/RadioButtonGroup.vue

@@ -0,0 +1,61 @@
+<template>
+  <RadioGroup v-bind="$attrs" v-model:value="valueRef" button-style="solid">
+    <template v-for="item in getOptions" :key="`${item.value}`">
+      <RadioButton :value="item.value"> {{ item.label }} </RadioButton>
+    </template>
+  </RadioGroup>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, PropType, watch, unref, computed } from 'vue';
+  import { Radio } from 'ant-design-vue';
+  import {} from 'ant-design-vue/es/radio/Group';
+  import { isString } from '/@/utils/is';
+
+  type OptionsItem = { label: string; value: string; disabled?: boolean };
+  type RadioItem = string | OptionsItem;
+  export default defineComponent({
+    name: 'RadioButtonGroup',
+    components: {
+      RadioGroup: Radio.Group,
+      RadioButton: Radio.Button,
+    },
+    props: {
+      value: {
+        type: String as PropType<string>,
+      },
+      options: {
+        type: Array as PropType<RadioItem[]>,
+        default: () => [],
+      },
+    },
+    setup(props, { emit }) {
+      const valueRef = ref('');
+
+      watch(
+        () => props.value,
+        (v = '') => {
+          valueRef.value = v;
+        },
+        { immediate: true }
+      );
+
+      watch(
+        () => unref(valueRef),
+        () => {
+          emit('change', valueRef.value);
+        },
+        { immediate: true }
+      );
+
+      const getOptions = computed((): OptionsItem[] => {
+        const { options } = props;
+        if (!options || options.length === 0) return [];
+        const isStringArr = options.some((item) => isString(item));
+        if (!isStringArr) return options as OptionsItem[];
+        return options.map((item) => ({ label: item, value: item })) as OptionsItem[];
+      });
+
+      return { valueRef, getOptions };
+    },
+  });
+</script>

+ 20 - 4
src/components/Form/src/types/form.ts

@@ -7,6 +7,10 @@ import { TableActionType } from '../../../Table/src/types/table';
 
 export type FieldMapToTime = [string, [string, string], string?][];
 
+export type Rule = RuleObject & {
+  trigger?: 'blur' | 'change' | ['change', 'blur'];
+};
+
 export interface RenderCallbackParams {
   schema: FormSchema;
   values: any;
@@ -98,7 +102,10 @@ export interface FormProps {
 export interface FormSchema {
   // 字段名
   field: string;
+  // 内部值更改触发的事件名,默认 change
   changeEvent?: string;
+  // v-model绑定的变量名 默认 value
+  valueField?: string;
   // 标签名
   label: string;
   // 文本右侧帮助文本
@@ -113,13 +120,18 @@ export interface FormSchema {
   component: ComponentType;
   // 组件参数
   componentProps?:
-    | ((opt: { schema: FormSchema; tableAction: TableActionType; formModel: any }) => any)
+    | ((opt: {
+        schema: FormSchema;
+        tableAction: TableActionType;
+        formActionType: FormActionType;
+        formModel: any;
+      }) => any)
     | object;
   // 必填
   required?: boolean;
 
   // 校验规则
-  rules?: RuleObject[];
+  rules?: Rule[];
   // 校验信息是否加入label
   rulesMessageJoinLabel?: boolean;
 
@@ -146,7 +158,11 @@ export interface FormSchema {
   // 渲染 col内容,需要外层包裹 form-item
   renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
 
-  renderComponentContent?: (renderCallbackParams: RenderCallbackParams) => any;
+  renderComponentContent?:
+    | ((renderCallbackParams: RenderCallbackParams) => any)
+    | VNode
+    | VNode[]
+    | string;
 
   // 自定义slot, 在 from-item内
   slot?: string;
@@ -156,7 +172,7 @@ export interface FormSchema {
 
   dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
 
-  dynamicRules?: (renderCallbackParams: RenderCallbackParams) => RuleObject[];
+  dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[];
 }
 export interface HelpComponentProps {
   maxWidth: string;

+ 2 - 2
src/components/Form/src/types/index.ts

@@ -93,8 +93,8 @@ export type ComponentType =
   | 'SelectOption'
   | 'TreeSelect'
   | 'Transfer'
-  | 'Radio'
-  | 'RadioButton'
+  // | 'Radio'
+  | 'RadioButtonGroup'
   | 'RadioGroup'
   | 'Checkbox'
   | 'CheckboxGroup'

+ 1 - 1
src/components/Table/src/types/table.ts

@@ -87,7 +87,7 @@ export interface GetColumnsParams {
 export type SizeType = 'default' | 'middle' | 'small' | 'large';
 
 export interface TableActionType {
-  reload: (opt?: FetchParams) => Promise<void>;
+  // reload: (opt?: FetchParams) => Promise<void>;
   getSelectRows: () => any[];
   clearSelectedRowKeys: () => void;
   getSelectRowKeys: () => string[];

+ 4 - 4
src/router/menus/modules/demo/comp.ts

@@ -38,10 +38,10 @@ const menu: MenuModule = {
         path: 'strength-meter',
         name: '密码强度组件',
       },
-      {
-        path: 'upload',
-        name: '上传组件',
-      },
+      // {
+      //   path: 'upload',
+      //   name: '上传组件',
+      // },
       {
         path: 'scroll',
         name: '滚动组件',

+ 20 - 0
src/router/menus/modules/demo/form.ts

@@ -4,10 +4,18 @@ const menu: MenuModule = {
   menu: {
     path: '/form',
     name: 'Form',
+    tag: {
+      type: 'warn',
+      dot: true,
+    },
     children: [
       {
         path: 'basic',
         name: '基础表单',
+        tag: {
+          type: 'warn',
+          content: 'updated',
+        },
       },
       {
         path: 'useForm',
@@ -24,14 +32,26 @@ const menu: MenuModule = {
       {
         path: 'ruleForm',
         name: '表单校验',
+        tag: {
+          type: 'warn',
+          content: 'updated',
+        },
       },
       {
         path: 'dynamicForm',
         name: '动态表单',
+        tag: {
+          type: 'warn',
+          content: 'updated',
+        },
       },
       {
         path: 'customerForm',
         name: '自定义组件',
+        tag: {
+          type: 'warn',
+          content: 'updated',
+        },
       },
     ],
   },

+ 17 - 3
src/views/demo/form/CustomerForm.vue

@@ -1,7 +1,11 @@
 <template>
   <div class="m-4">
     <CollapseContainer title="自定义表单">
-      <BasicForm @register="register" @submit="handleSubmit" />
+      <BasicForm @register="register" @submit="handleSubmit">
+        <template #f3="{ model, field }">
+          <a-input v-model:value="model[field]" placeholder="自定义slot" />
+        </template>
+      </BasicForm>
     </CollapseContainer>
   </div>
 </template>
@@ -15,7 +19,7 @@
     {
       field: 'field1',
       component: 'Input',
-      label: '字段1',
+      label: 'render方式',
       colProps: {
         span: 8,
       },
@@ -33,7 +37,7 @@
     {
       field: 'field2',
       component: 'Input',
-      label: '字段2',
+      label: 'render组件slot',
       colProps: {
         span: 8,
       },
@@ -44,6 +48,16 @@
         };
       },
     },
+    {
+      field: 'field3',
+      component: 'Input',
+      label: '自定义Slot',
+      slot: 'f3',
+      colProps: {
+        span: 8,
+      },
+      rules: [{ required: true }],
+    },
   ];
   export default defineComponent({
     components: { BasicForm, CollapseContainer },

+ 64 - 0
src/views/demo/form/DynamicForm.vue

@@ -9,6 +9,10 @@
     <CollapseContainer title="动态表单示例,动态根据表单内其他值改变">
       <BasicForm @register="register" />
     </CollapseContainer>
+
+    <CollapseContainer class="mt-5" title="componentProps动态改变">
+      <BasicForm @register="register1" />
+    </CollapseContainer>
   </div>
 </template>
 <script lang="ts">
@@ -120,6 +124,58 @@
     },
   ];
 
+  const schemas1: FormSchema[] = [
+    {
+      field: 'f1',
+      component: 'Input',
+      label: 'F1',
+      colProps: {
+        span: 12,
+      },
+      labelWidth: 200,
+      componentProps: ({ formModel }) => {
+        return {
+          placeholder: '同步f2的值为f1',
+          onChange: (e: ChangeEvent) => {
+            formModel.f2 = e.target.value;
+          },
+        };
+      },
+    },
+    {
+      field: 'f2',
+      component: 'Input',
+      label: 'F2',
+      colProps: {
+        span: 12,
+      },
+      labelWidth: 200,
+      componentProps: { disabled: true },
+    },
+    {
+      field: 'f3',
+      component: 'Input',
+      label: 'F3',
+      colProps: {
+        span: 12,
+      },
+      labelWidth: 200,
+      // @ts-ignore
+      componentProps: ({ formActionType, tableAction }) => {
+        return {
+          placeholder: '值改变时执行查询,查看控制台',
+          onChange: async () => {
+            const { validate } = formActionType;
+            // tableAction只适用于在表格内开启表单的例子
+            // const { reload } = tableAction;
+            const res = await validate();
+            console.log(res);
+          },
+        };
+      },
+    },
+  ];
+
   export default defineComponent({
     components: { BasicForm, CollapseContainer },
     setup() {
@@ -133,6 +189,13 @@
           span: 24,
         },
       });
+      const [register1] = useForm({
+        labelWidth: 120,
+        schemas: schemas1,
+        actionColOptions: {
+          span: 24,
+        },
+      });
       function changeLabel3() {
         updateSchema({
           field: 'field3',
@@ -170,6 +233,7 @@
       }
       return {
         register,
+        register1,
         schemas,
         setProps,
         changeLabel3,

+ 27 - 1
src/views/demo/form/RuleForm.vue

@@ -65,7 +65,33 @@
           },
         ],
       },
-      rules: [{ required: true, message: '请输入aa' }],
+      rules: [
+        {
+          required: true,
+          message: '请输入aa',
+        },
+      ],
+    },
+    {
+      field: 'field44',
+      component: 'Input',
+      label: '自定义校验',
+      colProps: {
+        span: 8,
+      },
+      rules: [
+        {
+          required: true,
+          // @ts-ignore
+          validator: async (rule, value) => {
+            if (value === '1') {
+              return Promise.reject('值不能为1');
+            }
+            return Promise.resolve();
+          },
+          trigger: 'blur',
+        },
+      ],
     },
     {
       field: 'field5',

+ 102 - 3
src/views/demo/form/index.vue

@@ -11,10 +11,11 @@
   </div>
 </template>
 <script lang="ts">
-  import { defineComponent } from 'vue';
+  import { defineComponent, ref } from 'vue';
   import { BasicForm, FormSchema } from '/@/components/Form/index';
   import { CollapseContainer } from '/@/components/Container/index';
   import { useMessage } from '/@/hooks/web/useMessage';
+
   const schemas: FormSchema[] = [
     {
       field: 'field1',
@@ -23,8 +24,11 @@
       colProps: {
         span: 8,
       },
-      defaultValue: '111',
-      componentProps: () => {
+      // componentProps:{},
+      // can func
+      componentProps: ({ schema, formModel }) => {
+        console.log('form:', schema);
+        console.log('formModel:', formModel);
         return {
           placeholder: '自定义placeholder',
           onChange: (e: any) => {
@@ -32,14 +36,26 @@
           },
         };
       },
+      renderComponentContent: () => {
+        return {
+          prefix: () => 'pSlot',
+          suffix: () => 'sSlot',
+        };
+      },
     },
     {
       field: 'field2',
       component: 'Input',
       label: '字段2',
+      defaultValue: '111',
       colProps: {
         span: 8,
       },
+      componentProps: {
+        onChange: (e: any) => {
+          console.log(e);
+        },
+      },
     },
     {
       field: 'field3',
@@ -111,17 +127,100 @@
         ],
       },
     },
+    {
+      field: 'field8',
+      component: 'Checkbox',
+      label: '字段8',
+      colProps: {
+        span: 8,
+      },
+      renderComponentContent: 'Check',
+    },
+    {
+      field: 'field9',
+      component: 'Switch',
+      label: '字段9',
+      colProps: {
+        span: 8,
+      },
+    },
+    {
+      field: 'field10',
+      component: 'RadioButtonGroup',
+      label: '字段10',
+      colProps: {
+        span: 8,
+      },
+      componentProps: {
+        options: [
+          {
+            label: '选项1',
+            value: '1',
+          },
+          {
+            label: '选项2',
+            value: '2',
+          },
+        ],
+      },
+    },
+    {
+      field: 'field11',
+      component: 'Cascader',
+      label: '字段11',
+      colProps: {
+        span: 8,
+      },
+      componentProps: {
+        options: [
+          {
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [
+              {
+                value: 'hangzhou',
+                label: 'Hangzhou',
+                children: [
+                  {
+                    value: 'xihu',
+                    label: 'West Lake',
+                  },
+                ],
+              },
+            ],
+          },
+          {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [
+              {
+                value: 'nanjing',
+                label: 'Nanjing',
+                children: [
+                  {
+                    value: 'zhonghuamen',
+                    label: 'Zhong Hua Men',
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    },
   ];
 
   export default defineComponent({
     components: { BasicForm, CollapseContainer },
     setup() {
+      const check = ref(null);
       const { createMessage } = useMessage();
       return {
         schemas,
         handleSubmit: (values: any) => {
           createMessage.success('click search,values:' + JSON.stringify(values));
         },
+        check,
       };
     },
   });