Vue3 Ant design关于Form的封装

776 阅读20分钟

简易表单

  • 输入时失焦校验
  • 提交时校验,成功/失败提示
  • 数据列默认值
  • 根据数据列配置项展示数据列
  • 数据列、label/value列固定宽度
<template>
  <Form ref="schemaFormRef" :model="formModel">
    <Row>
      <template v-for="item in schemas" :key="item.field">
        <Col :span="6">
          <Form.Item
            :label="item.label"
            :name="item.field"
            :label-col="{ style: { width: '100px' } }"
            :wrapper-col="{ style: { width: `calc(100% - 100px})` } }"
            :rules="getRules"
          >
            <Input v-model:value.trim="formModel[item.field]" />
          </Form.Item>
        </Col>
      </template>
      <Col>
        <Form.Item>
          <Button @click="submit">提交</Button>
        </Form.Item>
      </Col>
    </Row>
  </Form>
</template>
<script lang="tsx" setup>
  import { computed, ref, reactive } from 'vue';
  import { cloneDeep } from 'lodash-es';
  import { Form, Col, message, Input, Button } from 'ant-design-vue';
  import type { FormInstance } from 'ant-design-vue';
  import type { FormSchema } from '@/components/core/schema-form/src/types/form';
  import { isNull } from '@/utils/is';

  defineOptions({
    name: 'SchemaForm',
  });
    
  // 表单配置规则
  const schemas: FormSchema[] = [
    {
      field: 'name',
      component: 'Input',
      label: '姓名',
    },
  ];
    
  // 表单数据列默认值
  const initialValues = {
    name: '张三',
  };
  // 表单数据源
  const formModel = reactive(cloneDeep(initialValues));

  // 表单ref实例
  const schemaFormRef = ref<FormInstance>();

  // 拿到rules,在之前先校验输入类型
  const getRules = computed(() => {
    function validator(rule: any, value: any) {
      const defaultMsg = '请输入';
      const msg = rule.message || defaultMsg;

      if (value === undefined || isNull(value)) {
        // 空值
        return Promise.reject(msg);
      } else if (Array.isArray(value) && value.length === 0) {
        // 数组类型
        return Promise.reject(msg);
      } else if (typeof value === 'string' && value.trim() === '') {
        // 空字符串
        return Promise.reject(msg);
      } else if (
        typeof value === 'object' &&
        Reflect.has(value, 'checked') &&
        Reflect.has(value, 'halfChecked') &&
        Array.isArray(value.checked) &&
        Array.isArray(value.halfChecked) &&
        value.checked.length === 0 &&
        value.halfChecked.length === 0
      ) {
        // 非关联选择的tree组件
        return Promise.reject(msg);
      }
      return Promise.resolve();
    }
    const rules = [{ required: true, validator }];
    return rules;
  });

  // 提交成功后的操作
  const handleSubmit = (values) => {
    message.success('验证通过!', 3);
    console.log('values', values);
  };

  // 点击提交
  const submit = async (e?: Event) => {
    // 屏蔽按钮自带事件(如:自带html-type: submit会触发form的submit方法)
    e && e.preventDefault();
    try {
      const values = await schemaFormRef.value?.validate();
      // 表单校验通过,可以提交表单
      console.log('validate success:');
      handleSubmit(values);
    } catch (error: any) {
      message.error('验证失败!', 3);
      console.log('validate error:', error);
      return Promise.reject(error);
    }
  };
</script>

封装FormItem

  • 将整个Form移动到子组件(schema-form.vue)
  • 提供submit方法给父组件

子组件(Form组件)

  • 路径:@/components/core/schema-form/src/schema-form.vue
<template>
  <Form ref="schemaFormRef" :model="formModel">
    <Row>
      <template v-for="schemaItem in schemas" :key="schemaItem.field">
        <Col :span="6">
          <Form.Item
            :label="schemaItem.label"
            :name="schemaItem.field"
            :label-col="{ style: { width: '100px' } }"
            :wrapper-col="{ style: { width: `calc(100% - 100px})` } }"
            :rules="getRules"
          >
            <Input v-model:value.trim="formModel[schemaItem.field]" />
          </Form.Item>
        </Col>
      </template>
      <Col>
        <Form.Item>
          <Button @click="handleSubmit">提交</Button>
        </Form.Item>
      </Col>
    </Row>
  </Form>
</template>
<script lang="tsx" setup>
  import { computed, ref, reactive } from 'vue';
  import { cloneDeep } from 'lodash-es';
  import { Form, Col, message, Input, Button } from 'ant-design-vue';
  import type { FormInstance } from 'ant-design-vue';
  import type { FormSchema } from './types/form';
    
  defineOptions({
    name: 'SchemaForm',
  });
    
  // 提供submit方法给父组件
  const emit = defineEmits(['submit']); // add
  const attrs = useAttrs(); // add
    
  // 表单配置规则
  const schemas: FormSchema[] = [
    {
      field: 'name',
      component: 'Input',
      label: '姓名',
    },
  ];
    
  // 表单数据列默认值
  const initialValues = {
    name: '张三',
  };
  // 表单数据源
  const formModel = reactive(cloneDeep(initialValues));
    
  // 表单ref实例
  const schemaFormRef = ref<FormInstance>();
  
  // 拿到rules,在之前先校验输入类型
  const getRules = computed(() => {
    // ......
  });
  
  // 提交成功后的操作
  // const handleSubmit = (values) => {
  //   message.success('验证通过!', 3);
  //   console.log('values', values);
  // } // del
  
  // 点击提交
  const handleSubmit = async (e?: Event) => {
    // 屏蔽按钮自带事件(如:自带html-type: submit会触发form的submit方法)
    e && e.preventDefault();
    try {
      const values = await schemaFormRef.value?.validate();
      // 表单校验通过,可以提交表单
      console.log('validate success:');
      // handleSubmit(values); // del
      emit('submit', values); // add
    } catch (error: any) {
      message.error('验证失败!', 3);
      console.log('validate error:', error);
      return Promise.reject(error);
    }
  };

  // 提供属性和方法给父组件
  defineExpose({ submit: handleSubmit }); // add
</script>

父组件(业务组件)

  • 路径:@/views/demos/form/basic-form/index.vue
<template>
  <SchemaForm @submit="handleSubmit"></SchemaForm>
</template>

<script lang="tsx" setup>
  import { message } from 'ant-design-vue';
  import SchemaForm from '@/components/core/schema-form/src/schema-form.vue';

  defineOptions({
    name: 'BasicForm',
  });

  // 提交成功后的操作
  const handleSubmit = (values) => {
    message.success('验证通过!', 3);
    console.log('values', values);
  };
</script>

增加hooks

  • 路径:@/components/core/schema-form/src/hooks/useForm.tsx
  • 存储Form的ref实例
import { nextTick, ref, unref, watch } from 'vue';
import { isEmpty } from 'lodash-es';
import SchemaForm from '../schema-form.vue';
import type { Ref, SetupContext } from 'vue';
import type {
  SchemaFormInstance,
  SchemaFormProps,
} from '@/components/core/schema-form/src/schema-form';

export function useForm(props?: Partial<SchemaFormProps>) {
  const formRef = ref<SchemaFormInstance>({} as SchemaFormInstance);

  async function getFormInstance() {
    await nextTick();
    const form = unref(formRef);
    if (isEmpty(form)) {
      console.error('未获取表单实例!');
    }
    return form;
  }

  const methods = new Proxy<Ref<SchemaFormInstance>>(formRef, {
    get(target, key) {
      if (Reflect.has(target, key)) {
        return unref(target);
      }
      if (target.value && Reflect.has(target.value, key)) {
        return Reflect.get(target.value, key);
      }
      return async (...rest) => {
        const form = await getFormInstance();
        return form?.[key]?.(...rest);
      };
    },
  });

  /**
   * @param compProps 父组件传递过来的属性
   * @param attrs 父组件传递过来的props中没定义的属性
   * @param slots 父组件中的slot对象
   */
  const SchemaFormRender = (
    compProps: Partial<SchemaFormProps>,
    { attrs, slots }: SetupContext,
  ) => {
    return (
      <SchemaForm
        ref={formRef}
        {...{ ...attrs, ...props, ...compProps }}
        v-slots={slots}
      ></SchemaForm>
    );
  };

  return [SchemaFormRender, unref(methods)] as const;
}

