@tmagic/form 1.8.0-beta.10 → 1.8.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,728 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import { readonly } from 'vue';
20
+ import dayjs from 'dayjs';
21
+ import customParseFormat from 'dayjs/plugin/customParseFormat';
22
+
23
+ import { appendValidateSuggestion } from '@tmagic/design';
24
+ import { getValueByKeyPath } from '@tmagic/utils';
25
+
26
+ import type { CascaderOption, FormState, Rule } from '../schema';
27
+
28
+ // dayjs.extend 内部有防重复机制,可安全多次调用
29
+ dayjs.extend(customParseFormat);
30
+
31
+ // #region TypeMatchValidateContext
32
+ export interface TypeMatchValidateContext {
33
+ fieldType: string;
34
+ mForm: FormState | undefined;
35
+ props: any;
36
+ message?: string;
37
+ }
38
+ // #endregion TypeMatchValidateContext
39
+
40
+ // #region TypeMatchValidator
41
+ /** 自定义 type 校验器:返回错误文案;通过则返回 undefined */
42
+ export type TypeMatchValidator = (value: any, context: TypeMatchValidateContext) => string | undefined;
43
+ // #endregion TypeMatchValidator
44
+
45
+ const typeMatchRuleRegistry = new Map<string, TypeMatchValidator>();
46
+
47
+ const normalizeType = (type: string) => type.replace(/([A-Z])/g, '-$1').toLowerCase();
48
+
49
+ /** 注册或覆盖某个字段 type 的 typeMatch 校验规则 */
50
+ export const registerTypeMatchRule = (type: string, validator: TypeMatchValidator): void => {
51
+ typeMatchRuleRegistry.set(normalizeType(type), validator);
52
+ };
53
+
54
+ /** 批量注册 typeMatch 校验规则 */
55
+ export const registerTypeMatchRules = (rules: Record<string, TypeMatchValidator>): void => {
56
+ Object.entries(rules).forEach(([type, validator]) => {
57
+ registerTypeMatchRule(type, validator);
58
+ });
59
+ };
60
+
61
+ /** 获取某个字段 type 的自定义 typeMatch 校验规则 */
62
+ export const getTypeMatchRule = (type: string): TypeMatchValidator | undefined =>
63
+ typeMatchRuleRegistry.get(normalizeType(type));
64
+
65
+ /** 删除某个字段 type 的自定义 typeMatch 校验规则 */
66
+ export const deleteTypeMatchRule = (type: string): boolean => typeMatchRuleRegistry.delete(normalizeType(type));
67
+
68
+ /** 清空所有自定义 typeMatch 校验规则 */
69
+ export const clearTypeMatchRules = (): void => {
70
+ typeMatchRuleRegistry.clear();
71
+ };
72
+
73
+ /** 本地解析配置函数,避免与 form.ts 循环依赖 */
74
+ const resolveConfig = <T = any>(
75
+ mForm: FormState | undefined,
76
+ config: T | ((...args: any[]) => T) | undefined,
77
+ props: any,
78
+ ): T | undefined => {
79
+ if (typeof config === 'function') {
80
+ return (config as Function)(mForm, {
81
+ values: readonly(mForm?.initValues || {}),
82
+ model: readonly(props.model),
83
+ parent: readonly(mForm?.parentValues || {}),
84
+ formValue: readonly(mForm?.values || props.model),
85
+ prop: props.prop,
86
+ config: props.config,
87
+ index: props.index,
88
+ getFormValue: (prop: string) => getValueByKeyPath(prop, mForm?.values || props.model),
89
+ });
90
+ }
91
+
92
+ return config;
93
+ };
94
+
95
+ const SKIP_TYPES = new Set([
96
+ 'display',
97
+ 'hidden',
98
+ 'row',
99
+ 'tab',
100
+ 'dynamic-tab',
101
+ 'fieldset',
102
+ 'panel',
103
+ 'step',
104
+ 'flex-layout',
105
+ 'link',
106
+ 'component',
107
+ 'dynamic-field',
108
+ 'dynamicfield',
109
+ 'form',
110
+ 'container',
111
+ ]);
112
+
113
+ const STRING_TYPES = new Set(['text', 'textarea', 'color-picker', 'html', '']);
114
+
115
+ const DATE_LIKE_TYPES = new Set(['date', 'datetime', 'time']);
116
+
117
+ const DATE_LIKE_DEFAULT_VALUE_FORMAT: Record<string, string> = {
118
+ date: 'YYYY/MM/DD',
119
+ datetime: 'YYYY/MM/DD HH:mm:ss',
120
+ time: 'HH:mm:ss',
121
+ daterange: 'YYYY/MM/DD HH:mm:ss',
122
+ timerange: 'HH:mm:ss',
123
+ };
124
+
125
+ const TIMESTAMP_VALUE_FORMATS = new Set(['x', 'timestamp']);
126
+
127
+ /**
128
+ * 将值格式化为可读的参考示例字符串。
129
+ */
130
+ const stringifyExampleValue = (value: any): string => {
131
+ if (typeof value === 'string') return `"${value}"`;
132
+ if (value === null || value === undefined) return String(value);
133
+ if (typeof value === 'object') {
134
+ try {
135
+ return JSON.stringify(value);
136
+ } catch {
137
+ return String(value);
138
+ }
139
+ }
140
+ return String(value);
141
+ };
142
+
143
+ // 参考建议中最多展示的可选值个数,超出以「等」省略。
144
+ const MAX_SUGGESTION_OPTIONS = 5;
145
+
146
+ /**
147
+ * 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
148
+ * 可选值超过 MAX_SUGGESTION_OPTIONS 个时仅展示前若干个并以「等」省略。
149
+ */
150
+ const optionSuggestion = (optionValues: any[]): string => {
151
+ const values = optionValues.filter((item) => typeof item !== 'undefined');
152
+ if (!values.length) return '';
153
+ const shown = values.slice(0, MAX_SUGGESTION_OPTIONS).map(stringifyExampleValue);
154
+ const suffix = values.length > MAX_SUGGESTION_OPTIONS ? ' 等' : '';
155
+ return `请使用以下某一个值:${shown.join(';')}${suffix}`;
156
+ };
157
+
158
+ /**
159
+ * 基于真实 options 生成类型不匹配场景的「参考示例值」。
160
+ *
161
+ * - expectArray 为 true 时,取前若干个真实可选值组成数组示例;
162
+ * - 否则取第一个真实可选值作为标量示例。
163
+ * 无可用 options 时回退到通用示例。
164
+ */
165
+ const optionExampleSuggestion = (optionValues: any[], fallbackExample: string, expectArray: boolean): string => {
166
+ const values = optionValues.filter((item) => typeof item !== 'undefined');
167
+ if (!values.length) return `请参考以下示例值:${fallbackExample}`;
168
+ const example = expectArray ? stringifyExampleValue(values.slice(0, 2)) : stringifyExampleValue(values[0]);
169
+ return `请参考以下示例值:${example}`;
170
+ };
171
+
172
+ /**
173
+ * 生成开关类字段的参考建议,列出合法的开关值。
174
+ */
175
+ const toggleSuggestion = (activeValue: any, inactiveValue: any): string =>
176
+ `请使用以下某一个值:${stringifyExampleValue(activeValue)};${stringifyExampleValue(inactiveValue)}`;
177
+
178
+ /**
179
+ * 解析字段配置中的真实默认值(defaultValue),用于生成「真实」的参考示例值。
180
+ * defaultValue 可能是函数,交由 resolveConfig 处理;未配置时返回 undefined。
181
+ */
182
+ const resolveFieldDefaultValue = (mForm: FormState | undefined, props: any): any => {
183
+ const { defaultValue } = props.config || {};
184
+ if (typeof defaultValue === 'undefined' || defaultValue === 'undefined') return undefined;
185
+ return resolveConfig(mForm, defaultValue, props);
186
+ };
187
+
188
+ const isNumberValue = (value: any) => typeof value === 'number' && !Number.isNaN(value);
189
+ const isStringValue = (value: any) => typeof value === 'string';
190
+ const isObjectValue = (value: any) => typeof value === 'object' && value !== null && !Array.isArray(value);
191
+ const isObjectArrayValue = (value: any) => Array.isArray(value) && value.every((item) => isObjectValue(item));
192
+ const isNumberRangeValue = (value: any) =>
193
+ Array.isArray(value) && value.length === 2 && value.every((item) => isNumberValue(item));
194
+
195
+ /**
196
+ * 生成类型不匹配场景的「参考示例值」。
197
+ *
198
+ * 优先使用字段配置的真实默认值(defaultValue,需符合期望类型);无可用默认值时回退到通用示例。
199
+ */
200
+ const typeExampleSuggestion = (
201
+ mForm: FormState | undefined,
202
+ props: any,
203
+ fallbackExample: string,
204
+ isValid: (value: any) => boolean,
205
+ ): string => {
206
+ const defaultValue = resolveFieldDefaultValue(mForm, props);
207
+ const example =
208
+ typeof defaultValue !== 'undefined' && isValid(defaultValue)
209
+ ? stringifyExampleValue(defaultValue)
210
+ : fallbackExample;
211
+ return `请参考以下示例值:${example}`;
212
+ };
213
+
214
+ /**
215
+ * 返回最终错误文案。
216
+ *
217
+ * - 传入了自定义 `message` 时,直接使用自定义文案(不追加建议);
218
+ * - 否则使用默认文案 `fallback`,并在其后拼接「参考建议」。
219
+ * 参考建议给出一个可参考的示例值(如可选项列表、示例数据),仅用于错误汇总展示;
220
+ * form-item 行内错误不展示建议(由 @tmagic/design FormItem 在渲染时截断建议)。
221
+ * 主文案与建议的拼接/截断统一复用 @tmagic/design 的 `appendValidateSuggestion`。
222
+ */
223
+ const defaultMessage = (message: string | undefined, fallback: string, suggestion?: string) => {
224
+ if (message) return message;
225
+ return appendValidateSuggestion(fallback, suggestion);
226
+ };
227
+
228
+ const isEmptyValue = (value: any) => value === undefined || value === null || value === '';
229
+
230
+ const isEmptyArray = (value: any) => Array.isArray(value) && value.length === 0;
231
+
232
+ const resolveDateValueFormat = (fieldType: string, config: any): string =>
233
+ config.valueFormat || DATE_LIKE_DEFAULT_VALUE_FORMAT[fieldType] || 'YYYY/MM/DD HH:mm:ss';
234
+
235
+ const isTimestampValueFormat = (valueFormat: string) => TIMESTAMP_VALUE_FORMATS.has(valueFormat);
236
+
237
+ const isValidDateValueByFormat = (value: any, valueFormat: string): boolean => {
238
+ if (isTimestampValueFormat(valueFormat)) {
239
+ return typeof value === 'number' && !Number.isNaN(value);
240
+ }
241
+ if (typeof value !== 'string') {
242
+ return false;
243
+ }
244
+ // 按 Day.js format tokens 严格解析,参见 https://day.js.org/docs/en/display/format
245
+ return dayjs(value, valueFormat, true).isValid();
246
+ };
247
+
248
+ /**
249
+ * 生成单个日期类字段的参考示例值:时间戳格式给出当前时间戳,其余按 valueFormat 格式化当前时间。
250
+ */
251
+ const dateSuggestion = (valueFormat: string): string =>
252
+ isTimestampValueFormat(valueFormat)
253
+ ? `请参考以下示例值:${Date.now()}`
254
+ : `请参考以下示例值:"${dayjs().format(valueFormat)}"`;
255
+
256
+ /**
257
+ * 生成日期范围类字段的参考示例值(长度为 2 的数组)。
258
+ */
259
+ const dateRangeSuggestion = (valueFormat: string): string => {
260
+ if (isTimestampValueFormat(valueFormat)) {
261
+ return `请参考以下示例值:[${Date.now()}, ${Date.now()}]`;
262
+ }
263
+ const example = dayjs().format(valueFormat);
264
+ return `请参考以下示例值:["${example}", "${example}"]`;
265
+ };
266
+
267
+ const dateValueFormatErrorMessage = (message: string | undefined, valueFormat: string) =>
268
+ defaultMessage(
269
+ message,
270
+ isTimestampValueFormat(valueFormat) ? '值类型应为时间戳数字' : `值格式应为 ${valueFormat}`,
271
+ dateSuggestion(valueFormat),
272
+ );
273
+
274
+ const resolveFieldType = (mForm: FormState | undefined, props: any): string => {
275
+ let type = 'type' in (props.config || {}) ? props.config.type : '';
276
+ type = type ? resolveConfig<string>(mForm, type, props) || '' : '';
277
+ if (type === 'form' || type === 'container') return '';
278
+ return normalizeType(type || '') || (props.config?.items ? '' : 'text');
279
+ };
280
+
281
+ const resolveToggleValues = (config: any) => {
282
+ const filterIsNumber = config.filter === 'number';
283
+ const { activeValue: configActiveValue, inactiveValue: configInactiveValue } = config;
284
+
285
+ let activeValue = configActiveValue;
286
+ let inactiveValue = configInactiveValue;
287
+
288
+ if (typeof activeValue === 'undefined') {
289
+ activeValue = filterIsNumber ? 1 : true;
290
+ }
291
+
292
+ if (typeof inactiveValue === 'undefined') {
293
+ inactiveValue = filterIsNumber ? 0 : false;
294
+ }
295
+
296
+ return { activeValue, inactiveValue };
297
+ };
298
+
299
+ const flattenSelectOptions = (options: any[]): any[] => {
300
+ const values: any[] = [];
301
+
302
+ options.forEach((option) => {
303
+ if (Array.isArray(option?.options)) {
304
+ option.options.forEach((child: any) => {
305
+ if (typeof child?.value !== 'undefined') {
306
+ values.push(child.value);
307
+ }
308
+ });
309
+ return;
310
+ }
311
+
312
+ if (typeof option?.value !== 'undefined') {
313
+ values.push(option.value);
314
+ }
315
+ });
316
+
317
+ return values;
318
+ };
319
+
320
+ const resolveOptions = (mForm: FormState | undefined, props: any): any[] => {
321
+ const { options } = props.config || {};
322
+ if (Array.isArray(options)) return options;
323
+ if (typeof options === 'function') {
324
+ return resolveConfig(mForm, options, props) || [];
325
+ }
326
+ return [];
327
+ };
328
+
329
+ const includesOptionValue = (optionValues: any[], value: any) => optionValues.some((item) => Object.is(item, value));
330
+
331
+ const collectCascaderLeafValues = (options: CascaderOption[], result: any[] = []) => {
332
+ options.forEach((option) => {
333
+ if (option.children?.length) {
334
+ collectCascaderLeafValues(option.children, result);
335
+ } else if (typeof option.value !== 'undefined') {
336
+ result.push(option.value);
337
+ }
338
+ });
339
+ return result;
340
+ };
341
+
342
+ const isValidCascaderPath = (options: CascaderOption[], path: any[]): boolean => {
343
+ if (!path.length) return false;
344
+
345
+ let currentOptions = options;
346
+ for (let i = 0; i < path.length; i++) {
347
+ const node = currentOptions.find((option) => Object.is(option.value, path[i]));
348
+ if (!node) return false;
349
+ if (i === path.length - 1) return true;
350
+ currentOptions = node.children || [];
351
+ }
352
+
353
+ return false;
354
+ };
355
+
356
+ /**
357
+ * 取 cascader options 的第一条完整路径(从顶层到叶子的 value 数组)。
358
+ */
359
+ const firstCascaderPath = (options: CascaderOption[]): any[] => {
360
+ const path: any[] = [];
361
+ let current: CascaderOption[] | undefined = options;
362
+ while (current?.length) {
363
+ const node: CascaderOption = current[0];
364
+ if (typeof node.value === 'undefined') break;
365
+ path.push(node.value);
366
+ current = node.children;
367
+ }
368
+ return path;
369
+ };
370
+
371
+ /**
372
+ * 基于真实 cascader options 生成类型不匹配场景的「参考示例值」。
373
+ *
374
+ * 依据 valueSeparator / multiple / emitPath 组织真实选中值的形态;无可用 options 时回退到通用示例。
375
+ */
376
+ const cascaderExampleSuggestion = (
377
+ options: CascaderOption[],
378
+ config: any,
379
+ valueSeparator: string | undefined,
380
+ fallbackExample: string,
381
+ ): string => {
382
+ const path = firstCascaderPath(options);
383
+ if (!path.length) return `请参考以下示例值:${fallbackExample}`;
384
+
385
+ const emitPath = config.emitPath !== false;
386
+ const multiple = Boolean(config.multiple);
387
+ // 单个选中值:emitPath 时为完整路径数组,否则为叶子值
388
+ const single = emitPath ? path : path[path.length - 1];
389
+
390
+ let example: any;
391
+ if (valueSeparator) {
392
+ example = path.join(valueSeparator);
393
+ } else if (multiple) {
394
+ example = [single];
395
+ } else {
396
+ example = single;
397
+ }
398
+ return `请参考以下示例值:${stringifyExampleValue(example)}`;
399
+ };
400
+
401
+ const validateSelectValue = (
402
+ value: any,
403
+ config: any,
404
+ optionValues: any[],
405
+ message: string | undefined,
406
+ ): string | undefined => {
407
+ if (config.allowCreate || config.remote) {
408
+ if (config.multiple) {
409
+ if (!Array.isArray(value)) {
410
+ return defaultMessage(
411
+ message,
412
+ `${value} 类型应为数组`,
413
+ optionExampleSuggestion(optionValues, '["选项1", "选项2"]', true),
414
+ );
415
+ }
416
+ return undefined;
417
+ }
418
+
419
+ if (typeof value === 'object') {
420
+ return defaultMessage(
421
+ message,
422
+ `${value} 类型不合法`,
423
+ optionExampleSuggestion(optionValues, '"文本内容" 或 123', false),
424
+ );
425
+ }
426
+ return undefined;
427
+ }
428
+
429
+ // 仅当 options 为静态数组时才校验值是否在可选项中,动态 options(函数形式)跳过
430
+ const isStaticOptions = Array.isArray(config.options);
431
+
432
+ if (config.multiple) {
433
+ if (!Array.isArray(value)) {
434
+ return defaultMessage(
435
+ message,
436
+ `${value} 类型应为数组`,
437
+ optionExampleSuggestion(optionValues, '["选项1", "选项2"]', true),
438
+ );
439
+ }
440
+ if (isStaticOptions && value.some((item) => !includesOptionValue(optionValues, item))) {
441
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
442
+ }
443
+ return undefined;
444
+ }
445
+
446
+ if (isStaticOptions && !includesOptionValue(optionValues, value)) {
447
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
448
+ }
449
+ return undefined;
450
+ };
451
+
452
+ const validateCascaderValue = (
453
+ value: any,
454
+ config: any,
455
+ props: any,
456
+ mForm: FormState | undefined,
457
+ message: string | undefined,
458
+ ): string | undefined => {
459
+ const valueSeparator = resolveConfig<string | undefined>(mForm, config.valueSeparator, props);
460
+ const emitPath = config.emitPath !== false;
461
+ const multiple = Boolean(config.multiple);
462
+ const options = resolveOptions(mForm, props) as CascaderOption[];
463
+ const cascaderExample = (fallbackExample: string) =>
464
+ cascaderExampleSuggestion(options, config, valueSeparator, fallbackExample);
465
+
466
+ if (valueSeparator) {
467
+ if (typeof value !== 'string' && !Array.isArray(value)) {
468
+ return defaultMessage(
469
+ message,
470
+ `${value} 类型应为字符串或数组`,
471
+ cascaderExample('"选项1,选项2" 或 ["选项1", "选项2"]'),
472
+ );
473
+ }
474
+ } else if (multiple) {
475
+ if (!Array.isArray(value)) {
476
+ return defaultMessage(message, `${value} 类型应为数组`, cascaderExample('["选项1", "选项2"]'));
477
+ }
478
+ } else if (emitPath && !Array.isArray(value)) {
479
+ return defaultMessage(message, `${value} 类型应为数组`, cascaderExample('["选项1", "选项2"]'));
480
+ }
481
+
482
+ if (config.remote) {
483
+ return undefined;
484
+ }
485
+
486
+ if (!options.length) {
487
+ return undefined;
488
+ }
489
+
490
+ const normalizedValue = typeof value === 'string' && valueSeparator ? value.split(valueSeparator) : value;
491
+
492
+ if (multiple) {
493
+ if (!Array.isArray(normalizedValue)) {
494
+ return defaultMessage(message, `${value} 类型应为数组`, cascaderExample('["选项1", "选项2"]'));
495
+ }
496
+
497
+ const invalid = normalizedValue.some((item) => {
498
+ if (emitPath) {
499
+ return !Array.isArray(item) || !isValidCascaderPath(options, item);
500
+ }
501
+ return !includesOptionValue(collectCascaderLeafValues(options), item);
502
+ });
503
+
504
+ if (invalid) {
505
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options)));
506
+ }
507
+ return undefined;
508
+ }
509
+
510
+ if (emitPath) {
511
+ if (!Array.isArray(normalizedValue) || !isValidCascaderPath(options, normalizedValue)) {
512
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options)));
513
+ }
514
+ return undefined;
515
+ }
516
+
517
+ if (!includesOptionValue(collectCascaderLeafValues(options), normalizedValue)) {
518
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options)));
519
+ }
520
+ return undefined;
521
+ };
522
+
523
+ const validateBuiltinTypeMatch = (
524
+ value: any,
525
+ fieldType: string,
526
+ mForm: FormState | undefined,
527
+ props: any,
528
+ message?: string,
529
+ ): string | undefined => {
530
+ if (SKIP_TYPES.has(fieldType)) {
531
+ return undefined;
532
+ }
533
+
534
+ const config = props.config || {};
535
+
536
+ if (STRING_TYPES.has(fieldType)) {
537
+ if (config.filter === 'number') {
538
+ if (typeof value !== 'number' || Number.isNaN(value)) {
539
+ return defaultMessage(
540
+ message,
541
+ `${value} 类型应为数字`,
542
+ typeExampleSuggestion(mForm, props, '123', isNumberValue),
543
+ );
544
+ }
545
+ return undefined;
546
+ }
547
+
548
+ // 自定义 filter 函数会改写值类型,内置规则无法推断,跳过
549
+ if (typeof config.filter === 'function') {
550
+ return undefined;
551
+ }
552
+
553
+ if (typeof value !== 'string') {
554
+ return defaultMessage(
555
+ message,
556
+ `${value} 类型应为字符串`,
557
+ typeExampleSuggestion(mForm, props, '"文本内容"', isStringValue),
558
+ );
559
+ }
560
+ return undefined;
561
+ }
562
+
563
+ if (DATE_LIKE_TYPES.has(fieldType)) {
564
+ const valueFormat = resolveDateValueFormat(fieldType, config);
565
+ if (!isValidDateValueByFormat(value, valueFormat)) {
566
+ return dateValueFormatErrorMessage(message, valueFormat);
567
+ }
568
+ return undefined;
569
+ }
570
+
571
+ if (fieldType === 'number') {
572
+ if (typeof value !== 'number' || Number.isNaN(value)) {
573
+ return defaultMessage(
574
+ message,
575
+ `${value} 类型应为数字`,
576
+ typeExampleSuggestion(mForm, props, '123', isNumberValue),
577
+ );
578
+ }
579
+ return undefined;
580
+ }
581
+
582
+ if (fieldType === 'number-range') {
583
+ if (
584
+ !Array.isArray(value) ||
585
+ value.length !== 2 ||
586
+ value.some((item) => typeof item !== 'number' || Number.isNaN(item))
587
+ ) {
588
+ return defaultMessage(
589
+ message,
590
+ `${value} 类型应为长度为 2 的数字数组`,
591
+ typeExampleSuggestion(mForm, props, '[0, 100]', isNumberRangeValue),
592
+ );
593
+ }
594
+ return undefined;
595
+ }
596
+
597
+ if (fieldType === 'switch' || fieldType === 'checkbox') {
598
+ const { activeValue, inactiveValue } = resolveToggleValues(config);
599
+ if (!Object.is(value, activeValue) && !Object.is(value, inactiveValue)) {
600
+ return defaultMessage(message, `${value} 不在合法开关值中`, toggleSuggestion(activeValue, inactiveValue));
601
+ }
602
+ return undefined;
603
+ }
604
+
605
+ if (fieldType === 'select') {
606
+ const optionValues = flattenSelectOptions(resolveOptions(mForm, props));
607
+ return validateSelectValue(value, config, optionValues, message);
608
+ }
609
+
610
+ if (fieldType === 'radio-group') {
611
+ const optionValues = flattenSelectOptions(resolveOptions(mForm, props));
612
+ if (!includesOptionValue(optionValues, value)) {
613
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
614
+ }
615
+ return undefined;
616
+ }
617
+
618
+ if (fieldType === 'checkbox-group') {
619
+ const optionValues = flattenSelectOptions(resolveOptions(mForm, props));
620
+ if (!Array.isArray(value)) {
621
+ return defaultMessage(
622
+ message,
623
+ `${value} 类型应为数组`,
624
+ optionExampleSuggestion(optionValues, '["选项1", "选项2"]', true),
625
+ );
626
+ }
627
+ if (value.some((item) => !includesOptionValue(optionValues, item))) {
628
+ return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
629
+ }
630
+ return undefined;
631
+ }
632
+
633
+ if (fieldType === 'cascader') {
634
+ return validateCascaderValue(value, config, props, mForm, message);
635
+ }
636
+
637
+ if (fieldType === 'daterange' || fieldType === 'timerange') {
638
+ if (config.names) {
639
+ return undefined;
640
+ }
641
+ const valueFormat = resolveDateValueFormat(fieldType, config);
642
+ if (
643
+ !Array.isArray(value) ||
644
+ value.length !== 2 ||
645
+ value.some((item) => !isValidDateValueByFormat(item, valueFormat))
646
+ ) {
647
+ return defaultMessage(
648
+ message,
649
+ isTimestampValueFormat(valueFormat)
650
+ ? `${value} 类型应为长度为 2 的时间戳数字数组`
651
+ : `${value} 格式应为长度为 2 的 ${valueFormat} 数组`,
652
+ dateRangeSuggestion(valueFormat),
653
+ );
654
+ }
655
+ return undefined;
656
+ }
657
+
658
+ if (fieldType === 'table' || fieldType === 'group-list' || fieldType === 'grouplist') {
659
+ if (!Array.isArray(value) || value.some((item) => !isObjectValue(item))) {
660
+ return defaultMessage(
661
+ message,
662
+ `${value} 类型应为对象数组`,
663
+ typeExampleSuggestion(mForm, props, '[{}]', isObjectArrayValue),
664
+ );
665
+ }
666
+ return undefined;
667
+ }
668
+
669
+ return undefined;
670
+ };
671
+
672
+ export const validateTypeMatch = (
673
+ value: any,
674
+ mForm: FormState | undefined,
675
+ props: any,
676
+ message?: string,
677
+ ): string | undefined => {
678
+ if (isEmptyValue(value) || isEmptyArray(value)) {
679
+ return undefined;
680
+ }
681
+
682
+ const fieldType = resolveFieldType(mForm, props);
683
+ const customValidator = getTypeMatchRule(fieldType);
684
+
685
+ // 自定义规则优先:可覆盖内置规则,也可为业务自定义字段 type 扩展校验
686
+ if (customValidator) {
687
+ return customValidator(value, { fieldType, mForm, props, message });
688
+ }
689
+
690
+ return validateBuiltinTypeMatch(value, fieldType, mForm, props, message);
691
+ };
692
+
693
+ export const createTypeMatchValidator = (mForm: FormState | undefined, props: any, rule: Rule) => {
694
+ const originalValidator = typeof rule.validator === 'function' ? rule.validator : undefined;
695
+
696
+ return (asyncValidatorRule: any, value: any, callback: Function, source: any, options: any) => {
697
+ const actualValue = props.config?.names ? props.model : value;
698
+ const error = validateTypeMatch(actualValue, mForm, props, rule.message);
699
+
700
+ if (error) {
701
+ callback(new Error(error));
702
+ return;
703
+ }
704
+
705
+ if (originalValidator) {
706
+ return originalValidator(
707
+ {
708
+ rule: asyncValidatorRule,
709
+ value: actualValue,
710
+ callback,
711
+ source,
712
+ options,
713
+ },
714
+ {
715
+ values: mForm?.initValues || {},
716
+ model: props.model,
717
+ parent: mForm?.parentValues || {},
718
+ formValue: mForm?.values || props.model,
719
+ prop: props.prop,
720
+ config: props.config,
721
+ },
722
+ mForm,
723
+ );
724
+ }
725
+
726
+ callback();
727
+ };
728
+ };