feat(editor): 完善样式设置器字段 typeMatch 校验

为 Background/Font/Position 补充类型校验提示。
字重字段支持字符串与数字,并复用 validateDataSourceFieldSelectValue。
getRules 过滤无 validator 的 typeMatch:false 规则,避免 number 被误校验。
This commit is contained in:
roymondchen 2026-08-04 17:40:52 +08:00
parent 697830ff29
commit d5a2e25e18
7 changed files with 254 additions and 37 deletions

View File

@ -17,6 +17,7 @@
<script lang="ts" setup>
import { markRaw } from 'vue';
import { appendValidateSuggestion } from '@tmagic/design';
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
@ -47,6 +48,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'colorPicker',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('背景色应为字符串', '请参考以下示例值:"#000000"'),
},
],
},
{
name: 'backgroundImage',

View File

@ -17,9 +17,12 @@
<script lang="ts" setup>
import { markRaw } from 'vue';
import { appendValidateSuggestion } from '@tmagic/design';
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
import { validateDataSourceFieldSelectValue } from '@editor/utils/type-match-rules';
import { AlignCenter, AlignLeft, AlignRight } from '../icons/text-align';
defineProps<{
@ -48,6 +51,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'text',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('字号应为字符串或数字', '请参考以下示例值24 或 "24"'),
},
],
},
{
labelWidth: '68px',
@ -65,8 +74,10 @@ const formConfig = defineFormConfig([
text: '字重',
labelWidth: '68px',
type: 'data-source-field-select',
dataSourceFieldType: ['string', 'number'],
fieldConfig: {
type: 'select',
allowCreate: true,
options: ['normal', 'bold']
.concat(
Array(7)
@ -78,6 +89,46 @@ const formConfig = defineFormConfig([
text: item,
})),
},
rules: [
{
typeMatch: false,
},
{
validator: ({ value, callback }, { config, model, prop }, mForm) => {
if (value === '' || value === null || value === undefined) {
return callback();
}
const result = validateDataSourceFieldSelectValue(
value,
{
fieldType: 'data-source-field-select',
mForm,
props: { config, model, prop },
},
{
// string number 700
validatePlainValue: (plainValue) => {
if (typeof plainValue === 'string' || (typeof plainValue === 'number' && !Number.isNaN(plainValue))) {
return undefined;
}
return '字重应为字符串或数字';
},
},
);
if (result && typeof (result as Promise<string | undefined>).then === 'function') {
(result as Promise<string | undefined>).then(
(error) => callback(error),
(error) => callback(error),
);
return;
}
return callback(result);
},
},
],
},
{
labelWidth: '68px',
@ -87,6 +138,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'colorPicker',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('颜色应为字符串', '请参考以下示例值:"#000000"'),
},
],
},
{
name: 'textAlign',

View File

@ -15,6 +15,7 @@
</template>
<script lang="ts" setup>
import { appendValidateSuggestion } from '@tmagic/design';
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
import type { StyleSchema } from '@tmagic/schema';
@ -66,6 +67,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'text',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('left 应为字符串', '请参考以下示例值:"10"'),
},
],
},
{
name: 'top',
@ -74,6 +81,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'text',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('top 应为字符串', '请参考以下示例值:"10"'),
},
],
},
],
},
@ -89,6 +102,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'text',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('right 应为字符串', '请参考以下示例值:"10"'),
},
],
},
{
name: 'bottom',
@ -97,6 +116,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'text',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('bottom 应为字符串', '请参考以下示例值:"10"'),
},
],
},
],
},
@ -108,6 +133,12 @@ const formConfig = defineFormConfig([
fieldConfig: {
type: 'text',
},
rules: [
{
typeMatch: true,
message: appendValidateSuggestion('zIndex 应为数字', '请参考以下示例值10'),
},
],
},
]);

View File

