@base-web-kits/base-tools-ts 0.9.4 → 0.9.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base-web-kits/base-tools-ts",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "sideEffects": false,
5
5
  "description": "Independent TS utilities package built from src/ts.",
6
6
  "keywords": [
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 拖拽排序 (不改变原数组)
3
+ * @param list 原始数组
4
+ * @param fromIndex 要移动的元素的原始索引
5
+ * @param toIndex 要移动到的目标索引
6
+ * @returns 移动元素后的新数组
7
+ */
8
+ export function arrayMove<T>(list: T[], fromIndex: number, toIndex: number) {
9
+ const newList = [...list]; // 创建新数组副本
10
+ const [removed] = newList.splice(fromIndex, 1); // 移除 fromIndex 处的元素
11
+ newList.splice(toIndex, 0, removed); // 插入到 toIndex 处
12
+ return newList;
13
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 将 Promise 包装为 [data, error] 形式, 减少 try-catch 代码量
3
+ * @param p 要包装的 Promise
4
+ * @returns 一个 Promise,其结果为 [data, error] 形式
5
+ * @example
6
+ * const [data, err] = await toAsync(fetch('https://api.example.com/data'));
7
+ * if (err) {
8
+ * console.error(err);
9
+ * return;
10
+ * }
11
+ * console.log(data);
12
+ */
13
+ export async function toAsync<T>(p: Promise<T>): Promise<[T | null, unknown]> {
14
+ try {
15
+ const data = await p;
16
+ return [data, null];
17
+ } catch (err) {
18
+ return [null, err];
19
+ }
20
+ }
@@ -0,0 +1,61 @@
1
+ import mitt, { type Emitter, type EventType } from 'mitt';
2
+
3
+ type Events = Record<EventType, unknown>;
4
+
5
+ /**
6
+ * 总线式发布订阅
7
+ * @example
8
+ * const emitter = new EventBus(); // 支持链式调用
9
+ * emitter.on('xx', fn); // 订阅事件 xx
10
+ * emitter.once('xx', fn); // 订阅事件 xx 一次
11
+ * emitter.emit('xx', any); // 发布事件 xx,参数任意
12
+ * emitter.off('xx'); // 移除事件 xx 下全部监听
13
+ * emitter.off('xx', fn); // 移除事件 xx 下指定监听
14
+ * emitter.clear(); // 移除所有事件
15
+ *
16
+ * @example 类型约束
17
+ * type T = { a: number; b: string };
18
+ * const emitter = new EventBus<{ xx: T; yy: void }>();
19
+ * const fn = (arg: T) => {}
20
+ * emitter.on('xx', fn);
21
+ * emitter.off('xx', fn);
22
+ * emitter.emit('xx', { a: 123, b: '123' });
23
+ * emitter.emit('yy');
24
+ */
25
+ export class EventBus<T extends Events = Events> {
26
+ private readonly _emitter: Emitter<T> = mitt<T>();
27
+
28
+ /** 订阅 */
29
+ on<K extends keyof T>(type: K, fn: (event: T[K]) => void): this {
30
+ this._emitter.on(type, fn);
31
+ return this;
32
+ }
33
+
34
+ /** 订阅一次 */
35
+ once<K extends keyof T>(type: K, fn: (event: T[K]) => void): this {
36
+ const wrap = (event: T[K]) => {
37
+ this._emitter.off(type, wrap);
38
+ fn(event);
39
+ };
40
+ this._emitter.on(type, wrap);
41
+ return this;
42
+ }
43
+
44
+ /** 发布 */
45
+ emit<K extends keyof T>(type: K, event?: T[K]): this {
46
+ this._emitter.emit(type, event as T[K]);
47
+ return this;
48
+ }
49
+
50
+ /** 移除 */
51
+ off<K extends keyof T>(type: K, fn?: (event: T[K]) => void): this {
52
+ this._emitter.off(type, fn);
53
+ return this;
54
+ }
55
+
56
+ /** 清空 */
57
+ clear(): this {
58
+ this._emitter.all.clear();
59
+ return this;
60
+ }
61
+ }
@@ -0,0 +1 @@
1
+ export { EventBus } from './EventBus'; // 注意内部不能写export default,因为这里是export *
@@ -0,0 +1,184 @@
1
+ import dayjs from 'dayjs';
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat';
3
+ import utc from 'dayjs/plugin/utc';
4
+ import timezone from 'dayjs/plugin/timezone';
5
+ import relativeTime from 'dayjs/plugin/relativeTime';
6
+ import advancedFormat from 'dayjs/plugin/advancedFormat';
7
+ import 'dayjs/locale/zh-cn';
8
+ import { zeroPad } from '../number';
9
+
10
+ dayjs.extend(customParseFormat);
11
+ dayjs.extend(utc);
12
+ dayjs.extend(timezone);
13
+ dayjs.extend(relativeTime);
14
+ dayjs.extend(advancedFormat);
15
+ dayjs.locale('zh-cn');
16
+
17
+ type BaseTime = number | string | Date | dayjs.Dayjs | null | undefined;
18
+
19
+ /**
20
+ * 创建 dayjs 实例
21
+ * 文档: https://day.js.org/zh-CN/
22
+ * @param t 各种规范或不规范的时间
23
+ * @returns dayjs 实例
24
+ * @example
25
+ * const d = toDayjs('2021-01-01'); // dayjs 实例
26
+ * d.format('YYYY-MM-DD HH:mm:ss'); // "2025-12-10 11:33:16"
27
+ * d.valueOf(); // 毫秒时间戳,如 1765337596913
28
+ * d.unix(); // 秒时间戳,如 1765337596
29
+ * d.millisecond(); // 毫秒 913
30
+ * d.second(); // 秒 16
31
+ * d.minute(); // 分 33
32
+ * d.hour(); // 时 11
33
+ * d.date(); // 日 10
34
+ * d.day(); // 星期几 5(周日=0)
35
+ * d.month() + 1; // 月 12
36
+ * d.year(); // 年 2025
37
+ * d.startOf('day').valueOf(); // 当日零点
38
+ * d.startOf('month').format('YYYY-MM-DD HH:mm:ss'); // 月初 "2025-12-01 00:00:00"
39
+ * d.endOf('month').format('YYYY-MM-DD HH:mm:ss'); // 月末 "2025-12-31 23:59:59"
40
+ * d.fromNow(); // “刚刚”、“x分钟前/后”、“x小时前/后”、“x天前/后”、“x月前/后”、“x年前/后”
41
+ * d.isSame(t, 'day'); // 是否与t在同一天
42
+ * d.diff(); // 与当前时间相差的毫秒数
43
+ * d.diff(t); // 与t相差的毫秒数
44
+ * d.diff(t, 'second'); // 与t相差的秒数
45
+ * d.diff(t, 'minute'); // 与t相差的分钟数
46
+ * d.diff(t, 'hour'); // 与t相差的小时数
47
+ * d.diff(t, 'day'); // 与t相差的天数
48
+ * d.diff(t, 'week'); // 与t相差的周数
49
+ * d.diff(t, 'month'); // 与t相差的月数
50
+ * d.diff(t, 'quarter'); // 与t相差的季度数
51
+ * d.diff(t, 'year'); // 与t相差的年数
52
+ */
53
+ export function toDayjs(t: BaseTime, fmt?: dayjs.OptionType) {
54
+ if (t === null || t === undefined) return dayjs();
55
+ if (typeof t === 'number') {
56
+ const s = String(Math.trunc(t));
57
+ return dayjs(s.length === 10 ? t * 1000 : t, fmt);
58
+ }
59
+ if (typeof t === 'string') {
60
+ const s = t.trim();
61
+ if (/^\d{10}$/.test(s)) return dayjs(Number(s) * 1000, fmt);
62
+ if (/^\d{13}$/.test(s)) return dayjs(Number(s), fmt);
63
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return dayjs(s, fmt || 'YYYY-MM-DD');
64
+ if (/^\d{4}\/\d{2}\/\d{2}$/.test(s)) return dayjs(s, fmt || 'YYYY/MM/DD');
65
+ if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/.test(s))
66
+ return dayjs(s, fmt || 'YYYY-MM-DD HH:mm:ss');
67
+ if (/^\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}$/.test(s))
68
+ return dayjs(s, fmt || 'YYYY/MM/DD HH:mm:ss');
69
+ return dayjs(s, fmt);
70
+ }
71
+ return dayjs(t, fmt);
72
+ }
73
+
74
+ /**
75
+ * 获取“前几天”的日期范围
76
+ * @param offset 正整数天数
77
+ * @param fmt 日期格式,默认 `YYYY-MM-DD`
78
+ * @returns `[start, end]` 日期字符串数组
79
+ * @example
80
+ * 若今天为 2025-11-19:
81
+ * getDateRangeBefore(1) // ['2025-11-18', '2025-11-19']
82
+ * getDateRangeBefore(1, 'YYYY-MM-DD HH:mm:ss') // ['2025-11-18 00:00:00', '2025-11-19 23:59:59']
83
+ */
84
+ export function getDateRangeBefore(offset: number, fmt = 'YYYY-MM-DD') {
85
+ const now = toDayjs(Date.now());
86
+ const n = Math.max(0, Math.trunc(offset));
87
+ const hasTime = /H|h|m|s|S|A|a|x|X/.test(fmt);
88
+ const startDay = now.add(-n, 'day');
89
+ const endDay = now;
90
+ const start = (hasTime ? startDay.startOf('day') : startDay).format(fmt);
91
+ const end = (hasTime ? endDay.endOf('day') : endDay).format(fmt);
92
+ return [start, end];
93
+ }
94
+
95
+ /**
96
+ * 获取“后几天”的日期范围
97
+ * - 起点:今天;终点:`offset` 天后
98
+ * - 若 `fmt` 含时间令牌(如 `HH:mm:ss`),则返回整日范围:起点为当日零点,终点为当日末尾
99
+ * @param offset 正整数天数
100
+ * @param fmt 日期格式,默认 `YYYY-MM-DD`
101
+ * @returns `[start, end]` 日期字符串数组
102
+ * @example
103
+ * 若今天为 2025-11-19:
104
+ * getDateRangeAfter(1) // ['2025-11-19', '2025-11-20']
105
+ * getDateRangeAfter(1, 'YYYY-MM-DD HH:mm:ss') // ['2025-11-19 00:00:00', '2025-11-20 23:59:59']
106
+ */
107
+ export function getDateRangeAfter(offset: number, fmt = 'YYYY-MM-DD') {
108
+ const now = toDayjs(Date.now());
109
+ const n = Math.max(0, Math.trunc(offset));
110
+ const hasTime = /H|h|m|s|S|A|a|x|X/.test(fmt);
111
+ const startDay = now;
112
+ const endDay = now.add(n, 'day');
113
+ const start = (hasTime ? startDay.startOf('day') : startDay).format(fmt);
114
+ const end = (hasTime ? endDay.endOf('day') : endDay).format(fmt);
115
+ return [start, end];
116
+ }
117
+
118
+ /**
119
+ * 获取倒计时的时间分解(零填充字符串)
120
+ * @param diff 毫秒差值(正数表示剩余时间,负数/0表示已到期)
121
+ * @returns 包含天、时、分、秒、毫秒的零填充对象
122
+ * @example
123
+ * const diff = toDayjs(t).diff(); // 毫秒差值
124
+ * const parts = getCountdownParts(diff); // { d: '01', h: '02', m: '03', s: '04', ms: '567' }
125
+ */
126
+ export function getCountdownParts(diff: number) {
127
+ if (diff <= 0) return { d: '00', h: '00', m: '00', s: '00', ms: '000' };
128
+
129
+ const d = Math.floor(diff / (1000 * 60 * 60 * 24));
130
+ const h = Math.floor((diff / (1000 * 60 * 60)) % 24);
131
+ const m = Math.floor((diff / (1000 * 60)) % 60);
132
+ const s = Math.floor((diff / 1000) % 60);
133
+ const ms = diff % 1000;
134
+
135
+ return {
136
+ d: zeroPad(d),
137
+ h: zeroPad(h),
138
+ m: zeroPad(m),
139
+ s: zeroPad(s),
140
+ ms: zeroPad(ms, 3),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * 通过出生日期计算年龄
146
+ * @param birthdate 生日日期,支持多种格式(会被自动解析)
147
+ * @returns 年龄对象,包含 `age`(年龄数值)和 `type`(年龄单位,'year' 表示年,'month' 表示月)
148
+ * @example
149
+ * // 假设当前日期为 2025-11-19
150
+ * getAgeByBirthdate('2025-05-10'); // { age: 6, type: 'month' }
151
+ * getAgeByBirthdate('2020-11-19'); // { age: 5, type: 'year' }
152
+ * getAgeByBirthdate('2020-12-01'); // { age: 4, type: 'year' }(生日还没到, 所以年龄是4岁)
153
+ */
154
+ export function getAgeByBirthdate(birthdate: string) {
155
+ const birth = toDayjs(birthdate, 'YYYY-MM-DD');
156
+ const now = toDayjs(Date.now());
157
+
158
+ // 精确的月份计算
159
+ const totalMonths = (now.year() - birth.year()) * 12 + (now.month() - birth.month());
160
+
161
+ // 如果当前日期小于出生日期,月份减1
162
+ const adjustedMonths = now.date() < birth.date() ? totalMonths - 1 : totalMonths;
163
+
164
+ if (adjustedMonths >= 12) {
165
+ let age = Math.floor(adjustedMonths / 12);
166
+ // 检查生日是否已过
167
+ const birthdayThisYear = birth.add(age, 'year');
168
+ if (now.isBefore(birthdayThisYear)) {
169
+ age--;
170
+ }
171
+ return { age, type: 'year' };
172
+ }
173
+
174
+ return { age: adjustedMonths, type: 'month' };
175
+ }
176
+
177
+ /**
178
+ * 对外抛出 dayjs 以便全局配置
179
+ * @example
180
+ * 切换语言
181
+ * dayjs.locale('en'); // 切换为英文
182
+ * dayjs.locale('zh-cn'); // 切换为中文 (默认)
183
+ */
184
+ export { dayjs };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 内部统一导出, 外部快捷引入: import {xx} from 'base-tools/ts'
3
+ */
4
+ export * from './array';
5
+ export * from './async';
6
+ export * from './bean';
7
+ export * from './day';
8
+ export * from './lodash';
9
+ export * from './number';
10
+ export * from './object';
11
+ export * from './string';
12
+ export * from './typing';
13
+ export * from './url';
14
+ export * from './validator';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * re-export 全量 lodash-es
3
+ * 文档: https://www.lodashjs.com/
4
+ * 目的: 从工具库统一loadsh版本,避免项目多个版本冲突
5
+ * 注意: 需在tsup.config.ts加入noExternal: ['lodash-es'],确保打包时不被忽略
6
+ */
7
+ export * from 'lodash-es';
@@ -0,0 +1,192 @@
1
+ import BigNumber from 'bignumber.js';
2
+
3
+ /**
4
+ * 数值入参类型。
5
+ * 支持原生 `number`、`string`(包含小数、科学计数法等)以及 `BigNumber`。
6
+ */
7
+ export type NumLike = string | number | BigNumber;
8
+
9
+ /**
10
+ * 将任意 `NumLike` 统一转换为 `BigNumber` 实例。
11
+ * @param x 任意支持的数值入参。
12
+ * @returns `BigNumber` 实例。
13
+ * @example
14
+ * big('0.1'); // => BigNumber
15
+ * big(0.2); // => BigNumber
16
+ */
17
+ export function big(x: NumLike): BigNumber {
18
+ return x instanceof BigNumber ? x : new BigNumber(x);
19
+ }
20
+
21
+ /**
22
+ * 高精度加法(支持多个参数连加)。
23
+ * @example
24
+ * bigAdd(0.1, 0.2); // => 0.3
25
+ * bigAdd('0.1', '0.2'); // => 0.3
26
+ * bigAdd(1, 2, 3, 4); // => 10 // 多参数连加: 1+2+3+4
27
+ */
28
+ export function bigAdd(a: NumLike, ...rest: NumLike[]) {
29
+ let acc = big(a);
30
+ for (const x of rest) acc = acc.plus(big(x));
31
+ return acc.toNumber();
32
+ }
33
+
34
+ /**
35
+ * 高精度减法(支持多个参数连减)。
36
+ * @example
37
+ * bigSub(1, 0.9); // => 0.1
38
+ * bigSub('1.1', '0.2'); // => 0.9
39
+ * bigSub(10, 1, 2, 3); // => 4 // 多参数连减: 10-1-2-3
40
+ */
41
+ export function bigSub(a: NumLike, ...rest: NumLike[]) {
42
+ let acc = big(a);
43
+ for (const x of rest) acc = acc.minus(big(x));
44
+ return acc.toNumber();
45
+ }
46
+
47
+ /**
48
+ * 高精度乘法(支持多个参数连乘)。
49
+ * @example
50
+ * bigMul(0.1, 0.2); // => 0.02
51
+ * bigMul('1.5', '3'); // => 4.5
52
+ * bigMul(2, 3, 4); // => 24 // 多参数连乘: 2*3*4
53
+ */
54
+ export function bigMul(a: NumLike, ...rest: NumLike[]) {
55
+ let acc = big(a);
56
+ for (const x of rest) acc = acc.times(big(x));
57
+ return acc.toNumber();
58
+ }
59
+
60
+ /**
61
+ * 高精度除法(支持多个参数连除)。
62
+ * @example
63
+ * bigDiv(1, 3); // => 0.333333...
64
+ * bigDiv('10', '4'); // => 2.5
65
+ * bigDiv(100, 5, 2); // => 10 // 多参数连除: 100/5/2
66
+ */
67
+ export function bigDiv(a: NumLike, ...rest: NumLike[]) {
68
+ let acc = big(a);
69
+ for (const x of rest) acc = acc.div(big(x));
70
+ return acc.toNumber();
71
+ }
72
+
73
+ /**
74
+ * 指数运算
75
+ * @param x 底数。
76
+ * @param y 指数。
77
+ * @returns 计算结果。
78
+ * @example
79
+ * bigPow(2, 3); // => 8
80
+ * bigPow('2.5', 2); // => 6.25
81
+ */
82
+ export function bigPow(x: NumLike, y: NumLike) {
83
+ return big(x).pow(big(y)).toNumber();
84
+ }
85
+
86
+ /**
87
+ * 四舍五入到指定位数
88
+ * @param x 需要舍入的数值。
89
+ * @param dp 保留的小数位数,默认 `0`(取整)。
90
+ * @param rm 舍入模式,默认 `ROUND_HALF_UP`(四舍五入)。
91
+ * @returns 舍入后的数值。
92
+ * @example
93
+ * bigRound(1.6); // => 2
94
+ * bigRound('1.234', 2); // => 1.23
95
+ * bigRound('1.235', 2); // => 1.24
96
+ * bigRound('1.299', 2, BigNumber.ROUND_DOWN); // => 1.29
97
+ */
98
+ export function bigRound(x: NumLike, dp = 0, rm: BigNumber.RoundingMode = BigNumber.ROUND_HALF_UP) {
99
+ return big(x).decimalPlaces(dp, rm).toNumber();
100
+ }
101
+
102
+ /**
103
+ * 将数值按指定位数格式化为字符串(保留小数位)。
104
+ * @param x 需要格式化的数值。
105
+ * @param dp 保留的小数位数,默认 `2`。
106
+ * @param rm 舍入模式,默认 `ROUND_HALF_UP`(四舍五入)。
107
+ * @returns 格式化后的字符串。
108
+ * @example
109
+ * toFixed('1'); // => '1.00'
110
+ * +toFixed('1'); // => 1
111
+ * toFixed(1.2345); // => '1.23'
112
+ * toFixed(1.2345, 3); // => '1.235'
113
+ * toFixed('1.2345', 0, BigNumber.ROUND_UP); // => '2'
114
+ */
115
+ export function toFixed(
116
+ x: NumLike,
117
+ dp = 2,
118
+ rm: BigNumber.RoundingMode = BigNumber.ROUND_HALF_UP,
119
+ ): string {
120
+ return big(x).toFixed(dp, rm);
121
+ }
122
+
123
+ /**
124
+ * 比较两个数值大小。
125
+ * @example
126
+ * bigCompare('2', '10'); // => -1
127
+ * bigCompare(3, 3); // => 0
128
+ * bigCompare('10', 2); // => 1
129
+ */
130
+ export function bigCompare(a: NumLike, b: NumLike): -1 | 0 | 1 | null {
131
+ return big(a).comparedTo(big(b));
132
+ }
133
+
134
+ /**
135
+ * 判断两个数值是否相等。
136
+ * @example
137
+ * bigEqual('1.0', 1); // => true
138
+ * bigEqual(2, 1); // => false
139
+ */
140
+ export function bigEqual(a: NumLike, b: NumLike): boolean {
141
+ return big(a).isEqualTo(big(b));
142
+ }
143
+
144
+ /**
145
+ * 判断 a 是否大于 b。
146
+ * @example
147
+ * bigGt(2, 1); // => true
148
+ * bigGt(1, 2); // => false
149
+ */
150
+ export function bigGt(a: NumLike, b: NumLike): boolean {
151
+ return big(a).isGreaterThan(big(b));
152
+ }
153
+
154
+ /**
155
+ * 判断 a 是否大于等于 b。
156
+ * @example
157
+ * bigGte(2, 2); // => true
158
+ * bigGte(1, 2); // => false
159
+ */
160
+ export function bigGte(a: NumLike, b: NumLike): boolean {
161
+ return big(a).isGreaterThanOrEqualTo(big(b));
162
+ }
163
+
164
+ /**
165
+ * 判断 a 是否小于 b。
166
+ * @example
167
+ * bigLt(1, 2); // => true
168
+ * bigLt(2, 1); // => false
169
+ */
170
+ export function bigLt(a: NumLike, b: NumLike): boolean {
171
+ return big(a).isLessThan(big(b));
172
+ }
173
+
174
+ /**
175
+ * 判断 a 是否小于等于 b。
176
+ * @example
177
+ * bigLte(2, 2); // => true
178
+ * bigLte(1, 2); // => true
179
+ * bigLte(2, 1); // => false
180
+ */
181
+ export function bigLte(a: NumLike, b: NumLike): boolean {
182
+ return big(a).isLessThanOrEqualTo(big(b));
183
+ }
184
+
185
+ /**
186
+ * 导出 BigNumber 类
187
+ * @example
188
+ * BigNumber.ROUND_HALF_UP; // 使用类型
189
+ * BigNumber.set(config); // 设置全局配置
190
+ * const bn = new BigNumber('123456.789'); // 创建实例
191
+ */
192
+ export { BigNumber };