feat: CRUD & CRUD2 parsePrimitiveQuery配置支持转化数字类型

This commit is contained in:
lurunze1226 2023-11-30 17:02:20 +08:00
parent 2c05f9c7fb
commit 6be9dcfda8
5 changed files with 143 additions and 27 deletions

View File

@ -204,14 +204,28 @@ CRUD 组件对数据源接口的数据结构要求如下:
> `3.5.0`及以上版本 > `3.5.0`及以上版本
`syncLocation`开启后CRUD 在初始化数据域时,将会对 url 中的 Query 进行转换,将原始类型的字符串格式的转化为同位类型,目前仅支持**布尔类型** `syncLocation`开启后CRUD 在初始化数据域时,将会对 url 中的 Query 进行转换,将原始类型的字符串格式的转化为同位类型。`3.6.0`版本后支持对象格式,该配置默认开启,且默认仅转化布尔值。
``` #### ParsePrimitiveQueryOptions
"true" ==> true ```typescript
"false" ==> false interface ParsePrimitiveQueryOptions {
parsePrimitiveQuery: {
enable: boolean;
types?: ('boolean' | 'number')[]
}
}
``` ```
如果只想保持字符串格式,可以设置`"parsePrimitiveQuery": false`关闭该特性,具体效果参考[示例](../../../examples/crud/parse-primitive-query)。 比如开启设置 `{"parsePrimitiveQuery": {"enable": true, "types": ["boolean", "number"]}}` 后:
```
"true" ==> true
"false" ==> false
"123" ==> 123
"123.4" ==> 123.4
```
如果只想保持字符串格式,可以设置`"parsePrimitiveQuery": false` 或者 `"parsePrimitiveQuery": {"enable": false}` 关闭该特性,具体效果参考[示例](../../../examples/crud/parse-primitive-query)。如果想实现字段定制化转化类型,可以使用[配置API请求数据](../../docs/types/api#配置请求数据),通过表达式控制接口传递的参数类型。
## 功能 ## 功能
@ -3381,6 +3395,8 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据,
| autoFillHeight | `boolean``{height: number}` | | 内容区域自适应高度 | | autoFillHeight | `boolean``{height: number}` | | 内容区域自适应高度 |
| canAccessSuperData | `boolean` | `true` | 指定是否可以自动获取上层的数据并映射到表格行数据上,如果列也配置了该属性,则列的优先级更高 | | canAccessSuperData | `boolean` | `true` | 指定是否可以自动获取上层的数据并映射到表格行数据上,如果列也配置了该属性,则列的优先级更高 |
| matchFunc | `string` | [`CRUDMatchFunc`](#匹配函数) | 自定义匹配函数, 当开启`loadDataOnce`时,会基于该函数计算的匹配结果进行过滤,主要用于处理列字段类型较为复杂或者字段值格式和后端返回不一致的场景 | `3.5.0` | | matchFunc | `string` | [`CRUDMatchFunc`](#匹配函数) | 自定义匹配函数, 当开启`loadDataOnce`时,会基于该函数计算的匹配结果进行过滤,主要用于处理列字段类型较为复杂或者字段值格式和后端返回不一致的场景 | `3.5.0` |
| parsePrimitiveQuery | [`ParsePrimitiveQueryOptions`](#ParsePrimitiveQueryOptions) | `true` | 是否开启Query信息转换开启后将会对url中的Query进行转换默认开启默认仅转化布尔值 | `3.6.0` |
注意除了上面这些属性CRUD 在不同模式下的属性需要参考各自的文档,比如 注意除了上面这些属性CRUD 在不同模式下的属性需要参考各自的文档,比如

View File

@ -5,7 +5,7 @@ export default {
"type": "alert", "type": "alert",
"body": { "body": {
"type": "html", "type": "html",
"html": "<code>parsePrimitiveQuery</code>默认开启开启后会对url中的Query进行转换将原始类型的字符串格式的转化为同位类型,目前仅支持<strong>布尔类型</strong>" "html": "<code>parsePrimitiveQuery</code>默认开启开启后会对url中的Query进行转换将原始类型的字符串格式的转化为同位类型"
}, },
"level": "info", "level": "info",
"showCloseButton": true, "showCloseButton": true,
@ -17,6 +17,10 @@ export default {
"name": "crud", "name": "crud",
"syncLocation": true, "syncLocation": true,
"api": "/api/mock2/crud/table5", "api": "/api/mock2/crud/table5",
"parsePrimitiveQuery": {
"enable": true,
"types": ["boolean", "number"]
},
"filter": { "filter": {
"debug": true, "debug": true,
"title": "条件搜索", "title": "条件搜索",
@ -30,6 +34,16 @@ export default {
"label": "已核验", "label": "已核验",
"size": "sm", "size": "sm",
"value": false "value": false
},
{
"type": "select",
"name": "version",
"label": "版本",
"options": [
{"label": "5.5", value: 5.5},
{"label": "6", value: 6},
{"label": "7", value: 7}
]
} }
] ]
} }
@ -67,6 +81,10 @@ export default {
] ]
} }
}, },
{
"name": "version",
"label": "版本"
}
] ]
} }
] ]

