From f12f78c9e045d41cf03be3e867d15aec5b50d202 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 13 Oct 2026 17:11:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(form):=20=E6=97=A5=E6=9C=9F/=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=AD=97=E6=AE=B5=E6=94=AF=E6=8C=81=20valueFormat=20?= =?UTF-8?q?=E4=B8=BA=20X=20=E7=9A=84=E7=A7=92=E7=BA=A7=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 日期类字段 change 时经 normalizeTimestampValue 把选择器输出收成秒级 number,避免 Element Plus 写出秒字符串导致校验失败 - Date/DateTime/Time/Daterange/Timerange 回填 model 时,valueFormat 为 X 直接使用秒级数字,不再拼接展示格式 - typeMatch 对 X 格式要求有限整数,示例值与报错文案区分秒级/毫秒级 - datetimeFormatter 支持 X 格式输出秒级数字 - 补充相应单元测试,并更新 docs/form-config/rules.md 中的校验规则说明 --- docs/form-config/rules.md | 2 +- packages/form/src/fields/Date/Index.vue | 7 ++-- packages/form/src/fields/DateTime/Index.vue | 7 ++-- packages/form/src/fields/Daterange.vue | 32 +++++++++-------- packages/form/src/fields/Time.vue | 7 ++-- packages/form/src/fields/Timerange.vue | 26 ++++++++------ packages/form/src/utils/form.ts | 36 +++++++++++++++++-- packages/form/src/utils/typeMatch.ts | 19 ++++++---- packages/form/tests/unit/fields/Date.spec.ts | 14 ++++++++ .../form/tests/unit/fields/DateTime.spec.ts | 16 +++++++++ .../form/tests/unit/fields/Daterange.spec.ts | 28 +++++++++++++++ packages/form/tests/unit/fields/Time.spec.ts | 11 ++++++ .../form/tests/unit/fields/Timerange.spec.ts | 28 +++++++++++++++ packages/form/tests/unit/utils/form.spec.ts | 35 ++++++++++++++++++ .../form/tests/unit/utils/typeMatch.spec.ts | 25 +++++++++++-- 15 files changed, 247 insertions(+), 46 deletions(-) diff --git a/docs/form-config/rules.md b/docs/form-config/rules.md index 4d9f05d3..c050c932 100644 --- a/docs/form-config/rules.md +++ b/docs/form-config/rules.md @@ -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 枚举 | diff --git a/packages/form/src/fields/Date/Index.vue b/packages/form/src/fields/Date/Index.vue index fc4e0476..09c3ea15 100644 --- a/packages/form/src/fields/Date/Index.vue +++ b/packages/form/src/fields/Date/Index.vue @@ -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>(); 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)); }; diff --git a/packages/form/src/fields/DateTime/Index.vue b/packages/form/src/fields/DateTime/Index.vue index 807383e0..62226f79 100644 --- a/packages/form/src/fields/DateTime/Index.vue +++ b/packages/form/src/fields/DateTime/Index.vue @@ -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>(); 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)); }; diff --git a/packages/form/src/fields/Daterange.vue b/packages/form/src/fields/Daterange.vue index c31e3862..b9aec6a7 100644 --- a/packages/form/src/fields/Daterange.vue +++ b/packages/form/src/fields/Daterange.vue @@ -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 | 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({ diff --git a/packages/form/src/fields/Time.vue b/packages/form/src/fields/Time.vue index 87906565..932a68d9 100644 --- a/packages/form/src/fields/Time.vue +++ b/packages/form/src/fields/Time.vue @@ -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>(); 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)); }; diff --git a/packages/form/src/fields/Timerange.vue b/packages/form/src/fields/Timerange.vue index dc9ef501..826ba1b9 100644 --- a/packages/form/src/fields/Timerange.vue +++ b/packages/form/src/fields/Timerange.vue @@ -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 | 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({ diff --git a/packages/form/src/utils/form.ts b/packages/form/src/utils/form.ts index b9267d37..2f3d073f 100644 --- a/packages/form/src/utils/form.ts +++ b/packages/form/src/utils/form.ts @@ -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),无需额外判断 diff --git a/packages/form/src/utils/typeMatch.ts b/packages/form/src/utils/typeMatch.ts index 99deb4dd..93171386 100644 --- a/packages/form/src/utils/typeMatch.ts +++ b/packages/form/src/utils/typeMatch.ts @@ -150,7 +150,7 @@ const DATE_LIKE_DEFAULT_VALUE_FORMAT: Record = { 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), ); diff --git a/packages/form/tests/unit/fields/Date.spec.ts b/packages/form/tests/unit/fields/Date.spec.ts index 2c60335f..9f49b3db 100644 --- a/packages/form/tests/unit/fields/Date.spec.ts +++ b/packages/form/tests/unit/fields/Date.spec.ts @@ -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(); diff --git a/packages/form/tests/unit/fields/DateTime.spec.ts b/packages/form/tests/unit/fields/DateTime.spec.ts index 46b499ce..71728bfc 100644 --- a/packages/form/tests/unit/fields/DateTime.spec.ts +++ b/packages/form/tests/unit/fields/DateTime.spec.ts @@ -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(); diff --git a/packages/form/tests/unit/fields/Daterange.spec.ts b/packages/form/tests/unit/fields/Daterange.spec.ts index 07d7d795..363701bb 100644 --- a/packages/form/tests/unit/fields/Daterange.spec.ts +++ b/packages/form/tests/unit/fields/Daterange.spec.ts @@ -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); + }); }); diff --git a/packages/form/tests/unit/fields/Time.spec.ts b/packages/form/tests/unit/fields/Time.spec.ts index e9ce9656..32790a6b 100644 --- a/packages/form/tests/unit/fields/Time.spec.ts +++ b/packages/form/tests/unit/fields/Time.spec.ts @@ -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); + }); }); diff --git a/packages/form/tests/unit/fields/Timerange.spec.ts b/packages/form/tests/unit/fields/Timerange.spec.ts index df89ef15..1ad4936d 100644 --- a/packages/form/tests/unit/fields/Timerange.spec.ts +++ b/packages/form/tests/unit/fields/Timerange.spec.ts @@ -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); + }); }); diff --git a/packages/form/tests/unit/utils/form.spec.ts b/packages/form/tests/unit/utils/form.spec.ts index 8b453ae4..07cc2155 100644 --- a/packages/form/tests/unit/utils/form.spec.ts +++ b/packages/form/tests/unit/utils/form.spec.ts @@ -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', () => { diff --git a/packages/form/tests/unit/utils/typeMatch.spec.ts b/packages/form/tests/unit/utils/typeMatch.spec.ts index 7aacc9cc..4ae6f942 100644 --- a/packages/form/tests/unit/utils/typeMatch.spec.ts +++ b/packages/form/tests/unit/utils/typeMatch.spec.ts @@ -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();