diff --git a/packages/editor/src/fields/CodeSelect.vue b/packages/editor/src/fields/CodeSelect.vue index 0d9f508b..77824206 100644 --- a/packages/editor/src/fields/CodeSelect.vue +++ b/packages/editor/src/fields/CodeSelect.vue @@ -93,7 +93,7 @@ const codeConfig = computed(() => ({ { value: HookCodeType.CODE, text: '代码块' }, { value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' }, ], - rules: [{ typeMatch: true, trigger: 'blur' }], + rules: [{ typeMatch: true, trigger: 'change' }], defaultValue: HookCodeType.CODE, onChange: (_mForm, v: HookCodeType, { setModel }) => { if (v === HookCodeType.DATA_SOURCE_METHOD) { @@ -112,7 +112,7 @@ const codeConfig = computed(() => ({ labelWidth: 0, display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD, notEditable: () => !codeBlockService.getEditStatus(), - rules: [{ typeMatch: true, trigger: 'blur' }], + rules: [{ typeMatch: true, trigger: 'change' }], }, { type: 'data-source-method-select', @@ -121,7 +121,7 @@ const codeConfig = computed(() => ({ labelWidth: 0, display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD, notEditable: () => !dataSourceService.get('editable'), - rules: [{ typeMatch: true }], + rules: [{ typeMatch: true, trigger: 'change' }], }, ], }, diff --git a/packages/editor/src/fields/DisplayConds.vue b/packages/editor/src/fields/DisplayConds.vue index 0564ab41..c42650ff 100644 --- a/packages/editor/src/fields/DisplayConds.vue +++ b/packages/editor/src/fields/DisplayConds.vue @@ -98,9 +98,10 @@ const config = computed(() => ({ label: '字段', checkStrictly: false, onChange: fieldOnChange, + defaultValue: () => [], rules: [ { required: true, trigger: 'blur', message: '请选择字段' }, - { typeMatch: true, trigger: 'blur' }, + { typeMatch: true, trigger: 'change' }, ], } : { @@ -111,9 +112,10 @@ const config = computed(() => ({ checkStrictly: false, dataSourceFieldType: ['string', 'number', 'boolean', 'any'], onChange: fieldOnChange, + defaultValue: () => [], rules: [ { required: true, trigger: 'blur', message: '请选择字段' }, - { typeMatch: true, trigger: 'blur' }, + { typeMatch: true, trigger: 'change' }, ], }, { @@ -124,7 +126,7 @@ const config = computed(() => ({ name: 'op', rules: [ { required: true, trigger: 'blur', message: '请选择条件' }, - { typeMatch: true, trigger: 'blur' }, + { typeMatch: true, trigger: 'change' }, ], }, { diff --git a/packages/editor/src/layouts/props-panel/PropsPanel.vue b/packages/editor/src/layouts/props-panel/PropsPanel.vue index f9371aec..92c9d37b 100644 --- a/packages/editor/src/layouts/props-panel/PropsPanel.vue +++ b/packages/editor/src/layouts/props-panel/PropsPanel.vue @@ -167,7 +167,7 @@ const submit = async ( changeRecords: eventData?.changeRecords, historySource, // 启用校验联动时,仅校验失败(error 存在)才把错误信息随更新传入 editorService 记录; - // CodeEditor 源码保存与表单校验成功均不携带 invalidInfo,保持已有错误状态不变。 + // 其余情况(含表单校验成功、CodeEditor 源码保存)不携带 invalidInfo,由 editorService 在执行 update 时统一清除该节点错误。 ...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}), }); } catch (e: any) { diff --git a/packages/editor/src/plugin.ts b/packages/editor/src/plugin.ts index 482b02e8..9c590ebc 100644 --- a/packages/editor/src/plugin.ts +++ b/packages/editor/src/plugin.ts @@ -44,7 +44,7 @@ import StyleSetter from './fields/StyleSetter/Index.vue'; import uiSelect from './fields/UISelect.vue'; import CodeEditor from './layouts/CodeEditor.vue'; import { setEditorConfig } from './utils/config'; -import { editorTypeMatchRules } from './utils/typeMatchRules'; +import { editorTypeMatchRules } from './utils/type-match-rules'; import Editor from './Editor.vue'; import type { EditorInstallOptions } from './type'; diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index ef755989..461f022c 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -815,7 +815,7 @@ class Editor extends BaseService { ); // 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。 - this.applyInvalidInfo(invalidInfo); + this.applyInvalidInfo(config, invalidInfo); if (updateData[0].oldNode?.type !== NodeType.ROOT) { const curNodes = this.get('nodes'); @@ -1666,10 +1666,21 @@ class Editor extends BaseService { /** * 应用一次属性面板提交携带的校验错误信息(在写入历史记录之前调用,使历史快照与本次变更对齐)。 - * error 非空则记录错误,为空则清除对应来源的错误。 + * - invalidInfo 为空(调用方未携带):认为本次更新对应的节点已无校验错误,清除其全部来源的错误; + * - error 非空则记录错误,为空则清除对应来源的错误。 */ - private applyInvalidInfo(invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string }) { - if (!invalidInfo) return; + private applyInvalidInfo( + config: MNode | MNode[], + invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string }, + ) { + if (!invalidInfo) { + // 调用方未携带 invalidInfo:本次更新对应节点不应再保留校验错误,清除其全部来源。 + const ids = (Array.isArray(config) ? config : [config]) + .map((node) => node.id) + .filter((id): id is Id => id !== undefined); + ids.forEach((id) => this.deleteInvalidNode(id)); + return; + } const { id, source, error } = invalidInfo; if (error) { this.setInvalidNode(id, source, error); diff --git a/packages/editor/src/utils/index.ts b/packages/editor/src/utils/index.ts index f57c8754..308fd21f 100644 --- a/packages/editor/src/utils/index.ts +++ b/packages/editor/src/utils/index.ts @@ -30,6 +30,6 @@ export * from './undo-redo'; export * from './indexed-db'; export * from './history'; export * from './const'; -export * from './typeMatchRules'; +export * from './type-match-rules'; export * from './event'; export { default as loadMonaco } from './monaco-editor'; diff --git a/packages/editor/src/utils/props.ts b/packages/editor/src/utils/props.ts index 197d8c62..5915f75a 100644 --- a/packages/editor/src/utils/props.ts +++ b/packages/editor/src/utils/props.ts @@ -18,6 +18,7 @@ */ import { + HookType, NODE_CONDS_KEY, NODE_CONDS_RESULT_KEY, NODE_DISABLE_CODE_BLOCK_KEY, @@ -182,7 +183,17 @@ export const advancedTabConfig: TabPaneConfig = { labelPosition: 'top', type: 'code-select', extra: '组件初始化时执行', - rules: [{ typeMatch: true }], + rules: [ + { typeMatch: true, trigger: 'change' }, + { + validator: ({ value, callback }: any) => { + if (value && value.hookType !== HookType.CODE) { + return callback('hookType 必须是 code'); + } + callback(); + }, + }, + ], }, { name: 'mounted', @@ -190,7 +201,17 @@ export const advancedTabConfig: TabPaneConfig = { labelPosition: 'top', type: 'code-select', extra: '组件挂载到dom时执行', - rules: [{ typeMatch: true }], + rules: [ + { typeMatch: true, trigger: 'change' }, + { + validator: ({ value, callback }: any) => { + if (value && value.hookType !== HookType.CODE) { + return callback('hookType 必须是 code'); + } + callback(); + }, + }, + ], }, { name: 'display', @@ -198,7 +219,17 @@ export const advancedTabConfig: TabPaneConfig = { extra: '控制组件是否渲染,关系的代码块返回值为false时不渲染', labelPosition: 'top', type: 'code-select', - rules: [{ typeMatch: true }], + rules: [ + { typeMatch: true, trigger: 'change' }, + { + validator: ({ value, callback }: any) => { + if (value && value.hookType !== HookType.CODE) { + return callback('hookType 必须是 code'); + } + callback(); + }, + }, + ], }, ], }; diff --git a/packages/editor/src/utils/typeMatchRules.ts b/packages/editor/src/utils/type-match-rules.ts similarity index 95% rename from packages/editor/src/utils/typeMatchRules.ts rename to packages/editor/src/utils/type-match-rules.ts index 47e0716b..ddc3508f 100644 --- a/packages/editor/src/utils/typeMatchRules.ts +++ b/packages/editor/src/utils/type-match-rules.ts @@ -17,7 +17,7 @@ */ import type { DataSourceFieldType, DataSourceSchema, Id } from '@tmagic/core'; -import { HookCodeType, HookType, NodeType } from '@tmagic/core'; +import { NodeType } from '@tmagic/core'; import type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form'; import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, @@ -376,7 +376,7 @@ const validateDataSourceSelect: TypeMatchValidator = (value, { message, props }) }; const validateCodeSelect: TypeMatchValidator = (value, { message }) => { - if (!isPlainObject(value) || value.hookType !== HookType.CODE || !Array.isArray(value.hookData)) { + if (!isPlainObject(value) || !Array.isArray(value.hookData)) { return defaultMessage(message, `${value}类型不合法`); } @@ -387,29 +387,9 @@ const validateCodeSelect: TypeMatchValidator = (value, { message }) => { return defaultMessage(message, '钩子项结构不合法'); } - if (item.codeType !== HookCodeType.CODE && item.codeType !== HookCodeType.DATA_SOURCE_METHOD) { - return defaultMessage(message, '钩子项结构不合法'); - } - if (typeof item.params !== 'undefined' && !isPlainObject(item.params)) { return defaultMessage(message, '钩子项结构不合法'); } - - if (item.codeType === HookCodeType.CODE) { - if (typeof item.codeId !== 'string') { - return defaultMessage(message, '钩子项结构不合法'); - } - continue; - } - - // DATA_SOURCE_METHOD:仅校验元组形态,存在性交给单元格 - if ( - !Array.isArray(item.codeId) || - item.codeId.length !== 2 || - item.codeId.some((part: any) => typeof part !== 'string') - ) { - return defaultMessage(message, '钩子项结构不合法'); - } } return undefined; diff --git a/packages/editor/tests/unit/plugin.spec.ts b/packages/editor/tests/unit/plugin.spec.ts index dd190c8e..6c2e0381 100644 --- a/packages/editor/tests/unit/plugin.spec.ts +++ b/packages/editor/tests/unit/plugin.spec.ts @@ -77,7 +77,7 @@ describe('plugin install', () => { test('install 调用 design/form/table 插件并注册全局组件', async () => { const { registerTypeMatchRules } = await import('@tmagic/form'); - const { editorTypeMatchRules } = await import('@editor/utils/typeMatchRules'); + 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); diff --git a/packages/editor/tests/unit/services/editor.spec.ts b/packages/editor/tests/unit/services/editor.spec.ts index 919443b8..b014cce0 100644 --- a/packages/editor/tests/unit/services/editor.spec.ts +++ b/packages/editor/tests/unit/services/editor.spec.ts @@ -1235,12 +1235,12 @@ describe('invalidNodeIds 校验错误状态', () => { expect(editorService.getInvalidNodeInfo(NodeId.NODE_ID)).toBeUndefined(); }); - test('update 未携带 invalidInfo 时不改动已有错误状态', async () => { + test('update 未携带 invalidInfo 时清除对应节点的全部来源错误', async () => { await editorService.select(NodeId.PAGE_ID); editorService.setInvalidNode(NodeId.NODE_ID, 'props', 'props 错误'); await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'z' }); - expect(editorService.getInvalidNodeInfo(NodeId.NODE_ID)).toEqual({ props: 'props 错误' }); + expect(editorService.getInvalidNodeInfo(NodeId.NODE_ID)).toBeUndefined(); }); }); diff --git a/packages/editor/tests/unit/utils/typeMatchRules.spec.ts b/packages/editor/tests/unit/utils/typeMatchRules.spec.ts index 411df463..7c564a5c 100644 --- a/packages/editor/tests/unit/utils/typeMatchRules.spec.ts +++ b/packages/editor/tests/unit/utils/typeMatchRules.spec.ts @@ -21,7 +21,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { HookCodeType, HookType, NodeType } from '@tmagic/core'; import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_SET_DATA_METHOD_NAME } from '@tmagic/utils'; -import { ALL_COND_OPS, editorTypeMatchRules } from '@editor/utils/typeMatchRules'; +import { ALL_COND_OPS, editorTypeMatchRules } from '@editor/utils/type-match-rules'; const codeDslState = vi.hoisted(() => ({ value: null as Record | null })); const dataSourcesState = vi.hoisted(() => ({ value: [] as any[] }));