父组件(业务组件)

<template>
  <SchemaForm @submit="handleSubmit"></SchemaForm>
</template>

<script lang="tsx" setup>
  import { message } from 'ant-design-vue';
  import { useForm } from '@/components/core/schema-form/src/hooks/useForm';

  defineOptions({
    name: 'BasicForm',
  });

  const [SchemaForm] = useForm({}); // add

  // 提交成功后的操作
  const handleSubmit = (values) => {
    message.success('验证通过!', 3);
    console.log('values', values);
  };
</script>

增加types

  • 路径:@/components/core/schema-form/src/types/form.ts
  • 用于定义表单配置规则的类型
/** 表单项 */
export interface FormSchema<T = string> {
    // ......
}

拆分部分state

  • 拆分initialValues(props.initialValues)
  • 拆分schemas(computed(() => cloneDeep(props).schemas || []))
  • 拆分数据列、操作列的栅栏Col配置(v-bind="schemaItem.colProps"、v-bind="actionColOpt")
  • 拆分栅栏Row配置(v-bind="getRowConfig")

子组件(Form组件)

  • 将父组件(业务组件)的props全部透传给form,并且用pick过滤掉form中不需要的props
  • 将数据列配置项中的formItemProps全部透传给form-item
<template>
  <!-- <Form ref="schemaFormRef“ :model="formModel"> --> <!-- del -->
  <Form ref="schemaFormRef" v-bind="pick(getFormProps, aFormPropKeys)" :model="formModel"> <!-- add -->
    <!-- <Row> --> <!-- del -->
    <Row v-bind="getRowConfig"> <!-- add -->
      <!-- <template v-for="schemaItem in schemas" :key="schemaItem.field"> --> <!-- del -->
      <template v-for="schemaItem in formSchemasRef" :key="schemaItem.field"> <!-- add -->
        <!-- <Col :span="6"> --> <!-- del -->
        <Col v-bind="schemaItem.colProps"> <!-- add -->
          <Form.Item
            v-bind="{ ...(schemaItem.formItemProps || {}) }"
            :label="schemaItem.label"
            :name="schemaItem.field"
            :label-col="{ style: { width: '100px' } }"
            :wrapper-col="{ style: { width: `calc(100% - 100px})` } }"
            :rules="getRules"
          >
            <Input v-model:value.trim="formModel[schemaItem.field]" />
          </Form.Item>
        </Col>
      </template>
      <!-- <Col> --> <!-- del -->
      <Col v-bind="actionColOpt"> <!-- add -->
        <Form.Item>
          <Button @click="handleSubmit">提交</Button>
        </Form.Item>
      </Col>
    </Row>
  </Form>
</template>
<script lang="tsx" setup>
  import { computed, ref, unref, reactive, useAttrs } from 'vue';
  import { formProps } from 'ant-design-vue/es/form';
  import { cloneDeep, pick } from 'lodash-es';
  import { Form, Col, message, Input, Button } from 'ant-design-vue';
  import { schemaFormProps, type SchemaFormProps } from './schema-form';
  import type { FormInstance } from 'ant-design-vue';
  import { aFormPropKeys, type ColEx } from '@/components/core/schema-form';
    
  defineOptions({
    name: 'SchemaForm',
  });

  const props = defineProps(schemaFormProps); // add
  // 提供submit方法给父组件
  const emit = defineEmits(['submit']);
  const attrs = useAttrs(); // add
    
  // 表单配置规则
  // const schemas: FormSchema[] = [
  //   {
  //     field: 'name',
  //     component: 'Input',
  //     label: '姓名',
  //   },
  // ]; // del
    
  // 表单数据列默认值
  // const initialValues = {
  //   name: '张三',
  // }; // del
    
  // 表单数据源
  // const formModel = reactive(cloneDeep(initialValues)); // del
  const formModel = reactive(cloneDeep(props.initialValues)); // add
    
  // 表单ref实例
  const schemaFormRef = ref<FormInstance>();
  // TODO 将props克隆一份,避免修改原有的props
  const formPropsRef = ref<SchemaFormProps>(cloneDeep(props)); // add
  const formSchemasRef = computed(() => unref(formPropsRef).schemas || []); // add
    
  // 获取表单所有属性
  const getFormProps = computed(() => {
    return {
      ...attrs,
      ...formPropsRef.value,
    } as SchemaFormProps;
  });

  // 获取栅栏Row配置
  const getRowConfig = computed((): Recordable => {
    const { baseRowStyle = {}, rowProps } = unref(getFormProps);
    return {
      style: baseRowStyle,
      ...rowProps,
    };
  });

  // 获取操作列配置
  const actionColOpt = computed(() => {
    const { actionColOptions } = props;
    const actionColOpt: Partial<ColEx> = {
      style: { textAlign: 'right' },
      span: 4,
      ...actionColOptions,
    };
    return actionColOpt;
  });
  
  // 拿到rules,在之前先校验输入类型
  const getRules = computed(() => {
    // ......
  });
  
  // 点击提交
  const handleSubmit = async (e?: Event) => {
    // 屏蔽按钮自带事件(如:自带html-type: submit会触发form的submit方法)
    e && e.preventDefault();
    try {
      const values = await schemaFormRef.value?.validate();
      // 表单校验通过,可以提交表单
      console.log('validate success:');
      emit('submit', values);
    } catch (error: any) {
      message.error('验证失败!', 3);
      console.log('validate error:', error);
      return Promise.reject(error);
    }
  };

  // 提供属性和方法给父组件
  defineExpose({ submit: handleSubmit });
</script>

父组件(业务组件)

<template>
  <SchemaForm @submit="handleSubmit"></SchemaForm>
</template>

<script lang="tsx" setup>
  import { message } from 'ant-design-vue';
  import { useForm } from '@/components/core/schema-form/src/hooks/useForm';
  import { schemas } from './form-schema';

  defineOptions({
    name: 'BasicForm',
  });

  const [SchemaForm] = useForm({
    // 表单数据列默认值
    initialValues: {
      field1: '张三',
    }, // add
    schemas, // add
  });

  // 提交成功后的操作
  const handleSubmit = (values) => {
    message.success('验证通过!', 3);
    console.log('values', values);
  };
</script>

add:schema-form.ts

  • 路径:@/components/core/schema-form/src/schema-form.ts
  • 获取ant官方定义的form的所有props,用于过滤掉form传入的多余props
import { formProps } from 'ant-design-vue/es/form';
import type { ExtractPropTypes, CSSProperties } from 'vue';
import type { FormSchema, RowProps } from '@/components/core/schema-form/src/types/form';
import type { ColEx } from '@/components/core/schema-form';
import { isObject } from '@/utils/is';

// 获取ant官方定义的form的所有props
export const aFormPropKeys = Object.keys(formProps());

export const schemaFormProps = {
  ...formProps(),
  /** 预置字段默认值 */
  initialValues: {
    type: Object as PropType<Recordable>,
    default: () => ({}),
  },
  /** 表单配置规则 */
  schemas: {
    type: [Array] as PropType<FormSchema[]>,
    default: () => [],
  },
  /** Row的css样式 */
  baseRowStyle: {
    type: Object as PropType<CSSProperties>,
  },
  /** 操作列Col配置 */
  actionColOptions: Object as PropType<Partial<ColEx>>,
  /** Row的props */
  rowProps: Object as PropType<RowProps>,
};

export type SchemaFormProps<T = any> = Partial<
  ExtractPropTypes<typeof schemaFormProps> & {
    schemas: FormSchema<T>[];
  }
>;

export const schemaFormEmits = {
  submit: (formModel: Recordable<any>) => isObject(formModel),
};

export type SchemaFormEmits = typeof schemaFormEmits;

export type SchemaFormEmitFn = EmitFn<SchemaFormEmits>;

