You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
8.2 KiB
346 lines
8.2 KiB
<script lang="ts" setup> |
|
import type { Ref } from 'vue' |
|
import { computed, nextTick, onMounted, reactive, ref, unref, useAttrs, watch } from 'vue' |
|
import { type FormProps as AntFormProps, Form, Row } from 'ant-design-vue' |
|
import { useDebounceFn } from '@vueuse/core' |
|
import { cloneDeep } from 'lodash-es' |
|
import type { FormActionType, FormProps, FormSchemaInner as FormSchema } from './types/form' |
|
import type { AdvanceState } from './types/hooks' |
|
|
|
import FormItem from './components/FormItem.vue' |
|
import FormAction from './components/FormAction.vue' |
|
|
|
import { dateItemType } from './helper' |
|
|
|
import { useFormValues } from './hooks/useFormValues' |
|
import useAdvanced from './hooks/useAdvanced' |
|
import { useFormEvents } from './hooks/useFormEvents' |
|
import { createFormContext } from './hooks/useFormContext' |
|
import { useAutoFocus } from './hooks/useAutoFocus' |
|
import { basicProps } from './props' |
|
import { useModalContext } from '@/components/Modal' |
|
|
|
import { deepMerge } from '@/utils' |
|
import { dateUtil } from '@/utils/dateUtil' |
|
import { useDesign } from '@/hooks/web/useDesign' |
|
|
|
defineOptions({ name: 'BasicForm' }) |
|
|
|
const props = defineProps(basicProps) |
|
const emit = defineEmits(['advanced-change', 'reset', 'submit', 'register', 'field-value-change']) |
|
const attrs = useAttrs() |
|
const formModel = reactive({}) |
|
const modalFn = useModalContext() |
|
|
|
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<FormSchema[] | null>(null) |
|
const formElRef = ref<FormActionType | null>(null) |
|
|
|
const { prefixCls } = useDesign('basic-form') |
|
|
|
// Get the basic configuration of the form |
|
const getProps = computed(() => { |
|
return { ...props, ...unref(propsRef) } as FormProps |
|
}) |
|
|
|
const getFormClass = computed(() => { |
|
return [ |
|
prefixCls, |
|
{ |
|
[`${prefixCls}--compact`]: unref(getProps).compact, |
|
}, |
|
] |
|
}) |
|
|
|
// Get uniform row style and Row configuration for the entire form |
|
const getRow = computed(() => { |
|
const { baseRowStyle = {}, rowProps } = unref(getProps) |
|
return { |
|
style: baseRowStyle, |
|
...rowProps, |
|
} |
|
}) |
|
|
|
const getBindValue = computed( |
|
() => ({ ...attrs, ...props, ...unref(getProps) }) as AntFormProps, |
|
) |
|
|
|
const getSchema = computed((): FormSchema[] => { |
|
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any) |
|
for (const schema of schemas) { |
|
const { |
|
defaultValue, |
|
component, |
|
componentProps, |
|
isHandleDateDefaultValue = true, |
|
} = schema |
|
|
|
const valueFormat = componentProps ? componentProps.valueFormat : null |
|
// handle date type |
|
if ( |
|
isHandleDateDefaultValue |
|
&& defaultValue |
|
&& component |
|
&& dateItemType.includes(component) |
|
) { |
|
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)) |
|
}) |
|
schema.defaultValue = def |
|
} |
|
} |
|
} |
|
if (unref(getProps).showAdvancedButton) { |
|
return cloneDeep( |
|
schemas.filter(schema => schema.component !== 'Divider') as FormSchema[], |
|
) |
|
} |
|
else { |
|
return cloneDeep(schemas as FormSchema[]) |
|
} |
|
}) |
|
|
|
const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({ |
|
advanceState, |
|
emit, |
|
getProps, |
|
getSchema, |
|
formModel, |
|
defaultValueRef, |
|
}) |
|
|
|
const { handleFormValues, initDefault } = useFormValues({ |
|
getProps, |
|
defaultValueRef, |
|
getSchema, |
|
formModel, |
|
}) |
|
|
|
useAutoFocus({ |
|
getSchema, |
|
getProps, |
|
isInitedDefault: isInitedDefaultRef, |
|
formElRef: formElRef as Ref<FormActionType>, |
|
}) |
|
|
|
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?.() |
|
}) |
|
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: FormActionType = { |
|
getFieldsValue, |
|
setFieldsValue, |
|
resetFields, |
|
updateSchema, |
|
resetSchema, |
|
setProps, |
|
removeSchemaByField, |
|
appendSchemaByField, |
|
clearValidate, |
|
validateFields, |
|
validate, |
|
submit: handleSubmit, |
|
scrollToField, |
|
} |
|
|
|
onMounted(() => { |
|
initDefault() |
|
if (props.register) { |
|
props.register(formActionType) |
|
} |
|
else { |
|
emit('register', formActionType) |
|
} |
|
}) |
|
|
|
const getFormActionBindProps = computed(() => ({ ...getProps.value, ...advanceState }) as InstanceType<typeof FormAction>['$props']) |
|
</script> |
|
|
|
<template> |
|
<Form |
|
v-bind="getBindValue" |
|
ref="formElRef" |
|
:class="getFormClass" |
|
:model="formModel" |
|
@keypress.enter="handleEnterPress" |
|
> |
|
<Row v-bind="getRow"> |
|
<slot name="formHeader" /> |
|
<template v-for="schema in getSchema" :key="schema.field"> |
|
<FormItem |
|
:is-advanced="fieldsIsAdvancedMap[schema.field]" |
|
:table-action="tableAction" |
|
: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 ['resetBefore', 'submitBefore', 'advanceBefore', 'advanceAfter']" |
|
#[item]="data" |
|
> |
|
<slot :name="item" v-bind="data || {}" /> |
|
</template> |
|
</FormAction> |
|
<slot name="formFooter" /> |
|
</Row> |
|
</Form> |
|
</template> |
|
|
|
<style lang="less"> |
|
@prefix-cls: ~'@{namespace}-basic-form'; |
|
|
|
.@{prefix-cls} { |
|
.ant-form-item { |
|
&-label label::after { |
|
margin: 0 6px 0 2px; |
|
} |
|
|
|
&.suffix-item { |
|
.ant-form-item-children { |
|
display: flex; |
|
} |
|
|
|
.ant-form-item-control { |
|
margin-top: 4px; |
|
} |
|
|
|
.suffix { |
|
display: inline-flex; |
|
align-items: center; |
|
padding-left: 6px; |
|
margin-top: 1px; |
|
line-height: 1; |
|
} |
|
} |
|
} |
|
|
|
.ant-form-explain { |
|
font-size: 14px; |
|
} |
|
|
|
&--compact { |
|
.ant-form-item { |
|
margin-bottom: 8px !important; |
|
} |
|
} |
|
} |
|
</style>
|
|
|