BasicForm.vue 7.29 KB
<script setup lang="ts">
import type { FormProps as AntFormProps, RowProps } from 'ant-design-vue'
import { Form, Row } from 'ant-design-vue'
import { cloneDeep } from 'lodash-es'
import { computed, onMounted, reactive, ref, unref, useAttrs, watch } from 'vue'
import type { FormExpose } from 'ant-design-vue/es/form/Form'
import FormItem from './components/FormItem.vue'
import { ComponentEnum, FormLayoutEnum } from './enum'
import { dateItemType } from './helper'
import type { BasicFormEmitType, FormActionType, FormProps, FormSchema } from './types/form'
import { BasicFormEventEnum } from './types/form'
import type { AdvanceState } from './types/hook'
import { useFormValues } from './hooks/useFormValues'
import type { UseFormEventsParamsType } from './hooks/useFormEvents'
import { useFormEvents } from './hooks/useFormEvents'
import { createFormContext } from './hooks/useFormContext'
import { useAutoFocus } from './hooks/useAutoFocus'
import { useAdvanced } from './hooks/useAdvanced'
import FormAction from './components/FormAction.vue'
import { FORM_ACTION_SLOTS, SlotNameEnum } from './slotHelp'
import { dateUtil } from '@/utils/dateUtil'
import { deepMerge } from '@/utils'
import { useDesign } from '@/hooks/useDesign'
import { DateFormatEnum } from '@/enums/timeEnum'
const props = withDefaults(
  defineProps<FormProps>(),
  {
    model: () => ({}),
    labelWidth: 0,
    fieldMapToTime: () => [],
    schemas: () => [],
    mergetDynamicData: () => ({}),
    autoSetPlaceholder: true,
    autoSubmitOnEnter: false,
    emptySpan: 0,
    transformDateFunc: (date: any) => date?.format(DateFormatEnum.YYYY_MM_DD_HH_MM_SS) ?? date,
    rulesMessageJoinLabel: true,
    autoAdvancedLine: 3,
    alwaysShowLines: 1,
    showSubmitButton: true,
    layout: FormLayoutEnum.HORIZONTAL,
    placeholderJoinLabel: true,
  },
)

const emit = defineEmits<BasicFormEmitType>()

const attrs = useAttrs()

const formModel = reactive<Recordable>({})

const advanceState = reactive<AdvanceState>({
  isAdvanced: true,
  hideAdvanceBtn: false,
  isLoad: false,
  actionSpan: 6,
})

const defaultValueRef = ref({})

const isInitedDefaultRef = ref(false)

const propsRef = ref<Partial<FormProps>>({})

const schemaRef = ref<Nullable<FormSchema[]>>(null)

const formElRef = ref<Nullable<FormExpose>>(null)

const { prefixCls } = useDesign('basic-form')

const getProps = computed(() => {
  return { ...props, ...unref(propsRef) as FormProps } as FormProps
})

const getFormClass = computed(() => {
  return [prefixCls, {
    [`${prefixCls}--compact`]: unref(getProps).compact,
  }]
})

const getRow = computed<RowProps>(() => {
  const { baseRowStyle = {}, rowProps } = unref(getProps)
  return {
    style: baseRowStyle,
    ...rowProps,
  }
})

const getBindValue = computed<AntFormProps>(() => ({ ...attrs, ...props, ...unref(getProps) }) as AntFormProps)

const getFormActionBindProps = computed(() => ({ ...getProps.value as Recordable, ...advanceState }))

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
    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).showAdvanceButton)
    return cloneDeep(schemas.filter(schema => schema.component !== ComponentEnum.DIVIDER))
  else
    return cloneDeep(schemas)
})

const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({ advanceState, emit, getProps, getSchema, formModel, defaultValueRef })

const { handleFormValues, initDefault } = useFormValues({ getProps, defaultValueRef, getSchema, formModel })