add:form-schema.tsx

  • 路径:@/views/demos/form/basic-form/form-schema.tsx
import type { FormSchema } from '@/components/core/schema-form/src/types/form';

export const schemas: FormSchema[] = [
  {
    field: 'field1',
    component: 'Input',
    label: '字段1',
    colProps: {
      span: 8,
    },
    componentProps: () => {
      return {
        placeholder: '自定义placeholder',
        onChange: (e: any) => {
          console.log(e);
        },
      };
    },
    componentSlots: () => {
      return {
        prefix: () => 'pSlot',
        suffix: () => 'sSlot',
      };
    },
  }
]

拆分FormItem

子组件(Form组件)

<template>
  <Form ref="schemaFormRef" v-bind="pick(getFormProps, aFormPropKeys)" :model="formModel">
    <Row v-bind="getRowConfig">
      <template v-for="schemaItem in formSchemasRef" :key="schemaItem.field">
        <!-- <Col v-bind="schemaItem.colProps">
          <Form.Item
            v-bind="{ ...(schemaItem.formItemProps || {}) }"
            :label="schemaItem.label"
            :name="schemaItem.field"
            :label-col="{ style: { width: '100px' } }"
            :wrapper-col="{ style: { width: `calc(100% - 100px})` } }"
            :rules="getRules"
          >
            <Input v-model:value.trim="formModel[schemaItem.field]" />
          </Form.Item>
        </Col> --> <!-- del -->
        <SchemaFormItem v-model:formModel="formModel" :schema="schemaItem" /> <!-- add -->
      </template>
      <Col v-bind="actionColOpt">
        <Form.Item>
          <Button @click="handleSubmit">提交</Button>
        </Form.Item>
      </Col>
    </Row>
  </Form>
</template>
<script lang="tsx" setup>
  import { computed, ref, unref, reactive, useAttrs } from 'vue';
  import { formProps } from 'ant-design-vue/es/form';
  import { cloneDeep, pick } from 'lodash-es';
  import { Form, Col, message, Button } from 'ant-design-vue';
  import SchemaFormItem from './schema-form-item.vue';
  import { schemaFormProps, type SchemaFormProps } from './schema-form';
  import type { FormInstance } from 'ant-design-vue';
  import { aFormPropKeys, type ColEx } from '@/components/core/schema-form';
    
  defineOptions({
    name: 'SchemaForm',
  });

  const props = defineProps(schemaFormProps);
  // 提供submit方法给父组件
  const emit = defineEmits(['submit']);
  const attrs = useAttrs();
    
  // 表单数据源
  const formModel = reactive(cloneDeep(props.initialValues));
    
  // 表单ref实例
  const schemaFormRef = ref<FormInstance>();
  // TODO 将props克隆一份,避免修改原有的props
  const formPropsRef = ref<SchemaFormProps>(cloneDeep(props));
  const formSchemasRef = computed(() => unref(formPropsRef).schemas || []);
    
  // 获取表单所有属性
  const getFormProps = computed(() => {
    return {
      ...attrs,
      ...formPropsRef.value,
    } as SchemaFormProps;
  });

  // 获取栅栏Row配置
  const getRowConfig = computed((): Recordable => {
    const { baseRowStyle = {}, rowProps } = unref(getFormProps);
    return {
      style: baseRowStyle,
      ...rowProps,
    };
  });

  // 获取操作列配置
  const actionColOpt = computed(() => {
    const { actionColOptions } = props;
    const actionColOpt: Partial<ColEx> = {
      style: { textAlign: 'right' },
      span: 4,
      ...actionColOptions,
    };
    return actionColOpt;
  });
  
  // 拿到rules,在之前先校验输入类型
  // const getRules = computed(() => {
    // // ......
  // }); // del
  
  // 点击提交
  const handleSubmit = async (e?: Event) => {
    // 屏蔽按钮自带事件(如:自带html-type: submit会触发form的submit方法)
    e && e.preventDefault();
    try {
      const values = await schemaFormRef.value?.validate();
      // 表单校验通过,可以提交表单
      console.log('validate success:');
      emit('submit', values);
    } catch (error: any) {
      message.error('验证失败!', 3);
      console.log('validate error:', error);
      return Promise.reject(error);
    }
  };

  // 提供属性和方法给父组件
  defineExpose({ submit: handleSubmit });
</script>

孙组件(Form-Item组件)

  • 路径:@/components/core/schema-form/src/hooks/schema-form-item.vue
  • 定义计算属性modelValue(用于将子组件(Form组件)双向绑定)
<template>
  <Col v-bind="schema.colProps">
    <Form.Item
      v-bind="{ ...(schema.formItemProps || {}) }"
      :label="schema.label"
      :name="schema.field"
      :label-col="{ style: { width: '100px' } }"
      :wrapper-col="{ style: { width: `calc(100% - 100px})` } }"
      :rules="getRules"
    >
      <Input v-model:value.trim="modelValue" />
    </Form.Item>
  </Col>
</template>

<script setup lang="tsx">
  import { computed, toRefs } from 'vue';
  import { Form, Col, Input } from 'ant-design-vue';
  import { schemaFormItemProps } from './schema-form-item';

  defineOptions({
    name: 'SchemaFormItem',
  });

  const props = defineProps(schemaFormItemProps);
  // const emit = defineEmits(['update:formModel']);
  const { schema } = toRefs(props);

  const modelValue = computed({
    get() {
      return props.formModel[schema.value.field];
    },
    set(val) {
      const target = props.formModel;
      target[schema.value.field] = val;
      // ant4中没有提供方法setFieldsValue手动设置表单项的值
      // 需要手动修改form绑定的model对象中的属性
      // 因此update:formModel无效
      // emit('update:formModel', target);
    },
  });

  // 拿到rules,在之前先校验输入类型
  const getRules = computed(() => {
    // ......
  }); // add
</script>

FormItem替换为可配置的组件

孙组件(Form-Item组件)

<template>
  <Col v-bind="schema.colProps">
    <Form.Item
      v-bind="{ ...(schema.formItemProps || {}) }"
      :label="schema.label"
      :name="schema.field"
      :label-col="{ style: { width: '100px' } }"
      :wrapper-col="{ style: { width: `calc(100% - 100px})` } }"
      :rules="getRules"
    >
      <!-- <Input v-model:value.trim="modelValue" /> --> <!-- del -->
      <component :is="getComponent" v-bind="getComponentProps" v-model:value.trim="modelValue" /> <!-- add -->
    </Form.Item>
  </Col>
</template>

<script setup lang="tsx">
  import { computed, toRefs, isVNode } from 'vue';
  import { Form, Col } from 'ant-design-vue';
  import { componentMap } from './componentMap';
  import { schemaFormItemProps } from './schema-form-item';
  import { isFunction, isNull, isString } from '@/utils/is';

  defineOptions({
    name: 'SchemaFormItem',
  });

  const props = defineProps(schemaFormItemProps);
  const { schema } = toRefs(props);

  const modelValue = computed({
    get() {
      return props.formModel[schema.value.field];
    },
    set(val) {
      const target = props.formModel;
      target[schema.value.field] = val;
    },
  });

  /**
   * @description 当前表单项组件
   */
  const getComponent = computed(() => {
    const component = props.schema.component;
    // TODO:componentMap中不存在该组件时,是否应该创建自定义组件?
    return isString(component) ? componentMap[component] : {};
  }); <!-- add -->

  /**
   * @description 表单组件props
   */
  const getComponentProps = computed(() => {
    const { schema } = props;
    let { componentProps = {} } = schema;

    // 为函数时,拿到函数中的返回对象,并传参
    if (isFunction(componentProps)) {
      // TODO:传参对象待优化
      componentProps = componentProps({} as any) ?? {};
    }

    // 为node节点时,将node节点中的props与配置项的componentProps合并
    if (isVNode(getComponent.value)) {
      Object.assign(componentProps, getComponent.value.props);
    }

    return componentProps;
  }); <!-- add -->

  // 拿到rules,在之前先校验输入类型
  const getRules = computed(() => {
    // ......
  });
