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