mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-12 15:09:43 +00:00
feat(form,editor): 静默校验模式下跳过叶子字段渲染以提升性能
引入 silentLeafFieldTypes 白名单与 FORM_SILENT_MODE_KEY 注入。 Container 与编辑器重型字段在静默校验时跳过 UI 渲染,仅保留 FormItem 校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
53c4763a59
commit
44cd3d480f
@ -15,7 +15,7 @@
|
||||
:prop="prop"
|
||||
></MFormContainer>
|
||||
<MSelect
|
||||
v-else
|
||||
v-else-if="!silentMode"
|
||||
class="select"
|
||||
:config="selectConfig"
|
||||
:name="name"
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<!-- 查看/编辑按钮:对比模式为只读,不展示 -->
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode && model[name] && hasCodeBlockSidePanel"
|
||||
v-if="!isCompareMode && !silentMode && model[name] && hasCodeBlockSidePanel"
|
||||
class="m-fields-select-action-button"
|
||||
:size="size"
|
||||
@click="editCode(model[name])"
|
||||
@ -65,6 +65,7 @@ import {
|
||||
createValues,
|
||||
type FieldProps,
|
||||
filterFunction,
|
||||
FORM_SILENT_MODE_KEY,
|
||||
type FormItemConfig,
|
||||
type FormState,
|
||||
MContainer as MFormContainer,
|
||||
@ -83,6 +84,7 @@ defineOptions({
|
||||
});
|
||||
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
const { codeBlockService, uiService } = useServices();
|
||||
const eventBus = inject<EventBus>('eventBus');
|
||||
const emit = defineEmits<{
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
}"
|
||||
>
|
||||
<FieldSelect
|
||||
v-if="isSelectValid"
|
||||
v-if="isSelectValid && !silentMode"
|
||||
:model-value="model[name]"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@ -18,7 +18,7 @@
|
||||
></FieldSelect>
|
||||
|
||||
<component
|
||||
v-else
|
||||
v-else-if="!silentMode"
|
||||
:is="tagName"
|
||||
:config="config.fieldConfig"
|
||||
:model="model"
|
||||
@ -33,7 +33,7 @@
|
||||
></component>
|
||||
|
||||
<TMagicTooltip
|
||||
v-if="config.fieldConfig && !disabledDataSource && !mForm?.isCompare"
|
||||
v-if="!silentMode && config.fieldConfig && !disabledDataSource && !mForm?.isCompare"
|
||||
:disabled="showDataSourceFieldSelect"
|
||||
content="选择数据源"
|
||||
>
|
||||
@ -57,6 +57,7 @@ import {
|
||||
type ContainerChangeEventData,
|
||||
type DataSourceFieldSelectConfig,
|
||||
type FieldProps,
|
||||
FORM_SILENT_MODE_KEY,
|
||||
type FormState,
|
||||
getFormField,
|
||||
} from '@tmagic/form';
|
||||
@ -104,6 +105,7 @@ watch(
|
||||
|
||||
const { dataSourceService, propsService } = useServices();
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
const dataSources = computed(() => dataSourceService.get('dataSources') || []);
|
||||
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
</div>
|
||||
|
||||
<FloatingBox
|
||||
v-if="!silentMode"
|
||||
:body-style="{ padding: '0 16px' }"
|
||||
v-model:visible="addDialogVisible"
|
||||
v-model:width="width"
|
||||
@ -30,6 +31,7 @@
|
||||
</FloatingBox>
|
||||
|
||||
<FloatingBox
|
||||
v-if="!silentMode"
|
||||
:body-style="{ padding: '0 16px' }"
|
||||
v-model:visible="addFromJsonDialogVisible"
|
||||
v-model:width="width"
|
||||
@ -60,6 +62,7 @@ import {
|
||||
type ContainerChangeEventData,
|
||||
type DataSourceFieldsConfig,
|
||||
type FieldProps,
|
||||
FORM_SILENT_MODE_KEY,
|
||||
type FormConfig,
|
||||
type FormState,
|
||||
MFormBox,
|
||||
@ -89,6 +92,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { uiService } = useServices();
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
/** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
|
||||
const isCompare = computed(() => Boolean(mForm?.isCompare));
|
||||
@ -379,6 +383,8 @@ provide(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (silentMode) return;
|
||||
|
||||
const path = editingFieldPath.value;
|
||||
if (!path.length) return;
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<div class="m-fields-data-source-method-select">
|
||||
<div class="data-source-method-select-container">
|
||||
<MCascader
|
||||
v-if="!silentMode"
|
||||
class="select"
|
||||
:config="cascaderConfig"
|
||||
:model="model"
|
||||
@ -13,7 +14,7 @@
|
||||
></MCascader>
|
||||
|
||||
<TMagicTooltip
|
||||
v-if="model[name] && isCustomMethod && dataSourceSidePanel && !isCompare"
|
||||
v-if="!silentMode && model[name] && isCustomMethod && dataSourceSidePanel && !isCompare"
|
||||
:content="notEditable ? '查看' : '编辑'"
|
||||
>
|
||||
<TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler">
|
||||
@ -48,6 +49,7 @@ import {
|
||||
type DataSourceMethodSelectConfig,
|
||||
type FieldProps,
|
||||
filterFunction,
|
||||
FORM_SILENT_MODE_KEY,
|
||||
type FormItemConfig,
|
||||
type FormState,
|
||||
MCascader,
|
||||
@ -67,6 +69,7 @@ defineOptions({
|
||||
|
||||
const { dataSourceService, uiService } = useServices();
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
const eventBus = inject<EventBus>('eventBus');
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
|
||||
<CodeBlockEditor
|
||||
v-if="codeConfig"
|
||||
v-if="codeConfig && !silentMode"
|
||||
ref="codeBlockEditor"
|
||||
:disabled="disabled"
|
||||
:content="codeConfig"
|
||||
@ -26,7 +26,13 @@ import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { CodeBlockContent } from '@tmagic/core';
|
||||
import { TMagicButton, tMagicMessageBox } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, DataSourceMethodsConfig, FieldProps, FormState } from '@tmagic/form';
|
||||
import {
|
||||
type ContainerChangeEventData,
|
||||
type DataSourceMethodsConfig,
|
||||
type FieldProps,
|
||||
FORM_SILENT_MODE_KEY,
|
||||
type FormState,
|
||||
} from '@tmagic/form';
|
||||
import { type ColumnConfig, MagicTable } from '@tmagic/table';
|
||||
|
||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
||||
@ -43,6 +49,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodsConfig>>(), {
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
/** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
|
||||
const isCompare = computed(() => Boolean(mForm?.isCompare));
|
||||
@ -170,6 +177,8 @@ const editingMethodName = inject<ComputedRef<string | undefined>>(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (silentMode) return;
|
||||
|
||||
const methodName = editingMethodName.value;
|
||||
if (!methodName) return;
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
</div>
|
||||
|
||||
<FloatingBox
|
||||
v-if="!silentMode"
|
||||
:body-style="{ padding: '0 16px' }"
|
||||
v-model:visible="addDialogVisible"
|
||||
v-model:width="width"
|
||||
@ -34,7 +35,14 @@ import { computed, inject, Ref, ref } from 'vue';
|
||||
|
||||
import type { MockSchema } from '@tmagic/core';
|
||||
import { TMagicButton, tMagicMessageBox, TMagicSwitch } from '@tmagic/design';
|
||||
import { type DataSourceMocksConfig, type FieldProps, type FormConfig, type FormState, MFormBox } from '@tmagic/form';
|
||||
import {
|
||||
type DataSourceMocksConfig,
|
||||
type FieldProps,
|
||||
FORM_SILENT_MODE_KEY,
|
||||
type FormConfig,
|
||||
type FormState,
|
||||
MFormBox,
|
||||
} from '@tmagic/form';
|
||||
import { type ColumnConfig, MagicTable } from '@tmagic/table';
|
||||
import { getDefaultValueFromFields } from '@tmagic/utils';
|
||||
|
||||
@ -57,6 +65,7 @@ const emit = defineEmits(['change']);
|
||||
|
||||
const { uiService } = useServices();
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
/** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
|
||||
const isCompare = computed(() => Boolean(mForm?.isCompare));
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<MagicCodeEditor
|
||||
v-if="config.advanced && showCode"
|
||||
v-if="config.advanced && showCode && !silentMode"
|
||||
editor-custom-type="m-fields-key-value"
|
||||
language="javascript"
|
||||
:init-values="model[name]"
|
||||
@ -60,7 +60,7 @@
|
||||
></MagicCodeEditor>
|
||||
|
||||
<TMagicButton
|
||||
v-if="config.advanced && !isCompare"
|
||||
v-if="config.advanced && !isCompare && !silentMode"
|
||||
size="default"
|
||||
:disabled="disabled"
|
||||
link
|
||||
@ -75,7 +75,7 @@ import { computed, inject, ref, watchEffect } from 'vue';
|
||||
import { Delete, Plus } from '@element-plus/icons-vue';
|
||||
|
||||
import { TMagicButton, TMagicInput } from '@tmagic/design';
|
||||
import type { FieldProps, FormState, KeyValueConfig } from '@tmagic/form';
|
||||
import { type FieldProps, FORM_SILENT_MODE_KEY, type FormState, type KeyValueConfig } from '@tmagic/form';
|
||||
|
||||
import CodeIcon from '@editor/icons/CodeIcon.vue';
|
||||
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||
@ -93,6 +93,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
/** 对比模式下隐藏增删/代码切换等操作按钮,仅保留只读展示。 */
|
||||
const isCompare = computed(() => Boolean(mForm?.isCompare));
|
||||
|
||||
@ -21,7 +21,7 @@ import { type App } from 'vue';
|
||||
import type { DesignPluginOptions } from '@tmagic/design';
|
||||
import designPlugin from '@tmagic/design';
|
||||
import type { FormInstallOptions } from '@tmagic/form';
|
||||
import formPlugin, { registerTypeMatchRules } from '@tmagic/form';
|
||||
import formPlugin, { registerSilentLeafFieldTypes, registerTypeMatchRules } from '@tmagic/form';
|
||||
import tablePlugin from '@tmagic/table';
|
||||
|
||||
import Code from './fields/Code.vue';
|
||||
@ -68,6 +68,14 @@ export default {
|
||||
app.use(formPlugin, opt || {});
|
||||
app.use(tablePlugin);
|
||||
registerTypeMatchRules(editorTypeMatchRules);
|
||||
registerSilentLeafFieldTypes([
|
||||
'vs-code',
|
||||
'ui-select',
|
||||
'cond-op-select',
|
||||
'page-fragment-select',
|
||||
'data-source-select',
|
||||
'data-source-input',
|
||||
]);
|
||||
|
||||
app.config.globalProperties.$TMAGIC_EDITOR = option;
|
||||
setEditorConfig(option);
|
||||
|
||||
@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import CodeSelectCol from '@editor/fields/CodeSelectCol.vue';
|
||||
|
||||
const codeBlockService = {
|
||||
@ -113,6 +115,16 @@ describe('CodeSelectCol', () => {
|
||||
expect(wrapper.find('.fake-params').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('静默模式跳过选择器和编辑按钮但保留 CodeParams', () => {
|
||||
const wrapper = mount(CodeSelectCol, {
|
||||
props: baseProps() as any,
|
||||
global: { provide: { [FORM_SILENT_MODE_KEY as symbol]: true } },
|
||||
});
|
||||
expect(wrapper.findComponent({ name: 'MSelect' }).exists()).toBe(false);
|
||||
expect(wrapper.find('button').exists()).toBe(false);
|
||||
expect(wrapper.find('.fake-params').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('选择无 params 的代码块不渲染 CodeParams', () => {
|
||||
const wrapper = mount(CodeSelectCol, { props: baseProps({ model: { codeId: 'c2', params: {} } }) as any });
|
||||
expect(wrapper.find('.fake-params').exists()).toBe(false);
|
||||
|
||||
@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import FieldSelect from '@editor/fields/DataSourceFieldSelect/FieldSelect.vue';
|
||||
import DSFSIndex from '@editor/fields/DataSourceFieldSelect/Index.vue';
|
||||
|
||||
@ -196,6 +198,16 @@ describe('DataSourceFieldSelect Index', () => {
|
||||
expect(wrapper.findAll('.fake-cascader').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('静默模式跳过内部 FieldSelect', () => {
|
||||
const wrapper = mount(DSFSIndex, {
|
||||
props: { config: {}, model: { v: [] }, name: 'v' } as any,
|
||||
global: { provide: { [FORM_SILENT_MODE_KEY as symbol]: true } },
|
||||
});
|
||||
expect(wrapper.findAll('.fake-cascader').length).toBe(0);
|
||||
expect(wrapper.find('.fake-btn').exists()).toBe(false);
|
||||
expect(wrapper.find('fake-form-field').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('toggle showDataSourceFieldSelect', async () => {
|
||||
const wrapper = mount(DSFSIndex, {
|
||||
props: {
|
||||
|
||||
@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import DataSourceFields from '@editor/fields/DataSourceFields.vue';
|
||||
|
||||
const { messageBoxConfirm, messageError } = vi.hoisted(() => ({
|
||||
@ -132,6 +134,14 @@ describe('DataSourceFields', () => {
|
||||
expect(wrapper.findAll('.fake-btn').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test('静默模式不渲染编辑浮窗', () => {
|
||||
const wrapper = mount(DataSourceFields, {
|
||||
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
|
||||
global: { provide: { [FORM_SILENT_MODE_KEY as symbol]: true } },
|
||||
});
|
||||
expect(wrapper.findAll('.fake-floating')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('点击新增字段添加', async () => {
|
||||
const model: any = { fields: [] };
|
||||
const wrapper = mount(DataSourceFields, {
|
||||
|
||||
@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import DataSourceMethodSelect from '@editor/fields/DataSourceMethodSelect.vue';
|
||||
|
||||
const dataSourceService = {
|
||||
@ -137,6 +139,16 @@ describe('DataSourceMethodSelect', () => {
|
||||
expect(wrapper.find('.fake-params').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('静默模式跳过级联选择器和编辑按钮但保留 CodeParams', () => {
|
||||
const wrapper = mount(DataSourceMethodSelect, {
|
||||
props: baseProps() as any,
|
||||
global: { provide: { [FORM_SILENT_MODE_KEY as symbol]: true } },
|
||||
});
|
||||
expect(wrapper.findComponent({ name: 'MCascader' }).exists()).toBe(false);
|
||||
expect(wrapper.find('button').exists()).toBe(false);
|
||||
expect(wrapper.find('.fake-params').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('onChangeHandler emit change 包含 changeRecords', async () => {
|
||||
dataSourceService.getDataSourceById.mockReturnValue({ id: 'ds1', methods: [] });
|
||||
const wrapper = mount(DataSourceMethodSelect, { props: baseProps() as any });
|
||||
|
||||
@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h, nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import DataSourceMethods from '@editor/fields/DataSourceMethods.vue';
|
||||
|
||||
const { messageBoxConfirm, codeBlockEditorShow, codeBlockEditorHide } = vi.hoisted(() => ({
|
||||
@ -109,6 +111,26 @@ describe('DataSourceMethods.vue', () => {
|
||||
expect(codeBlockEditorShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('静默模式不根据注入的方法名打开编辑器', async () => {
|
||||
const wrapper = mount(DataSourceMethods, {
|
||||
props: {
|
||||
name: 'methods',
|
||||
prop: 'methods',
|
||||
config: {} as any,
|
||||
model: { methods: [{ name: 'm1', content: 'function () {}' }] } as any,
|
||||
} as any,
|
||||
global: {
|
||||
provide: {
|
||||
[FORM_SILENT_MODE_KEY as symbol]: true,
|
||||
editingDataSourceMethodName: { value: 'm1' },
|
||||
},
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
expect(wrapper.find('.fake-code-block-editor').exists()).toBe(false);
|
||||
expect(codeBlockEditorShow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('编辑 action - method.content 是 string', async () => {
|
||||
const wrapper = mount(DataSourceMethods, {
|
||||
props: {
|
||||
|
||||
@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import DataSourceMocks from '@editor/fields/DataSourceMocks.vue';
|
||||
|
||||
const { messageBoxConfirm } = vi.hoisted(() => ({ messageBoxConfirm: vi.fn(async () => true) }));
|
||||
@ -114,6 +116,14 @@ describe('DataSourceMocks', () => {
|
||||
expect(wrapper.find('.fake-add-btn').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('静默模式不渲染编辑浮窗', () => {
|
||||
const wrapper = mount(DataSourceMocks, {
|
||||
props: { config: {}, model: { mocks: [] }, name: 'mocks' } as any,
|
||||
global: { provide: { [FORM_SILENT_MODE_KEY as symbol]: true } },
|
||||
});
|
||||
expect(wrapper.find('.fake-floating').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('点击添加按钮显示 dialog', async () => {
|
||||
const wrapper = mount(DataSourceMocks, {
|
||||
props: { config: {}, model: { mocks: [] }, name: 'mocks' } as any,
|
||||
|
||||
@ -7,6 +7,8 @@ import { describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h, nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import KeyValue from '@editor/fields/KeyValue.vue';
|
||||
|
||||
vi.mock('@tmagic/design', () => ({
|
||||
@ -124,6 +126,17 @@ describe('KeyValue', () => {
|
||||
expect(wrapper.find('.code-editor').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('静默模式跳过高级代码编辑器', () => {
|
||||
const wrapper = mount(KeyValue, {
|
||||
props: baseProps({
|
||||
config: { advanced: true, type: 'key-value' },
|
||||
model: { kv: () => null },
|
||||
}) as any,
|
||||
global: { provide: { [FORM_SILENT_MODE_KEY as symbol]: true } },
|
||||
});
|
||||
expect(wrapper.find('.code-editor').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('CodeEditor save emit change', async () => {
|
||||
const wrapper = mount(KeyValue, {
|
||||
props: baseProps({
|
||||
|
||||
@ -15,6 +15,7 @@ vi.mock('@tmagic/form', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
default: { install: vi.fn() },
|
||||
registerSilentLeafFieldTypes: vi.fn(),
|
||||
registerTypeMatchRules: vi.fn(),
|
||||
};
|
||||
});
|
||||
@ -76,12 +77,20 @@ describe('plugin install', () => {
|
||||
};
|
||||
|
||||
test('install 调用 design/form/table 插件并注册全局组件', async () => {
|
||||
const { registerTypeMatchRules } = await import('@tmagic/form');
|
||||
const { registerSilentLeafFieldTypes, registerTypeMatchRules } = await import('@tmagic/form');
|
||||
const { editorTypeMatchRules } = await import('@editor/utils/type-match-rules');
|
||||
const { app, components } = buildApp();
|
||||
editorPlugin.install(app, { someOption: true } as any);
|
||||
expect(app.use).toHaveBeenCalledTimes(3);
|
||||
expect(registerTypeMatchRules).toHaveBeenCalledWith(editorTypeMatchRules);
|
||||
expect(registerSilentLeafFieldTypes).toHaveBeenCalledWith([
|
||||
'vs-code',
|
||||
'ui-select',
|
||||
'cond-op-select',
|
||||
'page-fragment-select',
|
||||
'data-source-select',
|
||||
'data-source-input',
|
||||
]);
|
||||
expect(Object.keys(components).length).toBeGreaterThan(10);
|
||||
expect(components.MEditor).toBeDefined();
|
||||
});
|
||||
|
||||
@ -27,7 +27,15 @@
|
||||
<template v-else-if="type && display && !showDiff">
|
||||
<TMagicFormItem v-bind="formItemProps" :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text }">
|
||||
<template #label>
|
||||
<slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
|
||||
<slot
|
||||
v-if="shouldRenderLeafField"
|
||||
name="label"
|
||||
:config="config"
|
||||
:type="type"
|
||||
:text="text"
|
||||
:prop="itemProp"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
@ -41,8 +49,25 @@
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<!-- 静默校验时只保留 FormItem:校验依赖其 prop / rules 与 model 值,与字段组件实例无关 -->
|
||||
<template v-if="shouldRenderLeafField">
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<component
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="model"
|
||||
:last-values="lastValues"
|
||||
:is-compare="isCompare"
|
||||
@change="onChangeHandler"
|
||||
@addDiffCount="onAddDiffCount"
|
||||
></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
|
||||
<component
|
||||
v-else
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="model"
|
||||
@ -51,24 +76,13 @@
|
||||
@change="onChangeHandler"
|
||||
@addDiffCount="onAddDiffCount"
|
||||
></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
|
||||
<component
|
||||
v-else
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="model"
|
||||
:last-values="lastValues"
|
||||
:is-compare="isCompare"
|
||||
@change="onChangeHandler"
|
||||
@addDiffCount="onAddDiffCount"
|
||||
></component>
|
||||
</template>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicTooltip
|
||||
v-if="shouldRenderLeafField && config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel"
|
||||
placement="top"
|
||||
>
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -88,7 +102,15 @@
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
|
||||
<slot
|
||||
v-if="shouldRenderLeafField"
|
||||
name="label"
|
||||
:config="config"
|
||||
:type="type"
|
||||
:text="text"
|
||||
:prop="itemProp"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
@ -98,8 +120,23 @@
|
||||
></FormLabel>
|
||||
</slot>
|
||||
</template>
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<template v-if="shouldRenderLeafField">
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<component
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="model"
|
||||
:last-values="lastValues"
|
||||
:is-compare="isCompare"
|
||||
@change="onChangeHandler"
|
||||
></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
|
||||
<component
|
||||
v-else
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="model"
|
||||
@ -107,20 +144,7 @@
|
||||
:is-compare="isCompare"
|
||||
@change="onChangeHandler"
|
||||
></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
|
||||
<component
|
||||
v-else
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="model"
|
||||
:last-values="lastValues"
|
||||
:is-compare="isCompare"
|
||||
@change="onChangeHandler"
|
||||
></component>
|
||||
</template>
|
||||
</TMagicFormItem>
|
||||
|
||||
<!-- 普通字段:渲染前后两份独立的组件用于对比 -->
|
||||
@ -131,7 +155,15 @@
|
||||
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-before-diff': true }"
|
||||
>
|
||||
<template #label>
|
||||
<slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
|
||||
<slot
|
||||
v-if="shouldRenderLeafField"
|
||||
name="label"
|
||||
:config="config"
|
||||
:type="type"
|
||||
:text="text"
|
||||
:prop="itemProp"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
@ -141,23 +173,28 @@
|
||||
></FormLabel>
|
||||
</slot>
|
||||
</template>
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<component v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
<template v-if="shouldRenderLeafField">
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<component v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
|
||||
<component
|
||||
v-else
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="lastValues"
|
||||
@change="onChangeHandler"
|
||||
></component>
|
||||
<component
|
||||
v-else
|
||||
v-bind="fieldsProps"
|
||||
:is="tagName"
|
||||
:model="lastValues"
|
||||
@change="onChangeHandler"
|
||||
></component>
|
||||
</template>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicTooltip
|
||||
v-if="shouldRenderLeafField && config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel"
|
||||
placement="top"
|
||||
>
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -171,7 +208,15 @@
|
||||
:class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-after-diff': true }"
|
||||
>
|
||||
<template #label>
|
||||
<slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
|
||||
<slot
|
||||
v-if="shouldRenderLeafField"
|
||||
name="label"
|
||||
:config="config"
|
||||
:type="type"
|
||||
:text="text"
|
||||
:prop="itemProp"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
@ -181,17 +226,22 @@
|
||||
></FormLabel>
|
||||
</slot>
|
||||
</template>
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<component v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
<template v-if="shouldRenderLeafField">
|
||||
<TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
|
||||
<component v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
|
||||
<template #content>
|
||||
<div v-html="tooltip.text"></div>
|
||||
</template>
|
||||
</TMagicTooltip>
|
||||
|
||||
<component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
|
||||
<component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
|
||||
</template>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicTooltip
|
||||
v-if="shouldRenderLeafField && config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel"
|
||||
placement="top"
|
||||
>
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -260,9 +310,10 @@ import type {
|
||||
FormValue,
|
||||
ToolTipConfigType,
|
||||
} from '../schema';
|
||||
import { FORM_DIFF_CONFIG_KEY, FORM_TYPE_MATCH_VALID_KEY } from '../schema';
|
||||
import { FORM_DIFF_CONFIG_KEY, FORM_SILENT_MODE_KEY, FORM_TYPE_MATCH_VALID_KEY } from '../schema';
|
||||
import { getField } from '../utils/config';
|
||||
import { createObjectProp, display as displayFunction, filterFunction, getRules } from '../utils/form';
|
||||
import { getSilentLeafFieldTypes } from '../utils/silentLeafFieldTypes';
|
||||
|
||||
import FormLabel from './FormLabel.vue';
|
||||
|
||||
@ -407,6 +458,11 @@ const effectiveSelfDiffFieldTypes = computed<Set<string>>(() => {
|
||||
|
||||
const isSelfDiffField = computed(() => effectiveSelfDiffFieldTypes.value.has(type.value));
|
||||
|
||||
// 静默模式下仅白名单叶子字段跳过渲染;扩展字段通过 registerSilentLeafFieldTypes 注册
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
const shouldRenderLeafField = computed(() => !silentMode || !getSilentLeafFieldTypes().has(type.value));
|
||||
|
||||
const disabled = computed(() => props.disabled || filterFunction(mForm, props.config.disabled, props));
|
||||
|
||||
const text = computed(() => filterFunction(mForm, props.config.text, props));
|
||||
|
||||
@ -63,6 +63,12 @@ export {
|
||||
registerField as registerFormField,
|
||||
} from './utils/config';
|
||||
|
||||
export {
|
||||
clearSilentLeafFieldTypes,
|
||||
getSilentLeafFieldTypes,
|
||||
registerSilentLeafFieldTypes,
|
||||
} from './utils/silentLeafFieldTypes';
|
||||
|
||||
export {
|
||||
clearTypeMatchRules,
|
||||
deleteTypeMatchRule,
|
||||
|
||||
@ -32,11 +32,12 @@ export const FORM_TYPE_MATCH_VALID_KEY: InjectionKey<ComputedRef<boolean>> = Sym
|
||||
/**
|
||||
* 静默模式标记,由 `submitForm` / `validateForm` 以隐藏方式挂载临时表单时 provide(debug 弹层模式不提供)。
|
||||
*
|
||||
* 重型字段组件(如 vs-code)可 inject 该标记并跳过自身渲染:字段校验由 FormItem 基于 model 值完成,
|
||||
* 与叶子 UI 组件实例无关,跳过可省去无谓的实例化开销(如 monaco worker / model 的创建)。
|
||||
* Container 会基于该标记,并对照 `getSilentLeafFieldTypes()`(默认 `LEAF_FIELD_TYPES`)
|
||||
* 跳过叶子字段的渲染,只保留 FormItem:字段校验由 FormItem 基于 model 值完成,与叶子 UI 组件实例无关。
|
||||
* 重型字段组件(如 vs-code)也可 inject 该标记跳过自身渲染,省去无谓的实例化开销。
|
||||
*
|
||||
* 注意:仅「挂载无值副作用」(不在 onMounted/watch immediate 中 emit change、不依赖 DOM 计算值)的
|
||||
* 字段组件可以安全跳过,接入前需逐一审计。
|
||||
* 字段组件可以安全跳过,接入前需逐一审计;业务扩展请使用 `registerSilentLeafFieldTypes`。
|
||||
*/
|
||||
export const FORM_SILENT_MODE_KEY: InjectionKey<boolean> = Symbol('mFormSilentMode');
|
||||
|
||||
|
||||
71
packages/form/src/utils/silentLeafFieldTypes.ts
Normal file
71
packages/form/src/utils/silentLeafFieldTypes.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { toLine } from '@tmagic/utils';
|
||||
|
||||
/**
|
||||
* 可在静默校验模式下跳过渲染的内置叶子字段(默认集合)。
|
||||
*
|
||||
* 这些字段只渲染一个输入控件,内部没有嵌套 FormItem,且不会依赖组件挂载修改 model;
|
||||
* 字段值由初始化逻辑处理(包括 defaultValue),因此跳过不影响校验结果。
|
||||
*
|
||||
* 业务侧可通过 `registerSilentLeafFieldTypes` 追加已审计的安全自定义字段。
|
||||
*/
|
||||
export const LEAF_FIELD_TYPES: ReadonlySet<string> = new Set([
|
||||
'text',
|
||||
'textarea',
|
||||
'number',
|
||||
'time',
|
||||
'daterange',
|
||||
'timerange',
|
||||
'checkbox',
|
||||
'radio-group',
|
||||
'switch',
|
||||
'select',
|
||||
'cascader',
|
||||
'color-picker',
|
||||
'link',
|
||||
]);
|
||||
|
||||
/** 业务侧追加的静默叶子字段(不含内置默认集合) */
|
||||
const extraSilentLeafFieldTypes = new Set<string>();
|
||||
|
||||
/**
|
||||
* 注册可在静默校验中跳过渲染的叶子字段 type(追加到默认 `LEAF_FIELD_TYPES` 之上)。
|
||||
*
|
||||
* 仅应纳入「无嵌套 FormItem、无挂载值副作用」的字段;复合 / 动态子表单不可加入。
|
||||
*/
|
||||
export const registerSilentLeafFieldTypes = (types: Iterable<string>): void => {
|
||||
for (const type of types) {
|
||||
if (typeof type !== 'string' || !type) continue;
|
||||
extraSilentLeafFieldTypes.add(toLine(type));
|
||||
}
|
||||
};
|
||||
|
||||
/** 当前生效的静默叶子字段集合:内置默认 ∪ 已注册扩展 */
|
||||
export const getSilentLeafFieldTypes = (): ReadonlySet<string> => {
|
||||
if (extraSilentLeafFieldTypes.size === 0) {
|
||||
return LEAF_FIELD_TYPES;
|
||||
}
|
||||
return new Set([...LEAF_FIELD_TYPES, ...extraSilentLeafFieldTypes]);
|
||||
};
|
||||
|
||||
/** 清空业务侧追加的静默叶子字段(不影响内置默认集合;主要用于单测) */
|
||||
export const clearSilentLeafFieldTypes = (): void => {
|
||||
extraSilentLeafFieldTypes.clear();
|
||||
};
|
||||
@ -17,7 +17,7 @@
|
||||
*/
|
||||
import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest';
|
||||
import { type AppContext, createApp, defineComponent, h, nextTick } from 'vue';
|
||||
import MagicForm, { validateForm } from '@form/index';
|
||||
import MagicForm, { clearSilentLeafFieldTypes, registerSilentLeafFieldTypes, validateForm } from '@form/index';
|
||||
import ElementPlus from 'element-plus';
|
||||
|
||||
let appContext: AppContext;
|
||||
@ -33,6 +33,7 @@ beforeAll(() => {
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
clearSilentLeafFieldTypes();
|
||||
});
|
||||
|
||||
// 说明:validateForm 内部会新建一个独立的 MForm 实例并复用其校验方法 `validate`(返回错误文案、不抛异常),
|
||||
@ -403,3 +404,189 @@ describe('validateForm —— debug 模式', () => {
|
||||
setTimeoutSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateForm —— 静默模式', () => {
|
||||
/**
|
||||
* 两个探针都由渲染过程驱动,与 DOM 时序无关:
|
||||
* - `extra` 在 Container 的 formItemProps 中求值,被调用即说明 FormItem 渲染了(rules 也随之生效);
|
||||
* - `options` 在 select 组件 setup 的 watchEffect 中求值,被调用即说明字段组件被实例化了。
|
||||
*/
|
||||
const createProbes = () => ({
|
||||
extra: vi.fn(() => ''),
|
||||
options: vi.fn(() => [{ text: 'a', value: 'a' }]),
|
||||
tooltip: vi.fn(() => ({ text: 'tip' })),
|
||||
});
|
||||
|
||||
const selectConfig = ({ extra, options, tooltip }: ReturnType<typeof createProbes>): any => [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'kind',
|
||||
text: '类型',
|
||||
extra,
|
||||
options,
|
||||
tooltip,
|
||||
rules: [{ required: true, message: '类型不能为空' }],
|
||||
},
|
||||
];
|
||||
|
||||
test('开启后只挂载 FormItem,不实例化内置叶子字段', async () => {
|
||||
const probes = createProbes();
|
||||
|
||||
const error = await validateForm({
|
||||
config: selectConfig(probes),
|
||||
initValues: { kind: 'a' },
|
||||
appContext,
|
||||
});
|
||||
|
||||
expect(error).toBe('');
|
||||
expect(probes.extra).toHaveBeenCalled();
|
||||
expect(probes.options).not.toHaveBeenCalled();
|
||||
expect(probes.tooltip).not.toHaveBeenCalled();
|
||||
expect(document.body.querySelector('.m-form')).toBeNull();
|
||||
});
|
||||
|
||||
test('带挂载初始化逻辑的字段不跳过渲染,保持校验值一致', async () => {
|
||||
const error = await validateForm({
|
||||
config: [
|
||||
{
|
||||
type: 'display',
|
||||
name: 'status',
|
||||
text: '状态',
|
||||
initValue: 'ready',
|
||||
rules: [{ required: true, message: '状态不能为空' }],
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'date',
|
||||
text: '日期',
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
] as any,
|
||||
initValues: { date: new Date('2021-07-17T15:37:00') },
|
||||
typeMatchValid: true,
|
||||
appContext,
|
||||
});
|
||||
|
||||
expect(error).toBe('');
|
||||
});
|
||||
|
||||
test('非内置叶子字段仍照常渲染,避免漏掉其内部嵌套字段的校验', async () => {
|
||||
const customSetup = vi.fn();
|
||||
const customField = defineComponent({
|
||||
name: 'CustomField',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
model: { type: Object, default: () => ({}) },
|
||||
name: { type: String, default: '' },
|
||||
},
|
||||
setup() {
|
||||
customSetup();
|
||||
return () => h('div', 'custom');
|
||||
},
|
||||
});
|
||||
|
||||
await validateForm({
|
||||
config: [{ type: 'component', name: 'custom', text: '自定义', component: customField }] as any,
|
||||
initValues: { custom: 'x' },
|
||||
appContext,
|
||||
});
|
||||
|
||||
expect(customSetup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('debug 模式下不生效:弹层需要完整渲染供人工操作', async () => {
|
||||
const probes = createProbes();
|
||||
|
||||
const pending = validateForm({
|
||||
config: selectConfig(probes),
|
||||
initValues: { kind: 'a' },
|
||||
appContext,
|
||||
debug: true,
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(probes.options).toHaveBeenCalled();
|
||||
|
||||
const confirm = Array.from(document.body.querySelectorAll('button')).find(
|
||||
(b) => (b.textContent || '').trim() === '确定',
|
||||
) as HTMLButtonElement;
|
||||
confirm.click();
|
||||
await pending;
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateForm —— 可配置静默叶子字段集合', () => {
|
||||
const createCustomField = (setupSpy: () => void) =>
|
||||
defineComponent({
|
||||
name: 'MFieldsCustomSafe',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
model: { type: Object, default: () => ({}) },
|
||||
name: { type: String, default: '' },
|
||||
},
|
||||
setup() {
|
||||
setupSpy();
|
||||
return () => h('div', 'custom-safe');
|
||||
},
|
||||
});
|
||||
|
||||
test('registerSilentLeafFieldTypes 后,自定义 type 在静默模式跳过渲染', async () => {
|
||||
const setupSpy = vi.fn();
|
||||
const parentApp = createApp(defineComponent({ render: () => h('div') }));
|
||||
parentApp.use(ElementPlus);
|
||||
parentApp.use(MagicForm);
|
||||
parentApp.component('m-fields-custom-safe', createCustomField(setupSpy));
|
||||
registerSilentLeafFieldTypes(['custom-safe']);
|
||||
|
||||
await validateForm({
|
||||
config: [{ type: 'custom-safe', name: 'safe', text: '安全自定义' }] as any,
|
||||
initValues: { safe: 'x' },
|
||||
appContext: parentApp._context,
|
||||
});
|
||||
|
||||
expect(setupSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('未注册时自定义 type 仍渲染', async () => {
|
||||
const setupSpy = vi.fn();
|
||||
const parentApp = createApp(defineComponent({ render: () => h('div') }));
|
||||
parentApp.use(ElementPlus);
|
||||
parentApp.use(MagicForm);
|
||||
parentApp.component('m-fields-custom-safe', createCustomField(setupSpy));
|
||||
|
||||
await validateForm({
|
||||
config: [{ type: 'custom-safe', name: 'safe', text: '安全自定义' }] as any,
|
||||
initValues: { safe: 'x' },
|
||||
appContext: parentApp._context,
|
||||
});
|
||||
|
||||
expect(setupSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('debug 模式下即使注册了自定义集合也完整渲染', async () => {
|
||||
const setupSpy = vi.fn();
|
||||
const parentApp = createApp(defineComponent({ render: () => h('div') }));
|
||||
parentApp.use(ElementPlus);
|
||||
parentApp.use(MagicForm);
|
||||
parentApp.component('m-fields-custom-safe', createCustomField(setupSpy));
|
||||
registerSilentLeafFieldTypes(['custom-safe']);
|
||||
|
||||
const pending = validateForm({
|
||||
config: [{ type: 'custom-safe', name: 'safe', text: '安全自定义' }] as any,
|
||||
initValues: { safe: 'x' },
|
||||
appContext: parentApp._context,
|
||||
debug: true,
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(setupSpy).toHaveBeenCalled();
|
||||
|
||||
const confirm = Array.from(document.body.querySelectorAll('button')).find(
|
||||
(b) => (b.textContent || '').trim() === '确定',
|
||||
) as HTMLButtonElement;
|
||||
confirm.click();
|
||||
await pending;
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user