</script>

add:componentMap.ts

  • 路径:@/components/core/schema-form/src/componentMap.ts
  • 用于定义可配置的FormItem组件名称的字典集合
import {
  Input,
} from 'ant-design-vue';

const componentMap = {
  Input,
  InputGroup: Input.Group,
  // ......
};

export type ComponentMapType = keyof typeof componentMap;

export { componentMap };

优化项

封装statemethodsevents的hook

子组件(Form组件)
<template>
  <Form ref="schemaFormRef" v-bind="pick(getFormProps, aFormPropKeys)" :model="formModel">
    <Row v-bind="getRowConfig">
      <template v-for="schemaItem in formSchemasRef" :key="schemaItem.field">
        <SchemaFormItem v-model:formModel="formModel" :schema="schemaItem" />
      </template>
      <Col v-bind="actionColOpt">
        <Form.Item>
          <Button @click="submit">提交</Button>
        </Form.Item>
      </Col>
    </Row>
  </Form>
</template>
<script lang="tsx" setup>
  import { computed, useAttrs } from 'vue';
  import { pick } from 'lodash-es';
  import { Form, Col, Button } from 'ant-design-vue';
  import SchemaFormItem from './schema-form-item.vue';
  import { useFormState, useFormMethods, useFormEvents } from './hooks';
  import { schemaFormProps } from './schema-form';
  import { aFormPropKeys, type ColEx } from '@/components/core/schema-form';

  defineOptions({
    name: 'SchemaForm',
  });

  const props = defineProps(schemaFormProps);
  // 提供submit方法给父组件
  const emit = defineEmits(['submit']);
  const attrs = useAttrs();

  // 表单内部状态
  const formState = useFormState({ props, attrs }); // add
  const { formModel, getRowConfig, schemaFormRef, getFormProps, formSchemasRef } = formState; // add

  // 表单内部方法
  const formMethods = useFormMethods({ ...formState }); // add
  const { initFormValues } = formMethods; // add

  // 初始化表单默认值
  initFormValues(); // add

  // a-form表单事件二次封装和扩展
  const formEvents = useFormEvents({ ...formState, emit }); // add
  const { submit } = formEvents; // add

  // 表单数据源
  // const formModel = reactive(cloneDeep(props.initialValues)); // del

  // 表单ref实例
  // const schemaFormRef = ref<FormInstance>(); // del
  // TODO 将props克隆一份,避免修改原有的props
  // const formPropsRef = ref<SchemaFormProps>(cloneDeep(props)); // del
  // const formSchemasRef = computed(() => unref(formPropsRef).schemas || []); // del

  // 获取表单所有属性
  // const getFormProps = computed(() => {
  //   return {
  //     ...attrs,
  //     ...formPropsRef.value,
  //   } as SchemaFormProps;
  // }); // del

  // 获取栅栏Row配置
  // const getRowConfig = computed((): Recordable => {
  //   const { baseRowStyle = {}, rowProps } = unref(getFormProps);
  //   return {
  //     style: baseRowStyle,
  //     ...rowProps,
  //   };
  // }); // del

  // 获取操作列配置
  const actionColOpt = computed(() => {
    const { actionColOptions } = props;
    const actionColOpt: Partial<ColEx> = {
      style: { textAlign: 'right' },
      span: 4,
      ...actionColOptions,
    };
    return actionColOpt;
  });

  // // 点击提交
  // const submit = async (e?: Event) => {
  //   // 屏蔽按钮自带事件(如:自带html-type: submit会触发form的submit方法)
  //   e && e.preventDefault();
  //   try {
  //     const values = await schemaFormRef.value?.validate();
  //     // 表单校验通过,可以提交表单
  //     console.log('validate success:');
  //     emit('submit', values);
  //   } catch (error: any) {
  //     message.error('验证失败!', 3);
  //     console.log('validate error:', error);
  //     return Promise.reject(error);
  //   }
  // }; // del

  // 提供属性和方法给父组件
  defineExpose({ submit });
</script>
add:useFormState
  • 路径:@/components/core/schema-form/src/hooks/useFormState.ts
import { computed, reactive, ref, unref } from 'vue';
import { cloneDeep } from 'lodash-es';
import type { SetupContext } from 'vue';
import type { SchemaFormProps } from '../schema-form';
import type { FormInstance } from 'ant-design-vue';

export type FormState = ReturnType<typeof useFormState>;

export type useFormStateParams = {
  props: SchemaFormProps;
  attrs: SetupContext['attrs'];
};

export const useFormState = ({ props, attrs }: useFormStateParams) => {
  // TODO 将formSchema克隆一份,避免修改原有的formSchema
  const formPropsRef = ref<SchemaFormProps>(cloneDeep(props));
  // 表单数据源
  const formModel = reactive(cloneDeep(props.initialValues || {}));
  // 表单实例
  const schemaFormRef = ref<FormInstance>();

  // 获取表单所有属性
  const getFormProps = computed(() => {
    return {
      ...attrs,
      ...formPropsRef.value,
    } as SchemaFormProps;
  });

  // 获取栅栏Row配置
  const getRowConfig = computed((): Recordable => {
    const { baseRowStyle = {}, rowProps } = unref(getFormProps);
    return {
      style: baseRowStyle,
      ...rowProps,
    };
  });

  return {
    formModel,
    schemaFormRef,
    formPropsRef,
    getFormProps,
    getRowConfig,
    formSchemasRef: computed(() => unref(formPropsRef).schemas || []),
  };
};
add:useFormMethods
import { unref } from 'vue';
import type { FormState } from './useFormState';
import { isNullOrUnDef } from '@/utils/is';

type UseFormMethodsContext = FormState;

export type FormMethods = ReturnType<typeof useFormMethods>;

export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const { formModel, formPropsRef } = formMethodsContext;

  // 初始化数据(用来初始化配置项中的defaultValue数据)
  const initFormValues = () => {
    unref(formPropsRef).schemas?.forEach((item) => {
      const { defaultValue } = item;
      if (!isNullOrUnDef(defaultValue)) {
        formModel[item.field] = defaultValue;
      }
    });
  };

  return {
    initFormValues,
  };
};
add:useFormEvents
import { unref } from 'vue';
import type { FormState } from './index';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { SchemaFormEmitFn } from '../schema-form';

type UseFormActionContext = FormState & {
  emit: SchemaFormEmitFn;
};

export type FormEvents = ReturnType<typeof useFormEvents>;

export function useFormEvents(formActionContext: UseFormActionContext) {
  const { emit, schemaFormRef } = formActionContext;

  async function validate(nameList?: NamePath[] | undefined) {
    return await schemaFormRef.value?.validate(nameList)!;
  }

  async function handleSubmit(e?: Event) {
    e && e.preventDefault();
    const formEl = unref(schemaFormRef);
    if (!formEl) return;
    try {
      const values = await validate();
      emit('submit', values);
      return values;
    } catch (error: any) {
      return Promise.reject(error);
    }
  }

  return {
    submit: handleSubmit,
  };
}

封装formInstance表单实例

  • 路径:@/components/core/schema-form/src/schema-form.vue
  • 包含所有表单状态、方法、二次封装和扩展事件
  • provide/inject向下传递给FormItem组件、schemas配置项(用于字段联动等场景)
<template>
  <!-- ...... -->
</template>
<script lang="tsx" setup>
  import {
    useFormState,
    useFormMethods,
    useFormEvents,
    createFormContext,
    type SchemaFormType,
  } from './hooks';
  // ......
    
  // 当前组件所有的状态和方法
  const instance = {
    ...formState,
    ...formMethods,
    ...formEvents,
  } as SchemaFormType; // add

  createFormContext(instance); // add

  // 提供属性和方法给父组件
  defineExpose({ submit });
</script>

封装FormAction

