Ben Lin
2024-10-28 08abfcfea8247c394b2034cad59734846b403dd9
src/components/Form/src/BasicForm.vue
@@ -37,269 +37,311 @@
    </Row>
  </Form>
</template>
<script lang="ts">
  import type { FormActionType, FormProps, FormSchema } from './types/form';
<script lang="ts" setup>
  import type { FormActionType, FormProps, FormSchemaInner as FormSchema } from './types/form';
  import type { AdvanceState } from './types/hooks';
  import type { Ref } from 'vue';
  import { defineComponent, reactive, ref, computed, unref, onMounted, watch, nextTick } from 'vue';
  import { Form, Row } from 'ant-design-vue';
  import { reactive, ref, computed, unref, onMounted, watch, nextTick, useAttrs } from 'vue';
  import { Form, Row, type FormProps as AntFormProps } from 'ant-design-vue';
  import FormItem from './components/FormItem.vue';
  import FormAction from './components/FormAction.vue';
  import { dateItemType } from './helper';
  import { dateUtil } from '/@/utils/dateUtil';
  import { dateItemType, isIncludeSimpleComponents } from './helper';
  import { dateUtil } from '@/utils/dateUtil';
  // import { cloneDeep } from 'lodash-es';
  import { deepMerge } from '/@/utils';
  import { deepMerge } from '@/utils';
  import { useFormValues } from './hooks/useFormValues';
  import useAdvanced from './hooks/useAdvanced';
  import { useFormEvents } from './hooks/useFormEvents';
  import { itemIsUploadComponent, useFormEvents } from './hooks/useFormEvents';
  import { createFormContext } from './hooks/useFormContext';
  import { useAutoFocus } from './hooks/useAutoFocus';
  import { useModalContext } from '/@/components/Modal';
  import { useModalContext } from '@/components/Modal';
  import { useDebounceFn } from '@vueuse/core';
  import { basicProps } from './props';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { useDesign } from '@/hooks/web/useDesign';
  import { cloneDeep } from 'lodash-es';
  import { TableActionType } from '@/components/Table';
  import { isArray, isFunction } from '@/utils/is';
  export default defineComponent({
    name: 'BasicForm',
    components: { FormItem, Form, Row, FormAction },
    props: basicProps,
    emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
    setup(props, { emit, attrs }) {
      const formModel = reactive({});
      const modalFn = useModalContext();
  defineOptions({ name: 'BasicForm' });
      const advanceState = reactive<AdvanceState>({
        isAdvanced: true,
        hideAdvanceBtn: false,
        isLoad: false,
        actionSpan: 6,
      });
  const props = defineProps(basicProps);
      const defaultValueRef = ref({});
      const isInitedDefaultRef = ref(false);
      const propsRef = ref<Partial<FormProps>>({});
      const schemaRef = ref<FormSchema[] | null>(null);
      const formElRef = ref<FormActionType | null>(null);
  const emit = defineEmits([
    'advanced-change',
    'reset',
    'submit',
    'register',
    'field-value-change',
  ]);
      const { prefixCls } = useDesign('basic-form');
  const attrs = useAttrs();
      // Get the basic configuration of the form
      const getProps = computed((): FormProps => {
        return { ...props, ...unref(propsRef) };
      });
  const formModel = reactive({});
  const modalFn = useModalContext();
      const getFormClass = computed(() => {
        return [
          prefixCls,
          {
            [`${prefixCls}--compact`]: unref(getProps).compact,
          },
        ];
      });
  const advanceState = reactive<AdvanceState>({
    isAdvanced: true,
    hideAdvanceBtn: false,
    isLoad: false,
    actionSpan: 6,
  });
      // Get uniform row style and Row configuration for the entire form
      const getRow = computed(() => {
        const { baseRowStyle = {}, rowProps } = unref(getProps);
        return {
          style: baseRowStyle,
          ...rowProps,
        };
      });
  const defaultValueRef = ref({});
  const isInitedDefaultRef = ref(false);
  const propsRef = ref<Partial<FormProps>>();
  const schemaRef = ref<FormSchema[] | null>(null);
  const formElRef = ref<FormActionType | null>(null);
      const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) }));
  const { prefixCls } = useDesign('basic-form');
      const getSchema = computed((): FormSchema[] => {
        const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
        for (const schema of schemas) {
          const { defaultValue, component, isHandleDateDefaultValue = true } = schema;
          // handle date type
          if (isHandleDateDefaultValue && defaultValue && dateItemType.includes(component)) {
            if (!Array.isArray(defaultValue)) {
              schema.defaultValue = dateUtil(defaultValue);
            } else {
              const def: any[] = [];
              defaultValue.forEach((item) => {
                def.push(dateUtil(item));
              });
              schema.defaultValue = def;
            }
          }
        }
        if (unref(getProps).showAdvancedButton) {
          return cloneDeep(
            schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[],
          );
        } else {
          return cloneDeep(schemas as FormSchema[]);
        }
      });
  // Get the basic configuration of the form
  const getProps = computed(() => {
    return { ...props, ...unref(propsRef) } as FormProps;
  });
      const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
        advanceState,
        emit,
        getProps,
        getSchema,
        formModel,
        defaultValueRef,
      });
  const getFormClass = computed(() => {
    return [
      prefixCls,
      {
        [`${prefixCls}--compact`]: unref(getProps).compact,
      },
    ];
  });
      const { handleFormValues, initDefault } = useFormValues({
        getProps,
        defaultValueRef,
        getSchema,
        formModel,
      });
  // Get uniform row style and Row configuration for the entire form
  const getRow = computed(() => {
    const { baseRowStyle = {}, rowProps } = unref(getProps);
    return {
      style: baseRowStyle,
      ...rowProps,
    };
  });
      useAutoFocus({
        getSchema,
        getProps,
        isInitedDefault: isInitedDefaultRef,
        formElRef: formElRef as Ref<FormActionType>,
      });
  const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) }) as AntFormProps);
  const getSchema = computed((): FormSchema[] => {
    const schemas: FormSchema[] = cloneDeep(unref(schemaRef) || (unref(getProps).schemas as any));
    for (const schema of schemas) {
      const {
        handleSubmit,
        setFieldsValue,
        clearValidate,
        validate,
        validateFields,
        getFieldsValue,
        updateSchema,
        resetSchema,
        appendSchemaByField,
        removeSchemaByField,
        resetFields,
        scrollToField,
      } = useFormEvents({
        emit,
        getProps,
        formModel,
        getSchema,
        defaultValueRef,
        formElRef: formElRef as Ref<FormActionType>,
        schemaRef: schemaRef as Ref<FormSchema[]>,
        handleFormValues,
      });
      createFormContext({
        resetAction: resetFields,
        submitAction: handleSubmit,
      });
      watch(
        () => unref(getProps).model,
        () => {
          const { model } = unref(getProps);
          if (!model) return;
          setFieldsValue(model);
        },
        {
          immediate: true,
        },
      );
      watch(
        () => unref(getProps).schemas,
        (schemas) => {
          resetSchema(schemas ?? []);
        },
      );
      watch(
        () => getSchema.value,
        (schema) => {
          nextTick(() => {
            //  Solve the problem of modal adaptive height calculation when the form is placed in the modal
            modalFn?.redoModalHeight?.();
        defaultValue,
        component,
        componentProps = {},
        isHandleDateDefaultValue = true,
        field,
        isHandleDefaultValue = true,
        valueFormat,
      } = schema;
      // handle date type
      if (
        isHandleDateDefaultValue &&
        defaultValue &&
        component &&
        dateItemType.includes(component)
      ) {
        const opt = {
          schema,
          tableAction: props.tableAction ?? ({} as TableActionType),
          formModel,
          formActionType: {} as FormActionType,
        };
        const valueFormat = componentProps
          ? typeof componentProps === 'function'
            ? componentProps(opt)['valueFormat']
            : componentProps['valueFormat']
          : null;
        if (!Array.isArray(defaultValue)) {
          schema.defaultValue = valueFormat
            ? dateUtil(defaultValue).format(valueFormat)
            : dateUtil(defaultValue);
        } else {
          const def: any[] = [];
          defaultValue.forEach((item) => {
            def.push(valueFormat ? dateUtil(item).format(valueFormat) : dateUtil(item));
          });
          if (unref(isInitedDefaultRef)) {
            return;
          }
          if (schema?.length) {
            initDefault();
            isInitedDefaultRef.value = true;
          }
        },
      );
      watch(
        () => formModel,
        useDebounceFn(() => {
          unref(getProps).submitOnChange && handleSubmit();
        }, 300),
        { deep: true },
      );
      async function setProps(formProps: Partial<FormProps>): Promise<void> {
        propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
          schema.defaultValue = def;
        }
      }
      function setFormModel(key: string, value: any, schema: FormSchema) {
        formModel[key] = value;
        emit('field-value-change', key, value);
        // TODO 优化验证,这里如果是autoLink=false手动关联的情况下才会再次触发此函数
        if (schema && schema.itemProps && !schema.itemProps.autoLink) {
          validateFields([key]).catch((_) => {});
      // handle upload type
      if (defaultValue && itemIsUploadComponent(schema?.component)) {
        if (isArray(defaultValue)) {
          schema.defaultValue = defaultValue;
        } else if (typeof defaultValue == 'string') {
          schema.defaultValue = [defaultValue];
        }
      }
      function handleEnterPress(e: KeyboardEvent) {
        const { autoSubmitOnEnter } = unref(getProps);
        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();
          }
        }
      // handle schema.valueFormat
      if (isHandleDefaultValue && defaultValue && component && isFunction(valueFormat)) {
        schema.defaultValue = valueFormat({
          value: defaultValue,
          schema,
          model: formModel,
          field,
        });
      }
    }
    if (unref(getProps).showAdvancedButton) {
      return schemas.filter(
        (schema) => !isIncludeSimpleComponents(schema.component),
      ) as FormSchema[];
    } else {
      return schemas as FormSchema[];
    }
  });
      const formActionType: Partial<FormActionType> = {
        getFieldsValue,
        setFieldsValue,
        resetFields,
        updateSchema,
        resetSchema,
        setProps,
        removeSchemaByField,
        appendSchemaByField,
        clearValidate,
        validateFields,
        validate,
        submit: handleSubmit,
        scrollToField: scrollToField,
      };
  const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
    advanceState,
    emit,
    getProps,
    getSchema,
    formModel,
    defaultValueRef,
  });
      onMounted(() => {
        initDefault();
        emit('register', formActionType);
      });
  const { handleFormValues, initDefault } = useFormValues({
    getProps,
    defaultValueRef,
    getSchema,
    formModel,
  });
      return {
        getBindValue,
        handleToggleAdvanced,
        handleEnterPress,
        formModel,
        defaultValueRef,
        advanceState,
        getRow,
        getProps,
        formElRef,
        getSchema,
        formActionType: formActionType as any,
        setFormModel,
        getFormClass,
        getFormActionBindProps: computed(() => ({ ...getProps.value, ...advanceState })),
        fieldsIsAdvancedMap,
        ...formActionType,
      };
  useAutoFocus({
    getSchema,
    getProps,
    isInitedDefault: isInitedDefaultRef,
    formElRef: formElRef as Ref<FormActionType>,
  });
  const {
    handleSubmit,
    setFieldsValue,
    clearValidate,
    validate,
    validateFields,
    getFieldsValue,
    updateSchema,
    resetSchema,
    appendSchemaByField,
    removeSchemaByField,
    resetFields,
    scrollToField,
    resetDefaultField,
  } = useFormEvents({
    emit,
    getProps,
    formModel,
    getSchema,
    defaultValueRef,
    formElRef: formElRef as Ref<FormActionType>,
    schemaRef: schemaRef as Ref<FormSchema[]>,
    handleFormValues,
  });
  createFormContext({
    resetAction: resetFields,
    submitAction: handleSubmit,
  });
  watch(
    () => unref(getProps).model,
    () => {
      const { model } = unref(getProps);
      if (!model) return;
      setFieldsValue(model);
    },
    {
      immediate: true,
    },
  );
  watch(
    () => props.schemas,
    (schemas) => {
      resetSchema(schemas ?? []);
    },
  );
  watch(
    () => getSchema.value,
    (schema) => {
      nextTick(() => {
        //  Solve the problem of modal adaptive height calculation when the form is placed in the modal
        modalFn?.redoModalHeight?.();
      });
      if (unref(isInitedDefaultRef)) {
        return;
      }
      if (schema?.length) {
        initDefault();
        isInitedDefaultRef.value = true;
      }
    },
  );
  watch(
    () => formModel,
    useDebounceFn(() => {
      unref(getProps).submitOnChange && handleSubmit();
    }, 300),
    { deep: true },
  );
  async function setProps(formProps: Partial<FormProps>): Promise<void> {
    propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
  }
  function setFormModel(key: string, value: any, schema: FormSchema) {
    formModel[key] = value;
    emit('field-value-change', key, value);
    // TODO 优化验证,这里如果是autoLink=false手动关联的情况下才会再次触发此函数
    if (schema && schema.itemProps && !schema.itemProps.autoLink) {
      validateFields([key]).catch((_) => {});
    }
  }
  function handleEnterPress(e: KeyboardEvent) {
    const { autoSubmitOnEnter } = unref(getProps);
    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();
      }
    }
  }
  const formActionType = {
    getFieldsValue,
    setFieldsValue,
    resetFields,
    updateSchema,
    resetSchema,
    setProps,
    removeSchemaByField,
    appendSchemaByField,
    clearValidate,
    validateFields,
    validate,
    submit: handleSubmit,
    scrollToField: scrollToField,
    resetDefaultField,
  };
  const getFormActionBindProps = computed(
    () => ({ ...getProps.value, ...advanceState }) as InstanceType<typeof FormAction>['$props'],
  );
  defineExpose({
    ...formActionType,
  });
  onMounted(() => {
    initDefault();
    emit('register', formActionType);
  });
</script>
<style lang="less">
@@ -311,21 +353,26 @@
        margin: 0 6px 0 2px;
      }
      &-with-help {
        margin-bottom: 0;
      }
      // &-with-help {
      //   margin-bottom: 0;
      // }
      &:not(.ant-form-item-with-help) {
        margin-bottom: 20px;
      }
      // &:not(.ant-form-item-with-help) {
      //   margin-bottom: 20px;
      // }
      &.suffix-item {
      &.suffix-item,
      &.prefix-item {
        .ant-form-item-children {
          display: flex;
        }
        .ant-form-item-control {
          margin-top: 4px;
        .prefix {
          display: inline-flex;
          align-items: center;
          margin-top: 1px;
          padding-right: 6px;
          line-height: 1;
        }
        .suffix {