@hzab/list-render 1.10.0-beta → 1.10.0-beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/common/constant.ts +2 -0
- package/src/common/handleQuerySchema.ts +207 -13
- package/src/list-render.jsx +38 -11
- package/src/query-render/index.jsx +2 -1
package/package.json
CHANGED
package/src/common/constant.ts
CHANGED
|
@@ -1,8 +1,67 @@
|
|
|
1
1
|
import _ from "lodash";
|
|
2
2
|
import { getFieldMap } from "./utils";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isRunStr,
|
|
5
|
+
handleSchemaStrVal,
|
|
6
|
+
isScopeKey as formIsScopeKey,
|
|
7
|
+
getScopeKeyVal,
|
|
8
|
+
} from "@hzab/form-render/src/common/schema-handler";
|
|
4
9
|
import { URL_PARAM_NAME } from "./constant";
|
|
5
10
|
|
|
11
|
+
// ===================== 类型定义 =====================
|
|
12
|
+
|
|
13
|
+
type AntdDateComponent = "DatePicker" | "DatePicker.RangePicker";
|
|
14
|
+
|
|
15
|
+
interface XComponentProps {
|
|
16
|
+
isSplitTimes?: boolean;
|
|
17
|
+
startKey?: string;
|
|
18
|
+
endKey?: string;
|
|
19
|
+
[k: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FormilyField {
|
|
23
|
+
type?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
"x-decorator"?: string;
|
|
26
|
+
"x-component"?: string;
|
|
27
|
+
"x-validator"?: unknown[];
|
|
28
|
+
"x-component-props"?: XComponentProps;
|
|
29
|
+
"x-decorator-props"?: Record<string, unknown>;
|
|
30
|
+
"x-designable-id"?: string;
|
|
31
|
+
"x-index"?: number;
|
|
32
|
+
[k: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FormilyV1SchemaNode {
|
|
36
|
+
type?: string;
|
|
37
|
+
properties?: Record<string, FormilyField>;
|
|
38
|
+
[k: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FormilyV1Schema {
|
|
42
|
+
form?: Record<string, unknown>;
|
|
43
|
+
schema: FormilyV1SchemaNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type URLQueryObject = Record<string, any> | null | undefined;
|
|
47
|
+
|
|
48
|
+
interface ExtractOptions {
|
|
49
|
+
/** 分拆开关字段名,位于 x-component-props;默认 'isSplitTimes' */
|
|
50
|
+
splitFlag?: string;
|
|
51
|
+
/** 分拆开关的期望值;默认 true */
|
|
52
|
+
splitFlagExpectedValue?: unknown;
|
|
53
|
+
/** 默认开始/结束键名(当 schema 未显式配置时),默认 'startTime' / 'endTime' */
|
|
54
|
+
defaultStartKey?: string;
|
|
55
|
+
defaultEndKey?: string;
|
|
56
|
+
/** 认为是 antd 日期组件的名单,默认 DatePicker / DatePicker.RangePicker */
|
|
57
|
+
antdDateComponents?: readonly AntdDateComponent[];
|
|
58
|
+
/**
|
|
59
|
+
* 命中并成功赋值后,是否从返回对象中删除对应的 startKey / endKey
|
|
60
|
+
* 默认 true(删除)
|
|
61
|
+
*/
|
|
62
|
+
removeMatchedKeys?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
6
65
|
export const handleQuerySchema = (opt) => {
|
|
7
66
|
const { schema, search, filters, onSearch, replaceComList, schemaScope } = opt || {};
|
|
8
67
|
const queryProperties = {};
|
|
@@ -215,7 +274,6 @@ export const handleHoc = (source, cb, opt) => {
|
|
|
215
274
|
|
|
216
275
|
let res = null;
|
|
217
276
|
if (isRunStr(source)) {
|
|
218
|
-
|
|
219
277
|
_source = handleSchemaStrVal(source, schemaScope);
|
|
220
278
|
}
|
|
221
279
|
if (formIsScopeKey(source, schemaScope)) {
|
|
@@ -300,6 +358,22 @@ export const getValByScope = (strKey, schemaScope = {}) => {
|
|
|
300
358
|
}
|
|
301
359
|
};
|
|
302
360
|
|
|
361
|
+
function removeEmptyValues(obj) {
|
|
362
|
+
return _.pickBy(obj, (value) => {
|
|
363
|
+
// 排除 undefined 和 null
|
|
364
|
+
if (value == null) return false;
|
|
365
|
+
|
|
366
|
+
// 排除空字符串
|
|
367
|
+
if (_.isString(value) && value.trim() === "") return false;
|
|
368
|
+
|
|
369
|
+
// 排除空数组
|
|
370
|
+
if (_.isArray(value) && value.length === 0) return false;
|
|
371
|
+
|
|
372
|
+
// 保留其他值(包括 0、false、非空对象等)
|
|
373
|
+
return true;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
303
377
|
/**
|
|
304
378
|
* 将对象序列化后写入 URL query 中
|
|
305
379
|
* @param obj 要存储的对象(保持原始类型)
|
|
@@ -313,7 +387,10 @@ export function setURLObjectQueryParam<T extends object>(obj: T, paramName = URL
|
|
|
313
387
|
const [origin, hash = ""] = href.split("#");
|
|
314
388
|
const [path, queryString = ""] = hash.split("?");
|
|
315
389
|
const searchParams = new URLSearchParams(queryString);
|
|
316
|
-
|
|
390
|
+
// 指定的空值不设置
|
|
391
|
+
const copyObj = _.cloneDeep(removeEmptyValues(obj));
|
|
392
|
+
|
|
393
|
+
const encoded = encodeURIComponent(JSON.stringify(copyObj));
|
|
317
394
|
|
|
318
395
|
searchParams.set(paramName, encoded);
|
|
319
396
|
|
|
@@ -326,11 +403,11 @@ export function setURLObjectQueryParam<T extends object>(obj: T, paramName = URL
|
|
|
326
403
|
}
|
|
327
404
|
|
|
328
405
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* @param
|
|
406
|
+
* 从哈希路由的查询参数中,删除某个“对象型参数”下的指定属性(不刷新页面)
|
|
407
|
+
* @param parentKey 父级对象参数名
|
|
408
|
+
* @param childKey 要删除的对象属性名
|
|
332
409
|
*/
|
|
333
|
-
|
|
410
|
+
export function removeURLObjectQueryParam(parentKey: string, childKey: string, reset?: boolean): void {
|
|
334
411
|
if (typeof window === "undefined") return;
|
|
335
412
|
|
|
336
413
|
try {
|
|
@@ -339,21 +416,52 @@ export function setURLObjectQueryParam<T extends object>(obj: T, paramName = URL
|
|
|
339
416
|
const [hashPath, queryString = ""] = hash.split("?");
|
|
340
417
|
const searchParams = new URLSearchParams(queryString);
|
|
341
418
|
|
|
342
|
-
searchParams.
|
|
419
|
+
const raw = searchParams.get(parentKey);
|
|
420
|
+
if (raw == null) return;
|
|
421
|
+
|
|
422
|
+
// URLSearchParams.get 已自动做了百分号解码,但是可能会遇到编码两次的情况,因此这里再尝试解码一次
|
|
423
|
+
let obj: unknown;
|
|
424
|
+
try {
|
|
425
|
+
obj = JSON.parse(decodeURIComponent(raw));
|
|
426
|
+
} catch {
|
|
427
|
+
// 若不是合法 JSON,就不处理该参数
|
|
428
|
+
console.error(`[removeURLObjectQueryParam] 序列化URL参数错误`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
343
431
|
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
432
|
+
// 如果是重置,直接清空整个parentKey
|
|
433
|
+
if (reset && parentKey) searchParams.delete(parentKey);
|
|
434
|
+
else {
|
|
435
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
436
|
+
const record = obj as Record<string, unknown>;
|
|
437
|
+
// 删除空值key
|
|
438
|
+
if (childKey || record[childKey] === "" || record[childKey] === undefined || record[childKey] === null)
|
|
439
|
+
delete record[childKey];
|
|
440
|
+
|
|
441
|
+
// 如果对象被删空,则移除整个 parentKey;否则写回
|
|
442
|
+
if (Object.keys(record).length === 0) {
|
|
443
|
+
searchParams.delete(parentKey);
|
|
444
|
+
} else {
|
|
445
|
+
searchParams.set(parentKey, encodeURIComponent(JSON.stringify(record)));
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
// parentKey 的值不是对象,安全起见不处理
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
348
451
|
}
|
|
349
452
|
|
|
350
|
-
|
|
453
|
+
// 重建哈希部分
|
|
454
|
+
let newHash = hashPath;
|
|
455
|
+
const qs = searchParams.toString();
|
|
456
|
+
if (qs) newHash += `?${qs}`;
|
|
351
457
|
|
|
458
|
+
const newUrl = `${origin}${newHash ? `#${newHash}` : ""}`;
|
|
352
459
|
window.history.replaceState({}, "", newUrl);
|
|
353
460
|
} catch (e) {
|
|
354
461
|
console.error(`[removeURLObjectQueryParam] 删除失败:`, e);
|
|
355
462
|
}
|
|
356
463
|
}
|
|
464
|
+
|
|
357
465
|
/**
|
|
358
466
|
* 从 URL 中解析序列化的对象
|
|
359
467
|
* @param paramName query 参数名
|
|
@@ -379,4 +487,90 @@ export function getURLObjectQueryParam<T = any>(paramName = URL_PARAM_NAME): T |
|
|
|
379
487
|
}
|
|
380
488
|
}
|
|
381
489
|
|
|
490
|
+
// ===================== 辅助函数 =====================
|
|
491
|
+
|
|
492
|
+
const pickFormilyProperties = (schema: FormilyV1Schema): Record<string, FormilyField> =>
|
|
493
|
+
schema?.schema?.properties ?? {};
|
|
494
|
+
|
|
495
|
+
const isAntdDateComponent = (comp: unknown, whitelist: readonly string[]) =>
|
|
496
|
+
typeof comp === "string" && whitelist.includes(comp);
|
|
497
|
+
|
|
498
|
+
const filterKeys = (props: Record<string, FormilyField>, filters: readonly string[]) => {
|
|
499
|
+
const filterSet = new Set(filters);
|
|
500
|
+
return Object.entries(props).filter(([k]) => filterSet.has(k));
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const matchSplitFlag = (field: FormilyField, splitFlag: string, expected: unknown) =>
|
|
504
|
+
field?.["x-component-props"]?.[splitFlag] === expected;
|
|
505
|
+
|
|
506
|
+
/** 两个值都缺失则返回 undefined;否则返回二元组 */
|
|
507
|
+
const getRangeTupleOrUndefined = (urlObj: URLQueryObject, startKey: string, endKey: string): [any, any] | undefined => {
|
|
508
|
+
const obj = (urlObj ?? {}) as Record<string, any>;
|
|
509
|
+
const hasStart = Object.prototype.hasOwnProperty.call(obj, startKey);
|
|
510
|
+
const hasEnd = Object.prototype.hasOwnProperty.call(obj, endKey);
|
|
511
|
+
if (!hasStart && !hasEnd) return undefined;
|
|
512
|
+
return [obj[startKey], obj[endKey]];
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// ===================== 主函数 =====================
|
|
516
|
+
/**
|
|
517
|
+
* 根据formilySchema配置逆解析表单key并将url query赋值
|
|
518
|
+
* 三层筛选:根据filters + splitFlag + antdDateComponents 提取formilySchema配置项然后通过配置的key赋值并返回
|
|
519
|
+
* */
|
|
520
|
+
export function extractSplitDateRanges(
|
|
521
|
+
filters: string[],
|
|
522
|
+
formilySchema: FormilyV1Schema,
|
|
523
|
+
urlObj: URLQueryObject,
|
|
524
|
+
options: ExtractOptions = {},
|
|
525
|
+
): Record<string, any> {
|
|
526
|
+
const {
|
|
527
|
+
splitFlag = "isSplitTimes",
|
|
528
|
+
splitFlagExpectedValue = true,
|
|
529
|
+
defaultStartKey = "startTime",
|
|
530
|
+
defaultEndKey = "endTime",
|
|
531
|
+
antdDateComponents = ["DatePicker", "DatePicker.RangePicker"],
|
|
532
|
+
removeMatchedKeys = true,
|
|
533
|
+
} = options;
|
|
534
|
+
|
|
535
|
+
const properties = pickFormilyProperties(formilySchema);
|
|
536
|
+
const candidates = filterKeys(properties, filters);
|
|
537
|
+
|
|
538
|
+
// 保持不变性
|
|
539
|
+
const base = _.cloneDeep(urlObj) as Record<string, any>;
|
|
540
|
+
|
|
541
|
+
// 统一延迟删除,避免顺序影响
|
|
542
|
+
const keysToDelete = new Set<string>();
|
|
543
|
+
|
|
544
|
+
for (const [fieldKey, field] of candidates) {
|
|
545
|
+
// 未命中则直接跳过本次循环
|
|
546
|
+
if (!isAntdDateComponent(field?.["x-component"], antdDateComponents)) continue;
|
|
547
|
+
if (!matchSplitFlag(field, splitFlag, splitFlagExpectedValue)) continue;
|
|
548
|
+
|
|
549
|
+
const props = field?.["x-component-props"] ?? {};
|
|
550
|
+
const startKey = (props?.startKey as string) || defaultStartKey;
|
|
551
|
+
const endKey = (props?.endKey as string) || defaultEndKey;
|
|
552
|
+
|
|
553
|
+
const tuple = getRangeTupleOrUndefined(base, startKey, endKey);
|
|
554
|
+
if (!tuple) continue; // 两个值都没有则不赋值、不删除
|
|
555
|
+
|
|
556
|
+
// 命中,给 fieldKey 赋值
|
|
557
|
+
base[fieldKey] = tuple;
|
|
558
|
+
|
|
559
|
+
// 按需记录要删除的键(只删除“命中的”)
|
|
560
|
+
if (removeMatchedKeys) {
|
|
561
|
+
if (Object.prototype.hasOwnProperty.call(base, startKey)) keysToDelete.add(startKey);
|
|
562
|
+
if (Object.prototype.hasOwnProperty.call(base, endKey)) keysToDelete.add(endKey);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// 统一执行删除,保证无顺序副作用
|
|
567
|
+
if (removeMatchedKeys) {
|
|
568
|
+
for (const k of keysToDelete) {
|
|
569
|
+
delete base[k];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return base;
|
|
574
|
+
}
|
|
575
|
+
|
|
382
576
|
export default handleQuerySchema;
|
package/src/list-render.jsx
CHANGED
|
@@ -15,8 +15,13 @@ import FormModal from "./FormModal";
|
|
|
15
15
|
import DetailModal from "./DetailModal";
|
|
16
16
|
|
|
17
17
|
import { objToFormData } from "./common/utils";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
setURLObjectQueryParam,
|
|
20
|
+
getURLObjectQueryParam,
|
|
21
|
+
removeURLObjectQueryParam,
|
|
22
|
+
extractSplitDateRanges,
|
|
23
|
+
} from "./common/handleQuerySchema";
|
|
24
|
+
import { CLEAR_LIST, URL_PARAM_NAME } from "./common/constant";
|
|
20
25
|
|
|
21
26
|
import "./index.less";
|
|
22
27
|
|
|
@@ -54,7 +59,7 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
54
59
|
/**
|
|
55
60
|
* 自定义设置url query对象参数的key
|
|
56
61
|
* */
|
|
57
|
-
appendUrlQueryKey = URL_PARAM_NAME
|
|
62
|
+
appendUrlQueryKey = URL_PARAM_NAME,
|
|
58
63
|
} = props;
|
|
59
64
|
const pageSizeOptions = props.paginationConf?.pageSizeOptions || pageSizeOptionMap[props.layout] || [10, 20, 50, 100];
|
|
60
65
|
// const [pageSizeOptions, setPageSizeOptions] = useState(
|
|
@@ -89,7 +94,7 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
89
94
|
onDel,
|
|
90
95
|
}));
|
|
91
96
|
|
|
92
|
-
const { schema = {}, config = {}, model = {}, msgConf = {} } = props;
|
|
97
|
+
const { schema = {}, config = {}, model = {}, msgConf = {}, filters } = props;
|
|
93
98
|
|
|
94
99
|
// useEffect(() => {
|
|
95
100
|
// const list = props.paginationConf?.pageSizeOptions || pageSizeOptionMap[props.layout] || [10, 20, 50, 100];
|
|
@@ -117,9 +122,13 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
117
122
|
// 延迟获取表单实例
|
|
118
123
|
Promise.resolve().then(() => {
|
|
119
124
|
if (queryRef.current && queryRef.current?.formRef?.current.formRender) {
|
|
120
|
-
|
|
125
|
+
const extractValues = extractSplitDateRanges(filters, schema, getUrlQuery);
|
|
126
|
+
|
|
127
|
+
queryRef.current.formRef.current.formRender?.setValues(extractValues);
|
|
128
|
+
|
|
129
|
+
formQueryRef.current = _.cloneDeep(getUrlQuery);
|
|
121
130
|
}
|
|
122
|
-
})
|
|
131
|
+
});
|
|
123
132
|
|
|
124
133
|
!props.closeAutoRequest && getList({ ...(modelQueryRef.current || {}), ...getUrlQuery });
|
|
125
134
|
}, []);
|
|
@@ -177,7 +186,7 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
177
186
|
delete mergedQueries.$timerange;
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
model.query = mergedQueries;
|
|
189
|
+
// model.query = mergedQueries;
|
|
181
190
|
// 取消上一次请求
|
|
182
191
|
getListSourceRef.current?.cancel({ code: 601, message: "取消上一次请求" });
|
|
183
192
|
|
|
@@ -220,16 +229,15 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
220
229
|
|
|
221
230
|
if (appendUrlQuery) {
|
|
222
231
|
const getUrlQuery = getURLObjectQueryParam(appendUrlQueryKey);
|
|
223
|
-
setURLObjectQueryParam({ ...getUrlQuery, ...paginationQueryRef.current }, appendUrlQueryKey)
|
|
232
|
+
setURLObjectQueryParam({ ...getUrlQuery, ...paginationQueryRef.current }, appendUrlQueryKey);
|
|
224
233
|
}
|
|
225
234
|
getList();
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
function onSearch(quer, source, isReset = false) {
|
|
229
238
|
const query = source === "queryRender" ? { ...quer } : { ...formQueryRef.current, ...quer };
|
|
230
|
-
|
|
231
239
|
// 重置操作时不赋值
|
|
232
|
-
if (!isReset && appendUrlQuery) setURLObjectQueryParam(query, appendUrlQueryKey);
|
|
240
|
+
if (source === "queryRender" && !isReset && appendUrlQuery) setURLObjectQueryParam(query, appendUrlQueryKey);
|
|
233
241
|
|
|
234
242
|
if (model && !model.query) {
|
|
235
243
|
model.query = {};
|
|
@@ -246,7 +254,25 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
function handleFormReset() {
|
|
249
|
-
appendUrlQuery && removeURLObjectQueryParam(appendUrlQueryKey)
|
|
257
|
+
appendUrlQuery && removeURLObjectQueryParam(appendUrlQueryKey, "", true);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function handleFieldValueChange(filed, form) {
|
|
261
|
+
// props?.onFieldValueChange?.(filed, form);
|
|
262
|
+
|
|
263
|
+
if (!appendUrlQuery) return;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const { componentType, value, path } = filed;
|
|
267
|
+
if (CLEAR_LIST.includes(componentType)) {
|
|
268
|
+
if (value === "" || value?.length === 0 || value === undefined) {
|
|
269
|
+
const key = path.entire;
|
|
270
|
+
key && removeURLObjectQueryParam(appendUrlQueryKey, key);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error("url参数清除失败", e);
|
|
275
|
+
}
|
|
250
276
|
}
|
|
251
277
|
|
|
252
278
|
function forceUpdate() {
|
|
@@ -406,6 +432,7 @@ const ListRender = forwardRef(function (props, parentRef) {
|
|
|
406
432
|
onReset={handleFormReset}
|
|
407
433
|
schemaScope={props.schemaScope}
|
|
408
434
|
components={props.components}
|
|
435
|
+
onFieldValueChange={handleFieldValueChange}
|
|
409
436
|
i18n={i18n}
|
|
410
437
|
/>
|
|
411
438
|
) : (
|
|
@@ -7,6 +7,7 @@ import FormRender from "@hzab/form-render";
|
|
|
7
7
|
import { handleQuerySchema } from "../common/handleQuerySchema";
|
|
8
8
|
|
|
9
9
|
import "./index.less";
|
|
10
|
+
import { onFieldValueChange } from "@formily/core";
|
|
10
11
|
|
|
11
12
|
function QueryRender(props, parentRef) {
|
|
12
13
|
const [schema, setSchema] = useState({});
|
|
@@ -58,7 +59,6 @@ function QueryRender(props, parentRef) {
|
|
|
58
59
|
props.onSearch && props.onSearch(_.cloneDeep(formRef?.current?.formRender?.values), "queryRender", true);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
62
|
return (
|
|
63
63
|
<div className="query-render">
|
|
64
64
|
{Object.keys(schema?.schema?.properties || {}).length > 0 ? (
|
|
@@ -69,6 +69,7 @@ function QueryRender(props, parentRef) {
|
|
|
69
69
|
schema={schema}
|
|
70
70
|
schemaScope={{ scenario: "query", ...(props.schemaScope || {}) }}
|
|
71
71
|
initialValues={props.queryFormInitialValues}
|
|
72
|
+
onFieldValueChange={props.onFieldValueChange}
|
|
72
73
|
Slots={() => {
|
|
73
74
|
return (
|
|
74
75
|
<div className="query-operation">
|