Jelajahi Sumber

feat: Form 自定义组件渲染 新增 opts: {disabled} 用于自定义渲染判断 示例: /comp/form/customerForm页面 (#2944)

* feat: Form 自定义组件渲染 示例: /comp/form/customerForm页面
1. 针对自定义渲染功能 FormSchema 中
render, renderColContent, renderComponentContent 新增 opts:{disabled} 扩展 帮助自定义渲染时做 条件判断、展示同步
渲染: ((renderCallbackParams) => any) ===> ((renderCallbackParams, opts) => any)
2. slot, colSlot 分别是 render, renderColContent 插槽,为方便插槽使用
slotFn 进行解构 #test={scope} scope==={...data, ...opts}

* feat: Form 自定义组件渲染 示例: /comp/form/customerForm页面

1. 针对自定义渲染功能 FormSchema 新增 [fields] 和 [defaultValueObj] 帮助
render, renderColContent 自定义渲染时 存在多个 表单字段操作(复合field 场景)
LanceJiang 1 tahun lalu
induk
melakukan
a065de4fbc

+ 7 - 6
src/components/Form/src/components/FormItem.vue

@@ -299,7 +299,7 @@
           return <Comp {...compAttr} />;
         }
         const compSlot = isFunction(renderComponentContent)
-          ? { ...renderComponentContent(unref(getValues)) }
+          ? { ...renderComponentContent(unref(getValues), { disabled: unref(getDisable) }) }
           : {
               default: () => renderComponentContent,
             };
@@ -333,7 +333,7 @@
         const { itemProps, slot, render, field, suffix, component } = props.schema;
         const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
         const { colon } = props.formProps;
