@hzab/data-model 1.8.8 → 1.8.9-alpha.0

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/ArrayUtils.js CHANGED
@@ -1,539 +1,539 @@
1
- import { nanoid } from "nanoid";
2
- import _ from "lodash";
3
- import dayjs from "dayjs";
4
-
5
- export const STATE_CODE = {
6
- SUC: 200,
7
- NOT_FOUNT: 404,
8
- ERR: 500,
9
- ERR_INPUT_ID: 501,
10
- };
11
-
12
- /**
13
- * 数组模拟数据库存储
14
- * TODO: FormData? 考虑到文件存储问题
15
- */
16
- export class ArrayUtils {
17
- _idKey = "id";
18
- _list = [];
19
- constructor(params) {
20
- const { idKey = "id", list } = params;
21
- /** 子项唯一标志字段 */
22
- this._idKey = idKey || "id";
23
- /** 列表数据 */
24
- this._list = list || [];
25
- }
26
-
27
- /**
28
- * 通过 id 获取子项
29
- * @param {*} id
30
- * @returns
31
- */
32
- findItemById(id) {
33
- if (this._hasId(id)) {
34
- return null;
35
- }
36
- const { _idKey } = this;
37
- return _.cloneDeep(this._list.find((it) => it[_idKey] === id));
38
- }
39
-
40
- /**
41
- * 通过 id 获取子项
42
- * @param {*} id
43
- * @returns
44
- */
45
- findItem(query = {}, options = {}) {
46
- const { fuzzy = false, ignoreCase = false } = options;
47
- const id = query[this._idKey];
48
-
49
- if (!this._hasId(id)) {
50
- return this.findItemById(id);
51
- }
52
-
53
- const validQuery = this._filterEmptyQuery(query);
54
- if (_.isEmpty(validQuery)) {
55
- return null;
56
- }
57
-
58
- const matchItem = this._list.find((item) => {
59
- return this._isItemMatchQuery(item, validQuery, { fuzzy, ignoreCase });
60
- });
61
-
62
- return matchItem ? _.cloneDeep(matchItem) : null;
63
- }
64
-
65
- /**
66
- * 通过 query 筛选列表(支持多条件筛选、分页、排序)
67
- * @param {Object} query - 综合查询参数
68
- * @param {number} [query.pageNum=1] - 页码(默认1)
69
- * @param {number} [query.pageSize=10] - 每页条数(默认10)
70
- * @param {string|string[]} [query.sortKey] - 排序字段(支持嵌套路径,如 'user.age')
71
- * @param {string} [query.sortType='desc'] - 排序方式(desc/asc)
72
- * @param {string} [query.orderByColumn] - 兼容旧排序字段参数
73
- * @param {string} [query.isAsc] - 兼容旧排序方式参数
74
- * @param {Object} [options={}] - 查询选项
75
- * @param {boolean} [options.fuzzy=false] - 字符串字段是否开启模糊匹配
76
- * @param {boolean} [options.ignoreCase=true] - 模糊匹配时是否忽略大小写
77
- * @returns {Object} 分页结果
78
- * @returns {Object[]} result.list - 筛选后的列表(深拷贝)
79
- * @returns {Object} result.pagination - 分页信息(current/total/pageSize)
80
- */
81
- findListByQuery(query = {}, options = {}) {
82
- const {
83
- pageNum = 1,
84
- pageSize = 10,
85
- sortKey,
86
- sortType,
87
- orderByColumn,
88
- isAsc,
89
- ...filterQuery // 提取筛选条件(排除分页排序参数)
90
- } = query;
91
-
92
- const { fuzzy = true, ignoreCase = false } = options;
93
-
94
- // 数据筛选(基于 query 条件)
95
- let filteredList = [...this._list];
96
- const validFilterQuery = this._filterEmptyQuery(filterQuery);
97
-
98
- if (!_.isEmpty(validFilterQuery)) {
99
- filteredList = filteredList.filter((item) => {
100
- return this._isItemMatchQuery(item, validFilterQuery, { fuzzy, ignoreCase });
101
- });
102
- }
103
-
104
- // 数据排序(基于筛选后的数据)
105
- const sortedList = this.getSortList(
106
- sortKey || orderByColumn,
107
- sortType || isAsc,
108
- filteredList, // 传入筛选后列表,避免排序影响原数据
109
- );
110
-
111
- // 分页处理(边界校验)
112
- const pagination = {
113
- current: Math.max(1, pageNum), // 页码最小为1
114
- pageSize: Math.max(1, pageSize), // 每页条数最小为1
115
- total: filteredList.length,
116
- };
117
-
118
- // 计算分页截取范围
119
- const startIndex = (pagination.current - 1) * pagination.pageSize;
120
- const endIndex = startIndex + pagination.pageSize;
121
- const paginatedList = sortedList.slice(startIndex, endIndex);
122
-
123
- return {
124
- list: _.cloneDeep(paginatedList),
125
- pagination: { ...pagination },
126
- };
127
- }
128
-
129
- /**
130
- * 对目标数据进行多维度排序
131
- * @param {string|string[]|Object[]} sortKey - 排序字段配置
132
- * @param {string} sortType - 排序方式(desc/asc)
133
- * @param {Object[]} [customList] - 自定义排序列表(默认使用内部 _list)
134
- * @returns {Object[]} 排序后的列表
135
- */
136
- getSortList(sortKey, sortType, customList) {
137
- const sourceList = customList || this._list;
138
- const _list = _.cloneDeep(sourceList);
139
-
140
- // 无排序条件时直接返回原列表
141
- if (!sortKey && !sortType) {
142
- return _list;
143
- }
144
-
145
- // 统一排序字段格式为数组
146
- const paths = Array.isArray(sortKey) ? sortKey : [sortKey];
147
-
148
- _list.sort((a, b) => {
149
- return this.compareData(a, b, { paths, sortType, pathIdx: 0 });
150
- });
151
-
152
- return _list;
153
- }
154
-
155
- /**
156
- * 比较两个数据对象的排序顺序(支持多字段优先级排序)
157
- * @param {Object} dataA - 比较对象A
158
- * @param {Object} dataB - 比较对象B
159
- * @param {Object} opt - 比较配置
160
- * @param {string[]|Object[]} opt.paths - 排序字段路径数组
161
- * @param {number} opt.pathIdx - 当前比较的字段索引
162
- * @param {string} opt.sortType - 排序方式(desc/asc)
163
- * @returns {number} 排序结果(-1/0/1)
164
- */
165
- compareData(dataA, dataB, opt = {}) {
166
- const { paths, pathIdx = 0, sortType = "desc" } = opt;
167
- const currentPath = paths[pathIdx];
168
-
169
- // 处理自定义比较逻辑
170
- if (currentPath && typeof currentPath === "object" && typeof currentPath.customCompare === "function") {
171
- const direction = sortType === "asc" ? -1 : 1;
172
- return direction * currentPath.customCompare(dataA, dataB, opt);
173
- }
174
-
175
- // 提取实际字段路径(支持配置对象格式)
176
- const path = typeof currentPath === "object" ? currentPath.path : currentPath;
177
- if (!path) {
178
- return 0;
179
- }
180
-
181
- // 确定排序方向(支持按字段单独配置排序方式)
182
- let direction = 1;
183
- let _sortType = sortType;
184
- if (typeof _sortType === "object") {
185
- _sortType = _sortType[path] || "desc";
186
- }
187
- if (_sortType === "asc") {
188
- direction = -1;
189
- }
190
-
191
- // 获取并处理字段值(支持日期转换、自定义格式化)
192
- const valueA = this.handleValByKey(dataA, currentPath);
193
- const valueB = this.handleValByKey(dataB, currentPath);
194
-
195
- // 数值比较
196
- if (valueA < valueB) {
197
- return -1 * direction;
198
- }
199
- if (valueA > valueB) {
200
- return 1 * direction;
201
- }
202
-
203
- // 当前字段值相等时,使用下一个优先级字段排序
204
- if (pathIdx + 1 < paths.length) {
205
- return this.compareData(dataA, dataB, { ...opt, pathIdx: pathIdx + 1 });
206
- }
207
-
208
- // 所有字段都相等时,保持原有顺序
209
- return 0;
210
- }
211
-
212
- /**
213
- * 根据路径获取并处理字段值(支持嵌套路径、类型转换、自定义格式化)
214
- * @param {Object} data - 数据源对象
215
- * @param {string|Object} pathConfig - 字段配置
216
- * @param {string} [pathConfig.path] - 字段路径(如 'user.address.city')
217
- * @param {string} [pathConfig.type] - 字段类型(支持 'date' 自动转时间戳)
218
- * @param {Function} [pathConfig.customFormat] - 自定义值处理函数
219
- * @returns {*} 处理后的字段值
220
- */
221
- handleValByKey(data, pathConfig) {
222
- // 处理字符串路径
223
- if (typeof pathConfig === "string") {
224
- return _.get(data, pathConfig);
225
- }
226
-
227
- // 处理空配置
228
- if (_.isNil(pathConfig)) {
229
- return undefined;
230
- }
231
-
232
- // 处理对象配置
233
- if (typeof pathConfig === "object") {
234
- let val = _.get(data, pathConfig.path);
235
-
236
- // 日期类型转换为时间戳(便于数值比较)
237
- if (pathConfig.type === "date") {
238
- val = dayjs(val).isValid() ? dayjs(val).valueOf() : 0;
239
- }
240
-
241
- // 自定义值格式化
242
- if (typeof pathConfig.customFormat === "function") {
243
- val = pathConfig.customFormat(val, data);
244
- }
245
-
246
- return val;
247
- }
248
-
249
- return undefined;
250
- }
251
-
252
- /**
253
- * 获取完整列表
254
- */
255
- findAllList() {
256
- return _.cloneDeep(this._list);
257
- }
258
-
259
- /**
260
- * 获取总数
261
- * @returns
262
- */
263
- getCount() {
264
- return this._list.length;
265
- }
266
-
267
- /**
268
- * push 数据
269
- * @param {*} data
270
- */
271
- pushItem(data) {
272
- // TODO: id 去重
273
- const items = Array.isArray(data) ? data : [data];
274
- items.forEach((item) => {
275
- const cloneItem = _.cloneDeep(item);
276
- this._setId(cloneItem);
277
- this._setCreateTime(cloneItem);
278
- this._list.push(cloneItem);
279
- });
280
- return STATE_CODE.SUC;
281
- }
282
-
283
- /**
284
- * unshift 数据
285
- * @param {*} data
286
- */
287
- unshiftItem(data) {
288
- // TODO: id 去重
289
- const item = _.cloneDeep(data);
290
- this._setId(item);
291
- this._setCreateTime(item);
292
- this._list.unshift(item);
293
- return STATE_CODE.SUC;
294
- }
295
-
296
- /**
297
- * 根据 id 更新子项——直接替换
298
- * @param {Object} data
299
- * @returns
300
- */
301
- replaceItem(data) {
302
- const { _idKey, _list } = this;
303
- const id = data[_idKey];
304
- if (this._hasId(id)) {
305
- return STATE_CODE.ERR_INPUT_ID;
306
- }
307
- const idx = _list.findIndex((it) => it[_idKey] === id);
308
- if (idx < 0) {
309
- return STATE_CODE.NOT_FOUNT;
310
- }
311
- _list.splice(idx, 1, data);
312
- this._setUpdateTime(data);
313
- return STATE_CODE.SUC;
314
- }
315
-
316
- /**
317
- * 根据 id 更新子项——仅修改传入的数据
318
- * @param {Object} data
319
- * @returns
320
- */
321
- updateItemValue(data) {
322
- const { _idKey, _list } = this;
323
- const id = data[_idKey];
324
- if (this._hasId(id)) {
325
- return STATE_CODE.ERR_INPUT_ID;
326
- }
327
- const item = _list.find((it) => it[_idKey] === id);
328
- if (!item) {
329
- return STATE_CODE.NOT_FOUNT;
330
- }
331
- Object.keys(data).forEach((key) => {
332
- item[key] = data[key];
333
- });
334
- this._setUpdateTime(data);
335
- return STATE_CODE.SUC;
336
- }
337
-
338
- /**
339
- * 删除子项
340
- * @param {*} id
341
- * @param {*} data
342
- * @returns
343
- */
344
- delItem(id) {
345
- const { _idKey, _list } = this;
346
- if (this._hasId(id)) {
347
- return STATE_CODE.ERR_INPUT_ID;
348
- }
349
- const idx = _list.findIndex((it) => it[_idKey] === id);
350
- if (idx < 0) {
351
- return STATE_CODE.NOT_FOUNT;
352
- }
353
- _list.splice(idx, 1);
354
- return STATE_CODE.SUC;
355
- }
356
-
357
- /**
358
- * 检查单个 item 是否匹配所有 query 条件
359
- * @private
360
- * @param {Object} item - 待检查的列表项
361
- * @param {Object} query - 查询条件
362
- * @param {Object} options - 匹配选项
363
- * @param {boolean} options.fuzzy - 是否开启模糊匹配
364
- * @param {boolean} options.ignoreCase - 模糊匹配是否忽略大小写
365
- * @returns {boolean} 是否匹配所有条件
366
- */
367
- _isItemMatchQuery(item, query, { fuzzy, ignoreCase }) {
368
- // 遍历所有查询条件,必须全部满足才返回 true
369
- return Object.entries(query).every(([key, targetValue]) => {
370
- // 获取 item 中对应字段的值(支持嵌套路径,如 'user.name')
371
- const itemValue = _.get(item, key);
372
-
373
- // 处理空值情况(null/undefined 仅匹配 null/undefined)
374
- if (_.isNil(itemValue)) {
375
- return _.isNil(targetValue);
376
- }
377
-
378
- // 处理数组类型查询(支持 in 操作,如 { status: [1, 2] })
379
- if (Array.isArray(targetValue)) {
380
- return targetValue.some((val) => this._compareSingleValue(itemValue, val, fuzzy, ignoreCase));
381
- }
382
-
383
- // 处理范围查询(如 { age: { $gt: 18, $lt: 30 } })
384
- if (typeof targetValue === "object" && !Array.isArray(targetValue)) {
385
- return this._handleRangeQuery(itemValue, targetValue);
386
- }
387
-
388
- // 处理普通值匹配(精确/模糊)
389
- return this._compareSingleValue(itemValue, targetValue, fuzzy, ignoreCase);
390
- });
391
- }
392
-
393
- /**
394
- * 比较单个值是否匹配(支持精确/模糊匹配)
395
- * @private
396
- * @param {*} itemValue - 列表项字段值
397
- * @param {*} targetValue - 查询目标值
398
- * @param {boolean} fuzzy - 是否模糊匹配
399
- * @param {boolean} ignoreCase - 是否忽略大小写
400
- * @returns {boolean} 是否匹配
401
- */
402
- _compareSingleValue(itemValue, targetValue, fuzzy, ignoreCase) {
403
- // 类型不同时直接不匹配(避免隐式类型转换导致的问题)
404
- if (typeof itemValue !== typeof targetValue) {
405
- // 特殊处理:数字和字符串数字的匹配(如 123 和 '123')
406
- if (
407
- (typeof itemValue === "number" && typeof targetValue === "string" && !isNaN(Number(targetValue))) ||
408
- (typeof itemValue === "string" && typeof targetValue === "number" && !isNaN(Number(itemValue)))
409
- ) {
410
- return Number(itemValue) === targetValue;
411
- }
412
- return false;
413
- }
414
-
415
- // 字符串处理(支持模糊匹配和大小写忽略)
416
- if (typeof itemValue === "string" && fuzzy) {
417
- const itemStr = ignoreCase ? itemValue.toLowerCase() : itemValue;
418
- const targetStr = ignoreCase ? String(targetValue).toLowerCase() : String(targetValue);
419
- return itemStr.includes(targetStr);
420
- }
421
-
422
- // 其他类型(数字、布尔等)精确匹配
423
- return itemValue === targetValue;
424
- }
425
-
426
- /**
427
- * 处理范围查询(如 $gt/$lt/$gte/$lte)
428
- * @private
429
- * @param {number|string|Date} itemValue - 列表项字段值
430
- * @param {Object} rangeConfig - 范围配置({ $gt: 18, $lt: 30 })
431
- * @returns {boolean} 是否在范围内
432
- */
433
- _handleRangeQuery(itemValue, rangeConfig) {
434
- const { $gt, $lt, $gte, $lte } = rangeConfig;
435
- let isValid = true;
436
-
437
- // 转换为可比较的类型(优先处理日期)
438
- const compareValue = this._convertToComparableValue(itemValue);
439
- const convertTarget = (val) => this._convertToComparableValue(val);
440
-
441
- // 大于(>)
442
- if ($gt !== undefined) {
443
- isValid = isValid && compareValue > convertTarget($gt);
444
- }
445
- // 小于(<)
446
- if ($lt !== undefined) {
447
- isValid = isValid && compareValue < convertTarget($lt);
448
- }
449
- // 大于等于(>=)
450
- if ($gte !== undefined) {
451
- isValid = isValid && compareValue >= convertTarget($gte);
452
- }
453
- // 小于等于(<=)
454
- if ($lte !== undefined) {
455
- isValid = isValid && compareValue <= convertTarget($lte);
456
- }
457
-
458
- return isValid;
459
- }
460
-
461
- /**
462
- * 将值转换为可比较的类型(统一数字、日期格式)
463
- * @private
464
- * @param {*} value - 待转换的值
465
- * @returns {number|string} 可比较的值
466
- */
467
- _convertToComparableValue(value) {
468
- // 日期转换为时间戳
469
- if (value instanceof Date || dayjs(value).isValid()) {
470
- return dayjs(value).valueOf();
471
- }
472
- // 字符串数字转换为数字
473
- if (typeof value === "string" && !isNaN(Number(value))) {
474
- return Number(value);
475
- }
476
- return value;
477
- }
478
-
479
- /**
480
- * 过滤空查询条件(移除 undefined/null/空字符串/空数组/空对象)
481
- * @private
482
- * @param {Object} query - 原始查询条件
483
- * @returns {Object} 过滤后的有效查询条件
484
- */
485
- _filterEmptyQuery(query) {
486
- return Object.entries(query).reduce((acc, [key, value]) => {
487
- // 保留非空值:
488
- // 1. 不是 null/undefined
489
- // 2. 不是空字符串
490
- // 3. 不是空数组
491
- // 4. 不是空对象
492
- if (
493
- !_.isNil(value) &&
494
- value !== "" &&
495
- !(Array.isArray(value) && value.length === 0) &&
496
- !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
497
- ) {
498
- acc[key] = value;
499
- }
500
- return acc;
501
- }, {});
502
- }
503
-
504
- _hasId(id) {
505
- return _.isNil(id) || id === "";
506
- }
507
-
508
- /**
509
- * 子项没有 id 的时候自动添加
510
- * @param {*} item
511
- * @returns
512
- */
513
- _setId(item) {
514
- if (_.isObject(item) && this._hasId(item[this._idKey])) {
515
- item[this._idKey] = nanoid();
516
- }
517
- return item;
518
- }
519
-
520
- /**
521
- * 设置创建时间
522
- * @param {*} item
523
- */
524
- _setCreateTime(item) {
525
- if (_.isObject(item) && this._hasId(item.createTime)) {
526
- item.createTime = Date.now();
527
- }
528
- }
529
-
530
- /**
531
- * 设置更新时间
532
- * @param {*} item
533
- */
534
- _setUpdateTime(item) {
535
- item.createTime = Date.now();
536
- }
537
- }
538
-
539
- export default ArrayUtils;
1
+ import { nanoid } from "nanoid";
2
+ import _ from "lodash";
3
+ import dayjs from "dayjs";
4
+
5
+ export const STATE_CODE = {
6
+ SUC: 200,
7
+ NOT_FOUNT: 404,
8
+ ERR: 500,
9
+ ERR_INPUT_ID: 501,
10
+ };
11
+
12
+ /**
13
+ * 数组模拟数据库存储
14
+ * TODO: FormData? 考虑到文件存储问题
15
+ */
16
+ export class ArrayUtils {
17
+ _idKey = "id";
18
+ _list = [];
19
+ constructor(params) {
20
+ const { idKey = "id", list } = params;
21
+ /** 子项唯一标志字段 */
22
+ this._idKey = idKey || "id";
23
+ /** 列表数据 */
24
+ this._list = list || [];
25
+ }
26
+
27
+ /**
28
+ * 通过 id 获取子项
29
+ * @param {*} id
30
+ * @returns
31
+ */
32
+ findItemById(id) {
33
+ if (this._hasId(id)) {
34
+ return null;
35
+ }
36
+ const { _idKey } = this;
37
+ return _.cloneDeep(this._list.find((it) => it[_idKey] === id));
38
+ }
39
+
40
+ /**
41
+ * 通过 id 获取子项
42
+ * @param {*} id
43
+ * @returns
44
+ */
45
+ findItem(query = {}, options = {}) {
46
+ const { fuzzy = false, ignoreCase = false } = options;
47
+ const id = query[this._idKey];
48
+
49
+ if (!this._hasId(id)) {
50
+ return this.findItemById(id);
51
+ }
52
+
53
+ const validQuery = this._filterEmptyQuery(query);
54
+ if (_.isEmpty(validQuery)) {
55
+ return null;
56
+ }
57
+
58
+ const matchItem = this._list.find((item) => {
59
+ return this._isItemMatchQuery(item, validQuery, { fuzzy, ignoreCase });
60
+ });
61
+
62
+ return matchItem ? _.cloneDeep(matchItem) : null;
63
+ }
64
+
65
+ /**
66
+ * 通过 query 筛选列表(支持多条件筛选、分页、排序)
67
+ * @param {Object} query - 综合查询参数
68
+ * @param {number} [query.pageNum=1] - 页码(默认1)
69
+ * @param {number} [query.pageSize=10] - 每页条数(默认10)
70
+ * @param {string|string[]} [query.sortKey] - 排序字段(支持嵌套路径,如 'user.age')
71
+ * @param {string} [query.sortType='desc'] - 排序方式(desc/asc)
72
+ * @param {string} [query.orderByColumn] - 兼容旧排序字段参数
73
+ * @param {string} [query.isAsc] - 兼容旧排序方式参数
74
+ * @param {Object} [options={}] - 查询选项
75
+ * @param {boolean} [options.fuzzy=false] - 字符串字段是否开启模糊匹配
76
+ * @param {boolean} [options.ignoreCase=true] - 模糊匹配时是否忽略大小写
77
+ * @returns {Object} 分页结果
78
+ * @returns {Object[]} result.list - 筛选后的列表(深拷贝)
79
+ * @returns {Object} result.pagination - 分页信息(current/total/pageSize)
80
+ */
81
+ findListByQuery(query = {}, options = {}) {
82
+ const {
83
+ pageNum = 1,
84
+ pageSize = 10,
85
+ sortKey,
86
+ sortType,
87
+ orderByColumn,
88
+ isAsc,
89
+ ...filterQuery // 提取筛选条件(排除分页排序参数)
90
+ } = query;
91
+
92
+ const { fuzzy = true, ignoreCase = false } = options;
93
+
94
+ // 数据筛选(基于 query 条件)
95
+ let filteredList = [...this._list];
96
+ const validFilterQuery = this._filterEmptyQuery(filterQuery);
97
+
98
+ if (!_.isEmpty(validFilterQuery)) {
99
+ filteredList = filteredList.filter((item) => {
100
+ return this._isItemMatchQuery(item, validFilterQuery, { fuzzy, ignoreCase });
101
+ });
102
+ }
103
+
104
+ // 数据排序(基于筛选后的数据)
105
+ const sortedList = this.getSortList(
106
+ sortKey || orderByColumn,
107
+ sortType || isAsc,
108
+ filteredList, // 传入筛选后列表,避免排序影响原数据
109
+ );
110
+
111
+ // 分页处理(边界校验)
112
+ const pagination = {
113
+ current: Math.max(1, pageNum), // 页码最小为1
114
+ pageSize: Math.max(1, pageSize), // 每页条数最小为1
115
+ total: filteredList.length,
116
+ };
117
+
118
+ // 计算分页截取范围
119
+ const startIndex = (pagination.current - 1) * pagination.pageSize;
120
+ const endIndex = startIndex + pagination.pageSize;
121
+ const paginatedList = sortedList.slice(startIndex, endIndex);
122
+
123
+ return {
124
+ list: _.cloneDeep(paginatedList),
125
+ pagination: { ...pagination },
126
+ };
127
+ }
128
+
129
+ /**
130
+ * 对目标数据进行多维度排序
131
+ * @param {string|string[]|Object[]} sortKey - 排序字段配置
132
+ * @param {string} sortType - 排序方式(desc/asc)
133
+ * @param {Object[]} [customList] - 自定义排序列表(默认使用内部 _list)
134
+ * @returns {Object[]} 排序后的列表
135
+ */
136
+ getSortList(sortKey, sortType, customList) {
137
+ const sourceList = customList || this._list;
138
+ const _list = _.cloneDeep(sourceList);
139
+
140
+ // 无排序条件时直接返回原列表
141
+ if (!sortKey && !sortType) {
142
+ return _list;
143
+ }
144
+
145
+ // 统一排序字段格式为数组
146
+ const paths = Array.isArray(sortKey) ? sortKey : [sortKey];
147
+
148
+ _list.sort((a, b) => {
149
+ return this.compareData(a, b, { paths, sortType, pathIdx: 0 });
150
+ });
151
+
152
+ return _list;
153
+ }
154
+
155
+ /**
156
+ * 比较两个数据对象的排序顺序(支持多字段优先级排序)
157
+ * @param {Object} dataA - 比较对象A
158
+ * @param {Object} dataB - 比较对象B
159
+ * @param {Object} opt - 比较配置
160
+ * @param {string[]|Object[]} opt.paths - 排序字段路径数组
161
+ * @param {number} opt.pathIdx - 当前比较的字段索引
162
+ * @param {string} opt.sortType - 排序方式(desc/asc)
163
+ * @returns {number} 排序结果(-1/0/1)
164
+ */
165
+ compareData(dataA, dataB, opt = {}) {
166
+ const { paths, pathIdx = 0, sortType = "desc" } = opt;
167
+ const currentPath = paths[pathIdx];
168
+
169
+ // 处理自定义比较逻辑
170
+ if (currentPath && typeof currentPath === "object" && typeof currentPath.customCompare === "function") {
171
+ const direction = sortType === "asc" ? -1 : 1;
172
+ return direction * currentPath.customCompare(dataA, dataB, opt);
173
+ }
174
+
175
+ // 提取实际字段路径(支持配置对象格式)
176
+ const path = typeof currentPath === "object" ? currentPath.path : currentPath;
177
+ if (!path) {
178
+ return 0;
179
+ }
180
+
181
+ // 确定排序方向(支持按字段单独配置排序方式)
182
+ let direction = 1;
183
+ let _sortType = sortType;
184
+ if (typeof _sortType === "object") {
185
+ _sortType = _sortType[path] || "desc";
186
+ }
187
+ if (_sortType === "asc") {
188
+ direction = -1;
189
+ }
190
+
191
+ // 获取并处理字段值(支持日期转换、自定义格式化)
192
+ const valueA = this.handleValByKey(dataA, currentPath);
193
+ const valueB = this.handleValByKey(dataB, currentPath);
194
+
195
+ // 数值比较
196
+ if (valueA < valueB) {
197
+ return -1 * direction;
198
+ }
199
+ if (valueA > valueB) {
200
+ return 1 * direction;
201
+ }
202
+
203
+ // 当前字段值相等时,使用下一个优先级字段排序
204
+ if (pathIdx + 1 < paths.length) {
205
+ return this.compareData(dataA, dataB, { ...opt, pathIdx: pathIdx + 1 });
206
+ }
207
+
208
+ // 所有字段都相等时,保持原有顺序
209
+ return 0;
210
+ }
211
+
212
+ /**
213
+ * 根据路径获取并处理字段值(支持嵌套路径、类型转换、自定义格式化)
214
+ * @param {Object} data - 数据源对象
215
+ * @param {string|Object} pathConfig - 字段配置
216
+ * @param {string} [pathConfig.path] - 字段路径(如 'user.address.city')
217
+ * @param {string} [pathConfig.type] - 字段类型(支持 'date' 自动转时间戳)
218
+ * @param {Function} [pathConfig.customFormat] - 自定义值处理函数
219
+ * @returns {*} 处理后的字段值
220
+ */
221
+ handleValByKey(data, pathConfig) {
222
+ // 处理字符串路径
223
+ if (typeof pathConfig === "string") {
224
+ return _.get(data, pathConfig);
225
+ }
226
+
227
+ // 处理空配置
228
+ if (_.isNil(pathConfig)) {
229
+ return undefined;
230
+ }
231
+
232
+ // 处理对象配置
233
+ if (typeof pathConfig === "object") {
234
+ let val = _.get(data, pathConfig.path);
235
+
236
+ // 日期类型转换为时间戳(便于数值比较)
237
+ if (pathConfig.type === "date") {
238
+ val = dayjs(val).isValid() ? dayjs(val).valueOf() : 0;
239
+ }
240
+
241
+ // 自定义值格式化
242
+ if (typeof pathConfig.customFormat === "function") {
243
+ val = pathConfig.customFormat(val, data);
244
+ }
245
+
246
+ return val;
247
+ }
248
+
249
+ return undefined;
250
+ }
251
+
252
+ /**
253
+ * 获取完整列表
254
+ */
255
+ findAllList() {
256
+ return _.cloneDeep(this._list);
257
+ }
258
+
259
+ /**
260
+ * 获取总数
261
+ * @returns
262
+ */
263
+ getCount() {
264
+ return this._list.length;
265
+ }
266
+
267
+ /**
268
+ * push 数据
269
+ * @param {*} data
270
+ */
271
+ pushItem(data) {
272
+ // TODO: id 去重
273
+ const items = Array.isArray(data) ? data : [data];
274
+ items.forEach((item) => {
275
+ const cloneItem = _.cloneDeep(item);
276
+ this._setId(cloneItem);
277
+ this._setCreateTime(cloneItem);
278
+ this._list.push(cloneItem);
279
+ });
280
+ return STATE_CODE.SUC;
281
+ }
282
+
283
+ /**
284
+ * unshift 数据
285
+ * @param {*} data
286
+ */
287
+ unshiftItem(data) {
288
+ // TODO: id 去重
289
+ const item = _.cloneDeep(data);
290
+ this._setId(item);
291
+ this._setCreateTime(item);
292
+ this._list.unshift(item);
293
+ return STATE_CODE.SUC;
294
+ }
295
+
296
+ /**
297
+ * 根据 id 更新子项——直接替换
298
+ * @param {Object} data
299
+ * @returns
300
+ */
301
+ replaceItem(data) {
302
+ const { _idKey, _list } = this;
303
+ const id = data[_idKey];
304
+ if (this._hasId(id)) {
305
+ return STATE_CODE.ERR_INPUT_ID;
306
+ }
307
+ const idx = _list.findIndex((it) => it[_idKey] === id);
308
+ if (idx < 0) {
309
+ return STATE_CODE.NOT_FOUNT;
310
+ }
311
+ _list.splice(idx, 1, data);
312
+ this._setUpdateTime(data);
313
+ return STATE_CODE.SUC;
314
+ }
315
+
316
+ /**
317
+ * 根据 id 更新子项——仅修改传入的数据
318
+ * @param {Object} data
319
+ * @returns
320
+ */
321
+ updateItemValue(data) {
322
+ const { _idKey, _list } = this;
323
+ const id = data[_idKey];
324
+ if (this._hasId(id)) {
325
+ return STATE_CODE.ERR_INPUT_ID;
326
+ }
327
+ const item = _list.find((it) => it[_idKey] === id);
328
+ if (!item) {
329
+ return STATE_CODE.NOT_FOUNT;
330
+ }
331
+ Object.keys(data).forEach((key) => {
332
+ item[key] = data[key];
333
+ });
334
+ this._setUpdateTime(data);
335
+ return STATE_CODE.SUC;
336
+ }
337
+
338
+ /**
339
+ * 删除子项
340
+ * @param {*} id
341
+ * @param {*} data
342
+ * @returns
343
+ */
344
+ delItem(id) {
345
+ const { _idKey, _list } = this;
346
+ if (this._hasId(id)) {
347
+ return STATE_CODE.ERR_INPUT_ID;
348
+ }
349
+ const idx = _list.findIndex((it) => it[_idKey] === id);
350
+ if (idx < 0) {
351
+ return STATE_CODE.NOT_FOUNT;
352
+ }
353
+ _list.splice(idx, 1);
354
+ return STATE_CODE.SUC;
355
+ }
356
+
357
+ /**
358
+ * 检查单个 item 是否匹配所有 query 条件
359
+ * @private
360
+ * @param {Object} item - 待检查的列表项
361
+ * @param {Object} query - 查询条件
362
+ * @param {Object} options - 匹配选项
363
+ * @param {boolean} options.fuzzy - 是否开启模糊匹配
364
+ * @param {boolean} options.ignoreCase - 模糊匹配是否忽略大小写
365
+ * @returns {boolean} 是否匹配所有条件
366
+ */
367
+ _isItemMatchQuery(item, query, { fuzzy, ignoreCase }) {
368
+ // 遍历所有查询条件,必须全部满足才返回 true
369
+ return Object.entries(query).every(([key, targetValue]) => {
370
+ // 获取 item 中对应字段的值(支持嵌套路径,如 'user.name')
371
+ const itemValue = _.get(item, key);
372
+
373
+ // 处理空值情况(null/undefined 仅匹配 null/undefined)
374
+ if (_.isNil(itemValue)) {
375
+ return _.isNil(targetValue);
376
+ }
377
+
378
+ // 处理数组类型查询(支持 in 操作,如 { status: [1, 2] })
379
+ if (Array.isArray(targetValue)) {
380
+ return targetValue.some((val) => this._compareSingleValue(itemValue, val, fuzzy, ignoreCase));
381
+ }
382
+
383
+ // 处理范围查询(如 { age: { $gt: 18, $lt: 30 } })
384
+ if (typeof targetValue === "object" && !Array.isArray(targetValue)) {
385
+ return this._handleRangeQuery(itemValue, targetValue);
386
+ }
387
+
388
+ // 处理普通值匹配(精确/模糊)
389
+ return this._compareSingleValue(itemValue, targetValue, fuzzy, ignoreCase);
390
+ });
391
+ }
392
+
393
+ /**
394
+ * 比较单个值是否匹配(支持精确/模糊匹配)
395
+ * @private
396
+ * @param {*} itemValue - 列表项字段值
397
+ * @param {*} targetValue - 查询目标值
398
+ * @param {boolean} fuzzy - 是否模糊匹配
399
+ * @param {boolean} ignoreCase - 是否忽略大小写
400
+ * @returns {boolean} 是否匹配
401
+ */
402
+ _compareSingleValue(itemValue, targetValue, fuzzy, ignoreCase) {
403
+ // 类型不同时直接不匹配(避免隐式类型转换导致的问题)
404
+ if (typeof itemValue !== typeof targetValue) {
405
+ // 特殊处理:数字和字符串数字的匹配(如 123 和 '123')
406
+ if (
407
+ (typeof itemValue === "number" && typeof targetValue === "string" && !isNaN(Number(targetValue))) ||
408
+ (typeof itemValue === "string" && typeof targetValue === "number" && !isNaN(Number(itemValue)))
409
+ ) {
410
+ return Number(itemValue) === targetValue;
411
+ }
412
+ return false;
413
+ }
414
+
415
+ // 字符串处理(支持模糊匹配和大小写忽略)
416
+ if (typeof itemValue === "string" && fuzzy) {
417
+ const itemStr = ignoreCase ? itemValue.toLowerCase() : itemValue;
418
+ const targetStr = ignoreCase ? String(targetValue).toLowerCase() : String(targetValue);
419
+ return itemStr.includes(targetStr);
420
+ }
421
+
422
+ // 其他类型(数字、布尔等)精确匹配
423
+ return itemValue === targetValue;
424
+ }
425
+
426
+ /**
427
+ * 处理范围查询(如 $gt/$lt/$gte/$lte)
428
+ * @private
429
+ * @param {number|string|Date} itemValue - 列表项字段值
430
+ * @param {Object} rangeConfig - 范围配置({ $gt: 18, $lt: 30 })
431
+ * @returns {boolean} 是否在范围内
432
+ */
433
+ _handleRangeQuery(itemValue, rangeConfig) {
434
+ const { $gt, $lt, $gte, $lte } = rangeConfig;
435
+ let isValid = true;
436
+
437
+ // 转换为可比较的类型(优先处理日期)
438
+ const compareValue = this._convertToComparableValue(itemValue);
439
+ const convertTarget = (val) => this._convertToComparableValue(val);
440
+
441
+ // 大于(>)
442
+ if ($gt !== undefined) {
443
+ isValid = isValid && compareValue > convertTarget($gt);
444
+ }
445
+ // 小于(<)
446
+ if ($lt !== undefined) {
447
+ isValid = isValid && compareValue < convertTarget($lt);
448
+ }
449
+ // 大于等于(>=)
450
+ if ($gte !== undefined) {
451
+ isValid = isValid && compareValue >= convertTarget($gte);
452
+ }
453
+ // 小于等于(<=)
454
+ if ($lte !== undefined) {
455
+ isValid = isValid && compareValue <= convertTarget($lte);
456
+ }
457
+
458
+ return isValid;
459
+ }
460
+
461
+ /**
462
+ * 将值转换为可比较的类型(统一数字、日期格式)
463
+ * @private
464
+ * @param {*} value - 待转换的值
465
+ * @returns {number|string} 可比较的值
466
+ */
467
+ _convertToComparableValue(value) {
468
+ // 日期转换为时间戳
469
+ if (value instanceof Date || dayjs(value).isValid()) {
470
+ return dayjs(value).valueOf();
471
+ }
472
+ // 字符串数字转换为数字
473
+ if (typeof value === "string" && !isNaN(Number(value))) {
474
+ return Number(value);
475
+ }
476
+ return value;
477
+ }
478
+
479
+ /**
480
+ * 过滤空查询条件(移除 undefined/null/空字符串/空数组/空对象)
481
+ * @private
482
+ * @param {Object} query - 原始查询条件
483
+ * @returns {Object} 过滤后的有效查询条件
484
+ */
485
+ _filterEmptyQuery(query) {
486
+ return Object.entries(query).reduce((acc, [key, value]) => {
487
+ // 保留非空值:
488
+ // 1. 不是 null/undefined
489
+ // 2. 不是空字符串
490
+ // 3. 不是空数组
491
+ // 4. 不是空对象
492
+ if (
493
+ !_.isNil(value) &&
494
+ value !== "" &&
495
+ !(Array.isArray(value) && value.length === 0) &&
496
+ !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
497
+ ) {
498
+ acc[key] = value;
499
+ }
500
+ return acc;
501
+ }, {});
502
+ }
503
+
504
+ _hasId(id) {
505
+ return _.isNil(id) || id === "";
506
+ }
507
+
508
+ /**
509
+ * 子项没有 id 的时候自动添加
510
+ * @param {*} item
511
+ * @returns
512
+ */
513
+ _setId(item) {
514
+ if (_.isObject(item) && this._hasId(item[this._idKey])) {
515
+ item[this._idKey] = nanoid();
516
+ }
517
+ return item;
518
+ }
519
+
520
+ /**
521
+ * 设置创建时间
522
+ * @param {*} item
523
+ */
524
+ _setCreateTime(item) {
525
+ if (_.isObject(item) && this._hasId(item.createTime)) {
526
+ item.createTime = Date.now();
527
+ }
528
+ }
529
+
530
+ /**
531
+ * 设置更新时间
532
+ * @param {*} item
533
+ */
534
+ _setUpdateTime(item) {
535
+ item.createTime = Date.now();
536
+ }
537
+ }
538
+
539
+ export default ArrayUtils;