| | |
| | | </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"> |
| | |
| | | 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 { |