View File

@ -2050,21 +2050,52 @@ export function isNumeric(value: any): boolean {
return /^[-+]?(?:\d*[.])?\d+$/.test(value); return /^[-+]?(?:\d*[.])?\d+$/.test(value);
} }
export type PrimitiveTypes = 'boolean' | 'number';
/** /**
* Query字符串中的原始类型 * Query字符串中的原始类型
* *
* @param query * @param query
* @param options
* @returns * @returns
*/ */
export function parsePrimitiveQueryString(rawQuery: Record<string, any>) { export function parsePrimitiveQueryString(
rawQuery: Record<string, any>,
options?: {
primitiveTypes: PrimitiveTypes[];
}
) {
if (!isPlainObject(rawQuery)) { if (!isPlainObject(rawQuery)) {
return rawQuery; return rawQuery;
} }
options = options || {primitiveTypes: ['boolean']};
if (
!Array.isArray(options.primitiveTypes) ||
options.primitiveTypes.length === 0
) {
options.primitiveTypes = ['boolean'];
}
const query = JSONValueMap(rawQuery, value => { const query = JSONValueMap(rawQuery, value => {
/** 解析布尔类型,后续有需要在这里扩充 */ if (
if (value === 'true' || value === 'false') { (options?.primitiveTypes?.includes('boolean') && value === 'true') ||
value === 'false'
) {
/** 解析布尔类型 */
return value === 'true'; return value === 'true';
} else if (
options?.primitiveTypes?.includes('number') &&
isNumeric(value) &&
isFinite(value) &&
value >= -Number.MAX_SAFE_INTEGER &&
value <= Number.MAX_SAFE_INTEGER
) {
/** 解析数字类型 */
const result = Number(value);
return !isNaN(result) ? result : value;
} }
return value; return value;
@ -2082,16 +2113,19 @@ export function parsePrimitiveQueryString(rawQuery: Record<string, any>) {
*/ */
export function parseQuery( export function parseQuery(
location?: Location | {query?: any; search?: any; [propName: string]: any}, location?: Location | {query?: any; search?: any; [propName: string]: any},
options?: {parsePrimitive?: boolean} options?: {
parsePrimitive?: boolean;
primitiveTypes?: PrimitiveTypes[];
}
): Record<string, any> { ): Record<string, any> {
const {parsePrimitive = false} = options || {}; const {parsePrimitive = false, primitiveTypes = ['boolean']} = options || {};
const query = const query =
(location && !(location instanceof Location) && location?.query) || (location && !(location instanceof Location) && location?.query) ||
(location && location?.search && qsparse(location.search.substring(1))) || (location && location?.search && qsparse(location.search.substring(1))) ||
(window.location.search && qsparse(window.location.search.substring(1))); (window.location.search && qsparse(window.location.search.substring(1)));
const normalizedQuery = isPlainObject(query) const normalizedQuery = isPlainObject(query)
? parsePrimitive ? parsePrimitive
? parsePrimitiveQueryString(query) ? parsePrimitiveQueryString(query, {primitiveTypes})
: query : query
: {}; : {};
/* 处理hash中的query */ /* 处理hash中的query */

View File

@ -26,7 +26,7 @@ import {
import {ScopedContext, IScopedContext} from 'amis-core'; import {ScopedContext, IScopedContext} from 'amis-core';
import {Button, SpinnerExtraProps, TooltipWrapper} from 'amis-ui'; import {Button, SpinnerExtraProps, TooltipWrapper} from 'amis-ui';
import {Select} from 'amis-ui'; import {Select} from 'amis-ui';
import {getExprProperties} from 'amis-core'; import {getExprProperties, isObject} from 'amis-core';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import {findDOMNode} from 'react-dom'; import {findDOMNode} from 'react-dom';
import {evalExpression, filter} from 'amis-core'; import {evalExpression, filter} from 'amis-core';
@ -376,9 +376,14 @@ export interface CRUDCommonSchema extends BaseSchema, SpinnerExtraProps {
autoFillHeight?: TableSchema['autoFillHeight']; autoFillHeight?: TableSchema['autoFillHeight'];
/** /**
* Query信息转换url中的Query进行转换 * Query信息转换url中的Query进行转换
*/ */
parsePrimitiveQuery?: boolean; parsePrimitiveQuery?:
| {
enable: boolean;
types?: ('boolean' | 'number')[];
}
| boolean;
} }
export type CRUDCardsSchema = CRUDCommonSchema & { export type CRUDCardsSchema = CRUDCommonSchema & {
@ -545,22 +550,22 @@ export default class CRUD extends React.Component<CRUDProps, any> {
pageField, pageField,
perPageField, perPageField,
syncLocation, syncLocation,
loadDataOnce, loadDataOnce
parsePrimitiveQuery
} = props; } = props;
const parseQueryOptions = this.getParseQueryOptions(props);
this.mounted = true; this.mounted = true;
if (syncLocation && location && (location.query || location.search)) { if (syncLocation && location && (location.query || location.search)) {
store.updateQuery( store.updateQuery(
parseQuery(location, {parsePrimitive: parsePrimitiveQuery}), parseQuery(location, parseQueryOptions),
undefined, undefined,
pageField, pageField,
perPageField perPageField
); );
} else if (syncLocation && !location && window.location.search) { } else if (syncLocation && !location && window.location.search) {
store.updateQuery( store.updateQuery(
parseQuery(window.location, {parsePrimitive: parsePrimitiveQuery}), parseQuery(window.location, parseQueryOptions),
undefined, undefined,
pageField, pageField,
perPageField perPageField
@ -654,7 +659,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
) { ) {
// 同步地址栏,那么直接检测 query 是否变了,变了就重新拉数据 // 同步地址栏,那么直接检测 query 是否变了,变了就重新拉数据
store.updateQuery( store.updateQuery(
parseQuery(props.location, {parsePrimitive: props.parsePrimitiveQuery}), parseQuery(props.location, this.getParseQueryOptions(props)),
undefined, undefined,
props.pageField, props.pageField,
props.perPageField props.perPageField
@ -705,6 +710,22 @@ export default class CRUD extends React.Component<CRUDProps, any> {
this.filterOnEvent.cache.clear?.(); this.filterOnEvent.cache.clear?.();
} }
getParseQueryOptions(props: CRUDProps) {
const {parsePrimitiveQuery} = props;
type PrimitiveQueryObj = Exclude<CRUDProps['parsePrimitiveQuery'], boolean>;
const normalizedOptions = {
parsePrimitive: !!(isObject(parsePrimitiveQuery)
? (parsePrimitiveQuery as PrimitiveQueryObj)?.enable
: parsePrimitiveQuery),
primitiveTypes: (parsePrimitiveQuery as PrimitiveQueryObj)?.types ?? [
'boolean'
]
};
return normalizedOptions;
}
/** 查找CRUD最近层级的父窗口 */ /** 查找CRUD最近层级的父窗口 */
getClosestParentContainer() { getClosestParentContainer() {
const dom = findDOMNode(this) as HTMLElement; const dom = findDOMNode(this) as HTMLElement;
@ -996,6 +1017,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
loadDataOnceFetchOnFilter, loadDataOnceFetchOnFilter,
parsePrimitiveQuery parsePrimitiveQuery
} = this.props; } = this.props;
const parseQueryOptions = this.getParseQueryOptions(this.props);
/** 找出clearValueOnHidden的字段, 保证updateQuery时不会使用上次的保留值 */ /** 找出clearValueOnHidden的字段, 保证updateQuery时不会使用上次的保留值 */
values = { values = {
@ -1008,7 +1030,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
/** 把布尔值反解出来 */ /** 把布尔值反解出来 */
if (parsePrimitiveQuery) { if (parsePrimitiveQuery) {
values = parsePrimitiveQueryString(values); values = parsePrimitiveQueryString(values, parseQueryOptions);
} }
store.updateQuery( store.updateQuery(

View File

@ -196,9 +196,14 @@ export interface CRUD2CommonSchema extends BaseSchema, SpinnerExtraProps {
primaryField?: string; primaryField?: string;
/** /**
* Query信息转换url中的Query进行转换 * Query信息转换url中的Query进行转换
*/ */
parsePrimitiveQuery?: boolean; parsePrimitiveQuery?:
| {
enable: boolean;
types?: ('boolean' | 'number')[];
}
| boolean;
} }
export type CRUD2CardsSchema = CRUD2CommonSchema & { export type CRUD2CardsSchema = CRUD2CommonSchema & {
@ -305,19 +310,20 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
perPageField, perPageField,
parsePrimitiveQuery parsePrimitiveQuery
} = props; } = props;
const parseQueryOptions = this.getParseQueryOptions(props);
this.mounted = true; this.mounted = true;
if (syncLocation && location && (location.query || location.search)) { if (syncLocation && location && (location.query || location.search)) {
store.updateQuery( store.updateQuery(
parseQuery(location, {parsePrimitive: parsePrimitiveQuery}), parseQuery(location, parseQueryOptions),
undefined, undefined,
pageField, pageField,
perPageField perPageField
); );
} else if (syncLocation && !location && window.location.search) { } else if (syncLocation && !location && window.location.search) {
store.updateQuery( store.updateQuery(
parseQuery(window.location, {parsePrimitive: parsePrimitiveQuery}), parseQuery(window.location, parseQueryOptions),
undefined, undefined,
pageField, pageField,
perPageField perPageField
@ -389,7 +395,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
) { ) {
// 同步地址栏,那么直接检测 query 是否变了,变了就重新拉数据 // 同步地址栏,那么直接检测 query 是否变了,变了就重新拉数据
store.updateQuery( store.updateQuery(
parseQuery(props.location, {parsePrimitive: parsePrimitiveQuery}), parseQuery(props.location, this.getParseQueryOptions(props)),
undefined, undefined,
props.pageField, props.pageField,
props.perPageField props.perPageField
@ -439,6 +445,25 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
clearTimeout(this.timer); clearTimeout(this.timer);
} }
getParseQueryOptions(props: CRUD2Props) {
const {parsePrimitiveQuery} = props;
type PrimitiveQueryObj = Exclude<
CRUD2Props['parsePrimitiveQuery'],
boolean
>;
const normalizedOptions = {
parsePrimitive: !!(isObject(parsePrimitiveQuery)
? (parsePrimitiveQuery as PrimitiveQueryObj)?.enable
: parsePrimitiveQuery),
primitiveTypes: (parsePrimitiveQuery as PrimitiveQueryObj)?.types ?? [
'boolean'
]
};
return normalizedOptions;
}
@autobind @autobind
controlRef(control: any) { controlRef(control: any) {
// 因为 control 有可能被 n 层 hoc 包裹。 // 因为 control 有可能被 n 层 hoc 包裹。
@ -502,6 +527,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
perPageField, perPageField,
parsePrimitiveQuery parsePrimitiveQuery
} = this.props; } = this.props;
const parseQueryOptions = this.getParseQueryOptions(this.props);
let {query, resetQuery, replaceQuery, loadMore, resetPage} = data || {}; let {query, resetQuery, replaceQuery, loadMore, resetPage} = data || {};
query = query =
@ -511,7 +537,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
/** 把布尔值反解出来 */ /** 把布尔值反解出来 */
if (parsePrimitiveQuery) { if (parsePrimitiveQuery) {
query = parsePrimitiveQueryString(query); query = parsePrimitiveQueryString(query, parseQueryOptions);
} }
store.updateQuery( store.updateQuery(