@zcrkey/js-utils 0.0.4 → 0.0.5

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/src/util.ts ADDED
@@ -0,0 +1,656 @@
1
+ import { cloneDeep as _cloneDeep } from 'lodash';
2
+ import Qs from 'qs';
3
+
4
+ export default class CrUtil {
5
+ /**
6
+ * 判断是否为数组
7
+ * @param value
8
+ * @returns boolean
9
+ */
10
+ static isArray<T = any>(value: any): value is T[] {
11
+ return Array.isArray(value);
12
+ }
13
+
14
+ /**
15
+ * 判断是否为对象
16
+ * @param value
17
+ * @returns boolean
18
+ */
19
+ static isObject<T = any>(
20
+ value: T,
21
+ ): value is T extends object ? (T extends any[] ? never : T) : never {
22
+ return (
23
+ typeof value === 'object' && value !== null && !CrUtil.isArray(value)
24
+ );
25
+ }
26
+
27
+ /**
28
+ * 判断是否为空对象
29
+ * @param value
30
+ * @returns boolean
31
+ */
32
+ static isEmptyObject(value: object): value is Record<string | number, never> {
33
+ return Object.keys(value).length === 0 && value.constructor === Object;
34
+ }
35
+
36
+ /**
37
+ * 判断是否对象的属性是否全部为空
38
+ * @param value
39
+ * @returns boolean
40
+ */
41
+ static isObjectPropertiesAllEmpty(
42
+ value: Record<string | number, any>,
43
+ ): boolean {
44
+ return Object.keys(value).every((key) => {
45
+ const _value = value[key];
46
+ return (
47
+ _value === undefined ||
48
+ _value === null ||
49
+ _value === '' ||
50
+ (_value === 0 && typeof _value === 'number') ||
51
+ isNaN(_value)
52
+ );
53
+ });
54
+ }
55
+
56
+ /**
57
+ * 判断是否为日期
58
+ * @param value
59
+ * @returns boolean
60
+ */
61
+ static isDate(value: unknown) {
62
+ return Object.prototype.toString.call(value) === '[object Date]';
63
+ }
64
+
65
+ /**
66
+ * 判断是否为字符串
67
+ * @param value
68
+ * @returns boolean
69
+ */
70
+ static isString(value: unknown): value is string {
71
+ return Object.prototype.toString.call(value) === '[object String]';
72
+ }
73
+
74
+ /**
75
+ * 判断是否为数字
76
+ * @param value
77
+ * @returns boolean
78
+ */
79
+ static isNumber(value: unknown): value is number {
80
+ return typeof value === 'number' && !isNaN(value);
81
+ }
82
+
83
+ /**
84
+ * 判断是否为文件 File
85
+ * @param value
86
+ * @returns boolean
87
+ */
88
+ static isFile(value: unknown): value is File {
89
+ return Object.prototype.toString.call(value) === '[object File]';
90
+ }
91
+
92
+ /**
93
+ * 判断是否为 Boolean
94
+ * @param value
95
+ * @returns boolean
96
+ */
97
+ static isBoolean(value: unknown): value is boolean {
98
+ return Object.prototype.toString.call(value) === '[object Boolean]';
99
+ }
100
+
101
+ /**
102
+ * 判断是否为 Function
103
+ * @param value
104
+ * @returns boolean
105
+ */
106
+ static isFunction(value: unknown): value is (...args: any[]) => any {
107
+ return typeof value === 'function';
108
+ }
109
+
110
+ /**
111
+ * 去掉字符串前后所有空格
112
+ * @param str
113
+ * @returns string
114
+ */
115
+ static trim(str: string) {
116
+ return (str + '').replace(/(^[\s\n\t]+|[\s\n\t]+$)/g, '');
117
+ }
118
+
119
+ /**
120
+ * 获取数组为几维数组
121
+ * @param arr
122
+ * @returns number
123
+ */
124
+ static getArrayDimension(arr: any[]): number {
125
+ // 如果不是一个数组,返回 0
126
+ if (!CrUtil.isArray(arr)) return 0;
127
+
128
+ // 找到数组中的最大维度
129
+ let maxDimension = 0;
130
+ for (let i = 0; i < arr.length; i++) {
131
+ const dimension = CrUtil.getArrayDimension(arr[i]);
132
+ maxDimension = Math.max(maxDimension, dimension);
133
+ }
134
+ // 返回最大维度加1,代表当前层数
135
+ return maxDimension + 1;
136
+ }
137
+
138
+ /**
139
+ * 深拷贝
140
+ * @param value
141
+ * @returns
142
+ */
143
+ static cloneDeep<T>(value: T): T {
144
+ return _cloneDeep(value);
145
+ }
146
+
147
+ /**
148
+ * 深拷贝
149
+ * @param target
150
+ * @deprecated 即将移除,使用 {@link CrUtil.cloneDeep} 替代
151
+ * @returns
152
+ */
153
+ static deepCopy<T = any>(target: T): T {
154
+ let _target: any;
155
+ if (CrUtil.isArray(target)) {
156
+ _target = [];
157
+ for (let i = 0, len = target.length; i < len; i++) {
158
+ if (CrUtil.isArray(target[i]) || CrUtil.isObject(target[i])) {
159
+ _target.push(CrUtil.deepCopy(target[i]));
160
+ } else {
161
+ _target.push(target[i]);
162
+ }
163
+ }
164
+ } else if (CrUtil.isObject(target)) {
165
+ _target = {};
166
+ for (let i in target) {
167
+ if (CrUtil.isArray(target[i]) || CrUtil.isObject(target[i])) {
168
+ _target[i] = CrUtil.deepCopy(target[i]);
169
+ } else {
170
+ _target[i] = target[i];
171
+ }
172
+ }
173
+ } else {
174
+ _target = target;
175
+ }
176
+ return _target;
177
+ }
178
+
179
+ /**
180
+ * 列表数据转树型数据
181
+ * @param listData
182
+ * @param settings
183
+ * @param settings.idField 默认值 id
184
+ * @param settings.pidField 默认值 parentId
185
+ * @param settings.childrenField 默认值 children
186
+ * @deprecated 即将移除,使用 {@link CrObjUtil.listToTree} 替代
187
+ * @returns
188
+ */
189
+ static listToTreeData(
190
+ listData: any[],
191
+ settings?: {
192
+ idField?: string;
193
+ pidField?: string;
194
+ childrenField?: string;
195
+ isDeepCopy?: boolean;
196
+ getData?: (item: any) => any;
197
+ },
198
+ ) {
199
+ const options = Object.assign(
200
+ {
201
+ idField: 'id',
202
+ pidField: 'parentId',
203
+ childrenField: 'children',
204
+ isDeepCopy: false,
205
+ getData: (item: any) => {
206
+ return item;
207
+ },
208
+ },
209
+ settings,
210
+ );
211
+
212
+ const { idField, pidField, childrenField, isDeepCopy, getData } = options;
213
+
214
+ if (isDeepCopy) {
215
+ listData = CrUtil.deepCopy(listData);
216
+ }
217
+
218
+ // 构建索引
219
+ const pidMap = new Map<any, any[]>();
220
+ const idMap = new Map<any, any>();
221
+ for (const item of listData) {
222
+ const pid = item[pidField];
223
+ const id = item[idField];
224
+ if (!pidMap.has(pid)) {
225
+ pidMap.set(pid, []);
226
+ }
227
+ pidMap.get(pid)!.push(item);
228
+ idMap.set(id, item);
229
+ }
230
+
231
+ // 递归构建树 + 字段过滤
232
+ const buildTree = (pid: any): any[] => {
233
+ const children = pidMap.get(pid) || [];
234
+ return children.map((item) => {
235
+ const rawChildren = buildTree(item[idField]);
236
+ const node = {
237
+ ...getData(item),
238
+ };
239
+ if (rawChildren.length > 0) {
240
+ node[childrenField] = rawChildren;
241
+ }
242
+ return node;
243
+ });
244
+ };
245
+
246
+ // 收集所有根节点 pid,去重
247
+ const rootPids = new Set<any>();
248
+ for (const pid of pidMap.keys()) {
249
+ if (pid === undefined || pid === null || pid === 0 || !idMap.has(pid)) {
250
+ rootPids.add(pid);
251
+ }
252
+ }
253
+
254
+ // 构建最终树
255
+ const tree: any[] = [];
256
+ for (const pid of rootPids) {
257
+ tree.push(...buildTree(pid));
258
+ }
259
+
260
+ // 只构建根节点(pid 为 null / 0 / undefined / 不存在)
261
+ // const tree = buildTree(undefined)
262
+ // .concat(buildTree(null))
263
+ // .concat(buildTree(0))
264
+ // .concat(
265
+ // Array.from(pidMap.keys())
266
+ // .filter((pid) => !idMap.has(pid))
267
+ // .flatMap((pid) => buildTree(pid)),
268
+ // );
269
+
270
+ return tree;
271
+ }
272
+
273
+ /**
274
+ * 树型数据转列表数据
275
+ * @param treeData
276
+ * @param settings
277
+ * @param settings.idField 默认值 id
278
+ * @param settings.pidField 默认值 parentId
279
+ * @param settings.childrenField 默认值 children
280
+ * @deprecated 即将移除,使用 {@link CrObjUtil.treeToList} 替代
281
+ * @returns
282
+ */
283
+ static treeDataToListData(
284
+ treeData: any[],
285
+ pid: string | number = 0,
286
+ settings?: {
287
+ childrenField?: string;
288
+ idField?: string;
289
+ pidField?: string;
290
+ isDeepCopy?: boolean;
291
+ },
292
+ ) {
293
+ let options = Object.assign(
294
+ {
295
+ idField: 'id',
296
+ pidField: 'parentId',
297
+ childrenField: 'children',
298
+ isDeepCopy: false,
299
+ },
300
+ settings,
301
+ );
302
+ let listData: any[] = [];
303
+ for (let i = 0; i < treeData.length; i++) {
304
+ const node = treeData[i];
305
+ const item: any = { ...node };
306
+ item[options.pidField] = pid;
307
+ delete item[options.childrenField];
308
+ listData.push(item);
309
+ if (node[options.childrenField]) {
310
+ const childrenList = this.treeDataToListData(
311
+ node[options.childrenField],
312
+ item[options.idField],
313
+ settings,
314
+ );
315
+ listData.push(...childrenList);
316
+ }
317
+ }
318
+ if (options.isDeepCopy) {
319
+ listData = CrUtil.deepCopy(listData);
320
+ }
321
+ return listData;
322
+ }
323
+
324
+ /**
325
+ * 获取所有父级数据(包含自身)
326
+ * @param listData
327
+ * @param value
328
+ * @param settings
329
+ * @param settings.valueField 默认值 value
330
+ * @param settings.idField 默认值 id
331
+ * @param settings.pidField 默认值 parentId
332
+ * @deprecated 即将移除,使用 {@link CrObjUtil.getFlatParentDatas} 替代
333
+ * @returns
334
+ */
335
+ static getParentNodes<T, R extends Record<string, any>>(
336
+ listData: R[],
337
+ value: number | string,
338
+ settings?: {
339
+ valueField?: string;
340
+ idField?: string;
341
+ pidField?: string;
342
+ getData?: (item: R) => T;
343
+ },
344
+ ): T[] {
345
+ const options = Object.assign(
346
+ {
347
+ valueField: 'value',
348
+ idField: 'id',
349
+ pidField: 'parentId',
350
+ getData: (item: R): T => {
351
+ return item as unknown as T;
352
+ },
353
+ },
354
+ settings,
355
+ );
356
+
357
+ if (!(listData && listData.length > 0) || !value) {
358
+ return [];
359
+ }
360
+ const idField = options.idField;
361
+ const pidField = options.pidField;
362
+ const valueField = options.valueField;
363
+ const nodes: T[] = [];
364
+ let node: R | undefined = listData.find(
365
+ (item: R) => item[valueField] == value,
366
+ );
367
+ if (node) {
368
+ nodes.push(options.getData(node));
369
+ while (node && node[pidField]) {
370
+ const parentOption: R | undefined = listData.find(
371
+ // eslint-disable-next-line @typescript-eslint/no-loop-func
372
+ (item: R) => item[idField] == (node as R)[pidField],
373
+ );
374
+ if (parentOption) {
375
+ nodes.push(options.getData(parentOption));
376
+ }
377
+ node = parentOption;
378
+ }
379
+ }
380
+ nodes.reverse();
381
+ return nodes;
382
+ }
383
+
384
+ /**
385
+ * 参数序列化
386
+ * @param params {a:'1',b:{},c:[]}
387
+ * @returns
388
+ */
389
+ static paramsSerializer(
390
+ params: Record<string, any>,
391
+ settings?: { isFilterNonNull: boolean },
392
+ ) {
393
+ const options = Object.assign(
394
+ {
395
+ isFilterNonNull: false,
396
+ },
397
+ settings,
398
+ );
399
+ if (options.isFilterNonNull) {
400
+ Object.keys(params).forEach((key) => {
401
+ if (
402
+ params[key] === null ||
403
+ params[key] === undefined ||
404
+ params[key] === ''
405
+ ) {
406
+ delete params[key];
407
+ }
408
+ });
409
+ }
410
+ return Qs.stringify(params);
411
+ }
412
+
413
+ /**
414
+ * 参数解析
415
+ * @param {*} str
416
+ * @param {*} settings
417
+ * @returns
418
+ */
419
+ static paramsParse(str: string, settings?: { ignoreQueryPrefix: boolean }) {
420
+ let obj = {};
421
+ let option = Object.assign(
422
+ {
423
+ ignoreQueryPrefix: true,
424
+ },
425
+ settings,
426
+ );
427
+ if (str) {
428
+ obj = Qs.parse(str, option);
429
+ }
430
+ return obj;
431
+ }
432
+
433
+ /**
434
+ * 获取 URL 参数
435
+ * @param {*} url
436
+ * @returns
437
+ * @example
438
+ * "https://example.com?foo=bar&baz=qux" => {"query":{"foo":"bar","baz":"qux"},"hash":{},"all":{"foo":"bar","baz":"qux"}}
439
+ * "https://example.com/page?foo=1#/?a=1&b=2" => {"query":{"foo":"1"},"hash":{"/":"","a":"1","b":"2"},"all":{"foo":"1","/":"","a":"1","b":"2"}},
440
+ */
441
+ static getQueryParams(url: string): {
442
+ query: Record<string, any>;
443
+ hash: Record<string, any>;
444
+ all: Record<string, any>;
445
+ } {
446
+ const query: Record<string, any> = {};
447
+ const hash: Record<string, any> = {};
448
+ const all: Record<string, any> = {};
449
+ const parse = (queryString: string) => {
450
+ if (!queryString) {
451
+ return {};
452
+ }
453
+ try {
454
+ return Qs.parse(queryString);
455
+ } catch {
456
+ const result: Record<string, any> = {};
457
+ if (!queryString) return result;
458
+ queryString.split('&').forEach((pair) => {
459
+ if (!pair) return;
460
+ const [key, val] = pair.split('=');
461
+ if (key)
462
+ result[decodeURIComponent(key)] = decodeURIComponent(val || '');
463
+ });
464
+ return result;
465
+ }
466
+ };
467
+ try {
468
+ const [base, hashPart] = url.split('#');
469
+ const queryPart = base.split('?')[1];
470
+ Object.assign(query, parse(queryPart));
471
+ if (hashPart) {
472
+ for (const part of hashPart.split('?')) {
473
+ Object.assign(hash, parse(part));
474
+ }
475
+ }
476
+ Object.assign(all, query, hash);
477
+ } catch (error) {
478
+ console.error('getQueryParams:', error);
479
+ }
480
+ return { query, hash, all };
481
+ }
482
+
483
+ /**
484
+ * 版本号比较
485
+ * @param v1
486
+ * @param v2
487
+ * @returns number
488
+ * @description v1大于v2(1)、v1小于v2(-1)、v1等于v2(0)
489
+ */
490
+ static compareVersion(v1: string, v2: string): number {
491
+ let v1Arr: Array<string> = CrUtil.trim(v1).split('.');
492
+ let v2Arr: Array<string> = CrUtil.trim(v2).split('.');
493
+ const len = Math.max(v1.length, v2.length);
494
+ while (v1Arr.length < len) {
495
+ v1Arr.push('0');
496
+ }
497
+ while (v2Arr.length < len) {
498
+ v2Arr.push('0');
499
+ }
500
+ for (let i = 0; i < len; i++) {
501
+ const num1 = parseInt(v1Arr[i]);
502
+ const num2 = parseInt(v2Arr[i]);
503
+ if (num1 > num2) {
504
+ return 1;
505
+ } else if (num1 < num2) {
506
+ return -1;
507
+ }
508
+ }
509
+ return 0;
510
+ }
511
+
512
+ /**
513
+ * 根据 ids 获取相对应的数据名称
514
+ * @param data
515
+ * @param ids
516
+ * @param settings
517
+ * @returns string
518
+ */
519
+ static getDataNameByIds<T extends Record<string, any>>(
520
+ data: T[],
521
+ ids: (string | number)[],
522
+ settings?: { sep?: string; nameField?: string; idField?: string },
523
+ ): string {
524
+ const options = Object.assign(
525
+ {
526
+ sep: '、',
527
+ idField: 'id',
528
+ nameField: 'name',
529
+ },
530
+ settings,
531
+ );
532
+ let str = '';
533
+ if (ids && ids.length > 0) {
534
+ str = ids
535
+ .map((id) => {
536
+ let item = data.find((item) => item[options.idField] == id);
537
+ if (item) {
538
+ return item[options.nameField];
539
+ } else {
540
+ return '';
541
+ }
542
+ })
543
+ .join(options.sep);
544
+ }
545
+ return str;
546
+ }
547
+
548
+ /**
549
+ * 追加html标签属性值
550
+ * @param htmlStr
551
+ * @param tagName
552
+ * @param attrName
553
+ * @param newAttrValue
554
+ * @returns string
555
+ */
556
+ static appendHtmlTagAttr(
557
+ htmlStr: string,
558
+ tagName: string,
559
+ attrName: string,
560
+ newAttrValue: string,
561
+ ) {
562
+ let regex = new RegExp(`(<${tagName}\\s+)([^>]*?)([^>]*>)`, 'g');
563
+ let replacedHtml = htmlStr.replace(regex, function (match, p1, p2, p3) {
564
+ let _regex = new RegExp(`${attrName}="(.*?)"`, 'g');
565
+ if (_regex.exec(match)) {
566
+ let __regex = new RegExp(
567
+ `(<${tagName}\\s+)([^>]*?)${attrName}="(.*)"([^>]*>)`,
568
+ 'g',
569
+ );
570
+ return match.replace(__regex, function (_match, _p1, _p2, _p3, _p4) {
571
+ return _p1 + _p2 + `${attrName}="${_p3} ${newAttrValue}"` + _p4;
572
+ });
573
+ } else {
574
+ return p1 + `${attrName}="${newAttrValue}" ` + p3;
575
+ }
576
+ });
577
+ replacedHtml = replacedHtml.replace(
578
+ new RegExp(`<${tagName}>`, 'g'),
579
+ `<${tagName} ${attrName}="${newAttrValue}">`,
580
+ );
581
+ return replacedHtml;
582
+ }
583
+
584
+ /**
585
+ * 比较数据差异
586
+ * @param data1
587
+ * @param data2
588
+ * @returns
589
+ */
590
+ static compareDataDiff(data1: any, data2: any) {
591
+ const diffFieldSet: Record<string, 'added' | 'removed' | 'modified'> = {};
592
+ const compare = (path: string, o1: any, o2: any) => {
593
+ if (CrUtil.isArray(o1) && CrUtil.isArray(o2)) {
594
+ // 如果两者都是数组,比较它们的长度和内容
595
+ const lengthDiff = o1.length - o2.length;
596
+ if (lengthDiff !== 0) {
597
+ // 数组长度不同
598
+ diffFieldSet[path] = 'modified';
599
+ if (lengthDiff > 0) {
600
+ // o1比o2长,所以o1中有多余的元素
601
+ for (let i = o2.length; i < o1.length; i++) {
602
+ diffFieldSet[`${path}[${i}]`] = 'removed'; // 只在o1中存在
603
+ }
604
+ } else {
605
+ // o2比o1长,所以o2中有新增的元素
606
+ for (let i = o1.length; i < o2.length; i++) {
607
+ diffFieldSet[`${path}[${i}]`] = 'added'; // 只在o2中存在
608
+ }
609
+ }
610
+ } else {
611
+ for (let i = 0; i < o1.length; i++) {
612
+ compare(`${path}[${i}]`, o1[i], o2[i]);
613
+ }
614
+ }
615
+ } else if (CrUtil.isObject(o1) && CrUtil.isObject(o2)) {
616
+ // 如果两者都是对象,遍历它们的属性
617
+ // eslint-disable-next-line guard-for-in
618
+ for (let key in o1) {
619
+ const _path = path ? `${path}.${key}` : key;
620
+ if (Object.prototype.hasOwnProperty.call(o1, key) && !(key in o2)) {
621
+ diffFieldSet[_path] = 'removed'; // 只在o1中存在
622
+ } else if (Object.prototype.hasOwnProperty.call(o2, key)) {
623
+ compare(_path, o1[key], o2[key]);
624
+ }
625
+ }
626
+ // 检查o2中是否存在o1中没有的属性
627
+ // eslint-disable-next-line guard-for-in
628
+ for (let key in o2) {
629
+ const _path = path ? `${path}.${key}` : key;
630
+ if (Object.prototype.hasOwnProperty.call(o2, key) && !(key in o1)) {
631
+ diffFieldSet[_path] = 'added'; // 只在o2中存在
632
+ }
633
+ }
634
+ } else if (o1 !== o2) {
635
+ // 既不是对象也不是数组,直接比较值
636
+ diffFieldSet[path] = 'modified';
637
+ }
638
+ };
639
+ compare('', data1, data2);
640
+ return diffFieldSet;
641
+ }
642
+
643
+ /**
644
+ * 格式化为千分位
645
+ * @param value
646
+ * @returns 12345.6789 => 12,345.6789
647
+ */
648
+ static fmtThousands(value: number | string | undefined): string {
649
+ if (value == undefined || value == null || isNaN(Number(value))) {
650
+ return String(value);
651
+ }
652
+ const [intPart, decimalPart] = String(value).split('.');
653
+ const formattedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
654
+ return decimalPart ? `${formattedInt}.${decimalPart}` : formattedInt;
655
+ }
656
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2015",
4
+ "downlevelIteration": true,
5
+ "strict": true,
6
+ "declaration": true,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "jsx": "react",
10
+ "baseUrl": "./",
11
+ "paths": {
12
+ "@@/*": [".dumi/tmp/*"],
13
+ "@zcrkey/js-utils": ["src"],
14
+ "@zcrkey/js-utils/*": ["src/*", "*"]
15
+ }
16
+ },
17
+ "include": [".dumirc.ts", "src/**/*", "docs/**/*"]
18
+ }