import type {
|
FormState,
|
GenericObject,
|
ResetFormOpts,
|
ValidationOptions,
|
} from 'vee-validate';
|
|
import type { ComponentPublicInstance } from 'vue';
|
|
import type { Recordable } from '@vben-core/typings';
|
|
import type { FormActions, FormSchema, VbenFormProps } from './types';
|
|
import { toRaw } from 'vue';
|
|
import { Store } from '@vben-core/shared/store';
|
import {
|
bindMethods,
|
createMerge,
|
formatDate,
|
isDate,
|
isDayjsObject,
|
isFunction,
|
isObject,
|
mergeWithArrayOverride,
|
StateHandler,
|
} from '@vben-core/shared/utils';
|
|
function getDefaultState(): VbenFormProps {
|
return {
|
actionWrapperClass: '',
|
collapsed: false,
|
collapsedRows: 1,
|
collapseTriggerResize: false,
|
commonConfig: {},
|
handleReset: undefined,
|
handleSubmit: undefined,
|
handleValuesChange: undefined,
|
layout: 'horizontal',
|
resetButtonOptions: {},
|
schema: [],
|
showCollapseButton: false,
|
showDefaultActions: true,
|
submitButtonOptions: {},
|
submitOnChange: false,
|
submitOnEnter: false,
|
wrapperClass: 'grid-cols-1',
|
};
|
}
|
|
export class FormApi {
|
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
|
public form = {} as FormActions;
|
isMounted = false;
|
|
public state: null | VbenFormProps = null;
|
stateHandler: StateHandler;
|
|
public store: Store<VbenFormProps>;
|
|
/**
|
* 组件实例映射
|
*/
|
private componentRefMap: Map<string, unknown> = new Map();
|
|
// 最后一次点击提交时的表单值
|
private latestSubmissionValues: null | Recordable<any> = null;
|
|
private prevState: null | VbenFormProps = null;
|
|
constructor(options: VbenFormProps = {}) {
|
const { ...storeState } = options;
|
|
const defaultState = getDefaultState();
|
|
this.store = new Store<VbenFormProps>(
|
{
|
...defaultState,
|
...storeState,
|
},
|
{
|
onUpdate: () => {
|
this.prevState = this.state;
|
this.state = this.store.state;
|
this.updateState();
|
},
|
},
|
);
|
|
this.state = this.store.state;
|
this.stateHandler = new StateHandler();
|
bindMethods(this);
|
}
|
|
/**
|
* 获取字段组件实例
|
* @param fieldName 字段名
|
* @returns 组件实例
|
*/
|
getFieldComponentRef<T = ComponentPublicInstance>(
|
fieldName: string,
|
): T | undefined {
|
return this.componentRefMap.has(fieldName)
|
? (this.componentRefMap.get(fieldName) as T)
|
: undefined;
|
}
|
|
/**
|
* 获取当前聚焦的字段,如果没有聚焦的字段则返回undefined
|
*/
|
getFocusedField() {
|
for (const fieldName of this.componentRefMap.keys()) {
|
const ref = this.getFieldComponentRef(fieldName);
|
if (ref) {
|
let el: HTMLElement | null = null;
|
if (ref instanceof HTMLElement) {
|
el = ref;
|
} else if (ref.$el instanceof HTMLElement) {
|
el = ref.$el;
|
}
|
if (!el) {
|
continue;
|
}
|
if (
|
el === document.activeElement ||
|
el.contains(document.activeElement)
|
) {
|
return fieldName;
|
}
|
}
|
}
|
return undefined;
|
}
|
|
getLatestSubmissionValues() {
|
return this.latestSubmissionValues || {};
|
}
|
|
getState() {
|
return this.state;
|
}
|
|
async getValues<T = Recordable<any>>() {
|
const form = await this.getForm();
|
return (form.values ? this.handleRangeTimeValue(form.values) : {}) as T;
|
}
|
|
async isFieldValid(fieldName: string) {
|
const form = await this.getForm();
|
return form.isFieldValid(fieldName);
|
}
|
|
merge(formApi: FormApi) {
|
const chain = [this, formApi];
|
const proxy = new Proxy(formApi, {
|
get(target: any, prop: any) {
|
if (prop === 'merge') {
|
return (nextFormApi: FormApi) => {
|
chain.push(nextFormApi);
|
return proxy;
|
};
|
}
|
if (prop === 'submitAllForm') {
|
return async (needMerge: boolean = true) => {
|
try {
|
const results = await Promise.all(
|
chain.map(async (api) => {
|
const validateResult = await api.validate();
|
if (!validateResult.valid) {
|
return;
|
}
|
const rawValues = toRaw((await api.getValues()) || {});
|
return rawValues;
|
}),
|
);
|
if (needMerge) {
|
const mergedResults = Object.assign({}, ...results);
|
return mergedResults;
|
}
|
return results;
|
} catch (error) {
|
console.error('Validation error:', error);
|
}
|
};
|
}
|
return target[prop];
|
},
|
});
|
|
return proxy;
|
}
|
|
mount(formActions: FormActions, componentRefMap: Map<string, unknown>) {
|
if (!this.isMounted) {
|
Object.assign(this.form, formActions);
|
this.stateHandler.setConditionTrue();
|
this.setLatestSubmissionValues({
|
...toRaw(this.handleRangeTimeValue(this.form.values)),
|
});
|
this.componentRefMap = componentRefMap;
|
this.isMounted = true;
|
}
|
}
|
|
/**
|
* 根据字段名移除表单项
|
* @param fields
|
*/
|
async removeSchemaByFields(fields: string[]) {
|
const fieldSet = new Set(fields);
|
const schema = this.state?.schema ?? [];
|
|
const filterSchema = schema.filter((item) => !fieldSet.has(item.fieldName));
|
|
this.setState({
|
schema: filterSchema,
|
});
|
}
|
|
/**
|
* 重置表单
|
*/
|
async resetForm(
|
state?: Partial<FormState<GenericObject>> | undefined,
|
opts?: Partial<ResetFormOpts>,
|
) {
|
const form = await this.getForm();
|
return form.resetForm(state, opts);
|
}
|
|
async resetValidate() {
|
const form = await this.getForm();
|
const fields = Object.keys(form.errors.value);
|
fields.forEach((field) => {
|
form.setFieldError(field, undefined);
|
});
|
}
|
|
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
const form = await this.getForm();
|
form.setFieldValue(field, value, shouldValidate);
|
}
|
|
setLatestSubmissionValues(values: null | Recordable<any>) {
|
this.latestSubmissionValues = { ...toRaw(values) };
|
}
|
|
setState(
|
stateOrFn:
|
| ((prev: VbenFormProps) => Partial<VbenFormProps>)
|
| Partial<VbenFormProps>,
|
) {
|
if (isFunction(stateOrFn)) {
|
this.store.setState((prev) => {
|
return mergeWithArrayOverride(stateOrFn(prev), prev);
|
});
|
} else {
|
this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev));
|
}
|
}
|
|
/**
|
* 设置表单值
|
* @param fields record
|
* @param filterFields 过滤不在schema中定义的字段 默认为true
|
* @param shouldValidate
|
*/
|
async setValues(
|
fields: Record<string, any>,
|
filterFields: boolean = true,
|
shouldValidate: boolean = false,
|
) {
|
const form = await this.getForm();
|
if (!filterFields) {
|
form.setValues(fields, shouldValidate);
|
return;
|
}
|
|
/**
|
* 合并算法有待改进,目前的算法不支持object类型的值。
|
* antd的日期时间相关组件的值类型为dayjs对象
|
* element-plus的日期时间相关组件的值类型可能为Date对象
|
* 以上两种类型需要排除深度合并
|
*/
|
const fieldMergeFn = createMerge((obj, key, value) => {
|
if (key in obj) {
|
obj[key] =
|
!Array.isArray(obj[key]) &&
|
isObject(obj[key]) &&
|
!isDayjsObject(obj[key]) &&
|
!isDate(obj[key])
|
? fieldMergeFn(obj[key], value)
|
: value;
|
}
|
return true;
|
});
|
const filteredFields = fieldMergeFn(fields, form.values);
|
form.setValues(filteredFields, shouldValidate);
|
}
|
|
async submitForm(e?: Event) {
|
e?.preventDefault();
|
e?.stopPropagation();
|
const form = await this.getForm();
|
await form.submitForm();
|
const rawValues = toRaw(await this.getValues());
|
await this.state?.handleSubmit?.(rawValues);
|
|
return rawValues;
|
}
|
|
unmount() {
|
this.form?.resetForm?.();
|
// this.state = null;
|
this.latestSubmissionValues = null;
|
this.isMounted = false;
|
this.stateHandler.reset();
|
}
|
|
updateSchema(schema: Partial<FormSchema>[]) {
|
const updated: Partial<FormSchema>[] = [...schema];
|
const hasField = updated.every(
|
(item) => Reflect.has(item, 'fieldName') && item.fieldName,
|
);
|
|
if (!hasField) {
|
console.error(
|
'All items in the schema array must have a valid `fieldName` property to be updated',
|
);
|
return;
|
}
|
const currentSchema = [...(this.state?.schema ?? [])];
|
|
const updatedMap: Record<string, any> = {};
|
|
updated.forEach((item) => {
|
if (item.fieldName) {
|
updatedMap[item.fieldName] = item;
|
}
|
});
|
|
currentSchema.forEach((schema, index) => {
|
const updatedData = updatedMap[schema.fieldName];
|
if (updatedData) {
|
currentSchema[index] = mergeWithArrayOverride(
|
updatedData,
|
schema,
|
) as FormSchema;
|
}
|
});
|
this.setState({ schema: currentSchema });
|
}
|
|
async validate(opts?: Partial<ValidationOptions>) {
|
const form = await this.getForm();
|
|
const validateResult = await form.validate(opts);
|
|
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
console.error('validate error', validateResult?.errors);
|
}
|
return validateResult;
|
}
|
|
async validateAndSubmitForm() {
|
const form = await this.getForm();
|
const { valid } = await form.validate();
|
if (!valid) {
|
return;
|
}
|
return await this.submitForm();
|
}
|
|
async validateField(fieldName: string, opts?: Partial<ValidationOptions>) {
|
const form = await this.getForm();
|
const validateResult = await form.validateField(fieldName, opts);
|
|
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
console.error('validate error', validateResult?.errors);
|
}
|
return validateResult;
|
}
|
|
private async getForm() {
|
if (!this.isMounted) {
|
// 等待form挂载
|
await this.stateHandler.waitForCondition();
|
}
|
if (!this.form?.meta) {
|
throw new Error('<VbenForm /> is not mounted');
|
}
|
return this.form;
|
}
|
|
private handleRangeTimeValue = (originValues: Record<string, any>) => {
|
const values = { ...originValues };
|
const fieldMappingTime = this.state?.fieldMappingTime;
|
|
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
|
return values;
|
}
|
|
fieldMappingTime.forEach(
|
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
|
if (startTimeKey && endTimeKey && values[field] === null) {
|
Reflect.deleteProperty(values, startTimeKey);
|
Reflect.deleteProperty(values, endTimeKey);
|
// delete values[startTimeKey];
|
// delete values[endTimeKey];
|
}
|
|
if (!values[field]) {
|
Reflect.deleteProperty(values, field);
|
// delete values[field];
|
return;
|
}
|
|
const [startTime, endTime] = values[field];
|
if (format === null) {
|
values[startTimeKey] = startTime;
|
values[endTimeKey] = endTime;
|
} else if (isFunction(format)) {
|
values[startTimeKey] = format(startTime, startTimeKey);
|
values[endTimeKey] = format(endTime, endTimeKey);
|
} else {
|
const [startTimeFormat, endTimeFormat] = Array.isArray(format)
|
? format
|
: [format, format];
|
|
values[startTimeKey] = startTime
|
? formatDate(startTime, startTimeFormat)
|
: undefined;
|
values[endTimeKey] = endTime
|
? formatDate(endTime, endTimeFormat)
|
: undefined;
|
}
|
// delete values[field];
|
Reflect.deleteProperty(values, field);
|
},
|
);
|
return values;
|
};
|
|
private updateState() {
|
const currentSchema = this.state?.schema ?? [];
|
const prevSchema = this.prevState?.schema ?? [];
|
// 进行了删除schema操作
|
if (currentSchema.length < prevSchema.length) {
|
const currentFields = new Set(
|
currentSchema.map((item) => item.fieldName),
|
);
|
const deletedSchema = prevSchema.filter(
|
(item) => !currentFields.has(item.fieldName),
|
);
|
for (const schema of deletedSchema) {
|
this.form?.setFieldValue?.(schema.fieldName, undefined);
|
}
|
}
|
}
|
}
|