@ -476,12 +476,42 @@ const isDataSourceFieldPathValue = (value: any, config: any): value is string[]
return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
};
const validateDataSourceFieldSelect: TypeMatchValidator = (value, { mForm, message, props }) => {
export type ValidateDataSourceFieldSelectOptions = {
/**
*
* fieldConfig typeMatch typeMatch
*/
validatePlainValue?: (
value: any,
context: TypeMatchValidateContext,
) => string | undefined | Promise<string | undefined>;
};
/**
* data-source-field-select typeMatch rules.validator
*/
export const validateDataSourceFieldSelectValue = (
value: any,
context: TypeMatchValidateContext,
options?: ValidateDataSourceFieldSelectOptions,
): string | undefined | Promise<string | undefined> => {
const { mForm, message, props } = context;
const config = props.config || {};
if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) {
// 值不是数据源字段路径时,按 fieldConfig 的类型校验(与表单项自身 typeMatch 行为一致)
return validateTypeMatch(value, mForm, { ...props, config: { name: config.name, ...config.fieldConfig } }, message);
if (!isDataSourceFieldPathValue(value, config)) {
if (options?.validatePlainValue) {
return options.validatePlainValue(value, context);
}
if (config.fieldConfig) {
// 值不是数据源字段路径时,按 fieldConfig 的类型校验(与表单项自身 typeMatch 行为一致)
return validateTypeMatch(
value,
mForm,
{ ...props, config: { name: config.name, ...config.fieldConfig } },
message,
);
}
}
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
@ -496,6 +526,9 @@ const validateDataSourceFieldSelect: TypeMatchValidator = (value, { mForm, messa
});
};
const validateDataSourceFieldSelect: TypeMatchValidator = (value, context) =>
validateDataSourceFieldSelectValue(value, context);
const validateDataSourceSelect: TypeMatchValidator = (value, { message, props }) => {
const config = props.config || {};
const dataSources = getDataSources();

View File

@ -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/type-match-rules';
import { ALL_COND_OPS, editorTypeMatchRules, validateDataSourceFieldSelectValue } from '@editor/utils/type-match-rules';
const codeDslState = vi.hoisted(() => ({ value: null as Record<string, any> | null }));
const dataSourcesState = vi.hoisted(() => ({ value: [] as any[] }));
@ -270,6 +270,59 @@ describe('editorTypeMatchRules', () => {
).toBeUndefined();
});
test('validateDataSourceFieldSelectValue 支持自定义 plain 值校验并保留数据源路径校验', () => {
const config = {
name: 'fontWeight',
type: 'data-source-field-select',
dataSourceFieldType: ['string', 'number'],
fieldConfig: { type: 'select', allowCreate: true, options: [{ text: '700', value: '700' }] },
};
const context = ctx(config);
const allowStringOrNumber = {
validatePlainValue: (value: any) => {
if (typeof value === 'string' || (typeof value === 'number' && !Number.isNaN(value))) {
return undefined;
}
return 'fontWeight 类型应为字符串或数字';
},
};
expect(validateDataSourceFieldSelectValue(700, context, allowStringOrNumber)).toBeUndefined();
expect(validateDataSourceFieldSelectValue('bold', context, allowStringOrNumber)).toBeUndefined();
expect(firstLine(validateDataSourceFieldSelectValue({ a: 1 }, context, allowStringOrNumber))).toBe(
'fontWeight 类型应为字符串或数字',
);
dataSourcesState.value = [
{
id: 'ds1',
type: 'base',
fields: [
{ name: 'title', type: 'string' },
{ name: 'weight', type: 'number' },
],
methods: [],
},
];
expect(
validateDataSourceFieldSelectValue(
[`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'weight'],
context,
allowStringOrNumber,
),
).toBeUndefined();
expect(
firstLine(
validateDataSourceFieldSelectValue(
[`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'missing'],
context,
allowStringOrNumber,
),
),
).toBe('数据源字段(missing)不存在');
});
test('data-source-select id / 对象形态', () => {
expect(run('data-source-select', { isBindDataSource: true, dataSourceId: 'ds1' })).toBeUndefined();
expect(run('data-source-select', 'ds1', { value: 'id' })).toBeUndefined();

View File

@ -327,39 +327,48 @@ export const getRules = function (
});
}
return rules.map((item) => {
if (item.typeMatch) {
(item as any).validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
return rules
.map((item) => {
if (item.typeMatch) {
(item as any).validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
return item;
}
if (typeof item.validator === 'function') {
const fnc = item.validator;
(item as any).validator = adaptFormValidator(
(rule: any, value: any, callback: Function, source: any, options: any) =>
fnc(
{
rule,
value: props.config.names ? props.model : value,
callback,
source,
options,
},
{
values: mForm?.initValues || {},
model: props.model,
parent: mForm?.parentValues || {},
formValue: mForm?.values || props.model,
prop: props.prop,
config: props.config,
},
mForm,
),
);
}
return item;
}
if (typeof item.validator === 'function') {
const fnc = item.validator;
(item as any).validator = adaptFormValidator(
(rule: any, value: any, callback: Function, source: any, options: any) =>
fnc(
{
rule,
value: props.config.names ? props.model : value,
callback,
source,
options,
},
{
values: mForm?.initValues || {},
model: props.model,
parent: mForm?.parentValues || {},
formValue: mForm?.values || props.model,
prop: props.prop,
config: props.config,
},
mForm,
),
);
}
return item;
});
})
.filter((item) => {
// typeMatch: false 仅用于关闭自动注入,本身没有校验能力。
// 若原样交给 async-validator会因默认 type=string 误杀 number 等合法值。
if (item.typeMatch === false && typeof item.validator !== 'function') {
return false;
}
return true;
});
};
export const initValue = async (

View File

@ -581,6 +581,33 @@ describe('getRules typeMatch', () => {
expect(custom).toHaveBeenCalled();
expect(okCallback).toHaveBeenCalledWith();
});
test('typeMatch: false 标记规则会被过滤,避免默认按 string 校验', () => {
const custom = vi.fn(({ callback }: any) => callback());
const rules: any = [{ typeMatch: false }, { validator: custom }];
const typeMatchValid = { value: true } as any;
const newRules: any = getRules(mForm, rules, propsOf({ type: 'select' }), typeMatchValid);
// 仅保留自定义 validator不会自动注入 typeMatch: true也不会留下空的 typeMatch:false 规则
expect(newRules).toHaveLength(1);
expect(newRules[0].typeMatch).toBeUndefined();
expect(typeof newRules[0].validator).toBe('function');
const callback = vi.fn();
newRules[0].validator({}, 700, callback);
expect(custom).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith();
});
test('typeMatch: false 与 validator 写在同一条 rule 时保留', () => {
const custom = vi.fn(({ callback }: any) => callback());
const rules: any = [{ typeMatch: false, validator: custom }];
const newRules: any = getRules(mForm, rules, propsOf({ type: 'select' }));
expect(newRules).toHaveLength(1);
expect(newRules[0].typeMatch).toBe(false);
expect(typeof newRules[0].validator).toBe('function');
});
});
describe('getRules tdesign validator', () => {