mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-12-15 03:26:57 +08:00
284 lines
7.8 KiB
Vue
284 lines
7.8 KiB
Vue
<template>
|
|
<TMagicForm
|
|
class="m-form"
|
|
ref="tMagicForm"
|
|
:model="values"
|
|
:label-width="labelWidth"
|
|
:style="`height: ${height}`"
|
|
:inline="inline"
|
|
:label-position="labelPosition"
|
|
@submit="submitHandler"
|
|
>
|
|
<template v-if="initialized && Array.isArray(config)">
|
|
<Container
|
|
v-for="(item, index) in config"
|
|
:disabled="disabled"
|
|
:key="(item as Record<string, any>)[keyProp] ?? index"
|
|
:config="item"
|
|
:model="values"
|
|
:last-values="lastValuesProcessed"
|
|
:is-compare="isCompare"
|
|
:label-width="item.labelWidth || labelWidth"
|
|
:step-active="stepActive"
|
|
:size="size"
|
|
@change="changeHandler"
|
|
></Container>
|
|
</template>
|
|
</TMagicForm>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch, watchEffect } from 'vue';
|
|
import { cloneDeep, isEqual } from 'lodash-es';
|
|
|
|
import { TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
|
|
import { setValueByKeyPath } from '@tmagic/utils';
|
|
|
|
import Container from './containers/Container.vue';
|
|
import { getConfig } from './utils/config';
|
|
import { initValue } from './utils/form';
|
|
import type { ChangeRecord, ContainerChangeEventData, FormConfig, FormState, FormValue, ValidateError } from './schema';
|
|
|
|
defineOptions({
|
|
name: 'MForm',
|
|
});
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
/** 表单配置 */
|
|
config: FormConfig;
|
|
/** 表单值 */
|
|
initValues: Record<string, any>;
|
|
/** 需对比的值(开启对比模式时传入) */
|
|
lastValues?: Record<string, any>;
|
|
/** 是否开启对比模式 */
|
|
isCompare?: boolean;
|
|
parentValues?: Record<string, any>;
|
|
labelWidth?: string;
|
|
disabled?: boolean;
|
|
height?: string;
|
|
stepActive?: string | number;
|
|
size?: 'small' | 'default' | 'large';
|
|
inline?: boolean;
|
|
labelPosition?: string;
|
|
keyProp?: string;
|
|
popperClass?: string;
|
|
preventSubmitDefault?: boolean;
|
|
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
|
}>(),
|
|
{
|
|
config: () => [],
|
|
initValues: () => ({}),
|
|
lastValues: () => ({}),
|
|
isCompare: false,
|
|
parentValues: () => ({}),
|
|
labelWidth: '200px',
|
|
disabled: false,
|
|
height: 'auto',
|
|
stepActive: 1,
|
|
inline: false,
|
|
labelPosition: 'right',
|
|
keyProp: '__key',
|
|
},
|
|
);
|
|
|
|
const emit = defineEmits(['change', 'error', 'field-input', 'field-change', 'update:stepActive']);
|
|
|
|
const tMagicFormRef = useTemplateRef('tMagicForm');
|
|
const initialized = ref(false);
|
|
const values = ref<FormValue>({});
|
|
const lastValuesProcessed = ref<FormValue>({});
|
|
const fields = new Map<string, any>();
|
|
|
|
const requestFuc = getConfig('request') as Function;
|
|
|
|
const formState: FormState = reactive<FormState>({
|
|
keyProp: props.keyProp,
|
|
popperClass: props.popperClass,
|
|
config: props.config,
|
|
initValues: props.initValues,
|
|
isCompare: props.isCompare,
|
|
lastValues: props.lastValues,
|
|
parentValues: props.parentValues,
|
|
values,
|
|
lastValuesProcessed,
|
|
$emit: emit as (_event: string, ..._args: any[]) => void,
|
|
fields,
|
|
setField: (prop: string, field: any) => fields.set(prop, field),
|
|
getField: (prop: string) => fields.get(prop),
|
|
deleteField: (prop: string) => fields.delete(prop),
|
|
$messageBox: tMagicMessageBox,
|
|
$message: tMagicMessage,
|
|
post: (options: any) => {
|
|
if (requestFuc) {
|
|
return requestFuc({
|
|
method: 'POST',
|
|
...options,
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
watchEffect(async () => {
|
|
formState.initValues = props.initValues;
|
|
formState.lastValues = props.lastValues;
|
|
formState.isCompare = props.isCompare;
|
|
formState.config = props.config;
|
|
formState.keyProp = props.keyProp;
|
|
formState.popperClass = props.popperClass;
|
|
formState.parentValues = props.parentValues;
|
|
|
|
if (typeof props.extendState === 'function') {
|
|
const state = (await props.extendState(formState)) || {};
|
|
Object.entries(state).forEach(([key, value]) => {
|
|
formState[key] = value;
|
|
});
|
|
}
|
|
});
|
|
|
|
provide('mForm', formState);
|
|
|
|
const changeRecords = shallowRef<ChangeRecord[]>([]);
|
|
|
|
watch(
|
|
[() => props.config, () => props.initValues],
|
|
([config], [preConfig]) => {
|
|
changeRecords.value = [];
|
|
|
|
if (!isEqual(toRaw(config), toRaw(preConfig))) {
|
|
initialized.value = false;
|
|
}
|
|
|
|
initValue(formState, {
|
|
initValues: props.initValues,
|
|
config: props.config,
|
|
}).then((value) => {
|
|
values.value = value;
|
|
// 非对比模式,初始化完成
|
|
initialized.value = !props.isCompare;
|
|
});
|
|
|
|
if (props.isCompare) {
|
|
// 对比模式下初始化待对比的表单值
|
|
initValue(formState, {
|
|
initValues: props.lastValues,
|
|
config: props.config,
|
|
}).then((value) => {
|
|
lastValuesProcessed.value = value;
|
|
initialized.value = true;
|
|
});
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
const changeHandler = (v: FormValue, eventData: ContainerChangeEventData) => {
|
|
if (eventData.changeRecords?.length) {
|
|
for (const record of eventData.changeRecords) {
|
|
if (record.propPath) {
|
|
const index = changeRecords.value.findIndex((item) => item.propPath === record.propPath);
|
|
if (index > -1) {
|
|
changeRecords.value[index] = record;
|
|
} else {
|
|
changeRecords.value.push(record);
|
|
}
|
|
|
|
setValueByKeyPath(record.propPath, record.value, values.value);
|
|
}
|
|
}
|
|
}
|
|
emit('change', values.value, eventData);
|
|
};
|
|
|
|
const submitHandler = (e: SubmitEvent) => {
|
|
if (props.preventSubmitDefault) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 通过 name 从 config 中查找对应的 text
|
|
* @param name - 字段名,支持点分隔的路径格式,如 'a.b.c'
|
|
* @param config - 表单配置数组
|
|
* @returns 找到的 text 值,如果未找到则返回 undefined
|
|
*/
|
|
const getTextByName = (name: string, config: FormConfig = props.config): string | undefined => {
|
|
if (!name || !Array.isArray(config)) return undefined;
|
|
|
|
const nameParts = name.split('.');
|
|
|
|
const findInConfig = (configs: FormConfig, parts: string[]): string | undefined => {
|
|
if (parts.length === 0) return undefined;
|
|
|
|
const [currentPart, ...remainingParts] = parts;
|
|
|
|
for (const item of configs) {
|
|
if (item.name === currentPart) {
|
|
if (remainingParts.length === 0) {
|
|
return typeof item.text === 'string' ? item.text : undefined;
|
|
}
|
|
|
|
if (item.items && Array.isArray(item.items)) {
|
|
const result = findInConfig(item.items, remainingParts);
|
|
if (result !== undefined) return result;
|
|
}
|
|
}
|
|
|
|
if (item.items && Array.isArray(item.items)) {
|
|
const result = findInConfig(item.items, parts);
|
|
if (result !== undefined) return result;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
return findInConfig(config, nameParts);
|
|
};
|
|
|
|
defineExpose({
|
|
values,
|
|
lastValuesProcessed,
|
|
formState,
|
|
initialized,
|
|
changeRecords,
|
|
|
|
changeHandler,
|
|
|
|
resetForm: () => {
|
|
tMagicFormRef.value?.resetFields();
|
|
changeRecords.value = [];
|
|
},
|
|
|
|
submitForm: async (native?: boolean): Promise<any> => {
|
|
try {
|
|
const result = await tMagicFormRef.value?.validate();
|
|
// tdesign 错误通过返回值返回
|
|
// element-plus 通过throw error
|
|
if (result !== true) {
|
|
throw result;
|
|
}
|
|
changeRecords.value = [];
|
|
return native ? values.value : cloneDeep(toRaw(values.value));
|
|
} catch (invalidFields: any) {
|
|
emit('error', invalidFields);
|
|
|
|
const error: string[] = [];
|
|
|
|
Object.entries(invalidFields).forEach(([prop, ValidateError]) => {
|
|
(ValidateError as ValidateError[]).forEach(({ field, message }) => {
|
|
const name = field || prop;
|
|
const text = getTextByName(name, props.config) || name;
|
|
|
|
error.push(`${text} -> ${message}`);
|
|
});
|
|
});
|
|
|
|
throw new Error(error.join('<br>'));
|
|
}
|
|
},
|
|
|
|
getTextByName,
|
|
});
|
|
</script>
|