子组件(Form组件)
<template>
  <Form ref="schemaFormRef" v-bind="pick(getFormProps, aFormPropKeys)" :model="formModel">
    <Row v-bind="getRowConfig">
      <template v-for="schemaItem in formSchemasRef" :key="schemaItem.field">
        <SchemaFormItem v-model:formModel="formModel" :schema="schemaItem" />
      </template>
      <!-- <Col v-bind="actionColOpt">
        <Form.Item>
          <Button @click="submit">提交</Button>
        </Form.Item>
      </Col> -->
      <FormAction v-bind="getFormActionBindProps"></FormAction>
      <!-- add -->
    </Row>
  </Form>
</template>
<script lang="tsx" setup>
  import { useAttrs } from 'vue';
  import { pick } from 'lodash-es';
  import { Form } from 'ant-design-vue';
  import SchemaFormItem from './schema-form-item.vue';
  import FormAction from './form-action.vue';
  import {
    useFormState,
    useFormMethods,
    useFormEvents,
    createFormContext,
    type SchemaFormType,
  } from './hooks';
  import { schemaFormProps } from './schema-form';
  import { aFormPropKeys } from '@/components/core/schema-form';

  defineOptions({
    name: 'SchemaForm',
  });

  const props = defineProps(schemaFormProps);
  // 提供submit方法给父组件
  const emit = defineEmits(['submit']);
  const attrs = useAttrs();

  // 表单内部状态
  const formState = useFormState({ props, attrs });
  const {
    formModel,
    getRowConfig,
    schemaFormRef,
    getFormProps,
    getFormActionBindProps,
    formSchemasRef,
  } = formState; // add

  // 表单内部方法
  const formMethods = useFormMethods({ ...formState });
  const { initFormValues } = formMethods;

  // 初始化表单默认值
  initFormValues();

  // a-form表单事件二次封装和扩展
  const formEvents = useFormEvents({ ...formState, emit });
  const { submit } = formEvents;

  // 当前组件所有的状态和方法
  const instance = {
    ...formState,
    ...formMethods,
    ...formEvents,
  } as SchemaFormType; // add

  // 获取操作列配置
  // const actionColOpt = computed(() => {
  //   const { actionColOptions } = props;
  //   const actionColOpt: Partial<ColEx> = {
  //     style: { textAlign: 'right' },
  //     span: 4,
  //     ...actionColOptions,
  //   };
  //   return actionColOpt;
  // }); // del

  createFormContext(instance); // add

  // 提供属性和方法给父组件
  defineExpose({ submit });
</script>
孙组件(Form-Action组件)
<template>
  <Col v-bind="actionColOpt">
    <Form.Item>
      <Button @click="submit">提交</Button>
    </Form.Item>
  </Col>
</template>
<script lang="ts" setup>
  import { computed } from 'vue';
  import { Form, Col } from 'ant-design-vue';
  import { useFormContext } from './hooks/useFormContext';
  import { type ColEx } from '@/components/core/schema-form';

  defineOptions({
    name: 'FormAction',
  });

  const props = defineProps({
    actionColOptions: {
      type: Object as PropType<Partial<ColEx>>,
      default: () => ({}),
    },
  });

  const { submit } = useFormContext();

  const actionColOpt = computed(() => {
    const { actionColOptions } = props;
    const actionColOpt: Partial<ColEx> = {
      style: { textAlign: 'right' },
      span: 4,
      ...actionColOptions,
    };
    return actionColOpt;
  });
</script>
<style lang="less" scoped></style>

字段联动

  • 说明:比如,修改省份后,城市选择的值清空,下拉列表联动变化
  • 实现:componentProps函数需要传入formModel(表单绑定的model)和formInstance(表单实例,包含所有表单状态、方法、二次封装和扩展事件)。当省份改变时,onChange事件中手动清空formModel中城市的值,并且调用formInstance实例中的updateSchema二次封装事件更新城市的下拉列表
孙组件(Form-Item组件)
  • FormItem中 props传递formModel、formInstance给表单组件
<script setup lang="tsx">
const getValues = computed<any>(() => {
  const { formModel, schema } = props;

  return {
    field: schema.field,
    formInstance: formContext,
    formModel,
    values: {
      ...formModel,
    } as Recordable,
    schema,
  };
});

/**
 * @description 表单组件props
 */
const getComponentProps = computed(() => {
  const { schema } = props;
  let { componentProps = {} } = schema;

  if (isFunction(componentProps)) {
    componentProps = componentProps(unref(getValues)) ?? {};
  }

  return componentProps;
});
</script>
form-schema.tsx
  • form-schema中操作formModel、formInstance
{
    // .......
    componentProps: ({ formModel, formInstance }) => {
      return {
        options: provincesOptions,
        placeholder: '省份与城市联动',
        onChange: (e: any) => {
          formModel.city = undefined; //  清空字段的值
          const { updateSchema } = formInstance;
          updateSchema({
            field: 'city',
            componentProps: {
              options: citiesOptionsData, // 设置关联字段的props
            },
          });
        },
      };
    },
}

useFormMethods增强

存储所有FormItem组件实例

孙组件(Form-Item组件)
<template>
  <!-- ....... -->
    <Form.Item>
      <component :ref="setItemRef(schema.field)">
      </component>
    </Form.Item>
  <!-- ....... -->
</template>
<script setup lang="tsx">
  // ......
  const { setItemRef } = useFormContext();
  // ......
</script>
useFormState.ts
export const useFormState = ({ props, attrs }: useFormStateParams) => {
  // 将所有的表单组件实例保存起来
  const compRefMap = new Map<string, DefineComponent<any>>();
  // ......
}
useFormMethods.ts
export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    compRefMap,
  } = formMethodsContext;
    
  // 将所有的表单组件实例保存起来, 方便外面通过表单组件实例操作
  const setItemRef = (field: string) => {
    return (el) => {
      if (el) {
        compRefMap.set(field, el);
      }
    };
  };
  // ......
}

设置某个字段的值

useFormMethods.ts
export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    formModel,
    cacheFormModel,
    getFormProps,
  } = formMethodsContext;
    
  // 设置某个字段的值
  const setFormModel = (key: Key, value: any) => {
    formModel[key] = value;
    // 缓存的表单值,用于恢复form-item v-if为true后的值
    cacheFormModel[key] = value;
    const { validateTrigger } = unref(getFormProps);
    // 先给表单model赋值,再检验值
    if (!validateTrigger || validateTrigger === 'change') {
      schemaFormRef.value?.validateFields([key]);
    }
  };
  // ......
}

删除某个字段

export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    formModel,
  } = formMethodsContext;

  // 删除某个字段
  const delFormModel = (key: Key) => {
    return Reflect.deleteProperty(formModel, key);
  };
  // ......
}

设置Form的props

export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    formPropsRef,
  } = formMethodsContext;

  // 更新Form的props
  const setSchemaFormProps = (formProps: Partial<SchemaFormProps>) => {
    formPropsRef.value = deepMerge(unref(formPropsRef) || {}, formProps);
  };
  // ......
}

处理Form的model中数据列的值

  • 对时间格式化(时间为dayjs对象时,转换为date?.format?.('YYYY-MM-DD HH:mm:ss'))

  • 使用场景:表格查询时,需要传Form的model给接口,并且可能需要对Form的model数据进行处理

  • 数据处理:清除字符串空格

  • 数据处理:dayjs时间对象转为字符串时间

  • 数据处理:dayjs时间对象数组转为字符串时间,并新增开始、结束时间的新字段到model中,并删除原先的时间对象数组字段。需要在可配置对象中传入fieldMapToTime数组,定义要新增、删除的字段名,以及字符串时间的格式

