From 44cd3d480f5f3b76bb6ba6d2bc4b2632f6a5e957 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Mon, 10 Aug 2026 19:42:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(form,editor):=20=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E6=A8=A1=E5=BC=8F=E4=B8=8B=E8=B7=B3=E8=BF=87?= =?UTF-8?q?=E5=8F=B6=E5=AD=90=E5=AD=97=E6=AE=B5=E6=B8=B2=E6=9F=93=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入 silentLeafFieldTypes 白名单与 FORM_SILENT_MODE_KEY 注入。 Container 与编辑器重型字段在静默校验时跳过 UI 渲染,仅保留 FormItem 校验。 Co-authored-by: Cursor --- packages/editor/src/fields/CodeSelectCol.vue | 6 +- .../fields/DataSourceFieldSelect/Index.vue | 8 +- .../editor/src/fields/DataSourceFields.vue | 6 + .../src/fields/DataSourceMethodSelect.vue | 5 +- .../editor/src/fields/DataSourceMethods.vue | 13 +- .../editor/src/fields/DataSourceMocks.vue | 11 +- packages/editor/src/fields/KeyValue.vue | 7 +- packages/editor/src/plugin.ts | 10 +- .../tests/unit/fields/CodeSelectCol.spec.ts | 12 ++ .../unit/fields/DataSourceFieldSelect.spec.ts | 12 ++ .../unit/fields/DataSourceFields.spec.ts | 10 + .../fields/DataSourceMethodSelect.spec.ts | 12 ++ .../unit/fields/DataSourceMethods.spec.ts | 22 ++ .../tests/unit/fields/DataSourceMocks.spec.ts | 10 + .../editor/tests/unit/fields/KeyValue.spec.ts | 13 ++ packages/editor/tests/unit/plugin.spec.ts | 11 +- packages/form/src/containers/Container.vue | 174 ++++++++++------ packages/form/src/index.ts | 6 + packages/form/src/schema.ts | 7 +- .../form/src/utils/silentLeafFieldTypes.ts | 71 +++++++ packages/form/tests/unit/validateForm.spec.ts | 189 +++++++++++++++++- 21 files changed, 538 insertions(+), 77 deletions(-) create mode 100644 packages/form/src/utils/silentLeafFieldTypes.ts diff --git a/packages/editor/src/fields/CodeSelectCol.vue b/packages/editor/src/fields/CodeSelectCol.vue index dcee5fff..aa64a9a8 100644 --- a/packages/editor/src/fields/CodeSelectCol.vue +++ b/packages/editor/src/fields/CodeSelectCol.vue @@ -15,7 +15,7 @@ :prop="prop" > ('mForm'); +const silentMode = inject(FORM_SILENT_MODE_KEY, false); const { codeBlockService, uiService } = useServices(); const eventBus = inject('eventBus'); const emit = defineEmits<{ diff --git a/packages/editor/src/fields/DataSourceFieldSelect/Index.vue b/packages/editor/src/fields/DataSourceFieldSelect/Index.vue index 8cf679fb..49bc2516 100644 --- a/packages/editor/src/fields/DataSourceFieldSelect/Index.vue +++ b/packages/editor/src/fields/DataSourceFieldSelect/Index.vue @@ -6,7 +6,7 @@ }" > @@ -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('mForm'); +const silentMode = inject(FORM_SILENT_MODE_KEY, false); const dataSources = computed(() => dataSourceService.get('dataSources') || []); const disabledDataSource = computed(() => propsService.getDisabledDataSource()); diff --git a/packages/editor/src/fields/DataSourceFields.vue b/packages/editor/src/fields/DataSourceFields.vue index e904e45e..1da7447c 100644 --- a/packages/editor/src/fields/DataSourceFields.vue +++ b/packages/editor/src/fields/DataSourceFields.vue @@ -8,6 +8,7 @@ ('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; diff --git a/packages/editor/src/fields/DataSourceMethodSelect.vue b/packages/editor/src/fields/DataSourceMethodSelect.vue index aabfe0be..018e58d1 100644 --- a/packages/editor/src/fields/DataSourceMethodSelect.vue +++ b/packages/editor/src/fields/DataSourceMethodSelect.vue @@ -2,6 +2,7 @@
@@ -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('mForm'); +const silentMode = inject(FORM_SILENT_MODE_KEY, false); const eventBus = inject('eventBus'); const emit = defineEmits(['change']); diff --git a/packages/editor/src/fields/DataSourceMethods.vue b/packages/editor/src/fields/DataSourceMethods.vue index 0fe9d6d3..3199c47e 100644 --- a/packages/editor/src/fields/DataSourceMethods.vue +++ b/packages/editor/src/fields/DataSourceMethods.vue @@ -9,7 +9,7 @@
>(), { const emit = defineEmits(['change']); const mForm = inject('mForm'); +const silentMode = inject(FORM_SILENT_MODE_KEY, false); /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */ const isCompare = computed(() => Boolean(mForm?.isCompare)); @@ -170,6 +177,8 @@ const editingMethodName = inject>( ); onMounted(() => { + if (silentMode) return; + const methodName = editingMethodName.value; if (!methodName) return; diff --git a/packages/editor/src/fields/DataSourceMocks.vue b/packages/editor/src/fields/DataSourceMocks.vue index ad64819e..9561be8b 100644 --- a/packages/editor/src/fields/DataSourceMocks.vue +++ b/packages/editor/src/fields/DataSourceMocks.vue @@ -7,6 +7,7 @@
('mForm'); +const silentMode = inject(FORM_SILENT_MODE_KEY, false); /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */ const isCompare = computed(() => Boolean(mForm?.isCompare)); diff --git a/packages/editor/src/fields/KeyValue.vue b/packages/editor/src/fields/KeyValue.vue index 7ab080a5..f123f794 100644 --- a/packages/editor/src/fields/KeyValue.vue +++ b/packages/editor/src/fields/KeyValue.vue @@ -44,7 +44,7 @@ (); const mForm = inject('mForm'); +const silentMode = inject(FORM_SILENT_MODE_KEY, false); /** 对比模式下隐藏增删/代码切换等操作按钮,仅保留只读展示。 */ const isCompare = computed(() => Boolean(mForm?.isCompare)); diff --git a/packages/editor/src/plugin.ts b/packages/editor/src/plugin.ts index 73de647e..e9fd66e0 100644 --- a/packages/editor/src/plugin.ts +++ b/packages/editor/src/plugin.ts @@ -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); diff --git a/packages/editor/tests/unit/fields/CodeSelectCol.spec.ts b/packages/editor/tests/unit/fields/CodeSelectCol.spec.ts index ee68234e..830c71ba 100644 --- a/packages/editor/tests/unit/fields/CodeSelectCol.spec.ts +++ b/packages/editor/tests/unit/fields/CodeSelectCol.spec.ts @@ -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); diff --git a/packages/editor/tests/unit/fields/DataSourceFieldSelect.spec.ts b/packages/editor/tests/unit/fields/DataSourceFieldSelect.spec.ts index ad63bcd8..a296a8ac 100644 --- a/packages/editor/tests/unit/fields/DataSourceFieldSelect.spec.ts +++ b/packages/editor/tests/unit/fields/DataSourceFieldSelect.spec.ts @@ -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: { diff --git a/packages/editor/tests/unit/fields/DataSourceFields.spec.ts b/packages/editor/tests/unit/fields/DataSourceFields.spec.ts index 0d7bd20b..1c6241e4 100644 --- a/packages/editor/tests/unit/fields/DataSourceFields.spec.ts +++ b/packages/editor/tests/unit/fields/DataSourceFields.spec.ts @@ -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, { diff --git a/packages/editor/tests/unit/fields/DataSourceMethodSelect.spec.ts b/packages/editor/tests/unit/fields/DataSourceMethodSelect.spec.ts index 2395ad35..975d58dc 100644 --- a/packages/editor/tests/unit/fields/DataSourceMethodSelect.spec.ts +++ b/packages/editor/tests/unit/fields/DataSourceMethodSelect.spec.ts @@ -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 }); diff --git a/packages/editor/tests/unit/fields/DataSourceMethods.spec.ts b/packages/editor/tests/unit/fields/DataSourceMethods.spec.ts index 8b2f0a22..0155ca93 100644 --- a/packages/editor/tests/unit/fields/DataSourceMethods.spec.ts +++ b/packages/editor/tests/unit/fields/DataSourceMethods.spec.ts @@ -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: { diff --git a/packages/editor/tests/unit/fields/DataSourceMocks.spec.ts b/packages/editor/tests/unit/fields/DataSourceMocks.spec.ts index 4054fcbf..5a53c562 100644 --- a/packages/editor/tests/unit/fields/DataSourceMocks.spec.ts +++ b/packages/editor/tests/unit/fields/DataSourceMocks.spec.ts @@ -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, diff --git a/packages/editor/tests/unit/fields/KeyValue.spec.ts b/packages/editor/tests/unit/fields/KeyValue.spec.ts index 76b2c238..0e6b0c93 100644 --- a/packages/editor/tests/unit/fields/KeyValue.spec.ts +++ b/packages/editor/tests/unit/fields/KeyValue.spec.ts @@ -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({ diff --git a/packages/editor/tests/unit/plugin.spec.ts b/packages/editor/tests/unit/plugin.spec.ts index 6c2e0381..729a89e2 100644 --- a/packages/editor/tests/unit/plugin.spec.ts +++ b/packages/editor/tests/unit/plugin.spec.ts @@ -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(); }); diff --git a/packages/form/src/containers/Container.vue b/packages/form/src/containers/Container.vue index 07149eee..c2b341c5 100644 --- a/packages/form/src/containers/Container.vue +++ b/packages/form/src/containers/Container.vue @@ -27,7 +27,15 @@