简易表单
- 输入时失焦校验
- 提交时校验,成功/失败提示
- 数据列默认值
- 根据数据列配置项展示数据列
- 数据列、
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 };
优化项
封装state、methods、events的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>