useFormMethods.ts
export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    getFormProps,
  } = formMethodsContext;
    

  // 处理Form数据列的值
  function handleFormValues(values: Recordable) {
    if (!isObject(values)) {
      return {};
    }
    const res: Recordable = {};
    for (const item of Object.entries(values)) {
      let [, value] = item;
      const [key] = item;
      if (!key || (isArray(value) && value.length === 0) || isFunction(value)) {
        continue;
      }
      const transformDateFunc = unref(getFormProps).transformDateFunc;
      if (isObject(value)) {
        value = transformDateFunc?.(value);
      }

      if (isArray(value) && value[0]?.format && value[1]?.format) {
        value = value.map((item) => transformDateFunc?.(item));
      }
      // Remove spaces
      if (isString(value)) {
        value = value.trim();
      }
      set(res, key, value);
    }
    return handleRangeTimeValue(res);
  }

  /**
   * @description: Processing time interval parameters
   */
  function handleRangeTimeValue(values: Recordable) {
    const fieldMapToTime = unref(getFormProps).fieldMapToTime;

    if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) {
      return values;
    }

    for (const [field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD'] of fieldMapToTime) {
      if (!field || !startTimeKey || !endTimeKey || !values[field]) {
        continue;
      }

      const [startTime, endTime]: string[] = values[field];

      values[startTimeKey] = dateUtil(startTime).format(format);
      values[endTimeKey] = dateUtil(endTime).format(format);
      Reflect.deleteProperty(values, field);
    }

    return values;
  }
  // ......
}
父组件(业务组件)
<template>
  <!-- ....... -->
</template>
<script setup lang="tsx">
  // ......
  const [SchemaForm, dynamicFormRef] = useForm({
    fieldMapToTime: [['fieldTime', ['startTime', 'endTime'], 'YYYY-MM']],
  });
  // ......
</script>
schema-form.ts
export const schemaFormProps = {
  // ......
  /** 转化时间 */
  transformDateFunc: {
    type: Function as PropType<Fn>,
    default: (date: any) => {
      return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date;
    },
  },
  // ......
}

useFormEvents增强

拿到处理后的Form的model中数据列的值

export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    formModel,
    schemaFormRef,
  } = formMethodsContext;

  function getFieldsValue(): Recordable {
    const formEl = unref(schemaFormRef);
    if (!formEl) return {};
    return handleFormValues(toRaw(unref(formModel)));
  }
  // ......
}

判断FormItem的组件是否是时间相关组件

export const useFormMethods = (formMethodsContext: UseFormMethodsContext) => {
  const {
    formModel,
    schemaFormRef,
  } = formMethodsContext;

  function itemIsDateType(key: string) {
    return unref(formPropsRef).schemas?.some((item) => {
      return item.field === key && isString(item.component)
        ? dateItemType.includes(item.component)
        : false;
    });
  }
  // ......
}

设置表单model的值

export function useFormEvents(formActionContext: UseFormActionContext) {
  const {
    formSchemasRef,
  } = formActionContext;
    
   /**
   * @description: 设置表单整个model的数据
   * @description: 遍历字段值,赋值给表单model下的字段
   */
  async function setFieldsValue(values: Recordable): Promise<void> {
    const schemas = unref(formSchemasRef);
    const fields = schemas.map((item) => item.field).filter(Boolean);

    Object.assign(cacheFormModel, values);

    const validKeys: string[] = [];
    // 遍历字段值,赋值给表单model下的字段
    Object.keys(values).forEach((key) => {
      const schema = schemas.find((item) => item.field === key);
      let value = values[key];

      const hasKey = Reflect.has(values, key);
      // input等组件将值转成字符串
      if (isString(schema?.component)) {
        value = handleInputNumberValue(schema?.component, value);
      }
      // 表单model中必须存在该字段
      if (hasKey && fields.includes(key)) {
        // 处理时间相关的数据
        if (itemIsDateType(key)) {
          // 时间范围类型数组,遍历转成标准时间的数组,再赋值给表单model
          if (Array.isArray(value)) {
            const arr: any[] = [];
            for (const ele of value) {
              arr.push(ele ? dayjs(ele) : null);
            }
            formModel[key] = arr;
          } else {
            // 时间类型数据,如果schema可配置项定义了valueFormat,则调用,否则转成标准时间
            const { componentProps } = schema || {};
            let _props = componentProps as any;
            if (isFunction(componentProps)) {
              _props = _props({ formPropsRef, formModel });
            }
            formModel[key] = value ? (_props?.valueFormat ? value : dayjs(value)) : null;
          }
        } else {
          formModel[key] = value;
        }
        validKeys.push(key);
      }
    });
    // 校验字段值
    validateFields(validKeys);
  }
}

重置schema可配置项

export function useFormEvents(formActionContext: UseFormActionContext) {
  const {
    formPropsRef,
  } = formActionContext;
    
  /**
   * @description: 重置schema可配置项
   */
  async function resetSchema(data: Partial<FormSchema> | Partial<FormSchema>[]) {
    let updateData: Partial<FormSchema>[] = [];
    if (isObject(data)) {
      updateData.push(data as FormSchema);
    }
    if (isArray(data)) {
      updateData = [...data];
    }
    // @ts-ignore
    unref(formPropsRef).schemas = updateData as FormSchema[];
  }
}

插入指定行

export function useFormEvents(formActionContext: UseFormActionContext) {
  const {
    formPropsRef,
    formSchemasRef,
  } = formActionContext;
    
   /**
   * @description: 插入到指定 filed 后面,如果没传指定 field,则插入到最后,当 first = true 时插入到第一个位置
   */
  async function appendSchemaByField(schemaItem: FormSchema, prefixField?: string, first = false) {
    const schemaList = cloneDeep(unref(formSchemasRef));

    const index = schemaList.findIndex((schema) => schema.field === prefixField);

    if (!prefixField || index === -1 || first) {
      first ? schemaList.unshift(schemaItem) : schemaList.push(schemaItem);
      formModel[schemaItem.field] = schemaItem.defaultValue;
      formPropsRef.value.schemas = schemaList;
      return;
    }
    if (index !== -1) {
      schemaList.splice(index + 1, 0, schemaItem);
    }
    formModel[schemaItem.field] = schemaItem.defaultValue;
    formPropsRef.value.schemas = schemaList;
  }
}

根据 field 删除 Schema

export function useFormEvents(formActionContext: UseFormActionContext) {
  const {
    formPropsRef,
    formSchemasRef,
  } = formActionContext;
    
   /**
   * @description: 根据 field 删除 Schema
   */
  async function removeSchemaByField(fields: string | string[]): Promise<void> {
    // @ts-ignore
    const schemaList = cloneDeep<FormSchema[]>(unref(formSchemasRef));

    if (!fields) {
      return;
    }

    let fieldList: string[] = isString(fields) ? [fields] : fields;
    if (isString(fields)) {
      fieldList = [fields];
    }
    for (const field of fieldList) {
      if (isString(field)) {
        const index = schemaList.findIndex((schema) => schema.field === field);
        if (index !== -1) {
          Reflect.deleteProperty(formModel, field);
          schemaList.splice(index, 1);
        }
      }
    }
    formPropsRef.value.schemas = schemaList;
  }
}

根据 field 查找 Schema

export function useFormEvents(formActionContext: UseFormActionContext) {
  const {
    formSchemasRef,
  } = formActionContext;
    
  /**
   * @description: 根据 field 查找 Schema
   */
  function getSchemaByFiled(fields: string | string[]): FormSchema | undefined {
    const schemaList = unref(formSchemasRef);
    const fieldList = ([] as string[]).concat(fields);
    return schemaList.find((schema) => fieldList.includes(schema.field));
  }
}

更新Schema

export function useFormEvents(formActionContext: UseFormActionContext) {
   /**
   * @description  更新formItemSchema
   */
  const updateSchema = (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
    let updateData: Partial<FormSchema>[] = [];
    if (isObject(data)) {
      updateData.push(data as FormSchema);
    }
    if (isArray(data)) {
      updateData = [...data];
    }

    const hasField = updateData.every(
      (item) => item.component === 'Divider' || (Reflect.has(item, 'field') && item.field),
    );

    if (!hasField) {
      return;
    }
    const schemas: FormSchema[] = [];
    updateData.forEach((item) => {
      unref(formSchemasRef).forEach((val) => {
        if (val.field === item.field) {
          const newSchema = deepMerge(val, item);
          if (originComponentPropsFnMap.has(val.field)) {
            const originCompPropsFn = originComponentPropsFnMap.get(val.field)!;
            const compProps = { ...newSchema.componentProps };
            newSchema.componentProps = (opt) => {
              const res = {
                ...originCompPropsFn(opt),
                ...compProps,
              };
              return res;
            };
          }
          schemas.push(newSchema);
        } else {
          schemas.push(val);
        }
      });
    });

    unref(formPropsRef).schemas = uniqBy(schemas, 'field');
  };
}

