refactor(editor): 完善内置 typeMatch 校验规则并规范文件名

- 重命名 typeMatchRules.ts 为 type-match-rules.ts
- 为 event-select、display-conds 等字段补充结构校验规则
- advancedTabConfig 的 code-select 字段增加 hookType 校验
- 修复 update 未携带 invalidInfo 时清除对应节点全部来源错误
- 将 code-select/display-conds 字段 typeMatch 触发时机调整为 change
This commit is contained in:
roymondchen 2026-07-14 20:28:03 +08:00
parent 0e4669261f
commit ce43fface8
11 changed files with 66 additions and 42 deletions

View File

@ -93,7 +93,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
{ value: HookCodeType.CODE, text: '代码块' }, { value: HookCodeType.CODE, text: '代码块' },
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' }, { value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
], ],
rules: [{ typeMatch: true, trigger: 'blur' }], rules: [{ typeMatch: true, trigger: 'change' }],
defaultValue: HookCodeType.CODE, defaultValue: HookCodeType.CODE,
onChange: (_mForm, v: HookCodeType, { setModel }) => { onChange: (_mForm, v: HookCodeType, { setModel }) => {
if (v === HookCodeType.DATA_SOURCE_METHOD) { if (v === HookCodeType.DATA_SOURCE_METHOD) {
@ -112,7 +112,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
labelWidth: 0, labelWidth: 0,
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD, display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
notEditable: () => !codeBlockService.getEditStatus(), notEditable: () => !codeBlockService.getEditStatus(),
rules: [{ typeMatch: true, trigger: 'blur' }], rules: [{ typeMatch: true, trigger: 'change' }],
}, },
{ {
type: 'data-source-method-select', type: 'data-source-method-select',
@ -121,7 +121,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
labelWidth: 0, labelWidth: 0,
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD, display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
notEditable: () => !dataSourceService.get('editable'), notEditable: () => !dataSourceService.get('editable'),
rules: [{ typeMatch: true }], rules: [{ typeMatch: true, trigger: 'change' }],
}, },
], ],
}, },

View File

