From eb28d2fba73364eb8d86666b788c169246960c64 Mon Sep 17 00:00:00 2001 From: RUNZE LU <36724300+lurunze1226@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:31:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20FormItem=E6=A0=BC=E5=BC=8F=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=94=AF=E6=8C=81=E6=97=A5=E6=9C=9F=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=A7=84=E5=88=99=20(#5241)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/form/formitem.md | 83 ++- examples/components/Form/Validation.jsx | 483 ++++++++++++++---- packages/amis-core/src/renderers/Item.tsx | 85 ++- packages/amis-core/src/store/form.ts | 20 +- packages/amis-core/src/store/formItem.ts | 8 +- packages/amis-core/src/utils/validations.ts | 162 +++++- .../src/components/calendar/TimeView.tsx | 6 +- packages/amis-ui/src/locale/de-DE.ts | 24 + packages/amis-ui/src/locale/en-US.ts | 24 + packages/amis-ui/src/locale/zh-CN.ts | 17 + .../amis/__tests__/utils/validations.test.ts | 432 ++++++++++++++++ 11 files changed, 1213 insertions(+), 131 deletions(-) diff --git a/docs/zh-CN/components/form/formitem.md b/docs/zh-CN/components/form/formitem.md index af6638298..2b9b5bc54 100755 --- a/docs/zh-CN/components/form/formitem.md +++ b/docs/zh-CN/components/form/formitem.md @@ -678,7 +678,7 @@ order: 1 ### 字符串形式(不推荐) -也可以配置字符串形式来指定,如下例,输入不合法的值,点击提交会报错并显示报错信息 +也可以配置字符串形式来指定,如下例,输入不合法的值,点击提交会报错并显示报错信息。(注意日期时间类的校验规则不支持字符串形式) ```schema: scope="body" { @@ -765,7 +765,7 @@ amis 会有默认的报错信息,如果你想自定义校验信息,配置`va } ``` -默认的校验信息如下,可以直接配置文字,也可用多语言中的 key。参考:https://github.com/baidu/amis/blob/master/src/locale/zh-CN.ts#L175-L201 +默认的校验信息如下,可以直接配置文字,也可用多语言中的 key。参考:https://github.com/baidu/amis/blob/master/packages/amis-ui/src/locale/zh-CN.ts#L250 ```js { @@ -794,7 +794,20 @@ amis 会有默认的报错信息,如果你想自定义校验信息,配置`va isPhoneNumber: 'validate.isPhoneNumber', isTelNumber: 'validate.isTelNumber', isZipcode: 'validate.isZipcode', - isId: 'validate.isId' + isId: 'validate.isId', + /* 日期时间相关校验规则 2.2.0 及以上版本生效 */ + isDateTimeSame: 'validate.isDateTimeSame', + isDateTimeBefore: 'validate.isDateTimeBefore', + isDateTimeAfter: 'validate.isDateTimeAfter', + isDateTimeSameOrBefore: 'validate.isDateTimeSameOrBefore', + isDateTimeSameOrAfter: 'validate.isDateTimeSameOrAfter', + isDateTimeBetween: 'validate.isDateTimeBetween', + isTimeSame: 'validate.isTimeSame', + isTimeBefore: 'validate.isTimeBefore', + isTimeAfter: 'validate.isTimeAfter', + isTimeSameOrBefore: 'validate.isTimeSameOrBefore', + isTimeSameOrAfter: 'validate.isTimeSameOrAfter', + isTimeBetween: 'validate.isTimeBetween' } ``` @@ -804,31 +817,45 @@ amis 会有默认的报错信息,如果你想自定义校验信息,配置`va ### 支持的格式校验 -- `isEmail` 必须是 Email。 -- `isUrl` 必须是 Url。 -- `isNumeric` 必须是 数值。 -- `isAlpha` 必须是 字母。 -- `isAlphanumeric` 必须是 字母或者数字。 -- `isInt` 必须是 整形。 -- `isFloat` 必须是 浮点形。 -- `isLength:length` 是否长度正好等于设定值。 -- `minLength:length` 最小长度。 -- `maxLength:length` 最大长度。 -- `maximum:number` 最大值。 -- `minimum:number` 最小值。 -- `equals:xxx` 当前值必须完全等于 xxx。 -- `equalsField:xxx` 当前值必须与 xxx 变量值一致。 -- `isJson` 是否是合法的 Json 字符串。 -- `isUrlPath` 是 url 路径。 -- `isPhoneNumber` 是否为合法的手机号码 -- `isTelNumber` 是否为合法的电话号码 -- `isZipcode` 是否为邮编号码 -- `isId` 是否为身份证号码,没做校验 -- `matchRegexp:/foo/` 必须命中某个正则。 -- `matchRegexp1:/foo/` 必须命中某个正则。 -- `matchRegexp2:/foo/` 必须命中某个正则。 -- `matchRegexp3:/foo/` 必须命中某个正则。 -- `matchRegexp4:/foo/` 必须命中某个正则。 +| 规则名称 | 说明 | 定义 | 版本 | +| ------------------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------- | +| `isEmail` | 必须是 Email。 | `(value: any) => boolean` | | +| `isUrl` | 必须是 Url。 | `(value: any) => boolean` | | +| `isNumeric` | 必须是 数值。 | `(value: any) => boolean` | | +| `isAlpha` | 必须是 字母。 | `(value: any) => boolean` | | +| `isAlphanumeric` | 必须是 字母或者数字。 | `(value: any) => boolean` | | +| `isInt` | 必须是 整形。 | `(value: any) => boolean` | | +| `isFloat` | 必须是 浮点形。 | `(value: any) => boolean` | | +| `isLength:length` | 是否长度正好等于设定值。 | `(value: any) => boolean` | | +| `minLength:length` | 最小长度。 | `(value: any, length: number) => boolean` | | +| `maxLength:length` | 最大长度。 | `(value: any, length: number) => boolean` | | +| `maximum:number` | 最大值。 | `(value: any, maximum: number) => boolean` | | +| `minimum:number` | 最小值。 | `(value: any, minimum:number) => boolean` | | +| `equals:xxx` | 当前值必须完全等于 xxx。 | `(value: any, targetValue: any) => boolean` | | +| `equalsField:xxx` | 当前值必须与 xxx 变量值一致。 | `(value: any, field: string) => boolean` | | +| `isJson` | 是否是合法的 Json 字符串。 | `(value: any) => boolean` | | +| `isUrlPath` | 是 url 路径。 | `(value: any) => boolean` | | +| `isPhoneNumber` | 是否为合法的手机号码 | `(value: any) => boolean` | | +| `isTelNumber` | 是否为合法的电话号码 | `(value: any) => boolean` | | +| `isZipcode` | 是否为邮编号码 | `(value: any) => boolean` | | +| `isId` | 是否为身份证号码,没做校验 | `(value: any) => boolean` | | +| `matchRegexp:/foo/` | 必须命中某个正则。 | `(value: any, regexp: string \| RegExp) => boolean` | | +| `matchRegexp1:/foo/` | 必须命中某个正则。 | `(value: any, regexp: string \| RegExp) => boolean` | | +| `matchRegexp2:/foo/` | 必须命中某个正则。 | `(value: any, regexp: string \| RegExp) => boolean` | | +| `matchRegexp3:/foo/` | 必须命中某个正则。 | `(value: any, regexp: string \| RegExp) => boolean` | | +| `matchRegexp4:/foo/` | 必须命中某个正则。 | `(value: any, regexp: string \| RegExp) => boolean` | | +| `isDateTimeSame` | 日期和目标日期相同,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetDate: any, granularity?: string) => boolean` | `2.2.0` | +| `isDateTimeBefore` | 日期早于目标日期,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetDate: any, granularity?: string) => boolean` | `2.2.0` | +| `isDateTimeAfter` | 日期晚于目标日期,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetDate: any, granularity?: string) => boolean` | `2.2.0` | +| `isDateTimeSameOrBefore` | 日期早于目标日期或和目标日期相同,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetDate: any, granularity?: string) => boolean` | `2.2.0` | +| `isDateTimeSameOrAfter` | 日期晚于目标日期或和目标日期相同,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetDate: any, granularity?: string) => boolean` | `2.2.0` | +| `isDateTimeBetween` | 日期处于目标日期范围,支持指定粒度和区间的开闭形式,默认到毫秒 `millisecond`,左右开区间`'()'` | `(value: any, lhs: any, rhs: any, granularity?: string, inclusivity?: '()' \| '[)' \| '(]' \| '[]') => boolean` | `2.2.0` | +| `isTimeSame` | 时间和目标时间相同,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetTime: any, granularity?: string) => boolean` | `2.2.0` | +| `isTimeBefore` | 时间早于目标时间,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetTime: any, granularity?: string) => boolean` | `2.2.0` | +| `isTimeAfter` | 时间晚于目标时间,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetTime: any, granularity?: string) => boolean` | `2.2.0` | +| `isTimeSameOrBefore` | 时间早于目标时间或和目标时间相同,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetTime: any, granularity?: string) => boolean` | `2.2.0` | +| `isTimeSameOrAfter` | 时间晚于目标时间或和目标时间相同,支持指定粒度,默认到毫秒 `millisecond` | `(value: any, targetTime: any, granularity?: string) => boolean` | `2.2.0` | +| `isTimeBetween` | 时间处于目标时间范围,支持指定粒度和区间的开闭形式,默认到毫秒 `millisecond`,左右开区间`'()'` | `(value: any, lhs: any, rhs: any, granularity?: string, inclusivity?: '()' \| '[)' \| '(]' \| '[]') => boolean` | `2.2.0` | #### 验证只允许 http 协议的 url 地址 diff --git a/examples/components/Form/Validation.jsx b/examples/components/Form/Validation.jsx index 099a7c806..a240f6796 100644 --- a/examples/components/Form/Validation.jsx +++ b/examples/components/Form/Validation.jsx @@ -3,107 +3,398 @@ export default { toolbar: "文档", body: [ { - type: 'form', - autoFocus: false, - messages: { - validateFailed: '请仔细检查表单规则,部分表单项没通过验证' - }, - title: '表单', - actions: [ + type: 'grid', + columns: [ { - type: 'submit', - label: '提交' - } - ], - api: '/api/mock2/form/saveFormFailed?waitSeconds=2', - mode: 'horizontal', - body: [ - { - type: 'input-text', - name: 'test', - label: '必填', - required: true + body: [ + { + type: 'form', + autoFocus: false, + messages: { + validateFailed: '请仔细检查表单规则,部分表单项没通过验证' + }, + title: '表单', + actions: [ + { + type: 'submit', + label: '提交' + } + ], + api: '/api/mock2/form/saveFormFailed?waitSeconds=2', + mode: 'horizontal', + body: [ + { + type: 'input-text', + name: 'test', + label: '必填', + required: true + }, + { + type: 'divider' + }, + { + name: 'test1', + type: 'input-email', + label: 'Email' + }, + { + type: 'divider' + }, + { + name: 'url', + type: 'input-url', + label: 'URL' + }, + { + type: 'divider' + }, + { + name: 'num', + type: 'input-text', + label: '数字', + validations: 'isNumeric' + }, + { + type: 'divider' + }, + { + name: 'alpha', + type: 'input-text', + label: '字母或数字', + validations: 'isAlphanumeric' + }, + { + type: 'divider' + }, + { + name: 'int', + type: 'input-text', + label: '整形', + validations: 'isInt' + }, + { + type: 'divider' + }, + { + name: 'minLength', + type: 'input-text', + label: '长度限制', + validations: 'minLength:2,maxLength:10' + }, + { + type: 'divider' + }, + { + name: 'min', + type: 'input-text', + label: '数值限制', + validations: 'maximum:10,minimum:2' + }, + { + type: 'divider' + }, + { + name: 'reg', + type: 'input-text', + label: '正则', + validations: 'matchRegexp:/^abc/', + validationErrors: { + matchRegexp: '请输入abc开头的好么?' + } + }, + { + type: 'divider' + }, + { + name: 'test2', + type: 'input-text', + label: '服务端验证' + } + ] + } + ] }, { - type: 'divider' + body: [ + { + type: 'form', + name: 'form2', + api: '/api/mock2/form/saveForm', + debug: true, + debugConfig: { + levelExpand: 3 + }, + title: '日期时间相关校验规则', + data: { + datetime1: '2022-09-10 00:00:00', + datetime2: '2022-10-01 00:00:00', + datetime3: '2022-10-01 00:00:00', + datetime4: '2022-10-01 00:00:01', + datetime5: '2022-09-30 23:59:59', + datetime6: '2022-09-30 23:59:59' + }, + body: [ + { + type: 'input-datetime', + label: 'isDateTimeSame', + name: 'datetime1', + format: 'YYYY-MM-DD HH:mm:ss', + validations: { + isDateTimeSame: '2022-10-01 00:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-datetime', + label: 'isDateTimeBefore', + name: 'datetime2', + format: 'YYYY-MM-DD HH:mm:ss', + validations: { + isDateTimeBefore: '2022-10-01 00:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-datetime', + label: 'isDateTimeAfter', + name: 'datetime3', + format: 'YYYY-MM-DD HH:mm:ss', + validations: { + isDateTimeAfter: '2022-10-01 00:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-datetime', + label: 'isDateTimeSameOrBefore', + name: 'datetime4', + format: 'YYYY-MM-DD HH:mm:ss', + validations: { + isDateTimeSameOrBefore: '2022-10-01 00:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-datetime', + label: 'isDateTimeSameOrAfter', + name: 'datetime5', + format: 'YYYY-MM-DD HH:mm:ss', + validations: { + isDateTimeSameOrAfter: '2022-10-01 00:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-datetime', + label: 'isDateTimeBetween', + name: 'datetime6', + format: 'YYYY-MM-DD HH:mm:ss', + validations: { + isDateTimeBetween: [ + '2022-10-01 00:00:00', + '2022-10-03 00:00:00', + 'second', + '[]' + ] + }, + validationErrors: { + isDateTimeBetween: + '选择的日期有误,日期必须在 $1 ~ $2 范围内' + } + } + ] + }, + { + type: 'form', + name: 'form3', + api: '/api/mock2/form/saveForm', + debug: true, + debugConfig: { + levelExpand: 3 + }, + title: '日期时间相关校验规则(带变量)', + body: [ + { + type: 'input-datetime', + label: '开始日期时间', + name: 'startTime', + inputFormat: 'YYYY-MM-DD HH:mm:ss', + format: 'YYYY-MM-DD HH:mm:ss', + required: true, + validations: { + isDateTimeSameOrBefore: '${endTime}' + }, + validationErrors: { + isDateTimeSameOrBefore: + '当前日期值不合法,开始时间不能大于结束时间' + } + }, + { + type: 'input-datetime', + label: '结束日期时间', + name: 'endTime', + inputFormat: 'YYYY-MM-DD HH:mm:ss', + format: 'YYYY-MM-DD HH:mm:ss', + required: true + } + ] + } + ] }, { - name: 'test1', - type: 'input-email', - label: 'Email' - }, - { - type: 'divider' - }, - { - name: 'url', - type: 'input-url', - label: 'URL' - }, - { - type: 'divider' - }, - { - name: 'num', - type: 'input-text', - label: '数字', - validations: 'isNumeric' - }, - { - type: 'divider' - }, - { - name: 'alpha', - type: 'input-text', - label: '字母或数字', - validations: 'isAlphanumeric' - }, - { - type: 'divider' - }, - { - name: 'int', - type: 'input-text', - label: '整形', - validations: 'isInt' - }, - { - type: 'divider' - }, - { - name: 'minLength', - type: 'input-text', - label: '长度限制', - validations: 'minLength:2,maxLength:10' - }, - { - type: 'divider' - }, - { - name: 'min', - type: 'input-text', - label: '数值限制', - validations: 'maximum:10,minimum:2' - }, - { - type: 'divider' - }, - { - name: 'reg', - type: 'input-text', - label: '正则', - validations: 'matchRegexp:/^abc/', - validationErrors: { - matchRegexp: '请输入abc开头的好么?' - } - }, - { - type: 'divider' - }, - { - name: 'test2', - type: 'input-text', - label: '服务端验证' + body: [ + { + type: 'form', + name: 'form4', + api: '/api/mock2/form/saveForm', + debug: true, + debugConfig: { + levelExpand: 3 + }, + title: '时间相关校验规则', + data: { + time1: '16:00:00', + time2: '16:00:00', + time3: '16:00:00', + time4: '16:00:01', + time5: '16:00:01', + time6: '15:00:01' + }, + body: [ + { + type: 'input-time', + label: 'isTimeSame', + name: 'time1', + format: 'HH:mm:ss', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + validations: { + isTimeSame: '16:00:01' + } + }, + { + type: 'divider' + }, + { + type: 'input-time', + label: 'isTimeBefore', + name: 'time2', + format: 'HH:mm:ss', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + validations: { + isTimeBefore: '15:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-time', + label: 'isTimeAfter', + name: 'time3', + format: 'HH:mm:ss', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + validations: { + isTimeAfter: '16:00:30' + } + }, + { + type: 'divider' + }, + { + type: 'input-time', + label: 'isTimeSameOrBefore', + name: 'time4', + format: 'HH:mm:ss', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + validations: { + isTimeSameOrBefore: '16:00:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-time', + label: 'isTimeSameOrAfter', + name: 'time5', + format: 'HH:mm:ss', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + validations: { + isTimeSameOrAfter: '16:30:00' + } + }, + { + type: 'divider' + }, + { + type: 'input-time', + label: 'isTimeBetween', + name: 'time6', + format: 'HH:mm:ss', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + validations: { + isTimeBetween: ['03:00:00', '15:00:00', 'second', '[]'] + }, + validationErrors: { + isTimeBetween: '选择的时间有误,时间必须在 $1 ~ $2 范围内' + } + } + ] + }, + { + type: 'form', + name: 'form5', + api: '/api/mock2/form/saveForm', + debug: true, + debugConfig: { + levelExpand: 3 + }, + title: '时间相关校验规则(带变量)', + body: [ + { + type: 'input-time', + label: '开始时间', + name: 'startTime', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + format: 'HH:mm:ss', + validations: { + isRequired: true, + isTimeSameOrBefore: '${endTime}' + }, + validationErrors: { + isTimeSameOrBefore: + '当前时间值不合法,开始时间不能大于结束时间 $1' + } + }, + { + type: 'input-time', + label: '结束时间', + name: 'endTime', + timeFormat: 'HH:mm:ss', + inputFormat: 'HH:mm:ss', + format: 'HH:mm:ss', + required: true + } + ] + } + ] } ] } diff --git a/packages/amis-core/src/renderers/Item.tsx b/packages/amis-core/src/renderers/Item.tsx index e21675835..59b8ec037 100644 --- a/packages/amis-core/src/renderers/Item.tsx +++ b/packages/amis-core/src/renderers/Item.tsx @@ -174,7 +174,18 @@ export interface FormBaseControl extends BaseSchemaWithoutType { maximum?: string; minLength?: string; minimum?: string; - + isDateTimeSame?: string; + isDateTimeBefore?: string; + isDateTimeAfter?: string; + isDateTimeSameOrBefore?: string; + isDateTimeSameOrAfter?: string; + isDateTimeBetween?: string; + isTimeSame?: string; + isTimeBefore?: string; + isTimeAfter?: string; + isTimeSameOrBefore?: string; + isTimeSameOrAfter?: string; + isTimeBetween?: string; [propName: string]: any; }; @@ -276,6 +287,78 @@ export interface FormBaseControl extends BaseSchemaWithoutType { */ minimum?: number; + /** + * 和目标日期相同,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isDateTimeSame?: string | string[]; + + /** + * 早于目标日期,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isDateTimeBefore?: string | string[]; + + /** + * 晚于目标日期,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isDateTimeAfter?: string | string[]; + + /** + * 早于目标日期或和目标日期相同,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isDateTimeSameOrBefore?: string | string[]; + + /** + * 晚于目标日期或和目标日期相同,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isDateTimeSameOrAfter?: string | string[]; + + /** + * 日期处于目标日期范围,支持指定粒度和区间的开闭形式,默认到毫秒, 左右开区间 + * @version 2.2.0 + */ + isDateTimeBetween?: string | string[]; + + /** + * 和目标时间相同,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isTimeSame?: string | string[]; + + /** + * 早于目标时间,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isTimeBefore?: string | string[]; + + /** + * 晚于目标时间,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isTimeAfter?: string | string[]; + + /** + * 早于目标时间或和目标时间相同,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isTimeSameOrBefore?: string | string[]; + + /** + * 晚于目标时间或和目标时间相同,支持指定粒度,默认到毫秒 + * @version 2.2.0 + */ + isTimeSameOrAfter?: string | string[]; + + /** + * 时间处于目标时间范围,支持指定粒度和区间的开闭形式,默认到毫秒, 左右开区间 + * @version 2.2.0 + */ + isTimeBetween?: string | string[]; + [propName: string]: any; }; diff --git a/packages/amis-core/src/store/form.ts b/packages/amis-core/src/store/form.ts index 13903fad1..ff4fbe1a1 100644 --- a/packages/amis-core/src/store/form.ts +++ b/packages/amis-core/src/store/form.ts @@ -1,5 +1,6 @@ import {types, getEnv, flow, isAlive, Instance} from 'mobx-state-tree'; import debounce from 'lodash/debounce'; +import toPairs from 'lodash/toPairs'; import {ServiceStore} from './service'; import type {IFormItemStore} from './formItem'; import {Api, ApiObject, fetchOptions, Payload} from '../types'; @@ -13,12 +14,14 @@ import { difference, isEmpty, mapObject, - keyToPath + keyToPath, + isObject } from '../utils/helper'; import isEqual from 'lodash/isEqual'; import flatten from 'lodash/flatten'; import find from 'lodash/find'; import {filter} from '../utils/tpl'; +import {isPureVariable} from '../utils/tpl-builtin'; import {normalizeApiResponseData} from '../utils/api'; export const FormStore = ServiceStore.named('FormStore') @@ -543,6 +546,21 @@ export const FormStore = ServiceStore.named('FormStore') // 先清除组合校验的错误 item.clearError('rules'); + /* 日期类校验存在表单项联动的情况,需要在提交前重置校验状态,避免变量更新后联动校验结果未更新 */ + if ( + item.validated && + isObject(item.rules) && + toPairs(item.rules) + .filter(([key, value]) => /^is(Date)?Time/.test(key)) + .some(([key, value]) => + Array.isArray(value) + ? value.some(item => isPureVariable(item)) + : isPureVariable(value) + ) + ) { + item.resetValidationStatus(); + } + // 验证过,或者是 unique 的表单项,或者强制验证,或者有远端校验api if ( !item.validated || diff --git a/packages/amis-core/src/store/formItem.ts b/packages/amis-core/src/store/formItem.ts index 0763640ad..54a833700 100644 --- a/packages/amis-core/src/store/formItem.ts +++ b/packages/amis-core/src/store/formItem.ts @@ -286,7 +286,7 @@ export const FormItemStore = StoreNode.named('FormItemStore') rules = { ...rules, - isRequired: self.required + isRequired: self.required || rules?.isRequired }; // todo 这个弄个配置由渲染器自己来决定 @@ -1174,6 +1174,11 @@ export const FormItemStore = StoreNode.named('FormItemStore') !keepErrors && clearError(); } + function resetValidationStatus(tag?: string) { + self.validated = false; + clearError(); + } + function openDialog( schema: any, data: any, @@ -1232,6 +1237,7 @@ export const FormItemStore = StoreNode.named('FormItemStore') setSubStore, getSubStore, reset, + resetValidationStatus, openDialog, closeDialog, changeTmpValue, diff --git a/packages/amis-core/src/utils/validations.ts b/packages/amis-core/src/utils/validations.ts index cddc57380..b16afd055 100644 --- a/packages/amis-core/src/utils/validations.ts +++ b/packages/amis-core/src/utils/validations.ts @@ -1,6 +1,8 @@ -import {createObject} from './helper'; +import moment from 'moment'; import {filter} from './tpl'; import {isPureVariable, resolveVariableAndFilter} from './tpl-builtin'; +import type {MomentInput, unitOfTime, MomentFormatSpecification} from 'moment'; + const isExisty = (value: any) => value !== null && value !== undefined; const isEmpty = (value: any) => value === ''; const makeRegexp = (reg: string | RegExp) => { @@ -94,7 +96,9 @@ export interface ValidateFn { value: any, arg1?: any, arg2?: any, - arg3?: any + arg3?: any, + arg4?: any, + arg5?: any ): boolean; } @@ -282,6 +286,146 @@ export const validations: { }, matchRegexp9: function (values, value, regexp) { return validations.matchRegexp(values, value, regexp); + }, + /** ============================ 日期时间相关 ============================= */ + isDateTimeSame: ( + values, + value: MomentInput, + targetDate: MomentInput, + granularity?: unitOfTime.StartOf + ) => { + return moment(value).isSame(moment(targetDate), granularity); + }, + isDateTimeBefore: ( + values, + value: MomentInput, + targetDate: MomentInput, + granularity?: unitOfTime.StartOf + ) => { + return moment(value).isBefore(moment(targetDate), granularity); + }, + isDateTimeAfter: ( + values, + value: MomentInput, + targetDate: MomentInput, + granularity?: unitOfTime.StartOf + ) => { + return moment(value).isAfter(moment(targetDate), granularity); + }, + isDateTimeSameOrBefore: ( + values, + value: MomentInput, + targetDate: MomentInput, + granularity?: unitOfTime.StartOf + ) => { + return moment(value).isSameOrBefore(moment(targetDate), granularity); + }, + isDateTimeSameOrAfter: ( + values, + value: MomentInput, + targetDate: MomentInput, + granularity?: unitOfTime.StartOf + ) => { + return moment(value).isSameOrAfter(moment(targetDate), granularity); + }, + isDateTimeBetween: ( + values, + value: MomentInput, + lhs: MomentInput, + rhs: MomentInput, + granularity?: unitOfTime.StartOf, + inclusivity?: '()' | '[)' | '(]' | '[]' + ) => { + return moment(value).isBetween( + moment(lhs), + moment(rhs), + granularity, + inclusivity + ); + }, + /** ============================ 时间相关 ============================= */ + isTimeSame: ( + values, + value: MomentInput, + targetTime: MomentInput, + granularity?: unitOfTime.StartOf, + format?: MomentFormatSpecification + ) => { + // 直接使用时间构造的moment object是不合法的,所以需要额外指定一下格式 + format = format ?? 'hh:mm:ss'; + return moment(value, format).isSame( + moment(targetTime, format), + granularity + ); + }, + isTimeBefore: ( + values, + value: MomentInput, + targetTime: MomentInput, + granularity?: unitOfTime.StartOf, + format?: MomentFormatSpecification + ) => { + format = format ?? 'hh:mm:ss'; + return moment(value, format).isBefore( + moment(targetTime, format), + granularity + ); + }, + isTimeAfter: ( + values, + value: MomentInput, + targetTime: MomentInput, + granularity?: unitOfTime.StartOf, + format?: MomentFormatSpecification + ) => { + format = format ?? 'hh:mm:ss'; + return moment(value, format).isAfter( + moment(targetTime, format), + granularity + ); + }, + isTimeSameOrBefore: ( + values, + value: MomentInput, + targetTime: MomentInput, + granularity?: unitOfTime.StartOf, + format?: MomentFormatSpecification + ) => { + format = format ?? 'hh:mm:ss'; + return moment(value, format).isSameOrBefore( + moment(targetTime, format), + granularity + ); + }, + isTimeSameOrAfter: ( + values, + value: MomentInput, + targetTime: MomentInput, + granularity?: unitOfTime.StartOf, + format?: MomentFormatSpecification + ) => { + format = format ?? 'hh:mm:ss'; + return moment(value, format).isSameOrAfter( + moment(targetTime, format), + granularity + ); + }, + isTimeBetween: ( + values, + value: MomentInput, + lhs: MomentInput, + rhs: MomentInput, + granularity?: unitOfTime.StartOf, + inclusivity?: '()' | '[)' | '(]' | '[]', + format?: MomentFormatSpecification + ) => { + format = format ?? 'hh:mm:ss'; + return moment(value, format).isBetween( + moment(lhs, format), + moment(rhs, format), + granularity, + inclusivity + ); } }; @@ -322,7 +466,19 @@ export const validateMessages: { isPhoneNumber: 'validate.isPhoneNumber', isTelNumber: 'validate.isTelNumber', isZipcode: 'validate.isZipcode', - isId: 'validate.isId' + isId: 'validate.isId', + isDateTimeSame: 'validate.isDateTimeSame', + isDateTimeBefore: 'validate.isDateTimeBefore', + isDateTimeAfter: 'validate.isDateTimeAfter', + isDateTimeSameOrBefore: 'validate.isDateTimeSameOrBefore', + isDateTimeSameOrAfter: 'validate.isDateTimeSameOrAfter', + isDateTimeBetween: 'validate.isDateTimeBetween', + isTimeSame: 'validate.isTimeSame', + isTimeBefore: 'validate.isTimeBefore', + isTimeAfter: 'validate.isTimeAfter', + isTimeSameOrBefore: 'validate.isTimeSameOrBefore', + isTimeSameOrAfter: 'validate.isTimeSameOrAfter', + isTimeBetween: 'validate.isTimeBetween' }; export function validate( diff --git a/packages/amis-ui/src/components/calendar/TimeView.tsx b/packages/amis-ui/src/components/calendar/TimeView.tsx index 24fe56331..931a39c60 100644 --- a/packages/amis-ui/src/components/calendar/TimeView.tsx +++ b/packages/amis-ui/src/components/calendar/TimeView.tsx @@ -712,7 +712,11 @@ export class CustomTimeView extends React.Component< }); inputs.length && inputs.pop(); - const quickLists = [{__('TimeNow')}]; + const quickLists = [ + + {__('TimeNow')} + + ]; return ( <>
diff --git a/packages/amis-ui/src/locale/de-DE.ts b/packages/amis-ui/src/locale/de-DE.ts index 6aa23aaec..f0441c777 100644 --- a/packages/amis-ui/src/locale/de-DE.ts +++ b/packages/amis-ui/src/locale/de-DE.ts @@ -281,6 +281,30 @@ register('de-DE', { 'validate.minimum': 'Der Eingabewert ist kleiner als der Mindestwert von $1.', 'validate.minLength': 'Geben Sie weitere Zeichen ein, mindestens $1.', 'validate.notEmptyString': 'Geben Sie nicht nur Leerzeichen ein.', + 'validate.isDateTimeSame': + 'Der aktuelle Datumswert ist ungültig, bitte geben Sie denselben Datumswert wie $1 ein', + 'validate.isDateTimeBefore': + 'Der aktuelle Datumswert ist ungültig, bitte geben Sie einen Datumswert vor $1 ein', + 'validate.isDateTimeAfter': + 'Der aktuelle Datumswert ist ungültig, bitte geben Sie nach $1 einen Datumswert ein', + 'validate.isDateTimeSameOrBefore': + 'Der aktuelle Datumswert ist ungültig. Bitte geben Sie einen Datumswert ein, der gleich oder älter als $1 ist', + 'validate.isDateTimeSameOrAfter': + 'Der aktuelle Datumswert ist ungültig. Bitte geben Sie einen Datumswert ein, der gleich oder nach $1 ist', + 'validate.isDateTimeBetween': + 'Der aktuelle Datumswert ist ungültig, bitte geben Sie einen Datumswert zwischen $1 und $2 ein', + 'validate.isTimeSame': + 'Der aktuelle Zeitwert ist ungültig, bitte geben Sie denselben Zeitwert wie 1 $ ein', + 'validate.isTimeBefore': + 'Der aktuelle Zeitwert ist ungültig, bitte geben Sie einen Zeitwert vor $1 ein', + 'validate.isTimeAfter': + 'Der aktuelle Zeitwert ist ungültig, bitte geben Sie nach $1 einen Zeitwert ein', + 'validate.isTimeSameOrBefore': + 'Der aktuelle Zeitwert ist ungültig. Bitte geben Sie einen Zeitwert ein, der gleich oder älter als $1 ist', + 'validate.isTimeSameOrAfter': + 'Der aktuelle Zeitwert ist ungültig. Bitte geben Sie einen Zeitwert ein, der gleich oder nach $1 ist', + 'validate.isTimeBetween': + 'Der aktuelle Zeitwert ist ungültig, bitte geben Sie einen Zeitwert zwischen $1 und $2 ein', 'validateFailed': 'Fehler bei der Überprüfung', 'Wizard.configError': 'Konfigurationsfehler', 'Wizard.finish': 'Ende', diff --git a/packages/amis-ui/src/locale/en-US.ts b/packages/amis-ui/src/locale/en-US.ts index dd68321eb..cba89788c 100644 --- a/packages/amis-ui/src/locale/en-US.ts +++ b/packages/amis-ui/src/locale/en-US.ts @@ -271,6 +271,30 @@ register('en-US', { 'validate.minimum': 'The input value is lower than the minimum value of $1', 'validate.minLength': 'Please enter more, at least $1 characters.', 'validate.notEmptyString': 'Please do not enter all blank characters', + 'validate.isDateTimeSame': + 'The current date value is invalid, please enter the same date value as $1', + 'validate.isDateTimeBefore': + 'The current date value is invalid, please enter a date value before $1', + 'validate.isDateTimeAfter': + 'The current date value is invalid, please enter a date value after $1', + 'validate.isDateTimeSameOrBefore': + 'The current date value is invalid, please enter a date value that is the same as or before $1', + 'validate.isDateTimeSameOrAfter': + 'The current date value is invalid, please enter a date value that is the same as or after $1', + 'validate.isDateTimeBetween': + 'The current date value is invalid, please enter a date value between $1 and $2', + 'validate.isTimeSame': + 'The current time value is invalid, please enter the same time value as $1', + 'validate.isTimeBefore': + 'The current time value is invalid, please enter a time value before $1', + 'validate.isTimeAfter': + 'The current time value is invalid, please enter a time value after $1', + 'validate.isTimeSameOrBefore': + 'The current time value is invalid, please enter a time value that is the same as or before $1', + 'validate.isTimeSameOrAfter': + 'The current time value is invalid, please enter a time value that is the same as or after $1', + 'validate.isTimeBetween': + 'The current time value is invalid, please enter a time value between $1 and $2', 'validateFailed': 'Validate failed', 'Wizard.configError': 'Config error', 'Wizard.finish': 'Finish', diff --git a/packages/amis-ui/src/locale/zh-CN.ts b/packages/amis-ui/src/locale/zh-CN.ts index f1527d1ab..2f5b432aa 100644 --- a/packages/amis-ui/src/locale/zh-CN.ts +++ b/packages/amis-ui/src/locale/zh-CN.ts @@ -273,6 +273,23 @@ register('zh-CN', { 'validate.minimum': '当前输入值低于最小值 $1', 'validate.minLength': '请输入更多的内容,至少输入 $1 个字符。', 'validate.notEmptyString': '请不要全输入空白字符', + 'validate.isDateTimeSame': '当前日期值不合法,请输入和 $1 相同的日期值', + 'validate.isDateTimeBefore': '当前日期值不合法,请输入 $1 之前的日期值', + 'validate.isDateTimeAfter': '当前日期值不合法,请输入 $1 之后的日期值', + 'validate.isDateTimeSameOrBefore': + '当前日期值不合法,请输入和 $1 相同或之前的日期值', + 'validate.isDateTimeSameOrAfter': + '当前日期值不合法,请输入和 $1 相同或之后的日期值', + 'validate.isDateTimeBetween': + '当前日期值不合法,请输入 $1 和 $2 之间的日期值', + 'validate.isTimeSame': '当前时间值不合法,请输入和 $1 相同的时间值', + 'validate.isTimeBefore': '当前时间值不合法,请输入 $1 之前的时间值', + 'validate.isTimeAfter': '当前时间值不合法,请输入 $1 之后的时间值', + 'validate.isTimeSameOrBefore': + '当前时间值不合法,请输入和 $1 相同或之前的时间值', + 'validate.isTimeSameOrAfter': + '当前时间值不合法,请输入和 $1 相同或之后的时间值', + 'validate.isTimeBetween': '当前时间值不合法,请输入 $1 和 $2 之间的时间值', 'validateFailed': '表单验证失败', 'Wizard.configError': '配置错误', 'Wizard.finish': '完成', diff --git a/packages/amis/__tests__/utils/validations.test.ts b/packages/amis/__tests__/utils/validations.test.ts index 734191635..167426919 100644 --- a/packages/amis/__tests__/utils/validations.test.ts +++ b/packages/amis/__tests__/utils/validations.test.ts @@ -907,3 +907,435 @@ test('validation:multiplestr2rules:noSlash', () => { matchRegexp2: ['123$'] }); }); + +/** ============================ 日期时间相关 ============================= */ +describe('validation:DateTime', () => { + describe('validation: isDateTimeSame', () => { + const targetDate = '2022-10-01 00:00:00'; + + test('validation: isDateTimeSame:valid', () => { + expect( + validate( + targetDate, + {}, + { + isDateTimeSame: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeSame:inValid', () => { + expect( + validate( + '2022-10-01 12:00:00', + {}, + { + isDateTimeSame: targetDate + } + ) + ).toMatchObject([ + {msg: 'validate.isDateTimeSame', rule: 'isDateTimeSame'} + ]); + }); + }); + + describe('validation: isDateTimeBefore', () => { + const targetDate = '2022-10-01 00:00:00'; + + test('validation: isDateTimeBefore:valid', () => { + expect( + validate( + '2022-08-25 10:00:00', + {}, + { + isDateTimeBefore: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeBefore:inValid', () => { + expect( + validate( + '2022-10-01 00:00:30', + {}, + { + isDateTimeBefore: targetDate + } + ) + ).toMatchObject([ + {msg: 'validate.isDateTimeBefore', rule: 'isDateTimeBefore'} + ]); + }); + }); + + describe('validation: isDateTimeAfter', () => { + const targetDate = '2022-10-01 00:00:00'; + + test('validation: isDateTimeAfter:valid', () => { + expect( + validate( + '2022-10-01 00:00:01', + {}, + { + isDateTimeAfter: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeAfter:inValid', () => { + expect( + validate( + '2022-09-30 23:59:59', + {}, + { + isDateTimeAfter: targetDate + } + ) + ).toMatchObject([ + {msg: 'validate.isDateTimeAfter', rule: 'isDateTimeAfter'} + ]); + }); + }); + + describe('validation: isDateTimeSameOrBefore', () => { + const targetDate = '2022-10-01 00:00:00'; + + test('validation: isDateTimeSameOrBefore:same valid', () => { + expect( + validate( + targetDate, + {}, + { + isDateTimeSameOrBefore: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeSameOrBefore:before valid', () => { + expect( + validate( + '2022-09-30 23:59:59', + {}, + { + isDateTimeSameOrBefore: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeSameOrBefore:inValid', () => { + expect( + validate( + '2022-10-01 00:00:10', + {}, + { + isDateTimeSameOrBefore: targetDate + } + ) + ).toMatchObject([ + {msg: 'validate.isDateTimeSameOrBefore', rule: 'isDateTimeSameOrBefore'} + ]); + }); + }); + + describe('validation: isDateTimeSameOrAfter', () => { + const targetDate = '2022-10-01 00:00:00'; + + test('validation: isDateTimeSameOrAfter:same valid', () => { + expect( + validate( + targetDate, + {}, + { + isDateTimeSameOrAfter: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeSameOrAfter:after valid', () => { + expect( + validate( + '2022-10-01 00:10:00', + {}, + { + isDateTimeSameOrAfter: targetDate + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeSameOrAfter:inValid', () => { + expect( + validate( + '2022-09-30 23:59:59', + {}, + { + isDateTimeSameOrAfter: targetDate + } + ) + ).toMatchObject([ + {msg: 'validate.isDateTimeSameOrAfter', rule: 'isDateTimeSameOrAfter'} + ]); + }); + }); + + describe('validation: isDateTimeBetween', () => { + const lhs = '2022-10-01 00:00:00'; + const rhs = '2022-10-03 00:00:00'; + + test('validation: isDateTimeBetween:default valid', () => { + expect( + validate( + '2022-10-01 00:00:01', + {}, + { + isDateTimeBetween: [lhs, rhs] + } + ) + ).toMatchObject([]); + }); + + test('validation: isDateTimeBetween:inclusivity with () endpoints of the interval', () => { + expect( + validate( + '2022-10-01 00:00:00', + {}, + { + isDateTimeBetween: [lhs, rhs, 'millisecond', '()'] + } + ) + ).toMatchObject([ + {msg: 'validate.isDateTimeBetween', rule: 'isDateTimeBetween'} + ]); + }); + + test('validation: isDateTimeBetween:inclusivity with [] endpoints of the interval', () => { + expect( + validate( + '2022-10-01 00:00:00', + {}, + { + isDateTimeBetween: [lhs, rhs, 'millisecond', '[]'] + } + ) + ).toMatchObject([]); + }); + }); +}); + +/** ============================ 时间相关 ============================= */ +describe('validation:Time', () => { + describe('validation: isTimeSame', () => { + const targetTime = '00:00:00'; + + test('validation: isTimeSame:valid', () => { + expect( + validate( + targetTime, + {}, + { + isTimeSame: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeSame:inValid', () => { + expect( + validate( + '12:00:00', + {}, + { + isTimeSame: targetTime + } + ) + ).toMatchObject([{msg: 'validate.isTimeSame', rule: 'isTimeSame'}]); + }); + }); + + describe('validation: isTimeBefore', () => { + const targetTime = '15:00:00'; + + test('validation: isTimeBefore:valid', () => { + expect( + validate( + '10:00:00', + {}, + { + isTimeBefore: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeBefore:inValid', () => { + expect( + validate( + '15:00:30', + {}, + { + isTimeBefore: targetTime + } + ) + ).toMatchObject([{msg: 'validate.isTimeBefore', rule: 'isTimeBefore'}]); + }); + }); + + describe('validation: isTimeAfter', () => { + const targetTime = '15:00:00'; + + test('validation: isTimeAfter:valid', () => { + expect( + validate( + '15:30:01', + {}, + { + isTimeAfter: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeAfter:inValid', () => { + expect( + validate( + '12:40:01', + {}, + { + isTimeAfter: targetTime + } + ) + ).toMatchObject([{msg: 'validate.isTimeAfter', rule: 'isTimeAfter'}]); + }); + }); + + describe('validation: isTimeSameOrBefore', () => { + const targetTime = '12:00:00'; + + test('validation: isTimeSameOrBefore:same valid', () => { + expect( + validate( + targetTime, + {}, + { + isTimeSameOrBefore: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeSameOrBefore:before valid', () => { + expect( + validate( + '11:59:59', + {}, + { + isTimeSameOrBefore: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeSameOrBefore:inValid', () => { + expect( + validate( + '12:00:01', + {}, + { + isTimeSameOrBefore: targetTime + } + ) + ).toMatchObject([ + {msg: 'validate.isTimeSameOrBefore', rule: 'isTimeSameOrBefore'} + ]); + }); + }); + + describe('validation: isTimeSameOrAfter', () => { + const targetTime = '12:00:00'; + + test('validation: isTimeSameOrAfter:same valid', () => { + expect( + validate( + targetTime, + {}, + { + isTimeSameOrAfter: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeSameOrAfter:after valid', () => { + expect( + validate( + '23:00:00', + {}, + { + isTimeSameOrAfter: targetTime + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeSameOrAfter:inValid', () => { + expect( + validate( + '08:00:00', + {}, + { + isTimeSameOrAfter: targetTime + } + ) + ).toMatchObject([ + {msg: 'validate.isTimeSameOrAfter', rule: 'isTimeSameOrAfter'} + ]); + }); + }); + + describe('validation: isTimeBetween', () => { + const lhs = '06:00:00'; + const rhs = '18:00:00'; + + test('validation: isTimeBetween:default valid', () => { + expect( + validate( + '12:00:00', + {}, + { + isTimeBetween: [lhs, rhs] + } + ) + ).toMatchObject([]); + }); + + test('validation: isTimeBetween:inclusivity with () endpoints of the interval', () => { + expect( + validate( + '06:00:00', + {}, + { + isTimeBetween: [lhs, rhs, 'millisecond', '()'] + } + ) + ).toMatchObject([{msg: 'validate.isTimeBetween', rule: 'isTimeBetween'}]); + }); + + test('validation: isTimeBetween:inclusivity with [] endpoints of the interval', () => { + expect( + validate( + '18:00:00', + {}, + { + isTimeBetween: [lhs, rhs, 'millisecond', '[]'] + } + ) + ).toMatchObject([]); + }); + }); +});