重置整个model的值

使用场景

  • 弹出框modal取消、关闭时
  • 弹出框modal提交完成后
export function useFormEvents(formActionContext: UseFormActionContext) {
   /**
   * @description  重置整个model的值
   */
  async function resetFields(): Promise<void> {
    const { resetFunc, submitOnReset } = unref(getFormProps);
    resetFunc && isFunction(resetFunc) && (await resetFunc());

    Object.keys(formModel).forEach((key) => {
      formModel[key] = defaultFormValues[key];
    });

    emit('reset', formModel);
    // 暂时没有使用场景
    // submitOnReset && handleSubmit();
    setTimeout(clearValidate);
  }
}

校验fields

export function useFormEvents(formActionContext: UseFormActionContext) {
  async function validateFields(nameList?: NamePath[] | undefined) {
    return schemaFormRef.value?.validateFields(nameList);
  }

  async function validate(nameList?: NamePath[] | undefined) {
    return await schemaFormRef.value?.validate(nameList)!;
  }
}

清空校验

export function useFormEvents(formActionContext: UseFormActionContext) {
  async function clearValidate(name?: string | string[]) {
    await schemaFormRef.value?.clearValidate(name);
  }
}

滚动到某个field

export function useFormEvents(formActionContext: UseFormActionContext) {
  async function scrollToField(name: NamePath, options?: ScrollOptions | undefined) {
    await schemaFormRef.value?.scrollToField(name, options);
  }
}

提交

export function useFormEvents(formActionContext: UseFormActionContext) {
  async function handleSubmit(e?: Event) {
    e && e.preventDefault();
    const { submitFunc } = unref(getFormProps);
    if (submitFunc && isFunction(submitFunc)) {
      await submitFunc();
      return;
    }
    const formEl = unref(schemaFormRef);
    if (!formEl) return;
    try {
      const values = await validate();
      const res = handleFormValues(values);
      emit('submit', res);
      return res;
    } catch (error: any) {
      return Promise.reject(error);
    }
  }
}

回车提交

export function useFormEvents(formActionContext: UseFormActionContext) {
  const handleEnterPress = (e: KeyboardEvent) => {
    const { autoSubmitOnEnter } = unref(formPropsRef);
    if (!autoSubmitOnEnter) return;
    if (e.key === 'Enter' && e.target && e.target instanceof HTMLElement) {
      const target: HTMLElement = e.target as HTMLElement;
      if (target && target.tagName && target.tagName.toUpperCase() == 'INPUT') {
        handleSubmit(e);
      }
    }
  };
}

schema-form-item增强

处理label和value的col

<template>
  <!-- ....... -->
    <Form.Item :label-col="itemLabelWidthProp.labelCol" :wrapper-col="itemLabelWidthProp.wrapperCol">
  	  <!-- ....... -->
    </Form.Item>
  <!-- ....... -->
</template>
import { computed, unref } from 'vue';
import type { Ref } from 'vue';
import type { FormSchema } from '../types/form';
import type { SchemaFormProps } from '../schema-form';
import { isNumber } from '@/utils/is';

export function useItemLabelWidth(schemaRef: Ref<FormSchema>, formPropsRef: Ref<SchemaFormProps>) {
  return computed(() => {
    const schemaItem = unref(schemaRef);
    const { labelCol = {}, wrapperCol = {} } = schemaItem.formItemProps || {};
    const { labelWidth, disabledLabelWidth } = schemaItem;

    const {
      labelWidth: globalLabelWidth,
      labelCol: globalLabelCol,
      wrapperCol: globWrapperCol,
    } = unref(formPropsRef);

    // 如果labelWidth是全局设置的,则会设置所有项
    if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) {
      labelCol.style = {
        textAlign: 'left',
      };
      return { labelCol, wrapperCol };
    }
    let width = labelWidth || globalLabelWidth;
    const col = { ...globalLabelCol, ...labelCol };
    const wrapCol = { ...globWrapperCol, ...wrapperCol };

    if (width) {
      width = isNumber(width) ? `${width}px` : width;
    }

    return {
      labelCol: { style: { width }, ...col },
      wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapCol },
    };
  });
}

绑定值支持深度嵌套对象

<template>
  <!-- ....... -->
    <Form.Item :name="namePath">
      <component v-model:value="modelValue">
      </component>
    </Form.Item>
  <!-- ....... -->
</template>
<script setup lang="tsx">
  import { schemaFormItemProps } from './schema-form-item';
  import { isArray } from '@/utils/is';
    
  // ......
  const props = defineProps(schemaFormItemProps);
  const { schema } = toRefs(props);

  const namePath = computed<string[]>(() => {
    return isArray(schema.value.field) ? schema.value.field : schema.value.field.split('.');
  });

  const modelValue = computed({
    get() {
      return namePath.value.reduce((prev, field) => prev?.[field], props.formModel);
    },
    set(val) {
      const namePath = schema.value.field.split('.');
      const prop = namePath.pop()!;
      const target = namePath.reduce((prev, field) => (prev[field] ??= {}), props.formModel);
      target[prop] = val;
      emit('update:formModel', props.formModel);
    },
  });
  // ......
</script>

处理不同component类型的modelValue

<template>
  <!-- ....... -->
    <Form.Item :name="namePath">
      <component v-model:[modelValueType]="modelValue">
      </component>
    </Form.Item>
  <!-- ....... -->
</template>
<script setup lang="tsx">
  import { schemaFormItemProps } from './schema-form-item';
    
  const props = defineProps(schemaFormItemProps);
  const { schema } = toRefs(props);
  // ......

  const modelValueType = computed<string>(() => {
    const { component, componentProps } = schema.value;
    if (!isFunction(componentProps) && componentProps?.vModelKey) {
      return componentProps.vModelKey;
    }
    const isCheck = isString(component) && ['Switch', 'Checkbox'].includes(component);
    const isUpload = component === 'Upload';
    return {
      true: 'value',
      [`${isCheck}`]: 'checked',
      [`${isUpload}`]: 'file-list',
    }['true'];
  });
</script>

控制表单显示隐藏

<template>
  <Col v-if="getShow.isIfShow" v-show="getShow.isShow">
  </Col>
</template>

<script setup lang="tsx">
  import { schemaFormItemProps } from './schema-form-item';

  const props = defineProps(schemaFormItemProps);

  const { schema } = toRefs(props);
  
  const getValues = computed<RenderCallbackParams>(() => {
    // ......
  });

  const getShow = computed<{ isShow: boolean; isIfShow: boolean }>(() => {
    const { vShow, vIf } = unref(schema);

    let isShow = true;
    let isIfShow = true;

    if (isBoolean(vShow)) {
      isShow = vShow;
    }
    if (isBoolean(vIf)) {
      isIfShow = vIf;
    }
    if (isFunction(vShow)) {
      isShow = vShow(unref(getValues));
    }
    if (isFunction(vIf)) {
      isIfShow = vIf(unref(getValues));
    }

    return { isShow, isIfShow };
  });
</script>

控制component是否禁用

<template>
  <Col>
    <Form.Item>
      <component
        :disabled="getDisable"
      >
      </component>
    </Form.Item>
  </Col>
</template>
<script setup lang="tsx">
  import { schemaFormItemProps } from './schema-form-item';
  import { useFormContext } from './hooks/useFormContext';
  
  const props = defineProps(schemaFormItemProps);

  // schemaForm组件实例
  const formContext = useFormContext();
  const { formPropsRef } = formContext;

    /**
   * @description 表单组件props
   */
  const getComponentProps = computed(() => {
    // .......
  });

  const getDisable = computed(() => {
    const { disabled: globDisabled } = unref(formPropsRef);
    const { dynamicDisabled } = props.schema;
    const { disabled: itemDisabled = false } = unref(getComponentProps);
    let disabled = !!globDisabled || itemDisabled;
    if (isBoolean(dynamicDisabled)) {
      disabled = dynamicDisabled;
    }
    // 异步禁用
    if (isFunction(dynamicDisabled)) {
      disabled = dynamicDisabled(unref(getValues));
    }
    return disabled;
  });