@ -98,9 +98,10 @@ const config = computed<GroupListConfig>(() => ({
label: '字段', label: '字段',
checkStrictly: false, checkStrictly: false,
onChange: fieldOnChange, onChange: fieldOnChange,
defaultValue: () => [],
rules: [ rules: [
{ required: true, trigger: 'blur', message: '请选择字段' }, { required: true, trigger: 'blur', message: '请选择字段' },
{ typeMatch: true, trigger: 'blur' }, { typeMatch: true, trigger: 'change' },
], ],
} }
: { : {
@ -111,9 +112,10 @@ const config = computed<GroupListConfig>(() => ({
checkStrictly: false, checkStrictly: false,
dataSourceFieldType: ['string', 'number', 'boolean', 'any'], dataSourceFieldType: ['string', 'number', 'boolean', 'any'],
onChange: fieldOnChange, onChange: fieldOnChange,
defaultValue: () => [],
rules: [ rules: [
{ required: true, trigger: 'blur', message: '请选择字段' }, { required: true, trigger: 'blur', message: '请选择字段' },
{ typeMatch: true, trigger: 'blur' }, { typeMatch: true, trigger: 'change' },
], ],
}, },
{ {
@ -124,7 +126,7 @@ const config = computed<GroupListConfig>(() => ({
name: 'op', name: 'op',
rules: [ rules: [
{ required: true, trigger: 'blur', message: '请选择条件' }, { required: true, trigger: 'blur', message: '请选择条件' },
{ typeMatch: true, trigger: 'blur' }, { typeMatch: true, trigger: 'change' },
], ],
}, },
{ {

View File

@ -167,7 +167,7 @@ const submit = async (
changeRecords: eventData?.changeRecords, changeRecords: eventData?.changeRecords,
historySource, historySource,
// error editorService // error editorService
// CodeEditor invalidInfo // CodeEditor invalidInfo editorService update
...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}), ...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}),
}); });
} catch (e: any) { } catch (e: any) {

View File

@ -44,7 +44,7 @@ import StyleSetter from './fields/StyleSetter/Index.vue';
import uiSelect from './fields/UISelect.vue'; import uiSelect from './fields/UISelect.vue';
import CodeEditor from './layouts/CodeEditor.vue'; import CodeEditor from './layouts/CodeEditor.vue';
import { setEditorConfig } from './utils/config'; import { setEditorConfig } from './utils/config';
import { editorTypeMatchRules } from './utils/typeMatchRules'; import { editorTypeMatchRules } from './utils/type-match-rules';
import Editor from './Editor.vue'; import Editor from './Editor.vue';
import type { EditorInstallOptions } from './type'; import type { EditorInstallOptions } from './type';

View File

@ -815,7 +815,7 @@ class Editor extends BaseService {
); );
// 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。 // 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。
this.applyInvalidInfo(invalidInfo); this.applyInvalidInfo(config, invalidInfo);
if (updateData[0].oldNode?.type !== NodeType.ROOT) { if (updateData[0].oldNode?.type !== NodeType.ROOT) {
const curNodes = this.get('nodes'); 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 }) { private applyInvalidInfo(
if (!invalidInfo) return; 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; const { id, source, error } = invalidInfo;
if (error) { if (error) {
this.setInvalidNode(id, source, error); this.setInvalidNode(id, source, error);

View File

@ -30,6 +30,6 @@ export * from './undo-redo';
export * from './indexed-db'; export * from './indexed-db';
export * from './history'; export * from './history';
export * from './const'; export * from './const';
export * from './typeMatchRules'; export * from './type-match-rules';
export * from './event'; export * from './event';
export { default as loadMonaco } from './monaco-editor'; export { default as loadMonaco } from './monaco-editor';

View File

@ -18,6 +18,7 @@
*/ */
import { import {
HookType,
NODE_CONDS_KEY, NODE_CONDS_KEY,
NODE_CONDS_RESULT_KEY, NODE_CONDS_RESULT_KEY,
NODE_DISABLE_CODE_BLOCK_KEY, NODE_DISABLE_CODE_BLOCK_KEY,
@ -182,7 +183,17 @@ export const advancedTabConfig: TabPaneConfig = {
labelPosition: 'top', labelPosition: 'top',
type: 'code-select', type: 'code-select',
extra: '组件初始化时执行', 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', name: 'mounted',
@ -190,7 +201,17 @@ export const advancedTabConfig: TabPaneConfig = {
labelPosition: 'top', labelPosition: 'top',
type: 'code-select', type: 'code-select',
extra: '组件挂载到dom时执行', 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', name: 'display',
@ -198,7 +219,17 @@ export const advancedTabConfig: TabPaneConfig = {
extra: '控制组件是否渲染关系的代码块返回值为false时不渲染', extra: '控制组件是否渲染关系的代码块返回值为false时不渲染',
labelPosition: 'top', labelPosition: 'top',
type: 'code-select', 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();
},
},
],
}, },
], ],
}; };

View File

@ -17,7 +17,7 @@
*/ */
import type { DataSourceFieldType, DataSourceSchema, Id } from '@tmagic/core'; 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 type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form';
import { import {
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX,
@ -376,7 +376,7 @@ const validateDataSourceSelect: TypeMatchValidator = (value, { message, props })
}; };
const validateCodeSelect: TypeMatchValidator = (value, { message }) => { 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}类型不合法`); return defaultMessage(message, `${value}类型不合法`);
} }
@ -387,29 +387,9 @@ const validateCodeSelect: TypeMatchValidator = (value, { message }) => {
return defaultMessage(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)) { if (typeof item.params !== 'undefined' && !isPlainObject(item.params)) {
return defaultMessage(message, '钩子项结构不合法'); 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; return undefined;

View File

@ -77,7 +77,7 @@ describe('plugin install', () => {
test('install 调用 design/form/table 插件并注册全局组件', async () => { test('install 调用 design/form/table 插件并注册全局组件', async () => {
const { registerTypeMatchRules } = await import('@tmagic/form'); 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(); const { app, components } = buildApp();
editorPlugin.install(app, { someOption: true } as any); editorPlugin.install(app, { someOption: true } as any);
expect(app.use).toHaveBeenCalledTimes(3); expect(app.use).toHaveBeenCalledTimes(3);

View File

@ -1235,12 +1235,12 @@ describe('invalidNodeIds 校验错误状态', () => {
expect(editorService.getInvalidNodeInfo(NodeId.NODE_ID)).toBeUndefined(); expect(editorService.getInvalidNodeInfo(NodeId.NODE_ID)).toBeUndefined();
}); });
test('update 未携带 invalidInfo 时不改动已有错误状态', async () => { test('update 未携带 invalidInfo 时清除对应节点的全部来源错误', async () => {
await editorService.select(NodeId.PAGE_ID); await editorService.select(NodeId.PAGE_ID);
editorService.setInvalidNode(NodeId.NODE_ID, 'props', 'props 错误'); editorService.setInvalidNode(NodeId.NODE_ID, 'props', 'props 错误');
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'z' }); 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();
}); });
}); });

View File

@ -21,7 +21,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { HookCodeType, HookType, NodeType } from '@tmagic/core'; import { HookCodeType, HookType, NodeType } from '@tmagic/core';
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_SET_DATA_METHOD_NAME } from '@tmagic/utils'; 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<string, any> | null })); const codeDslState = vi.hoisted(() => ({ value: null as Record<string, any> | null }));
const dataSourcesState = vi.hoisted(() => ({ value: [] as any[] })); const dataSourcesState = vi.hoisted(() => ({ value: [] as any[] }));