-
+        const opts = { disabled: unref(getDisable) };
         if (component === 'Divider') {
           return (
             <Col span={24}>
@@ -343,9 +343,9 @@
         } else {
           const getContent = () => {
             return slot
-              ? getSlot(slots, slot, unref(getValues))
+              ? getSlot(slots, slot, unref(getValues), opts)
               : render
-              ? render(unref(getValues))
+              ? render(unref(getValues), opts)
               : renderComponent();
           };
 
@@ -391,12 +391,13 @@
         const realColProps = { ...baseColProps, ...colProps };
         const { isIfShow, isShow } = getShow();
         const values = unref(getValues);
+        const opts = { disabled: unref(getDisable) };
 
         const getContent = () => {
           return colSlot
-            ? getSlot(slots, colSlot, values)
+            ? getSlot(slots, colSlot, values, opts)
             : renderColContent
-            ? renderColContent(values)
+            ? renderColContent(values, opts)
             : renderItem();
         };
 

+ 22 - 6
src/components/Form/src/hooks/useFormEvents.ts

@@ -87,6 +87,13 @@ export function useFormEvents({
 
     Object.keys(formModel).forEach((key) => {
       const schema = unref(getSchema).find((item) => item.field === key);
+      const defaultValueObj = schema?.defaultValueObj;
+      const fieldKeys = Object.keys(defaultValueObj || {});
+      if (fieldKeys.length) {
+        fieldKeys.map((field) => {
+          formModel[field] = defaultValueObj![field];
+        });
+      }
       const isInput = schema?.component && defaultValueComponents.includes(schema.component);
       const defaultValue = cloneDeep(defaultValueRef.value[key]);
       formModel[key] = isInput ? defaultValue || '' : defaultValue;
@@ -96,14 +103,17 @@ export function useFormEvents({
     emit('reset', toRaw(formModel));
     submitOnReset && handleSubmit();
   }
-
+  // 获取表单fields
+  const getAllFields = () =>
+    unref(getSchema)
+      .map((item) => [...(item.fields || []), item.field])
+      .flat(1)
+      .filter(Boolean);
   /**
    * @description: Set form value
    */
   async function setFieldsValue(values: Recordable): Promise<void> {
-    const fields = unref(getSchema)
-      .map((item) => item.field)
-      .filter(Boolean);
+    const fields = getAllFields();
 
     // key 支持 a.b.c 的嵌套写法
     const delimiter = '.';
@@ -340,8 +350,14 @@ export function useFormEvents({
     return unref(formElRef)?.validateFields(nameList);
   }
 
-  async function validate(nameList?: NamePath[] | undefined) {
-    return await unref(formElRef)?.validate(nameList);
+  async function validate(nameList?: NamePath[] | false | undefined) {
+    let _nameList: any;
+    if (nameList === undefined) {
+      _nameList = getAllFields();
+    } else {
+      _nameList = nameList === Array.isArray(nameList) ? nameList : undefined;
+    }
+    return await unref(formElRef)?.validate(_nameList);
   }
 
   async function clearValidate(name?: string | string[]) {

+ 10 - 1
src/components/Form/src/hooks/useFormValues.ts

@@ -127,7 +127,16 @@ export function useFormValues({
     const schemas = unref(getSchema);
     const obj: Recordable = {};
     schemas.forEach((item) => {
-      const { defaultValue } = item;
+      const { defaultValue, defaultValueObj } = item;
+      const fieldKeys = Object.keys(defaultValueObj || {});
+      if (fieldKeys.length) {
+        fieldKeys.map((field) => {
+          obj[field] = defaultValueObj![field];
+          if (formModel[field] === undefined) {
+            formModel[field] = defaultValueObj![field];
+          }
+        });
+      }
       if (!isNullOrUnDef(defaultValue)) {
         obj[item.field] = defaultValue;
 

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

@@ -39,7 +39,7 @@ export interface FormActionType {
     first?: boolean | undefined,
   ) => Promise<void>;
   validateFields: (nameList?: NamePath[]) => Promise<any>;
-  validate: (nameList?: NamePath[]) => Promise<any>;
+  validate: (nameList?: NamePath[] | false) => Promise<any>;
   scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>;
 }
 
@@ -123,15 +123,21 @@ export interface FormProps {
   transformDateFunc?: (date: any) => string;
   colon?: boolean;
 }
+export type RenderOpts = {
+  disabled: boolean;
+  [key: string]: any;
+};
 export interface FormSchema {
   // Field name
   field: string;
+  // Extra Fields name[]
+  fields?: string[];
   // Event name triggered by internal value change, default change
   changeEvent?: string;
   // Variable name bound to v-model Default value
   valueField?: string;
   // Label name
-  label: string | VNode;
+  label?: string | VNode;
   // Auxiliary text
   subLabel?: string;
   // Help text on the right side of the text
@@ -175,6 +181,9 @@ export interface FormSchema {
   // 默认值
   defaultValue?: any;
 
+  // 额外默认值数组对象
+  defaultValueObj?: { [key: string]: any };
+
   // 是否自动处理与时间相关组件的默认值
   isHandleDateDefaultValue?: boolean;
 
@@ -188,13 +197,19 @@ export interface FormSchema {
   show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
 
   // Render the content in the form-item tag
-  render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
+  render?: (
+    renderCallbackParams: RenderCallbackParams,
+    opts: RenderOpts,
+  ) => VNode | VNode[] | string;
 
   // Rendering col content requires outer wrapper form-item
-  renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
+  renderColContent?: (
+    renderCallbackParams: RenderCallbackParams,
+    opts: RenderOpts,
+  ) => VNode | VNode[] | string;
 
   renderComponentContent?:
-    | ((renderCallbackParams: RenderCallbackParams) => any)
+    | ((renderCallbackParams: RenderCallbackParams, opts: RenderOpts) => any)
     | VNode
     | VNode[]
     | string;

+ 4 - 2
src/utils/helper/tsxHelper.tsx

@@ -1,10 +1,11 @@
 import { Slots } from 'vue';
 import { isFunction } from '/@/utils/is';
+import { RenderOpts } from '/@/components/Form';
 
 /**
  * @description:  Get slot to prevent empty error
  */
-export function getSlot(slots: Slots, slot = 'default', data?: any) {
+export function getSlot(slots: Slots, slot = 'default', data?: any, opts?: RenderOpts) {
   if (!slots || !Reflect.has(slots, slot)) {
     return null;
   }
@@ -14,7 +15,8 @@ export function getSlot(slots: Slots, slot = 'default', data?: any) {
   }
   const slotFn = slots[slot];
   if (!slotFn) return null;
-  return slotFn(data);
+  const params = { ...data, ...opts };
+  return slotFn(params);
 }
 
 /**

+ 163 - 9
src/views/demo/form/CustomerForm.vue

@@ -2,21 +2,42 @@
   <PageWrapper title="自定义组件示例">
     <CollapseContainer title="自定义表单">
       <BasicForm @register="register" @submit="handleSubmit">
-        <template #f3="{ model, field }">
-          <a-input v-model:value="model[field]" placeholder="自定义slot" />
+        <template #f3="{ model, field, disabled }">
+          <a-input v-model:value="model[field]" :disabled="disabled" placeholder="自定义slot" />
+        </template>
+        <template #colSlot_field5="{ model, field, disabled }">
+          <FormItem :name="field" label="自定义colSlot" :rules="[{ required: true }]">
+            <a-input
+              v-model:value="model[field]"
+              :disabled="disabled"
+              placeholder="自定义colSlot"
+            />
+          </FormItem>
         </template>
       </BasicForm>
     </CollapseContainer>
   </PageWrapper>
 </template>
-<script lang="ts">
+<script lang="tsx">
   import { defineComponent, h } from 'vue';
   import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
   import { CollapseContainer } from '/@/components/Container/index';
   import { useMessage } from '/@/hooks/web/useMessage';
-  import { Input } from 'ant-design-vue';
+  import { Input, FormItem, Select } from 'ant-design-vue';
   import { PageWrapper } from '/@/components/Page';
 
+  const custom_typeKey2typeValueRules = (model) => {
+    return [
+      {
+        required: true,
+        validator: (rule, value, callback) => {
+          if (!model.typeKey) return callback('请选择类型');
+          if (!model.typeValue) return callback('请输入数据');
+          callback();
+        },
+      },
+    ];
+  };
   const schemas: FormSchema[] = [
     {
       field: 'field1',
@@ -25,14 +46,18 @@
       colProps: {
         span: 8,
       },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
       rules: [{ required: true }],
-      render: ({ model, field }) => {
+      render: ({ model, field }, { disabled }) => {
         return h(Input, {
           placeholder: '请输入',
           value: model[field],
-          onChange: (e: ChangeEvent) => {
+          onChange: (e) => {
             model[field] = e.target.value;
           },
+          disabled,
         });
       },
     },
@@ -43,10 +68,13 @@
       colProps: {
         span: 8,
       },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
       rules: [{ required: true }],
-      renderComponentContent: () => {
+      renderComponentContent: (_, { disabled }) => {
         return {
-          suffix: () => 'suffix',
+          suffix: () => (disabled ? 'suffix_disabled' : 'suffix_default'),
         };
       },
     },
@@ -58,11 +86,136 @@
       colProps: {
         span: 8,
       },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
       rules: [{ required: true }],
     },
+    {
+      field: 'field4',
+      component: 'Input',
+      // label: 'renderColContent渲染',
+      /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/
+      renderColContent({ model, field }, { disabled }) {
+        return (
+          <FormItem name="field4" label="renderColContent渲染" rules={[{ required: true }]}>
+            <Input placeholder="请输入" v-model:value={model[field]} disabled={disabled}></Input>
+          </FormItem>
+        );
+      },
+      colProps: {
+        span: 8,
+      },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
+    },
+    {
+      field: 'field5',
+      component: 'Input',
+      label: '自定义colSlot',
+      /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/
+      colSlot: 'colSlot_field5',
+      colProps: {
+        span: 8,
+      },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
+    },
+    // 复合field 场景 自定义表单控件 一个控件包含多个表单录入 示例: 选择+输入
+    {
+      required: true,
+      field: 'typeKey2',
+      defaultValue: '测试类型',
+      fields: ['typeValue2'],
+      defaultValueObj: { typeValue2: '默认测试_文字' },
+      component: 'Input',
+      label: '复合field render',
+      render({ model, field }, { disabled }) {
+        return (
+          <Input.Group compact>
+            <Select
+              disabled={disabled}
+              style="width: 120px"
+              allowClear
+              v-model:value={model[field]}
+            >
+              <Select.Option value="测试类型">测试类型</Select.Option>
+              <Select.Option value="测试名称">测试名称</Select.Option>
+            </Select>
+            <FormItem
+              name="typeValue2"
+              style="width: calc(100% - 120px); margin-left: -1px; border-right: 0; margin-bottom: 0;"
+              rules={[{ required: true }]}
+            >
+              <Input placeholder="请输入" v-model:value={model['typeValue2']} disabled={disabled} />
+            </FormItem>
+          </Input.Group>
+        );
+      },
+      colProps: {
+        span: 8,
+      },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
+    },
+    // 复合field 场景 自定义表单控件 一个控件包含多个表单录入 示例: 选择+输入
+    {
+      field: 'typeKey',
+      defaultValue: '公司名称',
+      fields: ['typeValue'],
+      defaultValueObj: { typeValue: '默认文字' },
+      component: 'Input',
+      // label: 'renderColContent渲染',
+      /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/
+      renderColContent({ model, field }, { disabled }) {
+        return (
+          <FormItem
+            name="typeKey"
+            label="复合field renderColContent"
+            rules={custom_typeKey2typeValueRules(model)}
+          >
+            <Input.Group compact>
+              <Select
+                allowClear
+                disabled={disabled}
+                style="width: 120px"
+                v-model:value={model[field]}
+              >
+                <Select.Option value="公司名称">公司名称</Select.Option>
+                <Select.Option value="产品名称">产品名称</Select.Option>
+              </Select>
+              <Input
+                style="width: calc(100% - 120px); margin-left: -1px;"
+                placeholder="请输入"
+                v-model:value={model['typeValue']}
+                disabled={disabled}
+              />
+            </Input.Group>
+          </FormItem>
+        );
+      },
+      colProps: {
+        span: 16,
+      },
+      dynamicDisabled: ({ values }) => {
+        return !!values.field_disabled;
+      },
+    },
+    {
+      field: 'field_disabled',
+      component: 'Switch',
+      label: '是否禁用 编辑字段',
+      colProps: {
+        span: 8,
+      },
+      labelWidth: 200,
+    },
   ];
   export default defineComponent({
-    components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input },
+    components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input, FormItem },
     setup() {
       const { createMessage } = useMessage();
       const [register, { setProps }] = useForm({
@@ -76,6 +229,7 @@
         register,
         schemas,
         handleSubmit: (values: any) => {
+          console.log('submit values', values);
           createMessage.success('click search,values:' + JSON.stringify(values));
         },
         setProps,