fix(form): 日期/时间字段支持 valueFormat 为 X 的秒级时间戳

- 日期类字段 change 时经 normalizeTimestampValue 把选择器输出收成秒级 number,避免 Element Plus 写出秒字符串导致校验失败
- Date/DateTime/Time/Daterange/Timerange 回填 model 时,valueFormat 为 X 直接使用秒级数字,不再拼接展示格式
- typeMatch 对 X 格式要求有限整数,示例值与报错文案区分秒级/毫秒级
- datetimeFormatter 支持 X 格式输出秒级数字
- 补充相应单元测试,并更新 docs/form-config/rules.md 中的校验规则说明
This commit is contained in:
roymondchen 2026-10-13 17:11:02 +08:00
parent 94fae10ae0
commit f12f78c9e0
15 changed files with 247 additions and 46 deletions

View File

@ -29,7 +29,7 @@
| `display` / `hidden` | 不校验 |
| `number` | `number`(非 NaN) |
| `number-range` | 长度为 2 的数字数组 |
| `date` / `datetime` / `time` | 按 `valueFormat` 校验;`x` / `timestamp` 期望 `number`,其余按 [Day.js format](https://day.js.org/docs/en/display/format) 严格解析字符串 |
| `date` / `datetime` / `time` | 按 `valueFormat` 校验;`x` / `X` / `timestamp` 期望 `number`(`x`、`timestamp` 为毫秒,`X` 为秒)。日期组件会把选择器写出的秒字符串收成 number。其余格式按 [Day.js format](https://day.js.org/docs/en/display/format) 严格解析字符串 |
| `daterange` / `timerange` | 无 `names` 时为长度为 2 的数组,元素按 `valueFormat` 校验;有 `names` 时跳过 |
| `switch` / `checkbox` | 值必须是解析后的 `activeValue` / `inactiveValue` 之一(显式配置优先;未配置且 `filter === 'number'` 时为 `1`/`0`;否则为 `true`/`false`) |
| `select` | 单选:值 ∈ options;`multiple`:数组且每项 ∈ options;`allowCreate` / `remote`:只校验基础形态,不做 options 枚举 |

View File

@ -15,6 +15,7 @@
import { TMagicDatePicker } from '@tmagic/design';
import type { DateConfig, FieldProps } from '@form/schema';
import { normalizeTimestampValue } from '@form/utils/form';
import { useAddField } from '@form/utils/useAddField';
defineOptions({
@ -24,12 +25,12 @@ defineOptions({
const props = defineProps<FieldProps<DateConfig>>();
const emit = defineEmits<{
change: [value: string];
change: [value: string | number | null];
}>();
useAddField(props.prop);
const changeHandler = (v: string) => {
emit('change', v);
const changeHandler = (v: string | number | null) => {
emit('change', normalizeTimestampValue(v, props.config.valueFormat));
};
</script>

View File

@ -17,6 +17,7 @@
import { TMagicDatePicker } from '@tmagic/design';
import type { DateTimeConfig, FieldProps } from '@form/schema';
import { normalizeTimestampValue } from '@form/utils/form';
import { useAddField } from '@form/utils/useAddField';
defineOptions({
@ -26,12 +27,12 @@ defineOptions({
const props = defineProps<FieldProps<DateTimeConfig>>();
const emit = defineEmits<{
change: [value: string];
change: [value: string | number | null];
}>();
useAddField(props.prop);
const changeHandler = (v: string) => {
emit('change', v);
const changeHandler = (v: string | number | null) => {
emit('change', normalizeTimestampValue(v, props.config.valueFormat));
};
</script>

View File

@ -23,7 +23,7 @@ import { onUnmounted, ref, watch } from 'vue';
import { TMagicDatePicker } from '@tmagic/design';
import type { ChangeRecord, DaterangeConfig, FieldProps } from '../schema';
import { datetimeFormatter } from '../utils/form';
import { datetimeFormatter, normalizeTimestampValue } from '../utils/form';
import { useAddField } from '../utils/useAddField';
defineOptions({
@ -37,7 +37,14 @@ const emit = defineEmits(['change']);
useAddField(props.prop);
const { names } = props.config;
const value = ref<(Date | string | undefined)[] | null>([]);
const value = ref<(Date | string | number | undefined)[] | null>([]);
const rangeFormat = () => `${props.config.dateFormat || 'YYYY/MM/DD'} ${props.config.timeFormat || 'HH:mm:ss'}`;
/** `X` 的选择器值就是秒级数字,展示格式不能拿去代替 valueFormat。 */
const toPickerValue = (item: string | number | Date) => {
if (!item) return undefined;
if (props.config.valueFormat === 'X') return normalizeTimestampValue(item, 'X');
return datetimeFormatter(item, '', rangeFormat()) as string;
};
if (props.model !== undefined) {
if (names?.length) {
@ -48,10 +55,9 @@ if (props.model !== undefined) {
value.value = [];
}
const format = `${props.config.dateFormat || 'YYYY/MM/DD'} ${props.config.timeFormat || 'HH:mm:ss'}`;
if (!start || !end) value.value = [];
if (start !== preStart) value.value[0] = datetimeFormatter(start, '', format) as string;
if (end !== preEnd) value.value[1] = datetimeFormatter(end, '', format) as string;
if (start !== preStart) value.value[0] = toPickerValue(start);
if (end !== preEnd) value.value[1] = toPickerValue(end);
},
{
immediate: true,
@ -65,12 +71,7 @@ if (props.model !== undefined) {
const unWatch = watch(
() => props.model[props.name],
(start, preStart) => {
const format = `${props.config.dateFormat || 'YYYY/MM/DD'} ${props.config.timeFormat || 'HH:mm:ss'}`;
if (start !== preStart)
value.value = start.map((item: string) =>
item ? (datetimeFormatter(item, '', format) as string) : undefined,
);
if (start !== preStart) value.value = start.map((item: string | number | Date) => toPickerValue(item));
},
{
immediate: true,
@ -82,8 +83,9 @@ if (props.model !== undefined) {
}
}
const changeHandler = (v: Date[]) => {
const value = v || [];
const changeHandler = (v: Array<Date | string | number> | null) => {
const normalized = normalizeTimestampValue(v, props.config.valueFormat);
const value = normalized || [];
if (props.name) {
emit('change', value);
@ -94,10 +96,10 @@ const changeHandler = (v: Date[]) => {
if (!props.model) {
return;
}
if (Array.isArray(v)) {
if (Array.isArray(normalized)) {
newChangeRecords.push({
propPath: props.prop ? `${props.prop}.${item}` : item,
value: v[index],
value: normalized[index],
});
} else {
newChangeRecords.push({

View File

@ -14,6 +14,7 @@
import { TMagicTimePicker } from '@tmagic/design';
import type { FieldProps, TimeConfig } from '../schema';
import { normalizeTimestampValue } from '../utils/form';
import { useAddField } from '../utils/useAddField';
defineOptions({
@ -23,12 +24,12 @@ defineOptions({
const props = defineProps<FieldProps<TimeConfig>>();
const emit = defineEmits<{
change: [value: string];
change: [value: string | number | null];
}>();
useAddField(props.prop);
const changeHandler = (v: string) => {
emit('change', v);
const changeHandler = (v: string | number | null) => {
emit('change', normalizeTimestampValue(v, props.config.valueFormat));
};
</script>

View File

@ -22,7 +22,7 @@ import dayjs from 'dayjs';
import { TMagicTimePicker } from '@tmagic/design';
import type { ChangeRecord, FieldProps, TimerangeConfig } from '../schema';
import { datetimeFormatter } from '../utils/form';
import { datetimeFormatter, normalizeTimestampValue } from '../utils/form';
import { useAddField } from '../utils/useAddField';
defineOptions({
@ -36,12 +36,18 @@ const emit = defineEmits(['change']);
useAddField(props.prop);
const { names } = props.config;
const value = ref<(Date | string | undefined)[] | null>([]);
const value = ref<(Date | string | number | undefined)[] | null>([]);
const getFullDatetime = (item: string) => (item ? `${dayjs().format('YYYY/MM/DD')} ${item}` : '');
const getFormat = () => `YYYY/MM/DD ${props.config.valueFormat || 'HH:mm:ss'}`;
const timeFormatter = (time: string, format: string) =>
(datetimeFormatter(getFullDatetime(time), '', format) as string).substring(11);
/** `X` 的选择器值就是秒级数字,不能再拼成日期后截取。 */
const toPickerValue = (time: string | number) => {
if (!time) return undefined;
if (props.config.valueFormat === 'X') return normalizeTimestampValue(time, 'X');
return timeFormatter(time as string, getFormat());
};
if (props.model !== undefined) {
if (names?.length) {
@ -52,10 +58,9 @@ if (props.model !== undefined) {
value.value = [];
}
const format = getFormat();
if (!start || !end) value.value = [];
if (start !== preStart) value.value[0] = timeFormatter(start, format);
if (end !== preEnd) value.value[1] = timeFormatter(end, format);
if (start !== preStart) value.value[0] = toPickerValue(start);
if (end !== preEnd) value.value[1] = toPickerValue(end);
},
{
immediate: true,
@ -69,7 +74,7 @@ if (props.model !== undefined) {
() => props.model[props.name],
(v = []) => {
if (Array.isArray(v)) {
value.value = v.map((item: string) => (item ? timeFormatter(item, getFormat()) : undefined));
value.value = v.map((item: string | number) => toPickerValue(item));
} else {
value.value = [];
}
@ -84,8 +89,9 @@ if (props.model !== undefined) {
}
}
const changeHandler = (v: Date[]) => {
const value = v || [];
const changeHandler = (v: Array<Date | string | number> | null) => {
const normalized = normalizeTimestampValue(v, props.config.valueFormat);
const value = normalized || [];
if (props.name) {
emit('change', value);
@ -96,10 +102,10 @@ const changeHandler = (v: Date[]) => {
if (!props.model) {
return;
}
if (Array.isArray(v)) {
if (Array.isArray(normalized)) {
newChangeRecords.push({
propPath: props.prop ? `${props.prop}.${item}` : item,
value: v[index],
value: normalized[index],
});
} else {
newChangeRecords.push({

View File

@ -42,7 +42,7 @@ import type {
} from '../schema';
import { getConfig } from './config';
import { createTypeMatchValidator } from './typeMatch';
import { createTypeMatchValidator, TIMESTAMP_VALUE_FORMATS } from './typeMatch';
export { createFormStateProxy, mergeFormContexts } from './formStateProxy';
@ -509,14 +509,44 @@ export const initValue = async (
return valuesTmp || {};
};
/**
* 把值收成 Unix 秒,无法解析时返回 undefined。
*
* 纯数字(含数字字符串)按已是秒处理:dayjs 会把数字当成毫秒,不能再过一遍。
* Date、ISO 和日期字符串则解析后取秒。
*/
const toUnixSeconds = (v: string | Date | number): number | undefined => {
if (typeof v === 'number') {
return Number.isFinite(v) ? Math.trunc(v) : undefined;
}
if (typeof v === 'string' && /^-?\d+$/.test(v.trim())) {
return Math.trunc(Number(v));
}
const parsed = dayjs(v);
return parsed.isValid() ? parsed.unix() : undefined;
};
/**
* `valueFormat: 'X'` 时把选择器值收成秒级 number,其余格式原样返回。
* Element Plus 对 `X` 写出的是秒字符串(`x` 才会直接给毫秒数字);空值、无法解析的值原样返回。
*/
export const normalizeTimestampValue = (value: any, valueFormat?: string): any => {
if (valueFormat !== 'X') return value;
if (Array.isArray(value)) return value.map((item) => normalizeTimestampValue(item, valueFormat));
if (value === null || value === undefined || value === '') return value;
return toUnixSeconds(value) ?? value;
};
export const datetimeFormatter = (
v: string | Date,
v: string | Date | number,
defaultValue = '-',
format = 'YYYY-MM-DD HH:mm:ss',
): string | number => {
if (v) {
let time: string | number;
if (['x', 'timestamp'].includes(format)) {
if (format === 'X') {
time = toUnixSeconds(v) ?? 'Invalid Date';
} else if (TIMESTAMP_VALUE_FORMATS.has(format)) {
time = dayjs(Number.isNaN(Number(v)) ? v : Number(v)).valueOf();
} else if ((typeof v === 'string' && v.includes('Z')) || v instanceof Date) {
// dayjs.extend 内部有防重复机制 (plugin.$i),无需额外判断

View File

@ -150,7 +150,7 @@ const DATE_LIKE_DEFAULT_VALUE_FORMAT: Record<string, string> = {
timerange: 'HH:mm:ss',
};
const TIMESTAMP_VALUE_FORMATS = new Set(['x', 'timestamp']);
export const TIMESTAMP_VALUE_FORMATS = new Set(['x', 'X', 'timestamp']);
/**
* 将值格式化为可读的参考示例字符串。
@ -281,7 +281,9 @@ const isTimestampValueFormat = (valueFormat: string) => TIMESTAMP_VALUE_FORMATS.
const isValidDateValueByFormat = (value: any, valueFormat: string): boolean => {
if (isTimestampValueFormat(valueFormat)) {
return typeof value === 'number' && !Number.isNaN(value);
if (typeof value !== 'number') return false;
// 秒级时间戳必须是有限整数,毫秒级只排除 NaN
return valueFormat === 'X' ? Number.isInteger(value) : !Number.isNaN(value);
}
if (typeof value !== 'string') {
return false;
@ -293,9 +295,11 @@ const isValidDateValueByFormat = (value: any, valueFormat: string): boolean => {
/**
* 生成单个日期类字段的参考示例值:时间戳格式给出当前时间戳,其余按 valueFormat 格式化当前时间。
*/
const timestampExample = (valueFormat: string) => (valueFormat === 'X' ? Math.floor(Date.now() / 1000) : Date.now());
const dateSuggestion = (valueFormat: string): string =>
isTimestampValueFormat(valueFormat)
? `请参考以下示例值:${Date.now()}`
? `请参考以下示例值:${timestampExample(valueFormat)}`
: `请参考以下示例值:"${dayjs().format(valueFormat)}"`;
/**
@ -303,16 +307,19 @@ const dateSuggestion = (valueFormat: string): string =>
*/
const dateRangeSuggestion = (valueFormat: string): string => {
if (isTimestampValueFormat(valueFormat)) {
return `请参考以下示例值:[${Date.now()}, ${Date.now()}]`;
const example = timestampExample(valueFormat);
return `请参考以下示例值:[${example}, ${example}]`;
}
const example = dayjs().format(valueFormat);
return `请参考以下示例值:["${example}", "${example}"]`;
};
const timestampTypeLabel = (valueFormat: string) => (valueFormat === 'X' ? '秒级时间戳数字' : '毫秒级时间戳数字');
const dateValueFormatErrorMessage = (message: string | undefined, valueFormat: string) =>
defaultMessage(
message,
isTimestampValueFormat(valueFormat) ? '值类型应为时间戳数字' : `值格式应为 ${valueFormat}`,
isTimestampValueFormat(valueFormat) ? `值类型应为${timestampTypeLabel(valueFormat)}` : `值格式应为 ${valueFormat}`,
dateSuggestion(valueFormat),
);
@ -748,7 +755,7 @@ const validateBuiltinTypeMatch = (
return defaultMessage(
message,
isTimestampValueFormat(valueFormat)
? `${formatErrorValue(value)} 类型应为长度为 2 的时间戳数字数组`
? `${formatErrorValue(value)} 类型应为长度为 2 的${timestampTypeLabel(valueFormat)}数组`
: `${formatErrorValue(value)} 格式应为长度为 2 的 ${valueFormat} 数组`,
dateRangeSuggestion(valueFormat),
);

View File

@ -79,6 +79,20 @@ describe('Date', () => {
expect(value.date).toMatch('2021-07-28');
});
test('valueFormat 为 X 时收成秒级数字', async () => {
const source = '2021-07-17T07:37:00.000Z';
const wrapper = getWrapper([{ text: 'date', type: 'date', name: 'date', valueFormat: 'X' }], { date: source });
await nextTick();
expect((wrapper.vm as any).values.date).toBe(Math.floor(new Date(source).getTime() / 1000));
wrapper.findComponent({ name: 'TMDatePicker' }).vm.$emit('update:modelValue', '1626507420');
await nextTick();
expect((wrapper.vm as any).values.date).toBe(1626507420);
});
test('清空', async () => {
const wrapper = getWrapper();

View File

@ -117,6 +117,22 @@ describe('DateTime', () => {
expect(value.datetime).toMatch('2021-07-28 00:00:00');
});
test('valueFormat 为 X 时收成秒级数字', async () => {
const source = '2021-07-17T07:37:00.000Z';
const wrapper = getWrapper([{ text: 'dateTime', type: 'datetime', name: 'datetime', valueFormat: 'X' }], {
datetime: source,
});
await nextTick();
expect((wrapper.vm as any).values.datetime).toBe(Math.floor(new Date(source).getTime() / 1000));
wrapper.findComponent({ name: 'TMDatePicker' }).vm.$emit('update:modelValue', '1626507420');
await nextTick();
expect((wrapper.vm as any).values.datetime).toBe(1626507420);
});
test('清空', async () => {
const wrapper = getWrapper();

View File

@ -66,4 +66,32 @@ describe('Daterange', () => {
const daterange = wrapper.findComponent(MDaterange);
expect(daterange.exists()).toBe(true);
});
test('valueFormat 为 X 时区间收成秒数字', async () => {
const wrapper = getWrapper([{ type: 'daterange', text: 'daterange', name: 'range', valueFormat: 'X' }], {
range: [1626507420, 1626593820],
});
await nextTick();
wrapper.findComponent({ name: 'TMDatePicker' }).vm.$emit('update:modelValue', ['1626507420', '1626593820']);
await nextTick();
expect((wrapper.vm as any).values.range).toEqual([1626507420, 1626593820]);
});
test('valueFormat 为 X 且 names 时分别写入秒数字', async () => {
const wrapper = getWrapper([{ type: 'daterange', text: 'daterange', names: ['start', 'end'], valueFormat: 'X' }], {
start: 1626507420,
end: 1626593820,
});
await nextTick();
wrapper.findComponent({ name: 'TMDatePicker' }).vm.$emit('update:modelValue', ['1626507420', '1626593820']);
await nextTick();
expect((wrapper.vm as any).values.start).toBe(1626507420);
expect((wrapper.vm as any).values.end).toBe(1626593820);
});
});

View File

@ -60,4 +60,15 @@ describe('Time', () => {
const value = await (wrapper.vm as any).submitForm();
expect(value.time).toMatch('12:00:00');
});
test('valueFormat 为 X 时把秒字符串收成数字', async () => {
const wrapper = getWrapper([{ type: 'time', text: 'time', name: 'time', valueFormat: 'X' }], { time: 1626507420 });
await nextTick();
wrapper.findComponent({ name: 'TMTimePicker' }).vm.$emit('update:modelValue', '1626507421');
await nextTick();
expect((wrapper.vm as any).values.time).toBe(1626507421);
});
});

View File

@ -31,4 +31,32 @@ describe('Timerange', () => {
await nextTick();
expect(wrapper.findComponent(MTimerange).exists()).toBe(true);
});
test('valueFormat 为 X 时区间收成秒数字', async () => {
const wrapper = mountForm([{ name: 'tr', type: 'timerange', text: 'tr', valueFormat: 'X' }], {
tr: [1626507420, 1626593820],
});
await nextTick();
wrapper.findComponent({ name: 'TMTimePicker' }).vm.$emit('change', ['1626507420', '1626593820']);
await nextTick();
expect((wrapper.vm as any).values.tr).toEqual([1626507420, 1626593820]);
});
test('valueFormat 为 X 且 names 时分别写入秒数字', async () => {
const wrapper = mountForm([{ type: 'timerange', text: 'tr', names: ['start', 'end'], valueFormat: 'X' }], {
start: 1626507420,
end: 1626593820,
});
await nextTick();
wrapper.findComponent({ name: 'TMTimePicker' }).vm.$emit('change', ['1626507420', '1626593820']);
await nextTick();
expect((wrapper.vm as any).values.start).toBe(1626507420);
expect((wrapper.vm as any).values.end).toBe(1626593820);
});
});

View File

@ -29,6 +29,7 @@ import {
getDataByPage,
getRules,
initValue,
normalizeTimestampValue,
sortArray,
sortChange,
} from '@form/utils/form';
@ -681,6 +682,40 @@ describe('datetimeFormatter', () => {
test('自定义format只显示时间', () => {
expect(datetimeFormatter(date, defaultValue, 'HH:mm:ss')).toBe('15:37:00');
});
test('format是X时日期转成秒级时间戳', () => {
const seconds = Math.floor(date.getTime() / 1000);
expect(datetimeFormatter(date.toISOString(), defaultValue, 'X')).toBe(seconds);
expect(datetimeFormatter(date, defaultValue, 'X')).toBe(seconds);
});
test('format是X时已是秒的数字原样保留', () => {
const seconds = Math.floor(date.getTime() / 1000);
expect(datetimeFormatter(String(seconds), defaultValue, 'X')).toBe(seconds);
expect(datetimeFormatter(seconds, defaultValue, 'X')).toBe(seconds);
});
test('format是X时非法值回退默认值', () => {
expect(datetimeFormatter('abc', defaultValue, 'X')).toBe(defaultValue);
});
});
describe('normalizeTimestampValue', () => {
test('非 X 格式原样返回', () => {
expect(normalizeTimestampValue('1626507420', 'x')).toBe('1626507420');
expect(normalizeTimestampValue('2021/07/17', undefined)).toBe('2021/07/17');
});
test('X 格式:空值原样返回,秒字符串和日期收成秒数字', () => {
expect(normalizeTimestampValue(null, 'X')).toBeNull();
expect(normalizeTimestampValue('', 'X')).toBe('');
expect(normalizeTimestampValue('1626507420', 'X')).toBe(1626507420);
expect(normalizeTimestampValue(1626507420.9, 'X')).toBe(1626507420);
expect(normalizeTimestampValue(['1626507420', '1626593820'], 'X')).toEqual([1626507420, 1626593820]);
expect(normalizeTimestampValue('2021-07-17T07:37:00.000Z', 'X')).toBe(1626507420);
expect(normalizeTimestampValue('not-a-date', 'X')).toBe('not-a-date');
expect(normalizeTimestampValue(Infinity, 'X')).toBe(Infinity);
});
});
describe('sortArray', () => {

View File

@ -153,13 +153,26 @@ describe('validateTypeMatch', () => {
validateTypeMatch('25/01/2019', mForm, propsOf({ type: 'date', valueFormat: 'DD/MM/YYYY' })),
).toBeUndefined();
// timestamp / x → number
// timestamp / x → 毫秒 number;X → 秒 number(日期选择器写出的秒字符串不合法)
expect(
validateTypeMatch(1710000000000, mForm, propsOf({ type: 'date', valueFormat: 'timestamp' })),
).toBeUndefined();
expect(validateTypeMatch(1710000000000, mForm, propsOf({ type: 'datetime', valueFormat: 'x' }))).toBeUndefined();
expect(validateTypeMatch('2020-01-01', mForm, propsOf({ type: 'date', valueFormat: 'x' }))).toMatch(
/^值类型应为时间戳数字\n\n请参考以下示例值:\d+$/,
/^值类型应为毫秒级时间戳数字\n\n请参考以下示例值:\d{13}$/,
);
expect(validateTypeMatch(1626507420, mForm, propsOf({ type: 'date', valueFormat: 'X' }))).toBeUndefined();
expect(validateTypeMatch(1626507420.5, mForm, propsOf({ type: 'date', valueFormat: 'X' }))).toMatch(
/^值类型应为秒级时间戳数字/,
);
expect(validateTypeMatch(Infinity, mForm, propsOf({ type: 'date', valueFormat: 'X' }))).toMatch(
/^值类型应为秒级时间戳数字/,
);
expect(validateTypeMatch('1626507420', mForm, propsOf({ type: 'datetime', valueFormat: 'X' }))).toMatch(
/^值类型应为秒级时间戳数字\n\n请参考以下示例值:\d{10}$/,
);
expect(validateTypeMatch('2020-01-01', mForm, propsOf({ type: 'time', valueFormat: 'X' }))).toMatch(
/^值类型应为秒级时间戳数字\n\n请参考以下示例值:\d{10}$/,
);
});
@ -553,6 +566,14 @@ describe('validateTypeMatch', () => {
/^\["a"\] 格式应为长度为 2 的 YYYY\/MM\/DD HH:mm:ss 数组\n\n请参考以下示例值:\["\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}", "\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}"\]$/,
);
expect(validateTypeMatch([1, 2], mForm, propsOf({ type: 'daterange', valueFormat: 'timestamp' }))).toBeUndefined();
expect(
validateTypeMatch([1626507420, 1626593820], mForm, propsOf({ type: 'daterange', valueFormat: 'X' })),
).toBeUndefined();
expect(
validateTypeMatch(['2020-01-01', '2020-01-02'], mForm, propsOf({ type: 'daterange', valueFormat: 'X' })),
).toMatch(
/^\["2020-01-01","2020-01-02"\] 类型应为长度为 2 的秒级时间戳数字数组\n\n请参考以下示例值:\[\d{10}, \d{10}\]$/,
);
expect(validateTypeMatch('x', mForm, propsOf({ type: 'daterange', names: ['a', 'b'] }))).toBeUndefined();
expect(validateTypeMatch([{ id: 1 }], mForm, propsOf({ type: 'table' }))).toBeUndefined();