import { nanoid } from "nanoid"; import { cloneDeep, isEmpty, isObject, isNil, get } from "lodash-es"; import dayjs from "dayjs"; /** * 状态码枚举 */ export const STATE_CODE = { /** 成功 */ SUC: 200, /** 未找到 */ NOT_FOUNT: 404, /** 错误 */ ERR: 500, /** 缺少 ID 错误 */ ERR_INPUT_ID: 501, } as const; /** * 状态码类型 */ export type StateCode = (typeof STATE_CODE)[keyof typeof STATE_CODE]; /** * 分页结果接口 */ export interface Pagination { /** 当前页码 */ current?: number; /** 总条数 */ total: number; /** 每页条数 */ pageSize?: number; } /** * 列表查询结果接口 */ export interface FindListResult> { /** 列表数据 */ list: T[]; /** 分页信息 */ pagination: Pagination; } /** * 排序路径配置接口 */ export interface SortPathConfig { /** 字段路径 */ path: string; /** 字段类型 */ type?: string; /** 自定义比较函数 */ customCompare?: (a: any, b: any, opt: any) => number; /** 自定义值格式化函数 */ customFormat?: (val: any, data: any) => any; } /** * 查询参数接口 */ export interface ItemQuery { /** 页码 */ pageNum?: number; /** 每页条数 */ pageSize?: number; /** 排序字段 */ sortKey?: string | string[] | SortPathConfig[]; /** 排序方式 */ sortType?: "asc" | "desc" | Record; /** 兼容旧排序字段参数 */ orderByColumn?: string | string[] | SortPathConfig[]; /** 兼容旧排序方式参数 */ isAsc?: "asc" | "desc" | boolean; /** 其他筛选条件 */ [key: string]: any; } /** * 查询选项接口 */ export interface QueryOptions { /** 字符串字段是否开启模糊匹配 */ fuzzy?: boolean; /** 模糊匹配时是否忽略大小写 */ ignoreCase?: boolean; } /** * 范围查询配置接口 */ export interface RangeQuery { /** 大于 */ $gt?: any; /** 小于 */ $lt?: any; /** 大于等于 */ $gte?: any; /** 小于等于 */ $lte?: any; } /** * ArrayUtils 构造函数参数接口 */ export interface ArrayUtilsOptions> { /** 子项唯一标志字段 */ idKey?: string; /** 列表数据 */ list?: T[]; } /** * 数组模拟数据库存储 * @template T - 列表项数据类型 */ export class ArrayUtils = Record> { /** 子项唯一标志字段 */ protected _idKey: string; /** 列表数据 */ protected _list: T[]; constructor(params: ArrayUtilsOptions = {}) { const { idKey = "id", list } = params; this._idKey = idKey || "id"; this._list = list ? cloneDeep(list) : []; } /** * 通过 id 获取子项 * @param id 目标 ID * @returns 匹配的子项或 null */ findItemById(id: string | number): T | null { if (this._isInvalidId(id)) { return null; } const { _idKey } = this; const item = this._list.find((it) => it[_idKey] === id); return item ? cloneDeep(item) : null; } /** * 通过 query 获取子项 * @param query 查询条件 * @param options 查询选项 * @returns 匹配的子项或 null */ findItem(query: Record = {}, options: QueryOptions = {}): T | null { const { fuzzy = false, ignoreCase = false } = options; const id = query[this._idKey]; // 如果有 ID 且 ID 有效,优先通过 ID 查询 if (!this._isInvalidId(id)) { return this.findItemById(id); } const validQuery = this._filterEmptyQuery(query); if (isEmpty(validQuery)) { return null; } const matchItem = this._list.find((item) => { return this._isItemMatchQuery(item, validQuery, { fuzzy, ignoreCase }); }); return matchItem ? cloneDeep(matchItem) : null; } /** * 通过 query 筛选列表(支持多条件筛选、分页、排序) * @param query - 综合查询参数 * @param query.pageNum - 页码(默认 1) * @param query.pageSize - 每页条数(默认 10) * @param query.sortKey - 排序字段(支持嵌套路径,如 'user.age') * @param query.sortType - 排序方式(desc/asc) * @param query.orderByColumn - 兼容旧排序字段参数 * @param query.isAsc - 兼容旧排序方式参数 * @param options - 查询选项 * @param options.fuzzy - 字符串字段是否开启模糊匹配 * @param options.ignoreCase - 模糊匹配时是否忽略大小写 * @returns 分页结果 */ findListByQuery(query: ItemQuery = {}, options: QueryOptions = {}): Promise> { const { pageNum = 1, pageSize = 10, sortKey, sortType, orderByColumn, isAsc, ...filterQuery } = query; const { fuzzy = true, ignoreCase = false } = options; // 数据筛选(基于 query 条件) let filteredList = [...this._list]; const validFilterQuery = this._filterEmptyQuery(filterQuery); if (!isEmpty(validFilterQuery)) { filteredList = filteredList.filter((item) => { return this._isItemMatchQuery(item, validFilterQuery, { fuzzy, ignoreCase }); }); } // 数据排序(基于筛选后的数据) const sortedList = this.getSortList(sortKey || orderByColumn, sortType || isAsc, filteredList); // 分页处理(边界校验) const pagination: Pagination = { current: Math.max(1, pageNum), pageSize: Math.max(1, pageSize), total: filteredList.length, }; // 计算分页截取范围 const startIndex = (pagination.current - 1) * pagination.pageSize; const endIndex = startIndex + pagination.pageSize; const paginatedList = sortedList.slice(startIndex, endIndex); return Promise.resolve({ list: cloneDeep(paginatedList), pagination: { ...pagination }, }); } /** * 对目标数据进行多维度排序 * @param sortKey - 排序字段配置 * @param sortType - 排序方式(desc/asc) * @param customList - 自定义排序列表(默认使用内部 _list) * @returns 排序后的列表 */ getSortList( sortKey?: string | string[] | SortPathConfig[], sortType?: "asc" | "desc" | Record | boolean, customList?: T[], ): T[] { const sourceList = customList || this._list; const _list = cloneDeep(sourceList); // 无排序条件时直接返回原列表 if (!sortKey && sortType === undefined) { return _list; } // 处理布尔类型的 sortType(兼容 isAsc 参数) let normalizedSortType: "asc" | "desc" | Record | undefined; if (typeof sortType === "boolean") { normalizedSortType = sortType ? "asc" : "desc"; } else { normalizedSortType = sortType; } // 统一排序字段格式为数组 let paths: (string | SortPathConfig)[]; if (Array.isArray(sortKey)) { paths = sortKey; } else if (sortKey) { paths = [sortKey]; } else { paths = []; } if (paths.length === 0 && !normalizedSortType) { return _list; } _list.sort((a, b) => { return this.compareData(a, b, { paths, sortType: normalizedSortType, pathIdx: 0 }); }); return _list; } /** * 比较两个数据对象的排序顺序(支持多字段优先级排序) * @param dataA - 比较对象 A * @param dataB - 比较对象 B * @param opt - 比较配置 * @returns 排序结果(-1/0/1) */ compareData( dataA: T, dataB: T, opt: { paths: (string | SortPathConfig)[]; pathIdx?: number; sortType?: "asc" | "desc" | Record; }, ): number { const { paths, pathIdx = 0, sortType = "desc" } = opt; const currentPath = paths[pathIdx]; // 处理自定义比较逻辑 if ( currentPath && typeof currentPath === "object" && typeof (currentPath as SortPathConfig).customCompare === "function" ) { const direction = sortType === "asc" ? -1 : 1; return direction * (currentPath as SortPathConfig).customCompare!(dataA, dataB, opt); } // 提取实际字段路径(支持配置对象格式) const path = typeof currentPath === "object" ? (currentPath as SortPathConfig).path : currentPath; if (!path) { // 没有更多字段时,继续下一个或返回 0 if (pathIdx + 1 < paths.length) { return this.compareData(dataA, dataB, { ...opt, pathIdx: pathIdx + 1 }); } return 0; } // 确定排序方向(支持按字段单独配置排序方式) let direction = 1; let currentSortType = sortType; if (typeof currentSortType === "object") { currentSortType = (currentSortType as Record)[path] || "desc"; } if (currentSortType === "asc") { direction = -1; } // 获取并处理字段值(支持日期转换、自定义格式化) const valueA = this.handleValByKey(dataA, currentPath); const valueB = this.handleValByKey(dataB, currentPath); // 数值比较 if (valueA < valueB) { return -1 * direction; } if (valueA > valueB) { return 1 * direction; } // 当前字段值相等时,使用下一个优先级字段排序 if (pathIdx + 1 < paths.length) { return this.compareData(dataA, dataB, { ...opt, pathIdx: pathIdx + 1 }); } // 所有字段都相等时,保持原有顺序 return 0; } /** * 根据路径获取并处理字段值(支持嵌套路径、类型转换、自定义格式化) * @param data - 数据源对象 * @param pathConfig - 字段配置 * @returns 处理后的字段值 */ handleValByKey(data: T, pathConfig: string | SortPathConfig | null | undefined): any { // 处理字符串路径 if (typeof pathConfig === "string") { return get(data, pathConfig); } // 处理空配置 if (isNil(pathConfig)) { return undefined; } // 处理对象配置 if (typeof pathConfig === "object") { let val: any = get(data, (pathConfig as SortPathConfig).path); // 日期类型转换为时间戳(便于数值比较) if ((pathConfig as SortPathConfig).type === "date" && val) { const parsed = dayjs(val); val = parsed.isValid() ? parsed.valueOf() : 0; } // 自定义值格式化 if (typeof (pathConfig as SortPathConfig).customFormat === "function") { val = (pathConfig as SortPathConfig).customFormat!(val, data); } return val; } return undefined; } /** * 获取完整列表 * @returns 完整列表的深拷贝 */ findAllList(): T[] { return cloneDeep(this._list); } /** * 获取总数 * @returns 列表总数 */ getCount(): number { return this._list.length; } /** * push 数据 * @param data 要添加的数据 * @returns 状态码 */ pushItem(data: Partial | Partial[]): StateCode { const items = Array.isArray(data) ? data : [data]; items.forEach((item) => { const cloneItem = cloneDeep(item) as T; this._setId(cloneItem); this._setCreateTime(cloneItem); this._list.push(cloneItem); }); return STATE_CODE.SUC; } /** * 批量添加数据 * @param {*} data */ batchData(data) { this._list = this._list.concat(data || []) return STATE_CODE.SUC; } /** * unshift 数据 * @param data 要添加的数据 * @returns 状态码 */ unshiftItem(data: Partial): StateCode { const item = cloneDeep(data) as T; this._setId(item); this._setCreateTime(item); this._list.unshift(item); return STATE_CODE.SUC; } /** * 根据 id 更新子项——直接替换 * @param data 新的数据对象 * @returns 状态码 */ replaceItem(data: T): StateCode { const { _idKey, _list } = this; const id = data[_idKey]; if (this._isInvalidId(id)) { return STATE_CODE.ERR_INPUT_ID; } const idx = _list.findIndex((it) => it[_idKey] === id); if (idx < 0) { return STATE_CODE.NOT_FOUNT; } _list.splice(idx, 1, cloneDeep(data)); this._setUpdateTime(_list[idx]); return STATE_CODE.SUC; } /** * 根据 id 更新子项——仅修改传入的数据 * @param data 要更新的数据 * @returns 状态码 */ updateItemValue(data: Partial & { [key: string]: any }): StateCode { const { _idKey, _list } = this; const id = data[_idKey]; if (this._isInvalidId(id)) { return STATE_CODE.ERR_INPUT_ID; } const index = _list.findIndex((it) => it[_idKey] === id); if (index < 0) { return STATE_CODE.NOT_FOUNT; } const item = _list[index]; Object.keys(data).forEach((key) => { if (key !== _idKey) { (item as any)[key] = data[key]; } }); this._setUpdateTime(item); return STATE_CODE.SUC; } /** * 删除子项 * @param id 要删除的子项 ID * @returns 状态码 */ delItem(id: any): StateCode { const { _idKey, _list } = this; if (this._isInvalidId(id)) { return STATE_CODE.ERR_INPUT_ID; } const idx = _list.findIndex((it) => it[_idKey] === id); if (idx < 0) { return STATE_CODE.NOT_FOUNT; } _list.splice(idx, 1); return STATE_CODE.SUC; } /** * 清空所有数据 * @returns 状态码 */ clearAll(): StateCode { this._list = []; return STATE_CODE.SUC; } /** * 批量删除 * @param ids 要删除的 ID 数组 * @returns 删除成功的数量 */ deleteItems(ids: any[]): number { let successCount = 0; ids.forEach((id) => { const res = this.delItem(id); if (res === STATE_CODE.SUC) { successCount++; } }); return successCount; } /** * 判断列表是否为空 * @returns 是否为空 */ isEmpty(): boolean { return this._list.length === 0; } /** * 获取指定范围的列表 * @param start 起始索引 * @param end 结束索引 * @returns 切片后的列表 */ slice(start?: number, end?: number): T[] { return cloneDeep(this._list.slice(start, end)); } /** * 检查单个 item 是否匹配所有 query 条件 * @param item - 待检查的列表项 * @param query - 查询条件 * @param options - 匹配选项 * @param options.fuzzy - 是否开启模糊匹配 * @param options.ignoreCase - 模糊匹配是否忽略大小写 * @returns 是否匹配所有条件 */ protected _isItemMatchQuery( item: T, query: Record, { fuzzy, ignoreCase }: QueryOptions, ): boolean { // 遍历所有查询条件,必须全部满足才返回 true return Object.entries(query).every(([key, targetValue]) => { // 获取 item 中对应字段的值(支持嵌套路径,如 'user.name') const itemValue = get(item, key); // 处理空值情况(null/undefined 仅匹配 null/undefined) if (isNil(itemValue)) { return isNil(targetValue); } // 处理数组类型查询(支持 in 操作,如 { status: [1, 2] }) if (Array.isArray(targetValue)) { return targetValue.some((val) => this._compareSingleValue(itemValue, val, fuzzy, ignoreCase), ); } // 处理范围查询(如 { age: { $gt: 18, $lt: 30 } }) if (typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) { return this._handleRangeQuery(itemValue, targetValue as RangeQuery); } // 处理普通值匹配(精确/模糊) return this._compareSingleValue(itemValue, targetValue, fuzzy, ignoreCase); }); } /** * 比较单个值是否匹配(支持精确/模糊匹配) * @param itemValue - 列表项字段值 * @param targetValue - 查询目标值 * @param fuzzy - 是否模糊匹配 * @param ignoreCase - 是否忽略大小写 * @returns 是否匹配 */ protected _compareSingleValue( itemValue: any, targetValue: any, fuzzy: boolean, ignoreCase: boolean, ): boolean { // 类型不同时直接不匹配(避免隐式类型转换导致的问题) if (typeof itemValue !== typeof targetValue) { // 特殊处理:数字和字符串数字的匹配(如 123 和 '123') if ( (typeof itemValue === "number" && typeof targetValue === "string" && !isNaN(Number(targetValue))) || (typeof itemValue === "string" && typeof targetValue === "number" && !isNaN(Number(itemValue))) ) { return Number(itemValue) === targetValue; } return false; } // 字符串处理(支持模糊匹配和大小写忽略) if (typeof itemValue === "string" && fuzzy) { const itemStr = ignoreCase ? itemValue.toLowerCase() : itemValue; const targetStr = ignoreCase ? String(targetValue).toLowerCase() : String(targetValue); return itemStr.includes(targetStr); } // 其他类型(数字、布尔等)精确匹配 return itemValue === targetValue; } /** * 处理范围查询(如 $gt/$lt/$gte/$lte) * @param itemValue - 列表项字段值 * @param rangeConfig - 范围配置 * @returns 是否在范围内 */ protected _handleRangeQuery(itemValue: any, rangeConfig: RangeQuery): boolean { const { $gt, $lt, $gte, $lte } = rangeConfig; let isValid = true; // 转换为可比较的类型(优先处理日期) const compareValue = this._convertToComparableValue(itemValue); const convertTarget = (val: any) => this._convertToComparableValue(val); // 大于(>) if ($gt !== undefined) { isValid = isValid && compareValue > convertTarget($gt); } // 小于(<) if ($lt !== undefined) { isValid = isValid && compareValue < convertTarget($lt); } // 大于等于(>=) if ($gte !== undefined) { isValid = isValid && compareValue >= convertTarget($gte); } // 小于等于(<=) if ($lte !== undefined) { isValid = isValid && compareValue <= convertTarget($lte); } return isValid; } /** * 将值转换为可比较的类型(统一数字、日期格式) * @param value - 待转换的值 * @returns 可比较的值 */ protected _convertToComparableValue(value: any): any { // null/undefined 处理 if (isNil(value)) { return value; } // 日期转换为时间戳 if (value instanceof Date || (typeof value === "string" && dayjs(value).isValid())) { return dayjs(value).valueOf(); } // 字符串数字转换为数字 if (typeof value === "string" && !isNaN(Number(value))) { return Number(value); } return value; } /** * 过滤空查询条件(移除 undefined/null/空字符串/空数组/空对象) * @param query - 原始查询条件 * @returns 过滤后的有效查询条件 */ protected _filterEmptyQuery(query: Record): Record { return Object.entries(query).reduce((acc: Record, [key, value]) => { if ( !isNil(value) && value !== "" && !(Array.isArray(value) && value.length === 0) && !( typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0 ) ) { acc[key] = value; } return acc; }, {}); } /** * 检查 ID 是否无效 * @param id 要检查的 ID * @returns ID 是否无效(为空) */ protected _isInvalidId(id: any): boolean { return isNil(id) || id === ""; } /** * 子项没有 id 的时候自动添加 * @param item 要设置 ID 的子项 * @returns 设置 ID 后的子项 */ protected _setId(item: T): T { if (isObject(item) && this._isInvalidId((item as any)[this._idKey])) { (item as any)[this._idKey] = nanoid(); } return item; } /** * 设置创建时间 * @param item 要设置创建时间的子项 */ protected _setCreateTime(item: T): void { if (isObject(item) && this._isInvalidId((item as any).createTime)) { (item as any).createTime = Date.now(); } } /** * 设置更新时间 * @param item 要设置更新时间的子项 */ protected _setUpdateTime(item: T): void { if (isObject(item)) { (item as any).updateTime = Date.now(); } } } export default ArrayUtils;