</script>

获取当前表单项组件节点

<template>
  <Col>
    <Form.Item>
      <component
        :is="getComponent"
        v-else-if="getComponent"
      >
      </component>
    </Form.Item>
  </Col>
</template>
<script setup lang="tsx">
  import { schemaFormItemProps } from './schema-form-item';

  const props = defineProps(schemaFormItemProps);

  const vnodeFactory = (
    component: FormSchema['componentSlots'] | FormSchema['component'],
    values = unref(getValues),
  ) => {
    if (isString(component)) {
      return <>{component}</>;
    } else if (isVNode(component)) {
      return component;
    } else if (isFunction(component)) {
      return vnodeFactory((component as CustomRenderFn)(values));
    } else if (isObject(component)) {
      const compKeys = Object.keys(component);
      // 如果是组件对象直接return
      if (compKeys.some((n) => n.startsWith('_') || ['setup', 'render'].includes(n))) {
        return component;
      }
      return compKeys.reduce<Recordable<CustomRenderFn>>((slots, slotName) => {
        slots[slotName] = (...rest: any) => vnodeFactory(component[slotName], ...rest);
        return slots;
      }, {});
    }
    return component;
  };

  /**
   * @description 当前表单项组件
   */
  const getComponent = computed(() => {
    const component = props.schema.component;
    return isString(component)
      ? componentMap[component] ?? vnodeFactory(component)
      : vnodeFactory(component);
  });
</script>

处理label

<script setup lang="tsx">
  const getLabel = computed(() => {
    const label = props.schema.label;
    return isFunction(label) ? label(unref(getValues)) : label;
  });
</script>

表单组件事件

<template>
  <Col>
    <Form.Item>
      <component
        v-on="componentEvents"
      >
      </component>
    </Form.Item>
  </Col>
</template>
<script setup lang="tsx">
  /**
   * @description 表单组件事件
   */
  const componentEvents = computed(() => {
    const componentProps = props.schema?.componentProps || {};
    return Object.keys(componentProps).reduce((prev, key) => {
      if (/on([A-Z])/.test(key)) {
        // eg: onChange => change
        const eventKey = key.replace(/on([A-Z])/, '$1').toLocaleLowerCase();
        prev[eventKey] = componentProps[key];
      }
      return prev;
    }, {});
  });
</script>

渲染label帮助提示

<template>
  <Col>
    <Form.Item>
      <component
        v-on="componentEvents"
      >
      </component>
    </Form.Item>
  </Col>
</template>
<script setup lang="tsx">
  const renderLabelHelpMessage = computed(() => {
    const { helpMessage, helpComponentProps, subLabel } = props.schema;
    const renderLabel = subLabel ? (
      <span>
        {getLabel.value} <span class="text-secondary">{subLabel}</span>
      </span>
    ) : (
      vnodeFactory(getLabel.value)
    );
    const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage;
    if (!getHelpMessage || (Array.isArray(getHelpMessage) && getHelpMessage.length === 0)) {
      return renderLabel;
    }
    return (
      <span>
        {renderLabel}
        <BasicHelp placement="top" class="mx-1" text={getHelpMessage} {...helpComponentProps} />
      </span>
    );
  });
</script>

远程请求

<script setup lang="tsx">
  const fetchRemoteData = async (request) => {
    if (request) {
      const { component } = unref(schema);

      try {
        const newSchema = {
          ...unref(schema),
          loading: true,
          componentProps: {
            ...unref(getComponentProps),
            options: [],
          } as ComponentProps,
        };
        updateSchema(newSchema);
        const result = await request(unref(getValues));
        if (['Select', 'RadioGroup', 'CheckBoxGroup'].some((n) => n === component)) {
          newSchema.componentProps.options = result;
        } else if (['TreeSelect', 'Tree'].some((n) => n === component)) {
          newSchema.componentProps.treeData = result;
        }
        if (newSchema.componentProps) {
          newSchema.componentProps.requestResult = result;
        }
        newSchema.loading = false;
        updateSchema(newSchema);
      } finally {
        nextTick(() => {
          schema.value.loading = false;
        });
      }
    }
  };

  const initRequestConfig = () => {
    const request = getComponentProps.value.request;
    if (request) {
      if (isFunction(request)) {
        fetchRemoteData(request);
      } else {
        const { watchFields = [], options = {}, wait = 0, callback } = request;
        const params = watchFields.map((field) => () => props.formModel[field]);
        watch(
          params,
          debounce(() => {
            fetchRemoteData(callback);
          }, wait),
          {
            ...options,
          },
        );
      }
    }
  };
</script>

校验增强

<script setup lang="tsx">
  function setComponentRuleType(
    rule: RuleObject,
    component: ComponentMapType,
    valueFormat: string,
  ) {
    if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) {
      rule.type = valueFormat ? 'string' : 'object';
    } else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) {
      rule.type = 'array';
    } else if (['InputNumber'].includes(component)) {
      rule.type = 'number';
    }
  }
  
  const getRules = computed(() => {
    const {
      rules: defRules = [],
      component,
      rulesMessageJoinLabel,
      dynamicRules,
      required,
    } = props.schema;

    if (isFunction(dynamicRules)) {
      return dynamicRules(unref(getValues)) as RuleObject[];
    }

    let rules = cloneDeep<RuleObject[]>(defRules);
    const { rulesMessageJoinLabel: globalRulesMessageJoinLabel } = unref(formPropsRef);

    const joinLabel = Reflect.has(unref(formPropsRef), 'rulesMessageJoinLabel')
      ? rulesMessageJoinLabel
      : globalRulesMessageJoinLabel;
    const defaultMsg = isString(component)
      ? `${createPlaceholderMessage(component, getLabel.value)}${joinLabel ? getLabel.value : ''}`
      : undefined;

    function validator(rule: any, value: any) {
      const msg = rule.message || defaultMsg;

      if (value === undefined || isNull(value)) {
        // 空值
        return Promise.reject(msg);
      } else if (Array.isArray(value) && value.length === 0) {
        // 数组类型
        return Promise.reject(msg);
      } else if (typeof value === 'string' && value.trim() === '') {
        // 空字符串
        return Promise.reject(msg);
      } else if (
        typeof value === 'object' &&
        Reflect.has(value, 'checked') &&
        Reflect.has(value, 'halfChecked') &&
        Array.isArray(value.checked) &&
        Array.isArray(value.halfChecked) &&
        value.checked.length === 0 &&
        value.halfChecked.length === 0
      ) {
        // 非关联选择的tree组件
        return Promise.reject(msg);
      }
      return Promise.resolve();
    }

    const getRequired = isFunction(required) ? required(unref(getValues)) : required;

    if ((!rules || rules.length === 0) && getRequired) {
      rules = [{ required: getRequired, validator }];
    }

    const requiredRuleIndex: number = rules.findIndex(
      (rule) => Reflect.has(rule, 'required') && !Reflect.has(rule, 'validator'),
    );

    if (requiredRuleIndex !== -1) {
      const rule = rules[requiredRuleIndex];

      if (component && isString(component)) {
        if (!Reflect.has(rule, 'type')) {
          rule.type = component === 'InputNumber' ? 'number' : 'string';
        }

        rule.message = rule.message || defaultMsg;

        if (component.includes('Input') || component.includes('Textarea')) {
          rule.whitespace = true;
        }
        const valueFormat = unref(getComponentProps)?.valueFormat;
        setComponentRuleType(rule, component, valueFormat);
      }
    }

    // Maximum input length rule check
    const characterInx = rules.findIndex((val) => val.max);
    if (characterInx !== -1 && !rules[characterInx].validator) {
      rules[characterInx].message =
        rules[characterInx].message ||
        t('component.form.maxTip', [rules[characterInx].max] as Recordable);
    }

    return rules;
  });
</script>