pen-it 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,1096 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/is/index.ts
3
+ /**
4
+ * 判断一个值是否为数组
5
+ * @param value - 要判断的值
6
+ * @returns 如果是数组返回 true,否则返回 false
7
+ * @example
8
+ * isArray([1, 2, 3])
9
+ * // => true
10
+ * isArray("hello")
11
+ * // => false
12
+ */
13
+ const isArray = (value) => {
14
+ return Array.isArray(value);
15
+ };
16
+ /**
17
+ * 判断一个值是否为对象(但不是数组)
18
+ * @param value - 要判断的值
19
+ * @returns 如果是对象且不是数组返回 true,否则返回 false
20
+ * @example
21
+ * isObject({ a: 1 })
22
+ * // => true
23
+ * isObject([1, 2])
24
+ * // => false
25
+ */
26
+ const isObject = (value) => {
27
+ return value !== null && typeof value === "object" && !Array.isArray(value);
28
+ };
29
+ /**
30
+ * 判断数据类型(内部工具函数)
31
+ * @param val - 要判断的数据
32
+ * @param type - 数据类型字符串,如 "Array"、"Function"
33
+ * @returns 如果数据是指定类型返回 true
34
+ * @example
35
+ * is([], "Array")
36
+ * // => true
37
+ */
38
+ const is = (val, type) => {
39
+ return Object.prototype.toString.call(val) === `[object ${type}]`;
40
+ };
41
+ /**
42
+ * 判断一个值是否为函数
43
+ * @param val - 要判断的数据
44
+ * @returns 如果是函数返回 true
45
+ * @example
46
+ * isFunction(() => {})
47
+ * // => true
48
+ */
49
+ const isFunction = (val) => {
50
+ return is(val, "Function") || !!(val && val.constructor && val.call && val.apply);
51
+ };
52
+ /**
53
+ * 判断一个值是否为 Date 对象
54
+ * @param val - 要判断的数据
55
+ * @returns 如果是 Date 对象返回 true
56
+ * @example
57
+ * isDate(new Date())
58
+ * // => true
59
+ */
60
+ const isDate = (val) => {
61
+ return is(val, "Date") || !!val && val.constructor === Date;
62
+ };
63
+ /**
64
+ * 判断一个值是否为数字(NaN 和 Infinity 也视为数字)
65
+ * @param val - 要判断的数据
66
+ * @returns 如果是数字返回 true
67
+ * @example
68
+ * isNumber(42)
69
+ * // => true
70
+ * isNumber("42")
71
+ * // => false
72
+ */
73
+ const isNumber = (val) => {
74
+ return Number(val) === val;
75
+ };
76
+ /**
77
+ * 判断一个值是否为整数
78
+ * @param val - 要判断的数据
79
+ * @returns 如果是整数返回 true
80
+ * @example
81
+ * isInt(42)
82
+ * // => true
83
+ * isInt(3.14)
84
+ * // => false
85
+ */
86
+ const isInt = (val) => {
87
+ return isNumber(val) && Number.isFinite(val) && val % 1 === 0;
88
+ };
89
+ /**
90
+ * 判断一个值是否为浮点数(有限小数)
91
+ * @param val - 要判断的数据
92
+ * @returns 如果是浮点数返回 true
93
+ * @example
94
+ * isFloat(3.14)
95
+ * // => true
96
+ * isFloat(42)
97
+ * // => false
98
+ */
99
+ const isFloat = (val) => {
100
+ return isNumber(val) && Number.isFinite(val) && val % 1 !== 0;
101
+ };
102
+ /**
103
+ * 判断一个值是否为异步函数
104
+ * @param val - 要判断的数据
105
+ * @returns 如果是异步函数返回 true
106
+ * @example
107
+ * isAsyncFunction(async () => {})
108
+ * // => true
109
+ */
110
+ const isAsyncFunction = (val) => {
111
+ return is(val, "AsyncFunction");
112
+ };
113
+ /**
114
+ * 判断一个值是否为 Promise
115
+ * @param value - 要判断的数据
116
+ * @returns 如果是 Promise 返回 true
117
+ * @example
118
+ * isPromise(Promise.resolve())
119
+ * // => true
120
+ */
121
+ const isPromise = (value) => {
122
+ if (!value) return false;
123
+ if (!value.then) return false;
124
+ if (!isFunction(value.then)) return false;
125
+ return true;
126
+ };
127
+ /**
128
+ * 判断一个值是否为字符串
129
+ * @param val - 要判断的数据
130
+ * @returns 如果是字符串返回 true
131
+ * @example
132
+ * isString("hello")
133
+ * // => true
134
+ * isString(42)
135
+ * // => false
136
+ */
137
+ const isString = (val) => {
138
+ return is(val, "String") || typeof val === "string" || val instanceof String;
139
+ };
140
+ /**
141
+ * 判断一个值是否为布尔值
142
+ * @param val - 要判断的数据
143
+ * @returns 如果是布尔值返回 true
144
+ * @example
145
+ * isBoolean(true)
146
+ * // => true
147
+ * isBoolean(1)
148
+ * // => false
149
+ */
150
+ const isBoolean = (val) => {
151
+ return typeof val === "boolean" || is(val, "Boolean");
152
+ };
153
+ /**
154
+ * 判断是否为 PC 端(浏览器环境)
155
+ * @returns 如果在浏览器环境中返回 true
156
+ * @example
157
+ * isPC()
158
+ * // => true(在浏览器中)
159
+ */
160
+ const isPC = () => {
161
+ return typeof window !== "undefined";
162
+ };
163
+ /**
164
+ * 判断一个值是否为 window 对象
165
+ * @param val - 要判断的数据
166
+ * @returns 如果是 window 对象返回 true
167
+ * @example
168
+ * isWindow(window)
169
+ * // => true(在浏览器中)
170
+ */
171
+ const isWindow = (val) => {
172
+ return typeof window !== "undefined" && is(val, "Window");
173
+ };
174
+ /**
175
+ * 判断一个值是否为 DOM 元素
176
+ * @param val - 要判断的数据
177
+ * @returns 如果是 DOM 元素返回 true
178
+ * @example
179
+ * isElement(document.body)
180
+ * // => true
181
+ */
182
+ const isElement = (val) => {
183
+ return isObject(val) && !!val.tagName;
184
+ };
185
+ /**
186
+ * 判断一个值是否为 null
187
+ * @param val - 要判断的数据
188
+ * @returns 如果是 null 返回 true
189
+ * @example
190
+ * isNull(null)
191
+ * // => true
192
+ */
193
+ const isNull = (val) => {
194
+ return val === null;
195
+ };
196
+ /**
197
+ * 判断一个值是否为十六进制颜色
198
+ * @param str - 要判断的字符串
199
+ * @returns 如果是有效的十六进制颜色值返回 true
200
+ * @example
201
+ * isHexColor("#fff")
202
+ * // => true
203
+ * isHexColor("#ff0000")
204
+ * // => true
205
+ */
206
+ const isHexColor = (str) => {
207
+ return /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(str);
208
+ };
209
+ /**
210
+ * 判断设备是否为 iOS
211
+ * @returns 如果是 iOS 设备返回 true
212
+ * @example
213
+ * isIOS()
214
+ * // => true(在 iPhone/iPad 上)
215
+ */
216
+ const isIOS = () => {
217
+ return /iPad|iPhone|iPod/.test(navigator.userAgent);
218
+ };
219
+ /**
220
+ * 判断一个值是否为有效的电子邮件
221
+ * @param email - 要判断的字符串
222
+ * @returns 如果是有效的电子邮件地址返回 true
223
+ * @example
224
+ * isValidEmail("test@example.com")
225
+ * // => true
226
+ */
227
+ const isValidEmail = (email) => {
228
+ return /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(email);
229
+ };
230
+ /**
231
+ * 判断一个值是否已定义(不是 undefined)
232
+ * @param val - 要判断的数据
233
+ * @returns 如果不是 undefined 返回 true
234
+ * @example
235
+ * isDef("hello")
236
+ * // => true
237
+ * isDef(undefined)
238
+ * // => false
239
+ */
240
+ const isDef = (val) => {
241
+ return typeof val !== "undefined";
242
+ };
243
+ /**
244
+ * 判断一个值是否为 undefined
245
+ * @param val - 要判断的数据
246
+ * @returns 如果是 undefined 返回 true
247
+ * @example
248
+ * isUnDef(undefined)
249
+ * // => true
250
+ */
251
+ const isUnDef = (val) => {
252
+ return typeof val === "undefined";
253
+ };
254
+ /**
255
+ * 判断一个值是否为 null 或 undefined
256
+ * @param val - 要判断的数据
257
+ * @returns 如果是 null 或 undefined 返回 true
258
+ * @example
259
+ * isNullOrUnDef(null)
260
+ * // => true
261
+ * isNullOrUnDef(undefined)
262
+ * // => true
263
+ * isNullOrUnDef("hello")
264
+ * // => false
265
+ */
266
+ const isNullOrUnDef = (val) => {
267
+ return isUnDef(val) || isNull(val);
268
+ };
269
+ /**
270
+ * 判断一个值是否为 symbol
271
+ * @param value - 要判断的数据
272
+ * @returns 如果是 symbol 返回 true
273
+ * @example
274
+ * isSymbol(Symbol("foo"))
275
+ * // => true
276
+ */
277
+ const isSymbol = (value) => {
278
+ return !!value && value.constructor === Symbol;
279
+ };
280
+ /**
281
+ * 判断一个值是否为原始类型
282
+ * 原始类型包括:number、string、boolean、symbol、bigint、undefined、null
283
+ * @param value - 要判断的值
284
+ * @returns 如果是原始类型返回 true
285
+ * @example
286
+ * isPrimitive(42)
287
+ * // => true
288
+ * isPrimitive({})
289
+ * // => false
290
+ */
291
+ const isPrimitive = (value) => {
292
+ return value === void 0 || value === null || typeof value !== "object" && typeof value !== "function";
293
+ };
294
+ /**
295
+ * 判断一个值是否为空
296
+ * 空值包括:null、undefined、空字符串、空数组、空对象、空 Map/Set、无效 Date
297
+ * @param value - 要判断的数据
298
+ * @returns 如果为空返回 true
299
+ * @example
300
+ * isEmpty("")
301
+ * // => true
302
+ * isEmpty([])
303
+ * // => true
304
+ * isEmpty(0)
305
+ * // => false
306
+ */
307
+ const isEmpty = (value) => {
308
+ if (value === null || value === void 0) return true;
309
+ if (isDate(value)) return isNaN(value.getTime());
310
+ if (isFunction(value) || isSymbol(value)) return false;
311
+ if (isNumber(value)) return false;
312
+ const length = value.length;
313
+ if (isNumber(length)) return length === 0;
314
+ const size = value.size;
315
+ if (isNumber(size)) return size === 0;
316
+ return Object.keys(value).length === 0;
317
+ };
318
+ /**
319
+ * 判断一个值是否为空(简化版)
320
+ * 支持:字符串、数组、对象、Map、Set、类型化数组
321
+ * @param value - 要检查的值
322
+ * @returns 如果为空返回 true
323
+ * @example
324
+ * isEmptySv("")
325
+ * // => true
326
+ * isEmptySv(null)
327
+ * // => true
328
+ * isEmptySv([])
329
+ * // => true
330
+ * isEmptySv({})
331
+ * // => true
332
+ * isEmptySv(new Set())
333
+ * // => true
334
+ * isEmptySv(new Map())
335
+ * // => true
336
+ */
337
+ function isEmptySv(value) {
338
+ if (value === null || value === void 0) return true;
339
+ if (typeof value === "string" || Array.isArray(value)) return value.length === 0;
340
+ if (value instanceof Map || value instanceof Set) return value.size === 0;
341
+ if (ArrayBuffer.isView(value)) return value.byteLength === 0;
342
+ if (typeof value === "object") return Object.keys(value).length === 0;
343
+ return false;
344
+ }
345
+ /**
346
+ * 判断两个值是否深度相等
347
+ * @param x - 要比较的第一个值
348
+ * @param y - 要比较的第二个值
349
+ * @returns 如果值深度相等返回 true
350
+ * @example
351
+ * isEqual({ a: 1 }, { a: 1 })
352
+ * // => true
353
+ * isEqual({ a: 1 }, { a: 2 })
354
+ * // => false
355
+ */
356
+ const isEqual = (x, y) => {
357
+ if (Object.is(x, y)) return true;
358
+ if (x instanceof Date && y instanceof Date) return x.getTime() === y.getTime();
359
+ if (x instanceof RegExp && y instanceof RegExp) return x.toString() === y.toString();
360
+ if (typeof x !== "object" || x === null || typeof y !== "object" || y === null) return false;
361
+ const keysX = Reflect.ownKeys(x);
362
+ const keysY = Reflect.ownKeys(y);
363
+ if (keysX.length !== keysY.length) return false;
364
+ for (let i = 0; i < keysX.length; i++) {
365
+ if (!Reflect.has(y, keysX[i])) return false;
366
+ if (!isEqual(x[keysX[i]], y[keysX[i]])) return false;
367
+ }
368
+ return true;
369
+ };
370
+ //#endregion
371
+ //#region src/format/index.ts
372
+ /**
373
+ * 完整日期时间(年/月/日 时分秒)_ 斜杠
374
+ * @param date - Date 对象
375
+ * @returns 格式化后的完整日期时间字符串,如 "2026/06/03 14:30:45"
376
+ * @example
377
+ * formatFull(new Date("2026-06-03T14:30:45"))
378
+ * // => "2026/06/03 14:30:45"
379
+ */
380
+ const formatFull = (date) => {
381
+ return new Intl.DateTimeFormat("zh-CN", {
382
+ year: "numeric",
383
+ month: "2-digit",
384
+ day: "2-digit",
385
+ hour: "2-digit",
386
+ minute: "2-digit",
387
+ second: "2-digit",
388
+ hour12: false
389
+ }).format(date);
390
+ };
391
+ /**
392
+ * 完整日期时间(年-月-日 时分秒)_ 短横线
393
+ * @param date - Date 对象
394
+ * @returns 格式化后的完整日期时间字符串,如 "2026-06-03 14:30:45"
395
+ * @example
396
+ * formatFullReplace(new Date("2026-06-03T14:30:45"))
397
+ * // => "2026-06-03 14:30:45"
398
+ */
399
+ const formatFullReplace = (date) => {
400
+ return formatFull(date).replace(/\//g, "-");
401
+ };
402
+ /**
403
+ * 中文年月日
404
+ * @param date - Date 对象
405
+ * @returns 格式化后的中文日期字符串,如 "2026年6月3日"
406
+ * @example
407
+ * formatYMD(new Date("2026-06-03"))
408
+ * // => "2026年6月3日"
409
+ */
410
+ const formatYMD = (date) => {
411
+ return new Intl.DateTimeFormat("zh-CN", {
412
+ year: "numeric",
413
+ month: "long",
414
+ day: "numeric"
415
+ }).format(date);
416
+ };
417
+ /**
418
+ * 星期几
419
+ * @param date - Date 对象
420
+ * @returns 中文星期字符串,如 "星期三"
421
+ * @example
422
+ * formatWeek(new Date("2026-06-03"))
423
+ * // => "星期三"
424
+ */
425
+ const formatWeek = (date) => {
426
+ return new Intl.DateTimeFormat("zh-CN", { weekday: "long" }).format(date);
427
+ };
428
+ /**
429
+ * 货币格式化
430
+ * @param value - 数值
431
+ * @param options - 格式化选项
432
+ * @returns 格式化后的货币字符串
433
+ * @example
434
+ * formatRmb(1234.56, { type: "zh-CN", currency: "CNY" })
435
+ * // => "¥1,234.56"
436
+ */
437
+ const formatRmb = (value, options) => {
438
+ return new Intl.NumberFormat(options.type, {
439
+ style: "currency",
440
+ currency: options.currency
441
+ }).format(value);
442
+ };
443
+ /**
444
+ * 千位分隔符(数字格式化),中文(逗号分隔)
445
+ * @param value - 数值
446
+ * @returns 格式化后的千位分隔数字字符串
447
+ * @example
448
+ * formatNum(1234567)
449
+ * // => "1,234,567"
450
+ */
451
+ const formatNum = (value) => {
452
+ return new Intl.NumberFormat("zh-CN").format(value);
453
+ };
454
+ /**
455
+ * 百分比格式化
456
+ * @param value - 百分比数值
457
+ * @param digit - 保留的小数位数,默认为 0
458
+ * @returns 格式化后的百分比字符串
459
+ * @example
460
+ * percentCN(0.1234, 2)
461
+ * // => "12.34%"
462
+ */
463
+ const percentCN = (value, digit = 0) => {
464
+ return new Intl.NumberFormat("zh-CN", {
465
+ style: "percent",
466
+ minimumFractionDigits: digit
467
+ }).format(value);
468
+ };
469
+ /**
470
+ * 紧凑计数法(大数简化)_ 英文缩写
471
+ * @param value - 数值
472
+ * @returns 格式化后的紧凑数字字符串(英文缩写)
473
+ * @example
474
+ * compactEN(12345)
475
+ * // => "12K"
476
+ */
477
+ const compactEN = (value) => {
478
+ return new Intl.NumberFormat("en-US", {
479
+ notation: "compact",
480
+ compactDisplay: "short"
481
+ }).format(value);
482
+ };
483
+ /**
484
+ * 紧凑计数法(大数简化)_ 中文缩写
485
+ * @param value - 数值
486
+ * @returns 格式化后的紧凑数字字符串(中文缩写)
487
+ * @example
488
+ * compactCN(12345)
489
+ * // => "1.2万"
490
+ */
491
+ const compactCN = (value) => {
492
+ return new Intl.NumberFormat("zh-CN", {
493
+ notation: "compact",
494
+ compactDisplay: "short"
495
+ }).format(value);
496
+ };
497
+ /**
498
+ * 带符号的正负数显示
499
+ * @param value - 数值
500
+ * @param digit - 保留的小数位数,默认为 0
501
+ * @returns 格式化后的带正负号的数值显示
502
+ * @example
503
+ * signed(42, 1)
504
+ * // => "+42.0"
505
+ * @example
506
+ * signed(-5)
507
+ * // => "-5"
508
+ */
509
+ const signed = (value, digit = 0) => {
510
+ return new Intl.NumberFormat("en-US", {
511
+ signDisplay: "always",
512
+ minimumFractionDigits: digit
513
+ }).format(value);
514
+ };
515
+ /**
516
+ * 手机号格式化脱敏 _ 隐藏中间 4 位数
517
+ * @param phone - 手机号码
518
+ * @returns 手机号格式化脱敏后的手机号
519
+ * @example
520
+ * maskPhone("13812345678")
521
+ * // => "138****5678"
522
+ */
523
+ const maskPhone = (phone) => {
524
+ return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
525
+ };
526
+ /**
527
+ * 手机号格式化 _ 空格分隔
528
+ * @param phone - 手机号
529
+ * @returns 手机号空格格式化的手机号
530
+ * @example
531
+ * spacePhone("13812345678")
532
+ * // => "138 1234 5678"
533
+ */
534
+ const spacePhone = (phone) => {
535
+ return phone.replace(/(\d{3})(\d{4})(\d{4})/, "$1 $2 $3");
536
+ };
537
+ /**
538
+ * 每个单词首字母大写
539
+ * @param str - 字符串
540
+ * @returns 每个单词首字母大写后的字符串
541
+ * @example
542
+ * capitalize("hello world")
543
+ * // => "Hello World"
544
+ */
545
+ const capitalize = (str) => {
546
+ return str.replace(/\b\w/g, (char) => char.toUpperCase());
547
+ };
548
+ /**
549
+ * 短横线连接转小驼峰(kebab-case → camelCase)
550
+ * @param str - 字符串
551
+ * @returns 转换后的小驼峰字符串
552
+ * @example
553
+ * kebabToCamel("hello-world")
554
+ * // => "helloWorld"
555
+ */
556
+ const kebabToCamel = (str) => {
557
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
558
+ };
559
+ /**
560
+ * 大驼峰或小驼峰 _ 转为 ‘-’ 短横线连接
561
+ * @param str - 字符串
562
+ * @returns 大驼峰或小驼峰命名格式化的字符串
563
+ * @example
564
+ * camelToKebab("helloWorld")
565
+ * // => "hello-world"
566
+ */
567
+ const camelToKebab = (str) => {
568
+ return str.replace(/([A-Z])/g, "-$1").toLowerCase();
569
+ };
570
+ /**
571
+ * 超长文本截断
572
+ * @param str - 内容
573
+ * @param maxLength - 截断位置
574
+ * @param suffix - 自定义的替换截断后的内容
575
+ * @returns 超长文本截断后内容
576
+ * @example
577
+ * truncate("Hello World", 8)
578
+ * // => "Hello..."
579
+ */
580
+ const truncate = (str, maxLength, suffix = "...") => {
581
+ if (str.length <= maxLength) return str;
582
+ return str.substring(0, maxLength - suffix.length) + suffix;
583
+ };
584
+ /**
585
+ * 按字数截断(中文友好)
586
+ * @param str - 内容
587
+ * @param maxWords - 截断位置
588
+ * @param suffix - 自定义的替换截断后的内容
589
+ * @returns 超长文本截断后内容
590
+ * @example
591
+ * truncateByWords("你好世界欢迎你", 4)
592
+ * // => "你好世界..."
593
+ */
594
+ const truncateByWords = (str, maxWords, suffix = "...") => {
595
+ const chars = str.split("");
596
+ if (chars.length <= maxWords) return str;
597
+ return chars.slice(0, maxWords).join("") + suffix;
598
+ };
599
+ /**
600
+ * 去除所有空格 _ 前后中间
601
+ * @param str - 内容
602
+ * @returns 去除空格后的内容
603
+ * @example
604
+ * trimAll(" hello world ")
605
+ * // => "helloworld"
606
+ */
607
+ const trimAll = (str) => str.replace(/\s+/g, "");
608
+ /**
609
+ * 下划线转驼峰(snake_case → camelCase)
610
+ * @param str - 字符串
611
+ * @returns 转换后的驼峰字符串
612
+ * @example
613
+ * toCamel("hello_world")
614
+ * // => "helloWorld"
615
+ */
616
+ const toCamel = (str) => {
617
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
618
+ };
619
+ /**
620
+ * 首字母大写
621
+ * @param str - 字符串
622
+ * @returns 首字母大写后的字符串
623
+ * @example
624
+ * firstUpper("hello")
625
+ * // => "Hello"
626
+ */
627
+ const firstUpper = (str) => {
628
+ return str.charAt(0).toUpperCase() + str.slice(1);
629
+ };
630
+ /**
631
+ * 首字母小写
632
+ * @param str - 字符串
633
+ * @returns 首字母小写后的字符串
634
+ * @example
635
+ * firstLower("Hello")
636
+ * // => "hello"
637
+ */
638
+ const firstLower = (str) => {
639
+ return str.charAt(0).toLowerCase() + str.slice(1);
640
+ };
641
+ /**
642
+ * 反转字符串
643
+ * @param str - 字符串
644
+ * @returns 反转后的字符串
645
+ * @example
646
+ * reverse("hello")
647
+ * // => "olleh"
648
+ */
649
+ const reverse = (str) => {
650
+ return str.split("").reverse().join("");
651
+ };
652
+ //#endregion
653
+ //#region src/storage/index.ts
654
+ /**
655
+ * 获取 localStorage
656
+ * @param key - 存储键名
657
+ * @returns 解析后的值,解析失败则返回原始字符串,键不存在返回 null
658
+ * @example
659
+ * localGet("user")
660
+ * // => { name: "张三" }
661
+ */
662
+ const localGet = (key) => {
663
+ const value = localStorage.getItem(key);
664
+ if (value === null) return null;
665
+ try {
666
+ return JSON.parse(value);
667
+ } catch {
668
+ return value;
669
+ }
670
+ };
671
+ /**
672
+ * 设置 localStorage
673
+ * @param key - 存储键名
674
+ * @param value - 存储值(会自动 JSON 序列化)
675
+ * @example
676
+ * localSet("user", { name: "张三" })
677
+ */
678
+ const localSet = (key, value) => {
679
+ localStorage.setItem(key, JSON.stringify(value));
680
+ };
681
+ /**
682
+ * 移除指定 localStorage
683
+ * @param key - 存储键名
684
+ * @example
685
+ * localRm("user")
686
+ */
687
+ const localRm = (key) => {
688
+ localStorage.removeItem(key);
689
+ };
690
+ /**
691
+ * 清除所有 localStorage
692
+ * @example
693
+ * localClear()
694
+ */
695
+ const localClear = () => {
696
+ localStorage.clear();
697
+ };
698
+ //#endregion
699
+ //#region src/copy/index.ts
700
+ /**
701
+ * 递归深拷贝
702
+ * 支持 Date、RegExp、Map、Set、Array、Object 以及循环引用
703
+ * @param obj - 要拷贝的值
704
+ * @param hash - 内部 WeakMap 用于处理循环引用,调用方无需传入
705
+ * @returns 深拷贝后的值
706
+ * @example
707
+ * const obj = { a: 1, b: { c: 2 }, d: new Date() };
708
+ * const cloned = deepClone(obj);
709
+ * // cloned.b !== obj.b
710
+ * // cloned.d !== obj.d
711
+ */
712
+ function deepClone(obj, hash = /* @__PURE__ */ new WeakMap()) {
713
+ if (obj === null || typeof obj !== "object") return obj;
714
+ if (hash.has(obj)) return hash.get(obj);
715
+ if (obj instanceof Date) return new Date(obj.getTime());
716
+ if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
717
+ if (obj instanceof Map) {
718
+ const clone = /* @__PURE__ */ new Map();
719
+ hash.set(obj, clone);
720
+ obj.forEach((value, key) => {
721
+ clone.set(key, deepClone(value, hash));
722
+ });
723
+ return clone;
724
+ }
725
+ if (obj instanceof Set) {
726
+ const clone = /* @__PURE__ */ new Set();
727
+ hash.set(obj, clone);
728
+ obj.forEach((value) => {
729
+ clone.add(deepClone(value, hash));
730
+ });
731
+ return clone;
732
+ }
733
+ if (Array.isArray(obj)) {
734
+ const clone = [];
735
+ hash.set(obj, clone);
736
+ obj.forEach((item, index) => {
737
+ clone[index] = deepClone(item, hash);
738
+ });
739
+ return clone;
740
+ }
741
+ const clone = {};
742
+ hash.set(obj, clone);
743
+ Object.keys(obj).forEach((key) => {
744
+ clone[key] = deepClone(obj[key], hash);
745
+ });
746
+ return clone;
747
+ }
748
+ /**
749
+ * JSON 深拷贝
750
+ * 使用 JSON 序列化与反序列化实现,仅支持 JSON 安全的数据类型
751
+ * (不支持函数、undefined、Date、RegExp、Map、Set 等)
752
+ * @param obj - 要拷贝的 JSON 安全值
753
+ * @returns 深拷贝后的值
754
+ * @example
755
+ * deepCloneWithJSON({ a: 1, b: [2, 3] })
756
+ * // => { a: 1, b: [2, 3] }
757
+ */
758
+ function deepCloneWithJSON(obj) {
759
+ return JSON.parse(JSON.stringify(obj));
760
+ }
761
+ /**
762
+ * 浅拷贝
763
+ * 仅拷贝第一层属性,嵌套对象仍共享引用
764
+ * @param obj - 要拷贝的对象或数组
765
+ * @returns 浅拷贝后的值
766
+ * @example
767
+ * const obj = { a: 1, b: { c: 2 } };
768
+ * const cloned = shallowClone(obj);
769
+ * // cloned.b === obj.b(共享引用)
770
+ */
771
+ function shallowClone(obj) {
772
+ if (obj === null || typeof obj !== "object") return obj;
773
+ if (Array.isArray(obj)) return [...obj];
774
+ return { ...obj };
775
+ }
776
+ //#endregion
777
+ //#region src/array/index.ts
778
+ /**
779
+ * Set 去重
780
+ * @param arr - 数组
781
+ * @returns 去重后的数组
782
+ * @example
783
+ * unique([1, 2, 2, 3])
784
+ * // => [1, 2, 3]
785
+ */
786
+ const unique = (arr) => {
787
+ return [...new Set(arr)];
788
+ };
789
+ /**
790
+ * 对象数组按 key 去重
791
+ * @param arr - 对象数组
792
+ * @param key - 用于去重判定的键名
793
+ * @returns 去重后的数组
794
+ * @example
795
+ * uniqueByKey([{ id: 1, name: "a" }, { id: 1, name: "b" }], "id")
796
+ * // => [{ id: 1, name: "a" }]
797
+ */
798
+ const uniqueByKey = (arr, key) => {
799
+ return [...new Map(arr.map((item) => [item[key], item])).values()];
800
+ };
801
+ /**
802
+ * 数值升序
803
+ * @param arr - 数值数组
804
+ * @returns 升序排序后的新数组
805
+ * @example
806
+ * sortNumAsc([3, 1, 2])
807
+ * // => [1, 2, 3]
808
+ */
809
+ const sortNumAsc = (arr) => {
810
+ return [...arr].sort((a, b) => a - b);
811
+ };
812
+ /**
813
+ * 数值降序
814
+ * @param arr - 数值数组
815
+ * @returns 降序排序后的新数组
816
+ * @example
817
+ * sortNumDesc([1, 3, 2])
818
+ * // => [3, 2, 1]
819
+ */
820
+ const sortNumDesc = (arr) => {
821
+ return [...arr].sort((a, b) => b - a);
822
+ };
823
+ /**
824
+ * 按对象属性排序
825
+ * @param arr - 对象数组
826
+ * @param key - 用于排序的属性名
827
+ * @param order - 排序方向,"asc" 升序 / "desc" 降序,默认 "asc"
828
+ * @returns 排序后的新数组
829
+ * @example
830
+ * sortByKey([{ age: 30 }, { age: 20 }], "age", "asc")
831
+ * // => [{ age: 20 }, { age: 30 }]
832
+ */
833
+ const sortByKey = (arr, key, order = "asc") => {
834
+ return [...arr].sort((a, b) => order === "asc" ? a[key] - b[key] : b[key] - a[key]);
835
+ };
836
+ /**
837
+ * 类数组转数组
838
+ * @param arrayLike - 类数组对象(如 NodeList、arguments)
839
+ * @returns 转换后的数组
840
+ * @example
841
+ * toArray(document.querySelectorAll("div"))
842
+ * // => [div, div, ...]
843
+ */
844
+ const toArray = (arrayLike) => {
845
+ return Array.from(arrayLike);
846
+ };
847
+ //#endregion
848
+ //#region src/cookie/index.ts
849
+ /**
850
+ * 设置 Cookie
851
+ * @param name - Cookie 名称
852
+ * @param value - Cookie 值
853
+ * @param days - 有效天数,默认 7 天
854
+ * @example
855
+ * setCookie("token", "abc123", 30)
856
+ */
857
+ const setCookie = (name, value, days = 0) => {
858
+ const expires = /* @__PURE__ */ new Date();
859
+ expires.setDate(expires.getDate() + days);
860
+ document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=/`;
861
+ };
862
+ /**
863
+ * 获取 Cookie
864
+ * @param name - Cookie 名称
865
+ * @returns Cookie 值,不存在则返回 null
866
+ * @example
867
+ * getCookie("token")
868
+ * // => "abc123"
869
+ */
870
+ const getCookie = (name) => {
871
+ const match = document.cookie.match(new RegExp("(^| )" + encodeURIComponent(name) + "=([^;]+)"));
872
+ return match ? decodeURIComponent(match[2]) : null;
873
+ };
874
+ /**
875
+ * 删除 Cookie
876
+ * @param name - Cookie 名称
877
+ * @example
878
+ * delCookie("token")
879
+ */
880
+ const delCookie = (name) => {
881
+ setCookie(name, "", -1);
882
+ };
883
+ //#endregion
884
+ //#region src/browser/index.ts
885
+ /**
886
+ * 获取 URL 参数对象
887
+ * @param url - URL 字符串,默认使用当前页面地址
888
+ * @returns 参数键值对对象
889
+ * @example
890
+ * getUrlParams("https://example.com?a=1&b=2")
891
+ * // => { a: "1", b: "2" }
892
+ */
893
+ const getUrlParams = (url) => {
894
+ const params = {};
895
+ new URL(url || window.location.href).searchParams.forEach((val, key) => {
896
+ params[key] = val;
897
+ });
898
+ return params;
899
+ };
900
+ /**
901
+ * 获取单个 URL 参数
902
+ * @param key - 参数名
903
+ * @param url - URL 字符串,默认使用当前页面地址
904
+ * @returns 参数值,不存在则返回 null
905
+ * @example
906
+ * getUrlParam("a", "https://example.com?a=1")
907
+ * // => "1"
908
+ */
909
+ const getUrlParam = (key, url) => {
910
+ return new URL(url || window.location.href).searchParams.get(key);
911
+ };
912
+ /**
913
+ * 对象转 URL 参数字符串
914
+ * @param params - 参数键值对对象,值为 null/undefined 的项会被过滤
915
+ * @returns URL 参数字符串(不含 ? 前缀)
916
+ * @example
917
+ * toQueryString({ a: 1, b: "hello", c: null })
918
+ * // => "a=1&b=hello"
919
+ */
920
+ const toQueryString = (params) => {
921
+ return Object.entries(params).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
922
+ };
923
+ /**
924
+ * 复制文本到剪贴板
925
+ * 优先使用 Clipboard API,失败时降级为 execCommand
926
+ * @param text - 要复制的文本
927
+ * @returns 复制成功返回 true,失败返回 false
928
+ * @example
929
+ * await copyToClipboard("Hello World")
930
+ * // => true
931
+ */
932
+ const copyToClipboard = async (text) => {
933
+ if (navigator.clipboard) try {
934
+ await navigator.clipboard.writeText(text);
935
+ return true;
936
+ } catch {
937
+ return false;
938
+ }
939
+ const textarea = document.createElement("textarea");
940
+ textarea.value = text;
941
+ textarea.style.position = "fixed";
942
+ textarea.style.opacity = "0";
943
+ document.body.appendChild(textarea);
944
+ textarea.select();
945
+ const result = document.execCommand("copy");
946
+ document.body.removeChild(textarea);
947
+ return result;
948
+ };
949
+ /**
950
+ * 下载文件(Blob)
951
+ * @param content - 文件内容
952
+ * @param filename - 文件名
953
+ * @param mimeType - MIME 类型,默认 "text/plain"
954
+ * @example
955
+ * downloadFile("Hello", "hello.txt")
956
+ */
957
+ const downloadFile = (content, filename, mimeType = "text/plain") => {
958
+ const blob = new Blob([content], { type: mimeType });
959
+ const url = URL.createObjectURL(blob);
960
+ const a = document.createElement("a");
961
+ a.href = url;
962
+ a.download = filename;
963
+ a.click();
964
+ URL.revokeObjectURL(url);
965
+ };
966
+ /**
967
+ * 导出 JSON 为文件
968
+ * @param data - 要导出的数据
969
+ * @param filename - 文件名,默认 "data.json"
970
+ * @example
971
+ * exportJSON({ name: "张三", age: 30 })
972
+ */
973
+ const exportJSON = (data, filename = "data.json") => {
974
+ downloadFile(JSON.stringify(data, null, 2), filename, "application/json");
975
+ };
976
+ /**
977
+ * 滚动到顶部
978
+ * @param behavior - 滚动行为,"smooth" 平滑 / "auto" 瞬间,默认 "smooth"
979
+ * @example
980
+ * scrollToTop()
981
+ */
982
+ const scrollToTop = (behavior = "smooth") => {
983
+ window.scrollTo({
984
+ top: 0,
985
+ behavior
986
+ });
987
+ };
988
+ /**
989
+ * 滚动到底部
990
+ * @param behavior - 滚动行为,"smooth" 平滑 / "auto" 瞬间,默认 "smooth"
991
+ * @example
992
+ * scrollToBottom()
993
+ */
994
+ const scrollToBottom = (behavior = "smooth") => {
995
+ window.scrollTo({
996
+ top: document.documentElement.scrollHeight,
997
+ behavior
998
+ });
999
+ };
1000
+ /**
1001
+ * 监听页面滚动(requestAnimationFrame 节流)
1002
+ * @param callback - 滚动回调,接收当前 scrollY 值
1003
+ * @returns 清理函数,调用后移除事件监听
1004
+ * @example
1005
+ * const cleanup = onScroll((y) => console.log(y));
1006
+ * cleanup(); // 停止监听
1007
+ */
1008
+ const onScroll = (callback) => {
1009
+ let ticking = false;
1010
+ const handler = () => {
1011
+ if (!ticking) {
1012
+ requestAnimationFrame(() => {
1013
+ callback(window.scrollY);
1014
+ ticking = false;
1015
+ });
1016
+ ticking = true;
1017
+ }
1018
+ };
1019
+ window.addEventListener("scroll", handler);
1020
+ return () => window.removeEventListener("scroll", handler);
1021
+ };
1022
+ //#endregion
1023
+ exports.camelToKebab = camelToKebab;
1024
+ exports.capitalize = capitalize;
1025
+ exports.compactCN = compactCN;
1026
+ exports.compactEN = compactEN;
1027
+ exports.copyToClipboard = copyToClipboard;
1028
+ exports.deepClone = deepClone;
1029
+ exports.deepCloneWithJSON = deepCloneWithJSON;
1030
+ exports.delCookie = delCookie;
1031
+ exports.downloadFile = downloadFile;
1032
+ exports.exportJSON = exportJSON;
1033
+ exports.firstLower = firstLower;
1034
+ exports.firstUpper = firstUpper;
1035
+ exports.formatFull = formatFull;
1036
+ exports.formatFullReplace = formatFullReplace;
1037
+ exports.formatNum = formatNum;
1038
+ exports.formatRmb = formatRmb;
1039
+ exports.formatWeek = formatWeek;
1040
+ exports.formatYMD = formatYMD;
1041
+ exports.getCookie = getCookie;
1042
+ exports.getUrlParam = getUrlParam;
1043
+ exports.getUrlParams = getUrlParams;
1044
+ exports.is = is;
1045
+ exports.isArray = isArray;
1046
+ exports.isAsyncFunction = isAsyncFunction;
1047
+ exports.isBoolean = isBoolean;
1048
+ exports.isDate = isDate;
1049
+ exports.isDef = isDef;
1050
+ exports.isElement = isElement;
1051
+ exports.isEmpty = isEmpty;
1052
+ exports.isEmptySv = isEmptySv;
1053
+ exports.isEqual = isEqual;
1054
+ exports.isFloat = isFloat;
1055
+ exports.isFunction = isFunction;
1056
+ exports.isHexColor = isHexColor;
1057
+ exports.isIOS = isIOS;
1058
+ exports.isInt = isInt;
1059
+ exports.isNull = isNull;
1060
+ exports.isNullOrUnDef = isNullOrUnDef;
1061
+ exports.isNumber = isNumber;
1062
+ exports.isObject = isObject;
1063
+ exports.isPC = isPC;
1064
+ exports.isPrimitive = isPrimitive;
1065
+ exports.isPromise = isPromise;
1066
+ exports.isString = isString;
1067
+ exports.isSymbol = isSymbol;
1068
+ exports.isUnDef = isUnDef;
1069
+ exports.isValidEmail = isValidEmail;
1070
+ exports.isWindow = isWindow;
1071
+ exports.kebabToCamel = kebabToCamel;
1072
+ exports.localClear = localClear;
1073
+ exports.localGet = localGet;
1074
+ exports.localRm = localRm;
1075
+ exports.localSet = localSet;
1076
+ exports.maskPhone = maskPhone;
1077
+ exports.onScroll = onScroll;
1078
+ exports.percentCN = percentCN;
1079
+ exports.reverse = reverse;
1080
+ exports.scrollToBottom = scrollToBottom;
1081
+ exports.scrollToTop = scrollToTop;
1082
+ exports.setCookie = setCookie;
1083
+ exports.shallowClone = shallowClone;
1084
+ exports.signed = signed;
1085
+ exports.sortByKey = sortByKey;
1086
+ exports.sortNumAsc = sortNumAsc;
1087
+ exports.sortNumDesc = sortNumDesc;
1088
+ exports.spacePhone = spacePhone;
1089
+ exports.toArray = toArray;
1090
+ exports.toCamel = toCamel;
1091
+ exports.toQueryString = toQueryString;
1092
+ exports.trimAll = trimAll;
1093
+ exports.truncate = truncate;
1094
+ exports.truncateByWords = truncateByWords;
1095
+ exports.unique = unique;
1096
+ exports.uniqueByKey = uniqueByKey;