@nickyzj2023/utils 1.0.47 → 1.0.49
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/AGENTS.md +58 -22
- package/dist/dom/index.d.ts +1 -0
- package/dist/function/index.d.ts +1 -0
- package/dist/hoc/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/is/index.d.ts +5 -0
- package/dist/is/isFalsy.d.ts +9 -0
- package/dist/is/isNil.d.ts +9 -0
- package/dist/is/isObject.d.ts +8 -0
- package/dist/is/isPrimitive.d.ts +9 -0
- package/dist/is/isTruthy.d.ts +8 -0
- package/dist/is/types.d.ts +2 -0
- package/dist/network/fetcher.d.ts +57 -0
- package/dist/network/getRealURL.d.ts +2 -0
- package/dist/network/image.d.ts +59 -0
- package/dist/network/index.d.ts +4 -0
- package/dist/network/to.d.ts +9 -0
- package/dist/number/index.d.ts +1 -0
- package/dist/object/index.d.ts +3 -0
- package/dist/object/mapKeys.d.ts +17 -0
- package/dist/object/mapValues.d.ts +21 -0
- package/dist/object/mergeObjects.d.ts +12 -0
- package/dist/object/types.d.ts +6 -0
- package/dist/string/case.d.ts +32 -0
- package/dist/string/compact.d.ts +22 -0
- package/dist/string/image.d.ts +59 -0
- package/dist/string/index.d.ts +2 -0
- package/dist/string.d.ts +13 -97
- package/dist/time/debounce.d.ts +20 -0
- package/dist/time/index.d.ts +3 -0
- package/dist/time/sleep.d.ts +7 -0
- package/dist/time/throttle.d.ts +20 -0
- package/docs/functions/camelToSnake.html +1 -1
- package/docs/functions/capitalize.html +1 -1
- package/docs/functions/compactStr.html +1 -1
- package/docs/functions/debounce.html +1 -1
- package/docs/functions/decapitalize.html +1 -1
- package/docs/functions/fetcher.html +1 -1
- package/docs/functions/getRealURL.html +1 -1
- package/docs/functions/imageUrlToBase64.html +1 -1
- package/docs/functions/isFalsy.html +1 -1
- package/docs/functions/isNil.html +1 -1
- package/docs/functions/isObject.html +1 -1
- package/docs/functions/isPrimitive.html +1 -1
- package/docs/functions/isTruthy.html +1 -1
- package/docs/functions/loopUntil.html +1 -1
- package/docs/functions/mapKeys.html +1 -1
- package/docs/functions/mapValues.html +1 -1
- package/docs/functions/mergeObjects.html +1 -1
- package/docs/functions/randomInt.html +1 -1
- package/docs/functions/sleep.html +1 -1
- package/docs/functions/snakeToCamel.html +1 -1
- package/docs/functions/throttle.html +1 -1
- package/docs/functions/timeLog.html +1 -1
- package/docs/functions/to.html +1 -1
- package/docs/functions/withCache.html +3 -3
- package/docs/types/CamelToSnake.html +1 -1
- package/docs/types/Capitalize.html +1 -1
- package/docs/types/Decapitalize.html +1 -1
- package/docs/types/DeepMapKeys.html +1 -1
- package/docs/types/DeepMapValues.html +1 -1
- package/docs/types/Falsy.html +1 -1
- package/docs/types/ImageCompressionOptions.html +3 -3
- package/docs/types/Primitive.html +1 -1
- package/docs/types/RequestInit.html +1 -1
- package/docs/types/SetTtl.html +1 -1
- package/docs/types/SnakeToCamel.html +1 -1
- package/package.json +4 -4
- package/src/dom/index.ts +1 -0
- package/src/function/index.ts +1 -0
- package/src/{function.ts → function/loopUntil.ts} +36 -36
- package/src/hoc/index.ts +1 -0
- package/src/{hoc.ts → hoc/withCache.ts} +117 -117
- package/src/is/index.ts +5 -0
- package/src/is/isFalsy.ts +12 -0
- package/src/is/isNil.ts +11 -0
- package/src/is/isObject.ts +10 -0
- package/src/is/isPrimitive.ts +23 -0
- package/src/is/isTruthy.ts +10 -0
- package/src/lru-cache.ts +50 -50
- package/src/{network.ts → network/fetcher.ts} +2 -37
- package/src/network/getRealURL.ts +18 -0
- package/src/network/image.ts +202 -0
- package/src/network/index.ts +4 -0
- package/src/network/to.ts +17 -0
- package/src/number/index.ts +1 -0
- package/src/object/index.ts +3 -0
- package/src/object/mapKeys.ts +50 -0
- package/src/object/mapValues.ts +77 -0
- package/src/object/mergeObjects.ts +50 -0
- package/src/string/case.ts +71 -0
- package/src/string/compact.ts +56 -0
- package/src/string/index.ts +12 -0
- package/src/time/debounce.ts +34 -0
- package/src/time/index.ts +3 -0
- package/src/time/sleep.ts +11 -0
- package/src/time/throttle.ts +34 -0
- package/.github/workflows/docs.yml +0 -39
- package/dist/is.d.ts +0 -43
- package/dist/object.d.ts +0 -50
- package/dist/time.d.ts +0 -47
- package/src/is.ts +0 -70
- package/src/object.ts +0 -179
- package/src/string.ts +0 -259
- package/src/time.ts +0 -81
- /package/dist/{dom.d.ts → dom/timeLog.d.ts} +0 -0
- /package/dist/{function.d.ts → function/loopUntil.d.ts} +0 -0
- /package/dist/{hoc.d.ts → hoc/withCache.d.ts} +0 -0
- /package/dist/{number.d.ts → number/randomInt.d.ts} +0 -0
- /package/src/{dom.ts → dom/timeLog.ts} +0 -0
- /package/src/{number.ts → number/randomInt.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isNil, isObject } from "
|
|
2
|
-
import { mergeObjects } from "
|
|
1
|
+
import { isNil, isObject } from "../is";
|
|
2
|
+
import { mergeObjects } from "../object";
|
|
3
3
|
|
|
4
4
|
// Bun 特有的 fetch 选项
|
|
5
5
|
type BunFetchOptions = {
|
|
@@ -121,38 +121,3 @@ export const fetcher = (baseURL = "", baseOptions: RequestInit = {}) => {
|
|
|
121
121
|
myFetch<T>(url, { ...options, method: "DELETE" }),
|
|
122
122
|
};
|
|
123
123
|
};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Go 语言风格的异步处理方式
|
|
127
|
-
* @param promise 一个能被 await 的异步函数
|
|
128
|
-
* @returns 如果成功,返回 [null, 异步函数结果],否则返回 [Error, undefined]
|
|
129
|
-
*
|
|
130
|
-
* @example
|
|
131
|
-
* const [error, response] = await to(fetcher().get<Blog>("/blogs/hello-world"));
|
|
132
|
-
*/
|
|
133
|
-
export const to = async <T, E = Error>(
|
|
134
|
-
promise: Promise<T>,
|
|
135
|
-
): Promise<[null, T] | [E, undefined]> => {
|
|
136
|
-
try {
|
|
137
|
-
return [null, await promise];
|
|
138
|
-
} catch (e) {
|
|
139
|
-
return [e as E, undefined];
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
/** 从 url 响应头获取真实链接 */
|
|
144
|
-
export const getRealURL = async (originURL: string) => {
|
|
145
|
-
const [error, response] = await to(
|
|
146
|
-
fetch(originURL, {
|
|
147
|
-
method: "HEAD", // 用 HEAD 减少数据传输
|
|
148
|
-
redirect: "manual", // 手动处理重定向
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
if (error) {
|
|
153
|
-
return originURL;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const location = response.headers.get("location");
|
|
157
|
-
return location || originURL;
|
|
158
|
-
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { to } from "./to";
|
|
2
|
+
|
|
3
|
+
/** 从 url 响应头获取真实链接 */
|
|
4
|
+
export const getRealURL = async (originURL: string) => {
|
|
5
|
+
const [error, response] = await to(
|
|
6
|
+
fetch(originURL, {
|
|
7
|
+
method: "HEAD", // 用 HEAD 减少数据传输
|
|
8
|
+
redirect: "manual", // 手动处理重定向
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
if (error) {
|
|
13
|
+
return originURL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const location = response.headers.get("location");
|
|
17
|
+
return location || originURL;
|
|
18
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将 ArrayBuffer 转换为 base64 字符串
|
|
3
|
+
* 兼容浏览器、Bun 和 Node.js
|
|
4
|
+
*/
|
|
5
|
+
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
|
|
6
|
+
const bytes = new Uint8Array(buffer);
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
9
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
10
|
+
}
|
|
11
|
+
return btoa(binary);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 尝试动态导入 sharp 模块
|
|
16
|
+
* 用于检测用户项目中是否安装了 sharp
|
|
17
|
+
*/
|
|
18
|
+
const tryImportSharp = async () => {
|
|
19
|
+
try {
|
|
20
|
+
// 动态导入 sharp,避免在浏览器环境中报错
|
|
21
|
+
// 使用 new Function 避免 TypeScript 编译时解析该模块(sharp 是可选依赖)
|
|
22
|
+
const dynamicImport = new Function(
|
|
23
|
+
"modulePath",
|
|
24
|
+
"return import(modulePath)",
|
|
25
|
+
);
|
|
26
|
+
const sharpModule = await dynamicImport("sharp");
|
|
27
|
+
return sharpModule.default || sharpModule;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 使用 sharp 压缩图片
|
|
35
|
+
*/
|
|
36
|
+
const compressWithSharp = async (
|
|
37
|
+
sharp: any,
|
|
38
|
+
arrayBuffer: ArrayBuffer,
|
|
39
|
+
mime: string,
|
|
40
|
+
quality: number,
|
|
41
|
+
): Promise<string> => {
|
|
42
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
43
|
+
let sharpInstance = sharp(buffer);
|
|
44
|
+
|
|
45
|
+
// 根据 MIME 类型设置压缩选项
|
|
46
|
+
if (mime === "image/jpeg") {
|
|
47
|
+
sharpInstance = sharpInstance.jpeg({ quality: Math.round(quality * 100) });
|
|
48
|
+
} else if (mime === "image/png") {
|
|
49
|
+
// PNG 使用 compressionLevel (0-9),将 quality (0-1) 映射到 compressionLevel
|
|
50
|
+
const compressionLevel = Math.round((1 - quality) * 9);
|
|
51
|
+
sharpInstance = sharpInstance.png({ compressionLevel });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const compressedBuffer = await sharpInstance.toBuffer();
|
|
55
|
+
return `data:${mime};base64,${compressedBuffer.toString("base64")}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 图片压缩选项
|
|
60
|
+
*/
|
|
61
|
+
export type ImageCompressionOptions = {
|
|
62
|
+
/** 压缩比率,默认 0.92 */
|
|
63
|
+
quality?: number;
|
|
64
|
+
/**
|
|
65
|
+
* 自定义压缩函数,用于覆盖默认压缩行为
|
|
66
|
+
* @param arrayBuffer 图片的 ArrayBuffer 数据
|
|
67
|
+
* @param mime 图片的 MIME 类型
|
|
68
|
+
* @param quality 压缩质量
|
|
69
|
+
* @returns 压缩后的 base64 字符串
|
|
70
|
+
*/
|
|
71
|
+
compressor?: (
|
|
72
|
+
arrayBuffer: ArrayBuffer,
|
|
73
|
+
mime: string,
|
|
74
|
+
quality: number,
|
|
75
|
+
) => Promise<string> | string;
|
|
76
|
+
/**
|
|
77
|
+
* 自定义 fetch 函数,用于使用自己封装的请求库读取图片
|
|
78
|
+
* 必须返回符合 Web 标准的 Response 对象
|
|
79
|
+
* @param url 图片地址
|
|
80
|
+
* @returns Promise<Response>
|
|
81
|
+
*/
|
|
82
|
+
fetcher?: (url: string) => Promise<Response>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 图片地址转 base64 数据
|
|
87
|
+
*
|
|
88
|
+
* @param imageUrl 图片地址
|
|
89
|
+
* @param options 可选配置
|
|
90
|
+
* @param options.quality 压缩比率,默认 0.92
|
|
91
|
+
* @param options.compressor 自定义压缩函数,用于覆盖默认压缩行为
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // 基本用法(浏览器自动使用 Canvas 压缩,Node.js/Bun 自动检测并使用 sharp)
|
|
95
|
+
* imageUrlToBase64("https://example.com/image.jpg");
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // 使用自定义 fetch 函数(如 axios 封装)
|
|
99
|
+
* imageUrlToBase64("https://example.com/image.jpg", {
|
|
100
|
+
* fetcher: async (url) => {
|
|
101
|
+
* // 使用 axios 或其他请求库,但必须返回 Response 对象
|
|
102
|
+
* const response = await axios.get(url, { responseType: 'arraybuffer' });
|
|
103
|
+
* return new Response(response.data, {
|
|
104
|
+
* status: response.status,
|
|
105
|
+
* statusText: response.statusText,
|
|
106
|
+
* headers: response.headers
|
|
107
|
+
* });
|
|
108
|
+
* }
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* // 使用自定义压缩函数覆盖默认行为
|
|
113
|
+
* imageUrlToBase64("https://example.com/image.jpg", {
|
|
114
|
+
* quality: 0.8,
|
|
115
|
+
* compressor: async (buffer, mime, quality) => {
|
|
116
|
+
* // 自定义压缩逻辑
|
|
117
|
+
* return `data:${mime};base64,...`;
|
|
118
|
+
* }
|
|
119
|
+
* });
|
|
120
|
+
*/
|
|
121
|
+
export const imageUrlToBase64 = async (
|
|
122
|
+
imageUrl: string,
|
|
123
|
+
options: ImageCompressionOptions = {},
|
|
124
|
+
): Promise<string> => {
|
|
125
|
+
const { quality = 0.92, compressor, fetcher = fetch } = options;
|
|
126
|
+
|
|
127
|
+
if (!imageUrl.startsWith("http")) {
|
|
128
|
+
throw new Error("图片地址必须以http或https开头");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 使用自定义 fetch 获取图片数据
|
|
132
|
+
const response = await fetcher(imageUrl);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`获取图片失败: ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mime = response.headers.get("Content-Type") || "image/jpeg";
|
|
138
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
139
|
+
|
|
140
|
+
// 对于非 JPEG/PNG 图片,直接返回 base64,不做压缩
|
|
141
|
+
if (mime !== "image/jpeg" && mime !== "image/png") {
|
|
142
|
+
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
143
|
+
return `data:${mime};base64,${base64}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 如果提供了自定义压缩函数,优先使用它
|
|
147
|
+
if (compressor) {
|
|
148
|
+
return await compressor(arrayBuffer, mime, quality);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 浏览器环境:使用 OffscreenCanvas 压缩
|
|
152
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
153
|
+
let bitmap: ImageBitmap | null = null;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const blob = new Blob([arrayBuffer], { type: mime });
|
|
157
|
+
bitmap = await createImageBitmap(blob);
|
|
158
|
+
|
|
159
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
160
|
+
const ctx = canvas.getContext("2d");
|
|
161
|
+
if (!ctx) {
|
|
162
|
+
throw new Error("无法获取 OffscreenCanvas context");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
166
|
+
bitmap.close();
|
|
167
|
+
bitmap = null;
|
|
168
|
+
|
|
169
|
+
// OffscreenCanvas 使用 convertToBlob 获取压缩后的图片
|
|
170
|
+
const compressedBlob = await canvas.convertToBlob({
|
|
171
|
+
type: mime,
|
|
172
|
+
quality: quality,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 将 Blob 转换为 base64
|
|
176
|
+
const compressedArrayBuffer = await compressedBlob.arrayBuffer();
|
|
177
|
+
const base64 = arrayBufferToBase64(compressedArrayBuffer);
|
|
178
|
+
return `data:${mime};base64,${base64}`;
|
|
179
|
+
} catch {
|
|
180
|
+
// OffscreenCanvas 压缩失败,返回原始 base64
|
|
181
|
+
bitmap?.close();
|
|
182
|
+
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
183
|
+
return `data:${mime};base64,${base64}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Node.js/Bun 环境:尝试使用 sharp 进行压缩
|
|
188
|
+
const sharp = await tryImportSharp();
|
|
189
|
+
if (sharp) {
|
|
190
|
+
try {
|
|
191
|
+
return await compressWithSharp(sharp, arrayBuffer, mime, quality);
|
|
192
|
+
} catch {
|
|
193
|
+
// sharp 压缩失败,返回原始 base64
|
|
194
|
+
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
195
|
+
return `data:${mime};base64,${base64}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 没有可用的压缩方式,直接返回原始 base64
|
|
200
|
+
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
201
|
+
return `data:${mime};base64,${base64}`;
|
|
202
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go 语言风格的异步处理方式
|
|
3
|
+
* @param promise 一个能被 await 的异步函数
|
|
4
|
+
* @returns 如果成功,返回 [null, 异步函数结果],否则返回 [Error, undefined]
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const [error, response] = await to(fetcher().get<Blog>("/blogs/hello-world"));
|
|
8
|
+
*/
|
|
9
|
+
export const to = async <T, E = Error>(
|
|
10
|
+
promise: Promise<T>,
|
|
11
|
+
): Promise<[null, T] | [E, undefined]> => {
|
|
12
|
+
try {
|
|
13
|
+
return [null, await promise];
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return [e as E, undefined];
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { randomInt } from "./randomInt";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isObject } from "../is";
|
|
2
|
+
|
|
3
|
+
// 这里的 DeepMapKeys 只能承诺 key 是 string,无法推断出 key 的具体字面量变化
|
|
4
|
+
// 因为 TS 不支持根据任意函数反推 Key 的字面量类型
|
|
5
|
+
export type DeepMapKeys<T> =
|
|
6
|
+
T extends Array<infer U>
|
|
7
|
+
? Array<DeepMapKeys<U>>
|
|
8
|
+
: T extends object
|
|
9
|
+
? { [key: string]: DeepMapKeys<T[keyof T]> }
|
|
10
|
+
: T;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 递归处理对象里的 key
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* 无法完整推导出类型,只能做到有递归,key 全为 string,value 为同层级的所有类型的联合
|
|
17
|
+
*
|
|
18
|
+
* @template T 要转换的对象
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const obj = { a: { b: 1 } };
|
|
22
|
+
* const result = mapKeys(obj, (key) => key.toUpperCase());
|
|
23
|
+
* console.log(result); // { A: { B: 1 } }
|
|
24
|
+
*/
|
|
25
|
+
export const mapKeys = <T>(
|
|
26
|
+
obj: T,
|
|
27
|
+
getNewKey: (key: string) => string,
|
|
28
|
+
): DeepMapKeys<T> => {
|
|
29
|
+
// 递归处理数组
|
|
30
|
+
if (Array.isArray(obj)) {
|
|
31
|
+
return obj.map((item) => mapKeys(item, getNewKey)) as any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 处理普通对象
|
|
35
|
+
if (isObject(obj)) {
|
|
36
|
+
const keys = Object.keys(obj);
|
|
37
|
+
return keys.reduce(
|
|
38
|
+
(result, key) => {
|
|
39
|
+
const newKey = getNewKey(key);
|
|
40
|
+
const value = (obj as any)[key];
|
|
41
|
+
result[newKey] = mapKeys(value, getNewKey);
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
{} as Record<string, any>,
|
|
45
|
+
) as DeepMapKeys<T>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 处理非数组/对象
|
|
49
|
+
return obj as any;
|
|
50
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isObject } from "../is";
|
|
2
|
+
|
|
3
|
+
// 这里的 DeepMapValues 尝试保留 key,但将 value 类型替换为 R
|
|
4
|
+
// 注意:如果原 value 是对象,我们递归处理结构,而不是把整个对象变成 R
|
|
5
|
+
export type DeepMapValues<T, R> =
|
|
6
|
+
T extends Array<infer U>
|
|
7
|
+
? Array<DeepMapValues<U, R>>
|
|
8
|
+
: T extends object
|
|
9
|
+
? { [K in keyof T]: T[K] extends object ? DeepMapValues<T[K], R> : R }
|
|
10
|
+
: R;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 递归处理对象里的 value
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* 无法完整推导出类型,所有 value 最终都会变为 any
|
|
17
|
+
*
|
|
18
|
+
* @template T 要转换的对象
|
|
19
|
+
* @template R 转换后的值类型,为 any,无法进一步推导
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const obj = { a: 1, b: { c: 2 } };
|
|
23
|
+
* const result = mapValues(obj, (value, key) => isPrimitive(value) ? value + 1 : value);
|
|
24
|
+
* console.log(result); // { a: 2, b: { c: 3 } }
|
|
25
|
+
*/
|
|
26
|
+
export const mapValues = <T, R = any>(
|
|
27
|
+
obj: T,
|
|
28
|
+
getNewValue: (value: any, key: string | number) => R,
|
|
29
|
+
options?: {
|
|
30
|
+
/** 过滤函数,返回 true 表示保留该字段 */
|
|
31
|
+
filter?: (value: any, key: string | number) => boolean;
|
|
32
|
+
},
|
|
33
|
+
): DeepMapValues<T, R> => {
|
|
34
|
+
const { filter } = options ?? {};
|
|
35
|
+
|
|
36
|
+
// 处理数组
|
|
37
|
+
if (Array.isArray(obj)) {
|
|
38
|
+
const mappedArray = obj.map((item, index) => {
|
|
39
|
+
// 如果元素是对象,则递归处理
|
|
40
|
+
if (isObject(item)) {
|
|
41
|
+
return mapValues(item, getNewValue, options);
|
|
42
|
+
}
|
|
43
|
+
// 如果元素是原始值,则直接应用 getNewValue(此时 key 为数组下标)
|
|
44
|
+
return getNewValue(item, index);
|
|
45
|
+
});
|
|
46
|
+
// 如果有过滤器,则过滤一遍元素
|
|
47
|
+
if (filter) {
|
|
48
|
+
return mappedArray.filter((item, index) => filter(item, index)) as any;
|
|
49
|
+
}
|
|
50
|
+
return mappedArray as any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 处理普通对象
|
|
54
|
+
if (isObject(obj)) {
|
|
55
|
+
const keys = Object.keys(obj);
|
|
56
|
+
return keys.reduce((result, key) => {
|
|
57
|
+
const value = (obj as any)[key];
|
|
58
|
+
let newValue: any;
|
|
59
|
+
// 如果值为对象或数组,则递归处理
|
|
60
|
+
if (isObject(value) || Array.isArray(value)) {
|
|
61
|
+
newValue = mapValues(value, getNewValue, options);
|
|
62
|
+
}
|
|
63
|
+
// 否则直接应用 getNewValue
|
|
64
|
+
else {
|
|
65
|
+
newValue = getNewValue(value, key);
|
|
66
|
+
}
|
|
67
|
+
// 如果存在过滤器,则看情况保留该字段
|
|
68
|
+
if (!filter || filter(newValue, key)) {
|
|
69
|
+
result[key] = newValue;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}, {} as any) as DeepMapValues<T, R>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 处理非数组/对象
|
|
76
|
+
return obj as any;
|
|
77
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isObject, isPrimitive } from "../is";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 深度合并两个对象,规则如下:
|
|
5
|
+
* 1. 原始值覆盖:如果两个值都是原始类型,则用后者覆盖;
|
|
6
|
+
* 2. 数组拼接:如果两个值都是数组,则拼接为大数组;
|
|
7
|
+
* 3. 对象递归合并:如果两个值都是对象,则进行递归深度合并;
|
|
8
|
+
*
|
|
9
|
+
* @template T 第一个对象
|
|
10
|
+
* @template U 第二个对象
|
|
11
|
+
* @param {T} obj1 要合并的第一个对象,相同字段会被 obj2 覆盖
|
|
12
|
+
* @param {U} obj2 要合并的第二个对象
|
|
13
|
+
*/
|
|
14
|
+
export const mergeObjects = <
|
|
15
|
+
T extends Record<string, any>,
|
|
16
|
+
U extends Record<string, any>,
|
|
17
|
+
>(
|
|
18
|
+
obj1: T,
|
|
19
|
+
obj2: U,
|
|
20
|
+
) => {
|
|
21
|
+
const result: Record<string, any> = { ...obj1 };
|
|
22
|
+
|
|
23
|
+
for (const key of Object.keys(obj2)) {
|
|
24
|
+
const val1 = result[key];
|
|
25
|
+
const val2 = obj2[key];
|
|
26
|
+
|
|
27
|
+
// 如果都是原始值,则用后者覆盖
|
|
28
|
+
if (isPrimitive(val1) && isPrimitive(val2)) {
|
|
29
|
+
result[key] = val2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 如果都是数组,则拼接为大数组
|
|
34
|
+
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
35
|
+
result[key] = val1.concat(val2);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 如果都是对象,则深度递归合并
|
|
40
|
+
if (isObject(val1) && isObject(val2)) {
|
|
41
|
+
result[key] = mergeObjects(val1, val2);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 其他情况用后者覆盖
|
|
46
|
+
result[key] = val2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result as T & U;
|
|
50
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type SnakeToCamel<S extends string> =
|
|
2
|
+
S extends `${infer Before}_${infer After}`
|
|
3
|
+
? After extends `${infer First}${infer Rest}`
|
|
4
|
+
? `${Before}${Uppercase<First>}${SnakeToCamel<Rest>}`
|
|
5
|
+
: Before
|
|
6
|
+
: S;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 下划线命名法转为驼峰命名法
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* snakeToCamel("user_name") // "userName"
|
|
13
|
+
*/
|
|
14
|
+
export const snakeToCamel = <S extends string>(str: S): SnakeToCamel<S> => {
|
|
15
|
+
return str.replace(/_([a-zA-Z])/g, (match, pattern) =>
|
|
16
|
+
pattern.toUpperCase(),
|
|
17
|
+
) as SnakeToCamel<S>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CamelToSnake<S extends string> =
|
|
21
|
+
S extends `${infer First}${infer Rest}`
|
|
22
|
+
? Rest extends Uncapitalize<Rest>
|
|
23
|
+
? `${Lowercase<First>}${CamelToSnake<Rest>}`
|
|
24
|
+
: `${Lowercase<First>}_${CamelToSnake<Rest>}`
|
|
25
|
+
: Lowercase<S>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 驼峰命名法转为下划线命名法
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* camelToSnake("shouldComponentUpdate") // "should_component_update"
|
|
32
|
+
*/
|
|
33
|
+
export const camelToSnake = <S extends string>(str: S): CamelToSnake<S> => {
|
|
34
|
+
return str.replace(
|
|
35
|
+
/([A-Z])/g,
|
|
36
|
+
(match, pattern) => `_${pattern.toLowerCase()}`,
|
|
37
|
+
) as CamelToSnake<S>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type Capitalize<S extends string> = S extends `${infer P1}${infer Rest}`
|
|
41
|
+
? P1 extends Capitalize<P1>
|
|
42
|
+
? S
|
|
43
|
+
: `${Uppercase<P1>}${Rest}`
|
|
44
|
+
: S;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 字符串首字母大写
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* capitalize("hello") // "Hello"
|
|
51
|
+
*/
|
|
52
|
+
export const capitalize = <S extends string>(s: S): Capitalize<S> => {
|
|
53
|
+
return (s.charAt(0).toUpperCase() + s.slice(1)) as Capitalize<S>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type Decapitalize<S extends string> =
|
|
57
|
+
S extends `${infer P1}${infer Rest}`
|
|
58
|
+
? P1 extends Lowercase<P1>
|
|
59
|
+
? P1
|
|
60
|
+
: `${Lowercase<P1>}${Rest}`
|
|
61
|
+
: S;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 字符串首字母小写
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* decapitalize("Hello") // "hello"
|
|
68
|
+
*/
|
|
69
|
+
export const decapitalize = <S extends string>(s: S): Decapitalize<S> => {
|
|
70
|
+
return (s.charAt(0).toLowerCase() + s.slice(1)) as Decapitalize<S>;
|
|
71
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将字符串压缩为单行精简格式
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // "Hello, world."
|
|
6
|
+
* compactStr(`
|
|
7
|
+
* Hello,
|
|
8
|
+
* world!
|
|
9
|
+
* `, {
|
|
10
|
+
* disableNewLineReplace: false,
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
export const compactStr = (
|
|
14
|
+
text: string = "",
|
|
15
|
+
options?: {
|
|
16
|
+
/** 最大保留长度,设为 0 或 Infinity 则不截断,默认 Infinity */
|
|
17
|
+
maxLength?: number;
|
|
18
|
+
/** 是否将换行符替换为字面量 \n,默认开启 */
|
|
19
|
+
disableNewLineReplace?: boolean;
|
|
20
|
+
/** 是否合并连续的空格/制表符为一个空格,默认开启 */
|
|
21
|
+
disableWhitespaceCollapse?: boolean;
|
|
22
|
+
/** 截断后的后缀,默认为 "..." */
|
|
23
|
+
omission?: string;
|
|
24
|
+
},
|
|
25
|
+
): string => {
|
|
26
|
+
if (!text) return "";
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
maxLength = Infinity,
|
|
30
|
+
disableNewLineReplace = false,
|
|
31
|
+
disableWhitespaceCollapse = false,
|
|
32
|
+
omission = "...",
|
|
33
|
+
} = options ?? {};
|
|
34
|
+
|
|
35
|
+
let result = text;
|
|
36
|
+
|
|
37
|
+
// 处理换行符
|
|
38
|
+
if (!disableNewLineReplace) {
|
|
39
|
+
result = result.replace(/\r?\n/g, "\\n");
|
|
40
|
+
} else {
|
|
41
|
+
result = result.replace(/\r?\n/g, " ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 合并连续空格
|
|
45
|
+
if (!disableWhitespaceCollapse) {
|
|
46
|
+
result = result.replace(/\s+/g, " ");
|
|
47
|
+
}
|
|
48
|
+
result = result.trim();
|
|
49
|
+
|
|
50
|
+
// 截断多出来的文字
|
|
51
|
+
if (maxLength > 0 && result.length > maxLength) {
|
|
52
|
+
return result.slice(0, maxLength) + omission;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 防抖:在指定时间内只执行最后一次调用
|
|
3
|
+
* @param fn 要防抖的函数
|
|
4
|
+
* @param delay 延迟时间,默认 300ms
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* 连续触发时,只有最后一次会执行。适合用于搜索框输入、窗口大小调整等场景。
|
|
8
|
+
* 例如:用户输入"hello"过程中,不会触发搜索,只有停下来时才执行。
|
|
9
|
+
*
|
|
10
|
+
* 防抖 vs 节流:
|
|
11
|
+
* - 防抖:等待触发停止后才执行(最后一次)
|
|
12
|
+
* - 节流:按固定节奏执行(每隔多久执行一次)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const search = debounce((keyword: string) => {
|
|
16
|
+
* console.log('搜索:', keyword);
|
|
17
|
+
* });
|
|
18
|
+
* search('hello'); // 300ms 后执行
|
|
19
|
+
*/
|
|
20
|
+
export const debounce = <T extends (...args: any[]) => any>(
|
|
21
|
+
fn: T,
|
|
22
|
+
delay = 300,
|
|
23
|
+
) => {
|
|
24
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
25
|
+
|
|
26
|
+
return (...args: Parameters<T>) => {
|
|
27
|
+
if (timer) {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
fn(...args);
|
|
32
|
+
}, delay);
|
|
33
|
+
};
|
|
34
|
+
};
|