mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-08-07 13:29:21 +00:00
feat(editor): 完善样式设置器字段 typeMatch 校验
为 Background/Font/Position 补充类型校验提示。 字重字段支持字符串与数字,并复用 validateDataSourceFieldSelectValue。 getRules 过滤无 validator 的 typeMatch:false 规则,避免 number 被误校验。
This commit is contained in:
parent
697830ff29
commit
d5a2e25e18
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user