useAutoFocus({ getSchema, getProps, isInitedDefault: isInitedDefaultRef, formElRef })

const {
  handleSubmit,
  clearValidate,
  validate,
  validateFields,
  getFieldsValue,
  updateSchema,
  resetSchema,
  appendSchemaByField,
  removeSchemaByField,
  resetFields,
  setFieldsValue,
  scrollToField,
} = useFormEvents({
  emit,
  getProps,
  formModel,
  getSchema,
  defaultValueRef,
  formElRef,
  schemaRef,
  handleFormValues,
} as UseFormEventsParamsType)

const formActionType: Partial<FormActionType> = {
  getFieldsValue,
  setFieldsValue,
  resetFields,
  updateSchema,
  resetSchema,
  setProps,
  removeSchemaByField,
  appendSchemaByField,
  clearValidate,
  validateFields,
  validate,
  submit: handleSubmit,
  scrollToField,
}

createFormContext({
  resetAction: resetFields!,
  submitAction: handleSubmit,
})

watch(
  () => unref(getProps).model,
  () => {
    const { model } = unref(getProps)
    if (!model) return
    setFieldsValue?.(model)
  },
  {
    immediate: true,
  },
)

watch(
  () => unref(getSchema),
  (schema) => {
    if (unref(isInitedDefaultRef)) return
    if (schema.length) {
      initDefault()
      isInitedDefaultRef.value = true
    }
  },
)

async function setProps(formProps: Partial<FormProps>) {
  propsRef.value = deepMerge(unref(propsRef) || {}, formProps) as any
}

function setFormModel(key: string, value: any, schema: FormSchema) {
  formModel[key] = value
  emit(BasicFormEventEnum.FILE_VALUE_CHANGE, key, value)
  if (schema && schema.itemProps && !schema.itemProps.autoLink)
    validateFields?.([key]).catch(() => { })
}

function handleEnterPress(event: KeyboardEvent) {
  const { autoSubmitOnEnter } = unref(getProps)
  if (!autoSubmitOnEnter) return
  if (event.key === 'Enter' && event.target && event.target instanceof HTMLElement) {
    const target = event.target as HTMLElement
    if (target && target.tagName && target.tagName.toUpperCase() === 'INPUT')
      handleSubmit()
  }
}

onMounted(() => {
  initDefault()
  emit(BasicFormEventEnum.REGISTER, formActionType as FormActionType)
})

defineExpose(formActionType)
</script>

<template>
  <Form ref="formElRef" v-bind="getBindValue" :class="getFormClass" :model="formModel" @keypress.enter="handleEnterPress">
    <Row v-bind="getRow">
      <slot :name="SlotNameEnum.FORM_HEADER" />
      <template v-for="schema in getSchema" :key="schema.field">
        <FormItem
          :is-advanced="fieldsIsAdvancedMap[schema.field]" :form-action-type="formActionType" :schema="schema"
          :form-props="getProps" :all-default-values="defaultValueRef" :form-model="formModel"
          :set-form-model="setFormModel"
        >
          <template v-for="item in Object.keys($slots)" #[item]="data">
            <slot :name="item" v-bind="data || {}" />
          </template>
        </FormItem>
      </template>

      <FormAction v-bind="getFormActionBindProps" @toggle-advanced="handleToggleAdvanced">
        <template v-for="item in FORM_ACTION_SLOTS" #[item]="data">
          <slot :name="item" v-bind="data || {}" />
        </template>
      </FormAction>

      <slot :name="SlotNameEnum.FORM_FOOTER" />
    </Row>
  </Form>
</template>

<style lang="less" scoped>
.@{namespace}-basic-form {
  > :deep(.ant-row) {
    >.ant-col {
      width: 100%;
    }

    .ant-btn {
      padding: 4px 15px;
    }

    .ant-switch {
      border-radius: 100px;
    }

    .ant-input-number {
      width: 100%;
    }
  }
}
</style>